migrations.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  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({ boardId: board._id }, { fields: { userId: 1 } }).fetch()
  134. )
  135. .pluck('userId')
  136. .uniq()
  137. .value();
  138. const currentUsers = _.pluck(board.members, 'userId');
  139. const formerUsers = _.difference(allUsersWithSomeActivity, currentUsers);
  140. const newMemberSet = [];
  141. board.members.forEach((member) => {
  142. member.isActive = true;
  143. newMemberSet.push(member);
  144. });
  145. formerUsers.forEach((userId) => {
  146. newMemberSet.push({
  147. userId,
  148. isAdmin: false,
  149. isActive: false,
  150. });
  151. });
  152. Boards.update(board._id, { $set: { members: newMemberSet } }, noValidate);
  153. });
  154. });
  155. Migrations.add('add-sort-checklists', () => {
  156. Checklists.find().forEach((checklist, index) => {
  157. if (!checklist.hasOwnProperty('sort')) {
  158. Checklists.direct.update(
  159. checklist._id,
  160. { $set: { sort: index } },
  161. noValidate
  162. );
  163. }
  164. checklist.items.forEach((item, index) => {
  165. if (!item.hasOwnProperty('sort')) {
  166. Checklists.direct.update(
  167. { _id: checklist._id, 'items._id': item._id },
  168. { $set: { 'items.$.sort': index } },
  169. noValidate
  170. );
  171. }
  172. });
  173. });
  174. });
  175. Migrations.add('add-swimlanes', () => {
  176. Boards.find().forEach((board) => {
  177. const swimlaneId = board.getDefaultSwimline()._id;
  178. Cards.find({ boardId: board._id }).forEach((card) => {
  179. if (!card.hasOwnProperty('swimlaneId')) {
  180. Cards.direct.update(
  181. { _id: card._id },
  182. { $set: { swimlaneId } },
  183. noValidate
  184. );
  185. }
  186. });
  187. });
  188. });
  189. Migrations.add('add-views', () => {
  190. Boards.find().forEach((board) => {
  191. if (!board.hasOwnProperty('view')) {
  192. Boards.direct.update(
  193. { _id: board._id },
  194. { $set: { view: 'board-view-swimlanes' } },
  195. noValidate
  196. );
  197. }
  198. });
  199. });
  200. Migrations.add('add-checklist-items', () => {
  201. Checklists.find().forEach((checklist) => {
  202. // Create new items
  203. _.sortBy(checklist.items, 'sort').forEach((item, index) => {
  204. ChecklistItems.direct.insert({
  205. title: item.title ? item.title : 'Checklist',
  206. sort: index,
  207. isFinished: item.isFinished,
  208. checklistId: checklist._id,
  209. cardId: checklist.cardId,
  210. });
  211. });
  212. // Delete old ones
  213. Checklists.direct.update(
  214. { _id: checklist._id },
  215. { $unset: { items: 1 } },
  216. noValidate
  217. );
  218. });
  219. });
  220. Migrations.add('add-profile-view', () => {
  221. Users.find().forEach((user) => {
  222. if (!user.hasOwnProperty('profile.boardView')) {
  223. // Set default view
  224. Users.direct.update(
  225. { _id: user._id },
  226. { $set: { 'profile.boardView': 'board-view-lists' } },
  227. noValidate
  228. );
  229. }
  230. });
  231. });
  232. Migrations.add('add-card-types', () => {
  233. Cards.find().forEach((card) => {
  234. Cards.direct.update(
  235. { _id: card._id },
  236. {
  237. $set: {
  238. type: 'cardType-card',
  239. linkedId: null,
  240. },
  241. },
  242. noValidate
  243. );
  244. });
  245. });
  246. Migrations.add('add-custom-fields-to-cards', () => {
  247. Cards.update(
  248. {
  249. customFields: {
  250. $exists: false,
  251. },
  252. },
  253. {
  254. $set: {
  255. customFields: [],
  256. },
  257. },
  258. noValidateMulti
  259. );
  260. });
  261. Migrations.add('add-requester-field', () => {
  262. Cards.update(
  263. {
  264. requestedBy: {
  265. $exists: false,
  266. },
  267. },
  268. {
  269. $set: {
  270. requestedBy: '',
  271. },
  272. },
  273. noValidateMulti
  274. );
  275. });
  276. Migrations.add('add-assigner-field', () => {
  277. Cards.update(
  278. {
  279. assignedBy: {
  280. $exists: false,
  281. },
  282. },
  283. {
  284. $set: {
  285. assignedBy: '',
  286. },
  287. },
  288. noValidateMulti
  289. );
  290. });
  291. Migrations.add('add-parent-field-to-cards', () => {
  292. Cards.update(
  293. {
  294. parentId: {
  295. $exists: false,
  296. },
  297. },
  298. {
  299. $set: {
  300. parentId: '',
  301. },
  302. },
  303. noValidateMulti
  304. );
  305. });
  306. Migrations.add('add-subtasks-boards', () => {
  307. Boards.update(
  308. {
  309. subtasksDefaultBoardId: {
  310. $exists: false,
  311. },
  312. },
  313. {
  314. $set: {
  315. subtasksDefaultBoardId: null,
  316. subtasksDefaultListId: null,
  317. },
  318. },
  319. noValidateMulti
  320. );
  321. });
  322. Migrations.add('add-subtasks-sort', () => {
  323. Boards.update(
  324. {
  325. subtaskSort: {
  326. $exists: false,
  327. },
  328. },
  329. {
  330. $set: {
  331. subtaskSort: -1,
  332. },
  333. },
  334. noValidateMulti
  335. );
  336. });
  337. Migrations.add('add-subtasks-allowed', () => {
  338. Boards.update(
  339. {
  340. allowsSubtasks: {
  341. $exists: false,
  342. },
  343. },
  344. {
  345. $set: {
  346. allowsSubtasks: true,
  347. },
  348. },
  349. noValidateMulti
  350. );
  351. });
  352. Migrations.add('add-subtasks-allowed', () => {
  353. Boards.update(
  354. {
  355. presentParentTask: {
  356. $exists: false,
  357. },
  358. },
  359. {
  360. $set: {
  361. presentParentTask: 'no-parent',
  362. },
  363. },
  364. noValidateMulti
  365. );
  366. });
  367. Migrations.add('add-authenticationMethod', () => {
  368. Users.update(
  369. {
  370. authenticationMethod: {
  371. $exists: false,
  372. },
  373. },
  374. {
  375. $set: {
  376. authenticationMethod: 'password',
  377. },
  378. },
  379. noValidateMulti
  380. );
  381. });
  382. Migrations.add('remove-tag', () => {
  383. Users.update(
  384. {},
  385. {
  386. $unset: {
  387. 'profile.tags': 1,
  388. },
  389. },
  390. noValidateMulti
  391. );
  392. });
  393. Migrations.add('remove-customFields-references-broken', () => {
  394. Cards.update(
  395. { 'customFields.$value': null },
  396. {
  397. $pull: {
  398. customFields: { value: null },
  399. },
  400. },
  401. noValidateMulti
  402. );
  403. });
  404. Migrations.add('add-product-name', () => {
  405. Settings.update(
  406. {
  407. productName: {
  408. $exists: false,
  409. },
  410. },
  411. {
  412. $set: {
  413. productName: '',
  414. },
  415. },
  416. noValidateMulti
  417. );
  418. });
  419. Migrations.add('add-hide-logo', () => {
  420. Settings.update(
  421. {
  422. hideLogo: {
  423. $exists: false,
  424. },
  425. },
  426. {
  427. $set: {
  428. hideLogo: false,
  429. },
  430. },
  431. noValidateMulti
  432. );
  433. });
  434. Migrations.add('add-custom-html-after-body-start', () => {
  435. Settings.update(
  436. {
  437. customHTMLafterBodyStart: {
  438. $exists: false,
  439. },
  440. },
  441. {
  442. $set: {
  443. customHTMLafterBodyStart: '',
  444. },
  445. },
  446. noValidateMulti
  447. );
  448. });
  449. Migrations.add('add-custom-html-before-body-end', () => {
  450. Settings.update(
  451. {
  452. customHTMLbeforeBodyEnd: {
  453. $exists: false,
  454. },
  455. },
  456. {
  457. $set: {
  458. customHTMLbeforeBodyEnd: '',
  459. },
  460. },
  461. noValidateMulti
  462. );
  463. });
  464. Migrations.add('add-displayAuthenticationMethod', () => {
  465. Settings.update(
  466. {
  467. displayAuthenticationMethod: {
  468. $exists: false,
  469. },
  470. },
  471. {
  472. $set: {
  473. displayAuthenticationMethod: true,
  474. },
  475. },
  476. noValidateMulti
  477. );
  478. });
  479. Migrations.add('add-defaultAuthenticationMethod', () => {
  480. Settings.update(
  481. {
  482. defaultAuthenticationMethod: {
  483. $exists: false,
  484. },
  485. },
  486. {
  487. $set: {
  488. defaultAuthenticationMethod: 'password',
  489. },
  490. },
  491. noValidateMulti
  492. );
  493. });
  494. Migrations.add('add-templates', () => {
  495. Boards.update(
  496. {
  497. type: {
  498. $exists: false,
  499. },
  500. },
  501. {
  502. $set: {
  503. type: 'board',
  504. },
  505. },
  506. noValidateMulti
  507. );
  508. Swimlanes.update(
  509. {
  510. type: {
  511. $exists: false,
  512. },
  513. },
  514. {
  515. $set: {
  516. type: 'swimlane',
  517. },
  518. },
  519. noValidateMulti
  520. );
  521. Lists.update(
  522. {
  523. type: {
  524. $exists: false,
  525. },
  526. swimlaneId: {
  527. $exists: false,
  528. },
  529. },
  530. {
  531. $set: {
  532. type: 'list',
  533. swimlaneId: '',
  534. },
  535. },
  536. noValidateMulti
  537. );
  538. Users.find({
  539. 'profile.templatesBoardId': {
  540. $exists: false,
  541. },
  542. }).forEach((user) => {
  543. // Create board and swimlanes
  544. Boards.insert(
  545. {
  546. title: TAPi18n.__('templates'),
  547. permission: 'private',
  548. type: 'template-container',
  549. members: [
  550. {
  551. userId: user._id,
  552. isAdmin: true,
  553. isActive: true,
  554. isNoComments: false,
  555. isCommentOnly: false,
  556. },
  557. ],
  558. },
  559. (err, boardId) => {
  560. // Insert the reference to our templates board
  561. Users.update(user._id, {
  562. $set: { 'profile.templatesBoardId': boardId },
  563. });
  564. // Insert the card templates swimlane
  565. Swimlanes.insert(
  566. {
  567. title: TAPi18n.__('card-templates-swimlane'),
  568. boardId,
  569. sort: 1,
  570. type: 'template-container',
  571. },
  572. (err, swimlaneId) => {
  573. // Insert the reference to out card templates swimlane
  574. Users.update(user._id, {
  575. $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
  576. });
  577. }
  578. );
  579. // Insert the list templates swimlane
  580. Swimlanes.insert(
  581. {
  582. title: TAPi18n.__('list-templates-swimlane'),
  583. boardId,
  584. sort: 2,
  585. type: 'template-container',
  586. },
  587. (err, swimlaneId) => {
  588. // Insert the reference to out list templates swimlane
  589. Users.update(user._id, {
  590. $set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
  591. });
  592. }
  593. );
  594. // Insert the board templates swimlane
  595. Swimlanes.insert(
  596. {
  597. title: TAPi18n.__('board-templates-swimlane'),
  598. boardId,
  599. sort: 3,
  600. type: 'template-container',
  601. },
  602. (err, swimlaneId) => {
  603. // Insert the reference to out board templates swimlane
  604. Users.update(user._id, {
  605. $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
  606. });
  607. }
  608. );
  609. }
  610. );
  611. });
  612. });
  613. Migrations.add('fix-circular-reference_', () => {
  614. Cards.find().forEach((card) => {
  615. if (card.parentId === card._id) {
  616. Cards.update(card._id, { $set: { parentId: '' } }, noValidateMulti);
  617. }
  618. });
  619. });
  620. Migrations.add('mutate-boardIds-in-customfields', () => {
  621. CustomFields.find().forEach((cf) => {
  622. CustomFields.update(
  623. cf,
  624. {
  625. $set: {
  626. boardIds: [cf.boardId],
  627. },
  628. $unset: {
  629. boardId: '',
  630. },
  631. },
  632. noValidateMulti
  633. );
  634. });
  635. });
  636. const firstBatchOfDbsToAddCreatedAndUpdated = [
  637. AccountSettings,
  638. Actions,
  639. Activities,
  640. Announcements,
  641. Boards,
  642. CardComments,
  643. Cards,
  644. ChecklistItems,
  645. Checklists,
  646. CustomFields,
  647. Integrations,
  648. InvitationCodes,
  649. Lists,
  650. Rules,
  651. Settings,
  652. Swimlanes,
  653. Triggers,
  654. UnsavedEdits,
  655. ];
  656. firstBatchOfDbsToAddCreatedAndUpdated.forEach((db) => {
  657. db.before.insert((userId, doc) => {
  658. doc.createdAt = Date.now();
  659. doc.updatedAt = doc.createdAt;
  660. });
  661. db.before.update((userId, doc, fieldNames, modifier, options) => {
  662. modifier.$set = modifier.$set || {};
  663. modifier.$set.updatedAt = new Date();
  664. });
  665. });
  666. const modifiedAtTables = [
  667. AccountSettings,
  668. Actions,
  669. Activities,
  670. Announcements,
  671. Boards,
  672. CardComments,
  673. Cards,
  674. ChecklistItems,
  675. Checklists,
  676. CustomFields,
  677. Integrations,
  678. InvitationCodes,
  679. Lists,
  680. Rules,
  681. Settings,
  682. Swimlanes,
  683. Triggers,
  684. UnsavedEdits,
  685. Users,
  686. ];
  687. Migrations.add('add-missing-created-and-modified', () => {
  688. Promise.all(
  689. modifiedAtTables.map((db) =>
  690. db
  691. .rawCollection()
  692. .update(
  693. { modifiedAt: { $exists: false } },
  694. { $set: { modifiedAt: new Date() } },
  695. { multi: true }
  696. )
  697. .then(() =>
  698. db
  699. .rawCollection()
  700. .update(
  701. { createdAt: { $exists: false } },
  702. { $set: { createdAt: new Date() } },
  703. { multi: true }
  704. )
  705. )
  706. )
  707. )
  708. .then(() => {
  709. // eslint-disable-next-line no-console
  710. console.info('Successfully added createdAt and updatedAt to all tables');
  711. })
  712. .catch((e) => {
  713. // eslint-disable-next-line no-console
  714. console.error(e);
  715. });
  716. });