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 { migrateProtocolV6SessionGroupsDocument } from './v6_protocoldocuments.js'; import { assertLegacySessionLayoutDocument, isV30ItemRefLayoutDocument, } from './v30_sessionlayoutitemrefs.js'; import { migrateV24LegacyTerminalLayoutDocument } from './v24_legacyterminallayout.js'; import type { IV24LegacySessionGroupsDocument } from './v24_legacyterminallayout.js'; const migrationPageLimit = 128; const persistedBodyFromRawDocument = ( valueArg: Record, ): IV24LegacySessionGroupsDocument => { const body = { ...valueArg }; delete body._id; delete body._smartdataRevision; return body as unknown as IV24LegacySessionGroupsDocument; }; const assertProtocolV6NormalizableLayout = ( valueArg: IV24LegacySessionGroupsDocument, ): void => { const normalized = migrateProtocolV6SessionGroupsDocument(valueArg); assertLegacySessionLayoutDocument(normalized.document); }; export class LegacyTerminalLayoutV24Migration { 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: the legacy shape // this migration describes no longer exists, and re-reading it would reject the document. if (isV30ItemRefLayoutDocument(body)) continue; const migration = migrateV24LegacyTerminalLayoutDocument(body); if (!migration.migrated) continue; assertProtocolV6NormalizableLayout(migration.document); 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 legacy terminal layout migration changed concurrently.', ); } const concurrentBody = persistedBodyFromRawDocument(concurrent); if (isV30ItemRefLayoutDocument(concurrentBody)) continue; const remaining = migrateV24LegacyTerminalLayoutDocument(concurrentBody); assertProtocolV6NormalizableLayout(remaining.document); if (remaining.migrated) { throw new AuthError( 'concurrent_change', 'The legacy terminal layout migration changed concurrently.', ); } } } const last = documents.at(-1); if (!last || documents.length < migrationPageLimit) break; lastId = last._id; } } }