2
0

migrations.js 16 KB

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