import * as plugins from '../ts/plugins.js'; import { ControllerSessionGroupsModel, } from '../ts/classes.authmodels.js'; import { AuthError } from '../ts/interfaces.auth.js'; import type { IControllerSessionGroupsDocument } from '../ts/interfaces.projects.js'; import { assertLegacySessionLayoutDocument, isV30ItemRefLayoutDocument, } from './v30_sessionlayoutitemrefs.js'; import { migrateProtocolV18SessionLayoutDocument } from './v18_resourcelayout.js'; import type { IProtocolV18LegacySessionGroupsDocument } from './v18_resourcelayout.js'; const migrationPageLimit = 128; const persistedBodyFromRawDocument = ( valueArg: Record, ): IProtocolV18LegacySessionGroupsDocument => { const body = { ...valueArg }; delete body._id; delete body._smartdataRevision; return body as unknown as IProtocolV18LegacySessionGroupsDocument; }; export class ProtocolV18ResourceLayoutMigration { constructor(private readonly database: plugins.smartdata.SmartdataDb | undefined) {} public async run(): Promise { if (!this.database) { throw new AuthError('not_initialized', 'The authentication store database is unavailable.'); } const collection = ControllerSessionGroupsModel.collection.mongoDbCollection; let lastId: plugins.smartdata.TStoredDocument['_id'] | undefined; while (true) { const cursor = collection.find(lastId ? { _id: { $gt: lastId } } : {}) .sort({ _id: 1 }) .limit(migrationPageLimit); let documents: Awaited>; try { documents = await cursor.toArray(); } finally { await cursor.close(); } for (const raw of documents) { const body = persistedBodyFromRawDocument(raw); // Already lifted to item refs by the v30 migration on an earlier start. if (isV30ItemRefLayoutDocument(body)) continue; const migration = migrateProtocolV18SessionLayoutDocument(body); assertLegacySessionLayoutDocument(migration.document); if (!migration.migrated) continue; const selector = raw._smartdataRevision === undefined ? { _id: raw._id, _smartdataRevision: { $exists: false } } : { _id: raw._id, _smartdataRevision: raw._smartdataRevision }; const replaced = await collection.findOneAndReplace( selector, { _id: raw._id, ...migration.document, _smartdataRevision: plugins.crypto.randomUUID() }, { returnDocument: 'after', includeResultMetadata: false, upsert: false }, ); if (!replaced) { const concurrent = await collection.findOne({ _id: raw._id }); if (!concurrent) throw new AuthError('concurrent_change', 'The resource layout migration changed concurrently.'); const concurrentBody = persistedBodyFromRawDocument(concurrent); // The concurrent writer may have been a later migration lifting the same document; // a lifted document is finished work here, exactly as on the first read. if (isV30ItemRefLayoutDocument(concurrentBody)) continue; const remaining = migrateProtocolV18SessionLayoutDocument(concurrentBody); assertLegacySessionLayoutDocument(remaining.document); if (remaining.migrated) { throw new AuthError('concurrent_change', 'The resource layout migration changed concurrently.'); } } } const last = documents.at(-1); if (!last || documents.length < migrationPageLimit) break; lastId = last._id; } } }