migrationManager.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. /**
  2. * Migration Manager
  3. * Handles all database migrations as steps during board loading
  4. * with detailed progress tracking and background persistence
  5. */
  6. import { ReactiveVar } from 'meteor/reactive-var';
  7. import { ReactiveCache } from '/imports/reactiveCache';
  8. // Reactive variables for migration progress
  9. export const migrationProgress = new ReactiveVar(0);
  10. export const migrationStatus = new ReactiveVar('');
  11. export const migrationCurrentStep = new ReactiveVar('');
  12. export const migrationSteps = new ReactiveVar([]);
  13. export const isMigrating = new ReactiveVar(false);
  14. export const migrationEstimatedTime = new ReactiveVar('');
  15. class MigrationManager {
  16. constructor() {
  17. this.migrationCache = new Map(); // Cache completed migrations
  18. this.steps = this.initializeMigrationSteps();
  19. this.currentStepIndex = 0;
  20. this.startTime = null;
  21. }
  22. /**
  23. * Initialize all migration steps with their details
  24. */
  25. initializeMigrationSteps() {
  26. return [
  27. {
  28. id: 'board-background-color',
  29. name: 'Board Background Colors',
  30. description: 'Setting up board background colors',
  31. weight: 1,
  32. completed: false,
  33. progress: 0
  34. },
  35. {
  36. id: 'add-cardcounterlist-allowed',
  37. name: 'Card Counter List Settings',
  38. description: 'Adding card counter list permissions',
  39. weight: 1,
  40. completed: false,
  41. progress: 0
  42. },
  43. {
  44. id: 'add-boardmemberlist-allowed',
  45. name: 'Board Member List Settings',
  46. description: 'Adding board member list permissions',
  47. weight: 1,
  48. completed: false,
  49. progress: 0
  50. },
  51. {
  52. id: 'lowercase-board-permission',
  53. name: 'Board Permission Standardization',
  54. description: 'Converting board permissions to lowercase',
  55. weight: 1,
  56. completed: false,
  57. progress: 0
  58. },
  59. {
  60. id: 'change-attachments-type-for-non-images',
  61. name: 'Attachment Type Standardization',
  62. description: 'Updating attachment types for non-images',
  63. weight: 2,
  64. completed: false,
  65. progress: 0
  66. },
  67. {
  68. id: 'card-covers',
  69. name: 'Card Covers System',
  70. description: 'Setting up card cover functionality',
  71. weight: 2,
  72. completed: false,
  73. progress: 0
  74. },
  75. {
  76. id: 'use-css-class-for-boards-colors',
  77. name: 'Board Color CSS Classes',
  78. description: 'Converting board colors to CSS classes',
  79. weight: 2,
  80. completed: false,
  81. progress: 0
  82. },
  83. {
  84. id: 'denormalize-star-number-per-board',
  85. name: 'Board Star Counts',
  86. description: 'Calculating star counts per board',
  87. weight: 3,
  88. completed: false,
  89. progress: 0
  90. },
  91. {
  92. id: 'add-member-isactive-field',
  93. name: 'Member Activity Status',
  94. description: 'Adding member activity tracking',
  95. weight: 2,
  96. completed: false,
  97. progress: 0
  98. },
  99. {
  100. id: 'add-sort-checklists',
  101. name: 'Checklist Sorting',
  102. description: 'Adding sort order to checklists',
  103. weight: 2,
  104. completed: false,
  105. progress: 0
  106. },
  107. {
  108. id: 'add-swimlanes',
  109. name: 'Swimlanes System',
  110. description: 'Setting up swimlanes functionality',
  111. weight: 4,
  112. completed: false,
  113. progress: 0
  114. },
  115. {
  116. id: 'add-views',
  117. name: 'Board Views',
  118. description: 'Adding board view options',
  119. weight: 2,
  120. completed: false,
  121. progress: 0
  122. },
  123. {
  124. id: 'add-checklist-items',
  125. name: 'Checklist Items',
  126. description: 'Setting up checklist items system',
  127. weight: 3,
  128. completed: false,
  129. progress: 0
  130. },
  131. {
  132. id: 'add-card-types',
  133. name: 'Card Types',
  134. description: 'Adding card type functionality',
  135. weight: 2,
  136. completed: false,
  137. progress: 0
  138. },
  139. {
  140. id: 'add-custom-fields-to-cards',
  141. name: 'Custom Fields',
  142. description: 'Adding custom fields to cards',
  143. weight: 3,
  144. completed: false,
  145. progress: 0
  146. },
  147. {
  148. id: 'add-requester-field',
  149. name: 'Requester Field',
  150. description: 'Adding requester field to cards',
  151. weight: 1,
  152. completed: false,
  153. progress: 0
  154. },
  155. {
  156. id: 'add-assigner-field',
  157. name: 'Assigner Field',
  158. description: 'Adding assigner field to cards',
  159. weight: 1,
  160. completed: false,
  161. progress: 0
  162. },
  163. {
  164. id: 'add-parent-field-to-cards',
  165. name: 'Card Parent Relationships',
  166. description: 'Adding parent field to cards',
  167. weight: 2,
  168. completed: false,
  169. progress: 0
  170. },
  171. {
  172. id: 'add-subtasks-boards',
  173. name: 'Subtasks Boards',
  174. description: 'Setting up subtasks board functionality',
  175. weight: 3,
  176. completed: false,
  177. progress: 0
  178. },
  179. {
  180. id: 'add-subtasks-sort',
  181. name: 'Subtasks Sorting',
  182. description: 'Adding sort order to subtasks',
  183. weight: 2,
  184. completed: false,
  185. progress: 0
  186. },
  187. {
  188. id: 'add-subtasks-allowed',
  189. name: 'Subtasks Permissions',
  190. description: 'Adding subtasks permissions',
  191. weight: 1,
  192. completed: false,
  193. progress: 0
  194. },
  195. {
  196. id: 'add-authenticationMethod',
  197. name: 'Authentication Methods',
  198. description: 'Adding authentication method tracking',
  199. weight: 2,
  200. completed: false,
  201. progress: 0
  202. },
  203. {
  204. id: 'remove-tag',
  205. name: 'Remove Tag Field',
  206. description: 'Removing deprecated tag field',
  207. weight: 1,
  208. completed: false,
  209. progress: 0
  210. },
  211. {
  212. id: 'remove-customFields-references-broken',
  213. name: 'Fix Custom Fields References',
  214. description: 'Fixing broken custom field references',
  215. weight: 2,
  216. completed: false,
  217. progress: 0
  218. },
  219. {
  220. id: 'add-product-name',
  221. name: 'Product Name Settings',
  222. description: 'Adding product name configuration',
  223. weight: 1,
  224. completed: false,
  225. progress: 0
  226. },
  227. {
  228. id: 'add-hide-logo',
  229. name: 'Hide Logo Setting',
  230. description: 'Adding hide logo option',
  231. weight: 1,
  232. completed: false,
  233. progress: 0
  234. },
  235. {
  236. id: 'add-hide-card-counter-list',
  237. name: 'Hide Card Counter Setting',
  238. description: 'Adding hide card counter option',
  239. weight: 1,
  240. completed: false,
  241. progress: 0
  242. },
  243. {
  244. id: 'add-hide-board-member-list',
  245. name: 'Hide Board Member List Setting',
  246. description: 'Adding hide board member list option',
  247. weight: 1,
  248. completed: false,
  249. progress: 0
  250. },
  251. {
  252. id: 'add-displayAuthenticationMethod',
  253. name: 'Display Authentication Method',
  254. description: 'Adding authentication method display option',
  255. weight: 1,
  256. completed: false,
  257. progress: 0
  258. },
  259. {
  260. id: 'add-defaultAuthenticationMethod',
  261. name: 'Default Authentication Method',
  262. description: 'Setting default authentication method',
  263. weight: 1,
  264. completed: false,
  265. progress: 0
  266. },
  267. {
  268. id: 'add-templates',
  269. name: 'Board Templates',
  270. description: 'Setting up board templates system',
  271. weight: 3,
  272. completed: false,
  273. progress: 0
  274. },
  275. {
  276. id: 'fix-circular-reference_',
  277. name: 'Fix Circular References',
  278. description: 'Fixing circular references in cards',
  279. weight: 2,
  280. completed: false,
  281. progress: 0
  282. },
  283. {
  284. id: 'mutate-boardIds-in-customfields',
  285. name: 'Custom Fields Board IDs',
  286. description: 'Updating board IDs in custom fields',
  287. weight: 2,
  288. completed: false,
  289. progress: 0
  290. },
  291. {
  292. id: 'add-missing-created-and-modified',
  293. name: 'Missing Timestamps',
  294. description: 'Adding missing created and modified timestamps',
  295. weight: 4,
  296. completed: false,
  297. progress: 0
  298. },
  299. {
  300. id: 'fix-incorrect-dates',
  301. name: 'Fix Incorrect Dates',
  302. description: 'Correcting incorrect date values',
  303. weight: 3,
  304. completed: false,
  305. progress: 0
  306. },
  307. {
  308. id: 'add-assignee',
  309. name: 'Assignee Field',
  310. description: 'Adding assignee field to cards',
  311. weight: 1,
  312. completed: false,
  313. progress: 0
  314. },
  315. {
  316. id: 'add-profile-showDesktopDragHandles',
  317. name: 'Desktop Drag Handles',
  318. description: 'Adding desktop drag handles preference',
  319. weight: 1,
  320. completed: false,
  321. progress: 0
  322. },
  323. {
  324. id: 'add-profile-hiddenMinicardLabelText',
  325. name: 'Hidden Minicard Labels',
  326. description: 'Adding hidden minicard label text preference',
  327. weight: 1,
  328. completed: false,
  329. progress: 0
  330. },
  331. {
  332. id: 'add-receiveddate-allowed',
  333. name: 'Received Date Permissions',
  334. description: 'Adding received date permissions',
  335. weight: 1,
  336. completed: false,
  337. progress: 0
  338. },
  339. {
  340. id: 'add-startdate-allowed',
  341. name: 'Start Date Permissions',
  342. description: 'Adding start date permissions',
  343. weight: 1,
  344. completed: false,
  345. progress: 0
  346. },
  347. {
  348. id: 'add-duedate-allowed',
  349. name: 'Due Date Permissions',
  350. description: 'Adding due date permissions',
  351. weight: 1,
  352. completed: false,
  353. progress: 0
  354. },
  355. {
  356. id: 'add-enddate-allowed',
  357. name: 'End Date Permissions',
  358. description: 'Adding end date permissions',
  359. weight: 1,
  360. completed: false,
  361. progress: 0
  362. },
  363. {
  364. id: 'add-members-allowed',
  365. name: 'Members Permissions',
  366. description: 'Adding members permissions',
  367. weight: 1,
  368. completed: false,
  369. progress: 0
  370. },
  371. {
  372. id: 'add-assignee-allowed',
  373. name: 'Assignee Permissions',
  374. description: 'Adding assignee permissions',
  375. weight: 1,
  376. completed: false,
  377. progress: 0
  378. },
  379. {
  380. id: 'add-labels-allowed',
  381. name: 'Labels Permissions',
  382. description: 'Adding labels permissions',
  383. weight: 1,
  384. completed: false,
  385. progress: 0
  386. },
  387. {
  388. id: 'add-checklists-allowed',
  389. name: 'Checklists Permissions',
  390. description: 'Adding checklists permissions',
  391. weight: 1,
  392. completed: false,
  393. progress: 0
  394. },
  395. {
  396. id: 'add-attachments-allowed',
  397. name: 'Attachments Permissions',
  398. description: 'Adding attachments permissions',
  399. weight: 1,
  400. completed: false,
  401. progress: 0
  402. },
  403. {
  404. id: 'add-comments-allowed',
  405. name: 'Comments Permissions',
  406. description: 'Adding comments permissions',
  407. weight: 1,
  408. completed: false,
  409. progress: 0
  410. },
  411. {
  412. id: 'add-assigned-by-allowed',
  413. name: 'Assigned By Permissions',
  414. description: 'Adding assigned by permissions',
  415. weight: 1,
  416. completed: false,
  417. progress: 0
  418. },
  419. {
  420. id: 'add-requested-by-allowed',
  421. name: 'Requested By Permissions',
  422. description: 'Adding requested by permissions',
  423. weight: 1,
  424. completed: false,
  425. progress: 0
  426. },
  427. {
  428. id: 'add-activities-allowed',
  429. name: 'Activities Permissions',
  430. description: 'Adding activities permissions',
  431. weight: 1,
  432. completed: false,
  433. progress: 0
  434. },
  435. {
  436. id: 'add-description-title-allowed',
  437. name: 'Description Title Permissions',
  438. description: 'Adding description title permissions',
  439. weight: 1,
  440. completed: false,
  441. progress: 0
  442. },
  443. {
  444. id: 'add-description-text-allowed',
  445. name: 'Description Text Permissions',
  446. description: 'Adding description text permissions',
  447. weight: 1,
  448. completed: false,
  449. progress: 0
  450. },
  451. {
  452. id: 'add-description-text-allowed-on-minicard',
  453. name: 'Minicard Description Permissions',
  454. description: 'Adding minicard description permissions',
  455. weight: 1,
  456. completed: false,
  457. progress: 0
  458. },
  459. {
  460. id: 'add-sort-field-to-boards',
  461. name: 'Board Sort Field',
  462. description: 'Adding sort field to boards',
  463. weight: 2,
  464. completed: false,
  465. progress: 0
  466. },
  467. {
  468. id: 'add-default-profile-view',
  469. name: 'Default Profile View',
  470. description: 'Setting default profile view',
  471. weight: 1,
  472. completed: false,
  473. progress: 0
  474. },
  475. {
  476. id: 'add-hide-logo-by-default',
  477. name: 'Hide Logo Default',
  478. description: 'Setting hide logo as default',
  479. weight: 1,
  480. completed: false,
  481. progress: 0
  482. },
  483. {
  484. id: 'add-hide-card-counter-list-by-default',
  485. name: 'Hide Card Counter Default',
  486. description: 'Setting hide card counter as default',
  487. weight: 1,
  488. completed: false,
  489. progress: 0
  490. },
  491. {
  492. id: 'add-hide-board-member-list-by-default',
  493. name: 'Hide Board Member List Default',
  494. description: 'Setting hide board member list as default',
  495. weight: 1,
  496. completed: false,
  497. progress: 0
  498. },
  499. {
  500. id: 'add-card-number-allowed',
  501. name: 'Card Number Permissions',
  502. description: 'Adding card number permissions',
  503. weight: 1,
  504. completed: false,
  505. progress: 0
  506. },
  507. {
  508. id: 'assign-boardwise-card-numbers',
  509. name: 'Board Card Numbers',
  510. description: 'Assigning board-wise card numbers',
  511. weight: 3,
  512. completed: false,
  513. progress: 0
  514. },
  515. {
  516. id: 'add-card-details-show-lists',
  517. name: 'Card Details Show Lists',
  518. description: 'Adding card details show lists option',
  519. weight: 1,
  520. completed: false,
  521. progress: 0
  522. },
  523. {
  524. id: 'migrate-attachments-collectionFS-to-ostrioFiles',
  525. name: 'Migrate Attachments to Meteor-Files',
  526. description: 'Migrating attachments from CollectionFS to Meteor-Files',
  527. weight: 8,
  528. completed: false,
  529. progress: 0
  530. },
  531. {
  532. id: 'migrate-avatars-collectionFS-to-ostrioFiles',
  533. name: 'Migrate Avatars to Meteor-Files',
  534. description: 'Migrating avatars from CollectionFS to Meteor-Files',
  535. weight: 6,
  536. completed: false,
  537. progress: 0
  538. },
  539. {
  540. id: 'migrate-attachment-drop-index-cardId',
  541. name: 'Drop Attachment Index',
  542. description: 'Dropping old attachment index',
  543. weight: 1,
  544. completed: false,
  545. progress: 0
  546. },
  547. {
  548. id: 'migrate-attachment-migration-fix-source-import',
  549. name: 'Fix Attachment Source Import',
  550. description: 'Fixing attachment source import field',
  551. weight: 2,
  552. completed: false,
  553. progress: 0
  554. },
  555. {
  556. id: 'attachment-cardCopy-fix-boardId-etc',
  557. name: 'Fix Attachment Card Copy',
  558. description: 'Fixing attachment card copy board IDs',
  559. weight: 2,
  560. completed: false,
  561. progress: 0
  562. },
  563. {
  564. id: 'remove-unused-planning-poker',
  565. name: 'Remove Planning Poker',
  566. description: 'Removing unused planning poker fields',
  567. weight: 1,
  568. completed: false,
  569. progress: 0
  570. },
  571. {
  572. id: 'remove-user-profile-hiddenSystemMessages',
  573. name: 'Remove Hidden System Messages',
  574. description: 'Removing hidden system messages field',
  575. weight: 1,
  576. completed: false,
  577. progress: 0
  578. },
  579. {
  580. id: 'remove-user-profile-hideCheckedItems',
  581. name: 'Remove Hide Checked Items',
  582. description: 'Removing hide checked items field',
  583. weight: 1,
  584. completed: false,
  585. progress: 0
  586. },
  587. {
  588. id: 'migrate-lists-to-per-swimlane',
  589. name: 'Migrate Lists to Per-Swimlane',
  590. description: 'Migrating lists to per-swimlane structure',
  591. weight: 5,
  592. completed: false,
  593. progress: 0
  594. }
  595. ];
  596. }
  597. /**
  598. * Check if any migrations need to be run for a specific board
  599. */
  600. needsMigration(boardId = null) {
  601. if (boardId) {
  602. // Check if specific board needs migration based on version
  603. const board = ReactiveCache.getBoard(boardId);
  604. return !board || !board.migrationVersion || board.migrationVersion < 1;
  605. }
  606. // Check if any migration step is not completed (global migrations)
  607. return this.steps.some(step => !step.completed);
  608. }
  609. /**
  610. * Get total weight of all migrations
  611. */
  612. getTotalWeight() {
  613. return this.steps.reduce((total, step) => total + step.weight, 0);
  614. }
  615. /**
  616. * Get completed weight
  617. */
  618. getCompletedWeight() {
  619. return this.steps.reduce((total, step) => {
  620. return total + (step.completed ? step.weight : step.progress * step.weight / 100);
  621. }, 0);
  622. }
  623. /**
  624. * Mark a board as migrated
  625. */
  626. markBoardAsMigrated(boardId) {
  627. try {
  628. Meteor.call('boardMigration.markAsMigrated', boardId, 'full_board_migration', (error, result) => {
  629. if (error) {
  630. console.error('Failed to mark board as migrated:', error);
  631. } else {
  632. console.log('Board marked as migrated:', boardId);
  633. }
  634. });
  635. } catch (error) {
  636. console.error('Error marking board as migrated:', error);
  637. }
  638. }
  639. /**
  640. * Start migration process using cron system
  641. */
  642. async startMigration() {
  643. if (isMigrating.get()) {
  644. return; // Already migrating
  645. }
  646. isMigrating.set(true);
  647. migrationSteps.set([...this.steps]);
  648. this.startTime = Date.now();
  649. try {
  650. // Start server-side cron migrations
  651. Meteor.call('cron.startAllMigrations', (error, result) => {
  652. if (error) {
  653. console.error('Failed to start cron migrations:', error);
  654. migrationStatus.set(`Migration failed: ${error.message}`);
  655. isMigrating.set(false);
  656. }
  657. });
  658. // Poll for progress updates
  659. this.pollCronMigrationProgress();
  660. } catch (error) {
  661. console.error('Migration failed:', error);
  662. migrationStatus.set(`Migration failed: ${error.message}`);
  663. isMigrating.set(false);
  664. }
  665. }
  666. /**
  667. * Poll for cron migration progress updates
  668. */
  669. pollCronMigrationProgress() {
  670. const pollInterval = setInterval(() => {
  671. Meteor.call('cron.getMigrationProgress', (error, result) => {
  672. if (error) {
  673. console.error('Failed to get cron migration progress:', error);
  674. clearInterval(pollInterval);
  675. return;
  676. }
  677. if (result) {
  678. migrationProgress.set(result.progress);
  679. migrationStatus.set(result.status);
  680. migrationCurrentStep.set(result.currentStep);
  681. migrationSteps.set(result.steps);
  682. isMigrating.set(result.isMigrating);
  683. // Update local steps
  684. if (result.steps) {
  685. this.steps = result.steps;
  686. }
  687. // If migration is complete, stop polling
  688. if (!result.isMigrating && result.progress === 100) {
  689. clearInterval(pollInterval);
  690. // Clear status after delay
  691. setTimeout(() => {
  692. migrationStatus.set('');
  693. migrationProgress.set(0);
  694. migrationEstimatedTime.set('');
  695. }, 3000);
  696. }
  697. }
  698. });
  699. }, 1000); // Poll every second
  700. }
  701. /**
  702. * Run a single migration step
  703. */
  704. async runMigrationStep(step) {
  705. // Simulate migration progress
  706. const steps = 10;
  707. for (let i = 0; i <= steps; i++) {
  708. step.progress = (i / steps) * 100;
  709. this.updateProgress();
  710. // Simulate work
  711. await new Promise(resolve => setTimeout(resolve, 50));
  712. }
  713. // In a real implementation, this would call the actual migration
  714. // For now, we'll simulate the migration
  715. // Running migration step
  716. }
  717. /**
  718. * Update progress variables
  719. */
  720. updateProgress() {
  721. const totalWeight = this.getTotalWeight();
  722. const completedWeight = this.getCompletedWeight();
  723. const progress = Math.round((completedWeight / totalWeight) * 100);
  724. migrationProgress.set(progress);
  725. migrationSteps.set([...this.steps]);
  726. // Calculate estimated time remaining
  727. if (this.startTime && progress > 0) {
  728. const elapsed = Date.now() - this.startTime;
  729. const rate = progress / elapsed; // progress per millisecond
  730. const remaining = 100 - progress;
  731. const estimatedMs = remaining / rate;
  732. migrationEstimatedTime.set(this.formatTime(estimatedMs));
  733. }
  734. }
  735. /**
  736. * Format time in milliseconds to human readable format
  737. */
  738. formatTime(ms) {
  739. if (ms < 1000) {
  740. return `${Math.round(ms)}ms`;
  741. }
  742. const seconds = Math.floor(ms / 1000);
  743. const minutes = Math.floor(seconds / 60);
  744. const hours = Math.floor(minutes / 60);
  745. if (hours > 0) {
  746. const remainingMinutes = minutes % 60;
  747. const remainingSeconds = seconds % 60;
  748. return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
  749. } else if (minutes > 0) {
  750. const remainingSeconds = seconds % 60;
  751. return `${minutes}m ${remainingSeconds}s`;
  752. } else {
  753. return `${seconds}s`;
  754. }
  755. }
  756. /**
  757. * Clear migration cache (for testing)
  758. */
  759. clearCache() {
  760. this.migrationCache.clear();
  761. this.steps.forEach(step => {
  762. step.completed = false;
  763. step.progress = 0;
  764. });
  765. }
  766. }
  767. // Export singleton instance
  768. export const migrationManager = new MigrationManager();