fixMissingListsMigration.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /**
  2. * Fix Missing Lists Migration
  3. *
  4. * This migration fixes the issue where cards have incorrect listId references
  5. * due to the per-swimlane lists change. It detects cards with mismatched
  6. * listId/swimlaneId and creates the missing lists.
  7. *
  8. * Issue: When upgrading from v7.94 to v8.02, cards that were in different
  9. * swimlanes but shared the same list now have wrong listId references.
  10. *
  11. * Example:
  12. * - Card1: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'sK69SseWkh3tMbJvg'
  13. * - Card2: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'XeecF9nZxGph4zcT4'
  14. *
  15. * Card2 should have a different listId that corresponds to its swimlane.
  16. */
  17. import { Meteor } from 'meteor/meteor';
  18. import { check } from 'meteor/check';
  19. import { ReactiveCache } from '/imports/reactiveCache';
  20. class FixMissingListsMigration {
  21. constructor() {
  22. this.name = 'fix-missing-lists';
  23. this.version = 1;
  24. }
  25. /**
  26. * Check if migration is needed for a board
  27. */
  28. needsMigration(boardId) {
  29. try {
  30. const board = ReactiveCache.getBoard(boardId);
  31. if (!board) return false;
  32. // Check if board has already been processed
  33. if (board.fixMissingListsCompleted) {
  34. return false;
  35. }
  36. // Check if there are cards with mismatched listId/swimlaneId
  37. const cards = ReactiveCache.getCards({ boardId });
  38. const lists = ReactiveCache.getLists({ boardId });
  39. // Create a map of listId -> swimlaneId for existing lists
  40. const listSwimlaneMap = new Map();
  41. lists.forEach(list => {
  42. listSwimlaneMap.set(list._id, list.swimlaneId || '');
  43. });
  44. // Check for cards with mismatched listId/swimlaneId
  45. for (const card of cards) {
  46. const expectedSwimlaneId = listSwimlaneMap.get(card.listId);
  47. if (expectedSwimlaneId && expectedSwimlaneId !== card.swimlaneId) {
  48. console.log(`Found mismatched card: ${card._id}, listId: ${card.listId}, card swimlaneId: ${card.swimlaneId}, list swimlaneId: ${expectedSwimlaneId}`);
  49. return true;
  50. }
  51. }
  52. return false;
  53. } catch (error) {
  54. console.error('Error checking if migration is needed:', error);
  55. return false;
  56. }
  57. }
  58. /**
  59. * Execute the migration for a board
  60. */
  61. async executeMigration(boardId) {
  62. try {
  63. console.log(`Starting fix missing lists migration for board ${boardId}`);
  64. const board = ReactiveCache.getBoard(boardId);
  65. if (!board) {
  66. throw new Error(`Board ${boardId} not found`);
  67. }
  68. const cards = ReactiveCache.getCards({ boardId });
  69. const lists = ReactiveCache.getLists({ boardId });
  70. const swimlanes = ReactiveCache.getSwimlanes({ boardId });
  71. // Create maps for efficient lookup
  72. const listSwimlaneMap = new Map();
  73. const swimlaneListsMap = new Map();
  74. lists.forEach(list => {
  75. listSwimlaneMap.set(list._id, list.swimlaneId || '');
  76. if (!swimlaneListsMap.has(list.swimlaneId || '')) {
  77. swimlaneListsMap.set(list.swimlaneId || '', []);
  78. }
  79. swimlaneListsMap.get(list.swimlaneId || '').push(list);
  80. });
  81. // Group cards by swimlaneId
  82. const cardsBySwimlane = new Map();
  83. cards.forEach(card => {
  84. if (!cardsBySwimlane.has(card.swimlaneId)) {
  85. cardsBySwimlane.set(card.swimlaneId, []);
  86. }
  87. cardsBySwimlane.get(card.swimlaneId).push(card);
  88. });
  89. let createdLists = 0;
  90. let updatedCards = 0;
  91. // Process each swimlane
  92. for (const [swimlaneId, swimlaneCards] of cardsBySwimlane) {
  93. if (!swimlaneId) continue;
  94. // Get existing lists for this swimlane
  95. const existingLists = swimlaneListsMap.get(swimlaneId) || [];
  96. const existingListTitles = new Set(existingLists.map(list => list.title));
  97. // Group cards by their current listId
  98. const cardsByListId = new Map();
  99. swimlaneCards.forEach(card => {
  100. if (!cardsByListId.has(card.listId)) {
  101. cardsByListId.set(card.listId, []);
  102. }
  103. cardsByListId.get(card.listId).push(card);
  104. });
  105. // For each listId used by cards in this swimlane
  106. for (const [listId, cardsInList] of cardsByListId) {
  107. const originalList = lists.find(l => l._id === listId);
  108. if (!originalList) continue;
  109. // Check if this list's swimlaneId matches the card's swimlaneId
  110. const listSwimlaneId = listSwimlaneMap.get(listId);
  111. if (listSwimlaneId === swimlaneId) {
  112. // List is already correctly assigned to this swimlane
  113. continue;
  114. }
  115. // Check if we already have a list with the same title in this swimlane
  116. let targetList = existingLists.find(list => list.title === originalList.title);
  117. if (!targetList) {
  118. // Create a new list for this swimlane
  119. const newListData = {
  120. title: originalList.title,
  121. boardId: boardId,
  122. swimlaneId: swimlaneId,
  123. sort: originalList.sort || 0,
  124. archived: originalList.archived || false,
  125. createdAt: new Date(),
  126. modifiedAt: new Date(),
  127. type: originalList.type || 'list'
  128. };
  129. // Copy other properties if they exist
  130. if (originalList.color) newListData.color = originalList.color;
  131. if (originalList.wipLimit) newListData.wipLimit = originalList.wipLimit;
  132. if (originalList.wipLimitEnabled) newListData.wipLimitEnabled = originalList.wipLimitEnabled;
  133. if (originalList.wipLimitSoft) newListData.wipLimitSoft = originalList.wipLimitSoft;
  134. if (originalList.starred) newListData.starred = originalList.starred;
  135. if (originalList.collapsed) newListData.collapsed = originalList.collapsed;
  136. // Insert the new list
  137. const newListId = ReactiveCache.getCollection('lists').insert(newListData);
  138. targetList = { _id: newListId, ...newListData };
  139. createdLists++;
  140. console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
  141. }
  142. // Update all cards in this group to use the correct listId
  143. for (const card of cardsInList) {
  144. ReactiveCache.getCollection('cards').update(card._id, {
  145. $set: {
  146. listId: targetList._id,
  147. modifiedAt: new Date()
  148. }
  149. });
  150. updatedCards++;
  151. }
  152. }
  153. }
  154. // Mark board as processed
  155. ReactiveCache.getCollection('boards').update(boardId, {
  156. $set: {
  157. fixMissingListsCompleted: true,
  158. fixMissingListsCompletedAt: new Date()
  159. }
  160. });
  161. console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
  162. return {
  163. success: true,
  164. createdLists,
  165. updatedCards
  166. };
  167. } catch (error) {
  168. console.error(`Error executing fix missing lists migration for board ${boardId}:`, error);
  169. throw error;
  170. }
  171. }
  172. /**
  173. * Get migration status for a board
  174. */
  175. getMigrationStatus(boardId) {
  176. try {
  177. const board = ReactiveCache.getBoard(boardId);
  178. if (!board) {
  179. return { status: 'board_not_found' };
  180. }
  181. if (board.fixMissingListsCompleted) {
  182. return {
  183. status: 'completed',
  184. completedAt: board.fixMissingListsCompletedAt
  185. };
  186. }
  187. const needsMigration = this.needsMigration(boardId);
  188. return {
  189. status: needsMigration ? 'needed' : 'not_needed'
  190. };
  191. } catch (error) {
  192. console.error('Error getting migration status:', error);
  193. return { status: 'error', error: error.message };
  194. }
  195. }
  196. }
  197. // Export singleton instance
  198. export const fixMissingListsMigration = new FixMissingListsMigration();
  199. // Meteor methods
  200. Meteor.methods({
  201. 'fixMissingListsMigration.check'(boardId) {
  202. check(boardId, String);
  203. if (!this.userId) {
  204. throw new Meteor.Error('not-authorized');
  205. }
  206. return fixMissingListsMigration.getMigrationStatus(boardId);
  207. },
  208. 'fixMissingListsMigration.execute'(boardId) {
  209. check(boardId, String);
  210. if (!this.userId) {
  211. throw new Meteor.Error('not-authorized');
  212. }
  213. return fixMissingListsMigration.executeMigration(boardId);
  214. },
  215. 'fixMissingListsMigration.needsMigration'(boardId) {
  216. check(boardId, String);
  217. if (!this.userId) {
  218. throw new Meteor.Error('not-authorized');
  219. }
  220. return fixMissingListsMigration.needsMigration(boardId);
  221. }
  222. });