fixMissingListsMigration.js 8.5 KB

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