fixMissingListsMigration.js 8.7 KB

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