import * as plugins from '../ts/plugins.js'; import { assertControllerSettingsDocument, ControllerSessionGroupsModel, ControllerSettingsModel, } from '../ts/classes.authmodels.js'; import { AuthError } from '../ts/interfaces.auth.js'; import { assertLegacySessionLayoutDocument, isV30ItemRefLayoutDocument, } from './v30_sessionlayoutitemrefs.js'; import type { IControllerSessionGroupsDocument, IControllerSettingsDocument, } from '../ts/interfaces.projects.js'; import { migrateProtocolV6SessionGroupsDocument, migrateProtocolV6SettingsDocument, } from './v6_protocoldocuments.js'; const migrationPageLimit = 128; const persistedBodyFromRawDocument = ( valueArg: Record, ): Record => { const body = { ...valueArg }; delete body._id; delete body._smartdataRevision; return body; }; export class ProtocolV6DocumentMigration { constructor(private readonly database: plugins.smartdata.SmartdataDb | undefined) {} /** Runs before store readiness and is safe to repeat after partial or concurrent completion. */ public async run(): Promise { if (!this.database) { throw new AuthError('not_initialized', 'The authentication store database is unavailable.'); } const settingsCollection = ControllerSettingsModel.collection.mongoDbCollection; let lastSettingsId: plugins.smartdata.TStoredDocument['_id'] | undefined; while (true) { const cursor = settingsCollection.find( lastSettingsId ? { _id: { $gt: lastSettingsId } } : {}, ).sort({ _id: 1 }).limit(migrationPageLimit); let settingsDocuments: Awaited>; try { settingsDocuments = await cursor.toArray(); } finally { await cursor.close(); } for (const rawDocument of settingsDocuments) { const migration = migrateProtocolV6SettingsDocument( persistedBodyFromRawDocument(rawDocument), ); assertControllerSettingsDocument(migration.document); if (!migration.migrated) continue; const revision = plugins.crypto.randomUUID(); const selector = rawDocument._smartdataRevision === undefined ? { _id: rawDocument._id, _smartdataRevision: { $exists: false } } : { _id: rawDocument._id, _smartdataRevision: rawDocument._smartdataRevision }; const replaced = await settingsCollection.findOneAndReplace( selector, { _id: rawDocument._id, ...migration.document, _smartdataRevision: revision }, { returnDocument: 'after', includeResultMetadata: false, upsert: false }, ); if (!replaced) { const concurrent = await settingsCollection.findOne({ _id: rawDocument._id }); if (!concurrent) { throw new AuthError( 'concurrent_change', 'The settings protocol migration changed concurrently.', ); } const concurrentMigration = migrateProtocolV6SettingsDocument( persistedBodyFromRawDocument(concurrent), ); assertControllerSettingsDocument(concurrentMigration.document); if (concurrentMigration.migrated) { throw new AuthError( 'concurrent_change', 'The settings protocol migration changed concurrently.', ); } } } const lastDocument = settingsDocuments.at(-1); if (!lastDocument || settingsDocuments.length < migrationPageLimit) break; lastSettingsId = lastDocument._id; } const layoutsCollection = ControllerSessionGroupsModel.collection.mongoDbCollection; let lastLayoutId: plugins.smartdata.TStoredDocument['_id'] | undefined; while (true) { const cursor = layoutsCollection.find( lastLayoutId ? { _id: { $gt: lastLayoutId } } : {}, ).sort({ _id: 1 }).limit(migrationPageLimit); let layoutDocuments: Awaited>; try { layoutDocuments = await cursor.toArray(); } finally { await cursor.close(); } for (const rawDocument of layoutDocuments) { const body = persistedBodyFromRawDocument(rawDocument); // Already lifted to item refs by the v30 migration on an earlier start. if (isV30ItemRefLayoutDocument(body)) continue; const migration = migrateProtocolV6SessionGroupsDocument(body); assertLegacySessionLayoutDocument(migration.document); if (!migration.migrated) continue; const revision = plugins.crypto.randomUUID(); const selector = rawDocument._smartdataRevision === undefined ? { _id: rawDocument._id, _smartdataRevision: { $exists: false } } : { _id: rawDocument._id, _smartdataRevision: rawDocument._smartdataRevision }; const replaced = await layoutsCollection.findOneAndReplace( selector, { _id: rawDocument._id, ...migration.document, _smartdataRevision: revision }, { returnDocument: 'after', includeResultMetadata: false, upsert: false }, ); if (!replaced) { const concurrent = await layoutsCollection.findOne({ _id: rawDocument._id }); if (!concurrent) { throw new AuthError( 'concurrent_change', 'The session layout protocol migration changed concurrently.', ); } const concurrentBody = persistedBodyFromRawDocument(concurrent); if (isV30ItemRefLayoutDocument(concurrentBody)) continue; const concurrentMigration = migrateProtocolV6SessionGroupsDocument(concurrentBody); assertLegacySessionLayoutDocument(concurrentMigration.document); if (concurrentMigration.migrated) { throw new AuthError( 'concurrent_change', 'The session layout protocol migration changed concurrently.', ); } } } const lastDocument = layoutDocuments.at(-1); if (!lastDocument || layoutDocuments.length < migrationPageLimit) break; lastLayoutId = lastDocument._id; } } }