migrations.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. import AccountSettings from '../models/accountSettings';
  2. import Actions from '../models/actions';
  3. import Activities from '../models/activities';
  4. import Announcements from '../models/announcements';
  5. import Boards from '../models/boards';
  6. import CardComments from '../models/cardComments';
  7. import Cards from '../models/cards';
  8. import ChecklistItems from '../models/checklistItems';
  9. import Checklists from '../models/checklists';
  10. import CustomFields from '../models/customFields';
  11. import Integrations from '../models/integrations';
  12. import InvitationCodes from '../models/invitationCodes';
  13. import Lists from '../models/lists';
  14. import Rules from '../models/rules';
  15. import Settings from '../models/settings';
  16. import Swimlanes from '../models/swimlanes';
  17. import Triggers from '../models/triggers';
  18. import UnsavedEdits from '../models/unsavedEdits';
  19. import Users from '../models/users';
  20. // Anytime you change the schema of one of the collection in a non-backward
  21. // compatible way you have to write a migration in this file using the following
  22. // API:
  23. //
  24. // Migrations.add(name, migrationCallback, optionalOrder);
  25. // Note that we have extra migrations defined in `sandstorm.js` that are
  26. // exclusive to Sandstorm and shouldn’t be executed in the general case.
  27. // XXX I guess if we had ES6 modules we could
  28. // `import { isSandstorm } from sandstorm.js` and define the migration here as
  29. // well, but for now I want to avoid definied too many globals.
  30. // In the context of migration functions we don't want to validate database
  31. // mutation queries against the current (ie, latest) collection schema. Doing
  32. // that would work at the time we write the migration but would break in the
  33. // future when we'll update again the concerned collection schema.
  34. //
  35. // To prevent this bug we always have to disable the schema validation and
  36. // argument transformations. We generally use the shorthandlers defined below.
  37. const noValidate = {
  38. validate: false,
  39. filter: false,
  40. autoConvert: false,
  41. removeEmptyStrings: false,
  42. getAutoValues: false,
  43. };
  44. const noValidateMulti = { ...noValidate, multi: true };
  45. Migrations.add('board-background-color', () => {
  46. const defaultColor = '#16A085';
  47. Boards.update(
  48. {
  49. background: {
  50. $exists: false,
  51. },
  52. },
  53. {
  54. $set: {
  55. background: {
  56. type: 'color',
  57. color: defaultColor,
  58. },
  59. },
  60. },
  61. noValidateMulti,
  62. );
  63. });
  64. Migrations.add('lowercase-board-permission', () => {
  65. ['Public', 'Private'].forEach(permission => {
  66. Boards.update(
  67. { permission },
  68. { $set: { permission: permission.toLowerCase() } },
  69. noValidateMulti,
  70. );
  71. });
  72. });
  73. // Security migration: see https://github.com/wekan/wekan/issues/99
  74. Migrations.add('change-attachments-type-for-non-images', () => {
  75. const newTypeForNonImage = 'application/octet-stream';
  76. Attachments.find().forEach(file => {
  77. if (!file.isImage()) {
  78. Attachments.update(
  79. file._id,
  80. {
  81. $set: {
  82. 'original.type': newTypeForNonImage,
  83. 'copies.attachments.type': newTypeForNonImage,
  84. },
  85. },
  86. noValidate,
  87. );
  88. }
  89. });
  90. });
  91. Migrations.add('card-covers', () => {
  92. Cards.find().forEach(card => {
  93. const cover = Attachments.findOne({ cardId: card._id, cover: true });
  94. if (cover) {
  95. Cards.update(card._id, { $set: { coverId: cover._id } }, noValidate);
  96. }
  97. });
  98. Attachments.update({}, { $unset: { cover: '' } }, noValidateMulti);
  99. });
  100. Migrations.add('use-css-class-for-boards-colors', () => {
  101. const associationTable = {
  102. '#27AE60': 'nephritis',
  103. '#C0392B': 'pomegranate',
  104. '#2980B9': 'belize',
  105. '#8E44AD': 'wisteria',
  106. '#2C3E50': 'midnight',
  107. '#E67E22': 'pumpkin',
  108. };
  109. Boards.find().forEach(board => {
  110. const oldBoardColor = board.background.color;
  111. const newBoardColor = associationTable[oldBoardColor];
  112. Boards.update(
  113. board._id,
  114. {
  115. $set: { color: newBoardColor },
  116. $unset: { background: '' },
  117. },
  118. noValidate,
  119. );
  120. });
  121. });
  122. Migrations.add('denormalize-star-number-per-board', () => {
  123. Boards.find().forEach(board => {
  124. const nStars = Users.find({ 'profile.starredBoards': board._id }).count();
  125. Boards.update(board._id, { $set: { stars: nStars } }, noValidate);
  126. });
  127. });
  128. // We want to keep a trace of former members so we can efficiently publish their
  129. // infos in the general board publication.
  130. Migrations.add('add-member-isactive-field', () => {
  131. Boards.find({}, { fields: { members: 1 } }).forEach(board => {
  132. const allUsersWithSomeActivity = _.chain(
  133. Activities.find(
  134. { boardId: board._id },
  135. { fields: { userId: 1 } },
  136. ).fetch(),
  137. )
  138. .pluck('userId')
  139. .uniq()
  140. .value();
  141. const currentUsers = _.pluck(board.members, 'userId');
  142. const formerUsers = _.difference(allUsersWithSomeActivity, currentUsers);
  143. const newMemberSet = [];
  144. board.members.forEach(member => {
  145. member.isActive = true;
  146. newMemberSet.push(member);
  147. });
  148. formerUsers.forEach(userId => {
  149. newMemberSet.push({
  150. userId,
  151. isAdmin: false,
  152. isActive: false,
  153. });
  154. });
  155. Boards.update(board._id, { $set: { members: newMemberSet } }, noValidate);
  156. });
  157. });
  158. Migrations.add('add-sort-checklists', () => {
  159. Checklists.find().forEach((checklist, index) => {
  160. if (!checklist.hasOwnProperty('sort')) {
  161. Checklists.direct.update(
  162. checklist._id,
  163. { $set: { sort: index } },
  164. noValidate,
  165. );
  166. }
  167. checklist.items.forEach((item, index) => {
  168. if (!item.hasOwnProperty('sort')) {
  169. Checklists.direct.update(
  170. { _id: checklist._id, 'items._id': item._id },
  171. { $set: { 'items.$.sort': index } },
  172. noValidate,
  173. );
  174. }
  175. });
  176. });
  177. });
  178. Migrations.add('add-swimlanes', () => {
  179. Boards.find().forEach(board => {
  180. const swimlaneId = board.getDefaultSwimline()._id;
  181. Cards.find({ boardId: board._id }).forEach(card => {
  182. if (!card.hasOwnProperty('swimlaneId')) {
  183. Cards.direct.update(
  184. { _id: card._id },
  185. { $set: { swimlaneId } },
  186. noValidate,
  187. );
  188. }
  189. });
  190. });
  191. });
  192. Migrations.add('add-views', () => {
  193. Boards.find().forEach(board => {
  194. if (!board.hasOwnProperty('view')) {
  195. Boards.direct.update(
  196. { _id: board._id },
  197. { $set: { view: 'board-view-swimlanes' } },
  198. noValidate,
  199. );
  200. }
  201. });
  202. });
  203. Migrations.add('add-checklist-items', () => {
  204. Checklists.find().forEach(checklist => {
  205. // Create new items
  206. _.sortBy(checklist.items, 'sort').forEach((item, index) => {
  207. ChecklistItems.direct.insert({
  208. title: item.title ? item.title : 'Checklist',
  209. sort: index,
  210. isFinished: item.isFinished,
  211. checklistId: checklist._id,
  212. cardId: checklist.cardId,
  213. });
  214. });
  215. // Delete old ones
  216. Checklists.direct.update(
  217. { _id: checklist._id },
  218. { $unset: { items: 1 } },
  219. noValidate,
  220. );
  221. });
  222. });
  223. Migrations.add('add-profile-view', () => {
  224. Users.find().forEach(user => {
  225. if (!user.hasOwnProperty('profile.boardView')) {
  226. // Set default view
  227. Users.direct.update(
  228. { _id: user._id },
  229. { $set: { 'profile.boardView': 'board-view-lists' } },
  230. noValidate,
  231. );
  232. }
  233. });
  234. });
  235. Migrations.add('add-card-types', () => {
  236. Cards.find().forEach(card => {
  237. Cards.direct.update(
  238. { _id: card._id },
  239. {
  240. $set: {
  241. type: 'cardType-card',
  242. linkedId: null,
  243. },
  244. },
  245. noValidate,
  246. );
  247. });
  248. });
  249. Migrations.add('add-custom-fields-to-cards', () => {
  250. Cards.update(
  251. {
  252. customFields: {
  253. $exists: false,
  254. },
  255. },
  256. {
  257. $set: {
  258. customFields: [],
  259. },
  260. },
  261. noValidateMulti,
  262. );
  263. });
  264. Migrations.add('add-requester-field', () => {
  265. Cards.update(
  266. {
  267. requestedBy: {
  268. $exists: false,
  269. },
  270. },
  271. {
  272. $set: {
  273. requestedBy: '',
  274. },
  275. },
  276. noValidateMulti,
  277. );
  278. });
  279. Migrations.add('add-assigner-field', () => {
  280. Cards.update(
  281. {
  282. assignedBy: {
  283. $exists: false,
  284. },
  285. },
  286. {
  287. $set: {
  288. assignedBy: '',
  289. },
  290. },
  291. noValidateMulti,
  292. );
  293. });
  294. Migrations.add('add-parent-field-to-cards', () => {
  295. Cards.update(
  296. {
  297. parentId: {
  298. $exists: false,
  299. },
  300. },
  301. {
  302. $set: {
  303. parentId: '',
  304. },
  305. },
  306. noValidateMulti,
  307. );
  308. });
  309. Migrations.add('add-subtasks-boards', () => {
  310. Boards.update(
  311. {
  312. subtasksDefaultBoardId: {
  313. $exists: false,
  314. },
  315. },
  316. {
  317. $set: {
  318. subtasksDefaultBoardId: null,
  319. subtasksDefaultListId: null,
  320. },
  321. },
  322. noValidateMulti,
  323. );
  324. });
  325. Migrations.add('add-subtasks-sort', () => {
  326. Boards.update(
  327. {
  328. subtaskSort: {
  329. $exists: false,
  330. },
  331. },
  332. {
  333. $set: {
  334. subtaskSort: -1,
  335. },
  336. },
  337. noValidateMulti,
  338. );
  339. });
  340. Migrations.add('add-subtasks-allowed', () => {
  341. Boards.update(
  342. {
  343. allowsSubtasks: {
  344. $exists: false,
  345. },
  346. },
  347. {
  348. $set: {
  349. allowsSubtasks: true,
  350. },
  351. },
  352. noValidateMulti,
  353. );
  354. });
  355. Migrations.add('add-subtasks-allowed', () => {
  356. Boards.update(
  357. {
  358. presentParentTask: {
  359. $exists: false,
  360. },
  361. },
  362. {
  363. $set: {
  364. presentParentTask: 'no-parent',
  365. },
  366. },
  367. noValidateMulti,
  368. );
  369. });
  370. Migrations.add('add-authenticationMethod', () => {
  371. Users.update(
  372. {
  373. authenticationMethod: {
  374. $exists: false,
  375. },
  376. },
  377. {
  378. $set: {
  379. authenticationMethod: 'password',
  380. },
  381. },
  382. noValidateMulti,
  383. );
  384. });
  385. Migrations.add('remove-tag', () => {
  386. Users.update(
  387. {},
  388. {
  389. $unset: {
  390. 'profile.tags': 1,
  391. },
  392. },
  393. noValidateMulti,
  394. );
  395. });
  396. Migrations.add('remove-customFields-references-broken', () => {
  397. Cards.update(
  398. { 'customFields.$value': null },
  399. {
  400. $pull: {
  401. customFields: { value: null },
  402. },
  403. },
  404. noValidateMulti,
  405. );
  406. });
  407. Migrations.add('add-product-name', () => {
  408. Settings.update(
  409. {
  410. productName: {
  411. $exists: false,
  412. },
  413. },
  414. {
  415. $set: {
  416. productName: '',
  417. },
  418. },
  419. noValidateMulti,
  420. );
  421. });
  422. Migrations.add('add-hide-logo', () => {
  423. Settings.update(
  424. {
  425. hideLogo: {
  426. $exists: false,
  427. },
  428. },
  429. {
  430. $set: {
  431. hideLogo: false,
  432. },
  433. },
  434. noValidateMulti,
  435. );
  436. });
  437. Migrations.add('add-custom-html-after-body-start', () => {
  438. Settings.update(
  439. {
  440. customHTMLafterBodyStart: {
  441. $exists: false,
  442. },
  443. },
  444. {
  445. $set: {
  446. customHTMLafterBodyStart: '',
  447. },
  448. },
  449. noValidateMulti,
  450. );
  451. });
  452. Migrations.add('add-custom-html-before-body-end', () => {
  453. Settings.update(
  454. {
  455. customHTMLbeforeBodyEnd: {
  456. $exists: false,
  457. },
  458. },
  459. {
  460. $set: {
  461. customHTMLbeforeBodyEnd: '',
  462. },
  463. },
  464. noValidateMulti,
  465. );
  466. });
  467. Migrations.add('add-displayAuthenticationMethod', () => {
  468. Settings.update(
  469. {
  470. displayAuthenticationMethod: {
  471. $exists: false,
  472. },
  473. },
  474. {
  475. $set: {
  476. displayAuthenticationMethod: true,
  477. },
  478. },
  479. noValidateMulti,
  480. );
  481. });
  482. Migrations.add('add-defaultAuthenticationMethod', () => {
  483. Settings.update(
  484. {
  485. defaultAuthenticationMethod: {
  486. $exists: false,
  487. },
  488. },
  489. {
  490. $set: {
  491. defaultAuthenticationMethod: 'password',
  492. },
  493. },
  494. noValidateMulti,
  495. );
  496. });
  497. Migrations.add('add-templates', () => {
  498. Boards.update(
  499. {
  500. type: {
  501. $exists: false,
  502. },
  503. },
  504. {
  505. $set: {
  506. type: 'board',
  507. },
  508. },
  509. noValidateMulti,
  510. );
  511. Swimlanes.update(
  512. {
  513. type: {
  514. $exists: false,
  515. },
  516. },
  517. {
  518. $set: {
  519. type: 'swimlane',
  520. },
  521. },
  522. noValidateMulti,
  523. );
  524. Lists.update(
  525. {
  526. type: {
  527. $exists: false,
  528. },
  529. swimlaneId: {
  530. $exists: false,
  531. },
  532. },
  533. {
  534. $set: {
  535. type: 'list',
  536. swimlaneId: '',
  537. },
  538. },
  539. noValidateMulti,
  540. );
  541. Users.find({
  542. 'profile.templatesBoardId': {
  543. $exists: false,
  544. },
  545. }).forEach(user => {
  546. // Create board and swimlanes
  547. Boards.insert(
  548. {
  549. title: TAPi18n.__('templates'),
  550. permission: 'private',
  551. type: 'template-container',
  552. members: [
  553. {
  554. userId: user._id,
  555. isAdmin: true,
  556. isActive: true,
  557. isNoComments: false,
  558. isCommentOnly: false,
  559. },
  560. ],
  561. },
  562. (err, boardId) => {
  563. // Insert the reference to our templates board
  564. Users.update(user._id, {
  565. $set: { 'profile.templatesBoardId': boardId },
  566. });
  567. // Insert the card templates swimlane
  568. Swimlanes.insert(
  569. {
  570. title: TAPi18n.__('card-templates-swimlane'),
  571. boardId,
  572. sort: 1,
  573. type: 'template-container',
  574. },
  575. (err, swimlaneId) => {
  576. // Insert the reference to out card templates swimlane
  577. Users.update(user._id, {
  578. $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
  579. });
  580. },
  581. );
  582. // Insert the list templates swimlane
  583. Swimlanes.insert(
  584. {
  585. title: TAPi18n.__('list-templates-swimlane'),
  586. boardId,
  587. sort: 2,
  588. type: 'template-container',
  589. },
  590. (err, swimlaneId) => {
  591. // Insert the reference to out list templates swimlane
  592. Users.update(user._id, {
  593. $set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
  594. });
  595. },
  596. );
  597. // Insert the board templates swimlane
  598. Swimlanes.insert(
  599. {
  600. title: TAPi18n.__('board-templates-swimlane'),
  601. boardId,
  602. sort: 3,
  603. type: 'template-container',
  604. },
  605. (err, swimlaneId) => {
  606. // Insert the reference to out board templates swimlane
  607. Users.update(user._id, {
  608. $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
  609. });
  610. },
  611. );
  612. },
  613. );
  614. });
  615. });
  616. Migrations.add('fix-circular-reference_', () => {
  617. Cards.find().forEach(card => {
  618. if (card.parentId === card._id) {
  619. Cards.update(card._id, { $set: { parentId: '' } }, noValidateMulti);
  620. }
  621. });
  622. });
  623. Migrations.add('mutate-boardIds-in-customfields', () => {
  624. CustomFields.find().forEach(cf => {
  625. CustomFields.update(
  626. cf,
  627. {
  628. $set: {
  629. boardIds: [cf.boardId],
  630. },
  631. $unset: {
  632. boardId: '',
  633. },
  634. },
  635. noValidateMulti,
  636. );
  637. });
  638. });
  639. const firstBatchOfDbsToAddCreatedAndUpdated = [
  640. AccountSettings,
  641. Actions,
  642. Activities,
  643. Announcements,
  644. Boards,
  645. CardComments,
  646. Cards,
  647. ChecklistItems,
  648. Checklists,
  649. CustomFields,
  650. Integrations,
  651. InvitationCodes,
  652. Lists,
  653. Rules,
  654. Settings,
  655. Swimlanes,
  656. Triggers,
  657. UnsavedEdits,
  658. ];
  659. firstBatchOfDbsToAddCreatedAndUpdated.forEach(db => {
  660. db.before.insert((userId, doc) => {
  661. doc.createdAt = Date.now();
  662. doc.updatedAt = doc.createdAt;
  663. });
  664. db.before.update((userId, doc, fieldNames, modifier) => {
  665. modifier.$set = modifier.$set || {};
  666. modifier.$set.updatedAt = new Date();
  667. });
  668. });
  669. const modifiedAtTables = [
  670. AccountSettings,
  671. Actions,
  672. Activities,
  673. Announcements,
  674. Boards,
  675. CardComments,
  676. Cards,
  677. ChecklistItems,
  678. Checklists,
  679. CustomFields,
  680. Integrations,
  681. InvitationCodes,
  682. Lists,
  683. Rules,
  684. Settings,
  685. Swimlanes,
  686. Triggers,
  687. UnsavedEdits,
  688. Users,
  689. ];
  690. Migrations.add('add-missing-created-and-modified', () => {
  691. Promise.all(
  692. modifiedAtTables.map(db =>
  693. db
  694. .rawCollection()
  695. .update(
  696. { modifiedAt: { $exists: false } },
  697. { $set: { modifiedAt: new Date() } },
  698. { multi: true },
  699. )
  700. .then(() =>
  701. db
  702. .rawCollection()
  703. .update(
  704. { createdAt: { $exists: false } },
  705. { $set: { createdAt: new Date() } },
  706. { multi: true },
  707. ),
  708. ),
  709. ),
  710. )
  711. .then(() => {
  712. // eslint-disable-next-line no-console
  713. console.info('Successfully added createdAt and updatedAt to all tables');
  714. })
  715. .catch(e => {
  716. // eslint-disable-next-line no-console
  717. console.error(e);
  718. });
  719. });