import * as plugins from '../ts/plugins.js'; import { assertControllerAuthDocument, ControllerAuthModel, } from '../ts/classes.authmodels.js'; import { AuthError, type IControllerAuthDocument } from '../ts/interfaces.auth.js'; const migrationPageLimit = 128; const assertRuntimeConfigDocument: ( valueArg: unknown, ) => asserts valueArg is IControllerAuthDocument = assertControllerAuthDocument; const persistedBodyFromRawDocument = ( valueArg: Record, ): Record => { const body = { ...valueArg }; delete body._id; delete body._smartdataRevision; return body; }; const isAbsoluteNormalizedPath = (valueArg: unknown): valueArg is string => ( typeof valueArg === 'string' && valueArg.length > 0 && plugins.path.isAbsolute(valueArg) && plugins.path.normalize(valueArg) === valueArg ); export const migrateV23RuntimeConfigDocument = ( valueArg: Record, ): { document: IControllerAuthDocument; migrated: boolean } => { const config = valueArg.config; if (!config || typeof config !== 'object' || Array.isArray(config)) { throw new Error('Invalid legacy controller runtime configuration.'); } const configRecord = config as Record; if (!Object.hasOwn(configRecord, 'workspaceDirectory')) { assertRuntimeConfigDocument(valueArg); return { document: valueArg, migrated: false }; } if (!isAbsoluteNormalizedPath(configRecord.workspaceDirectory)) { throw new Error('Legacy controller workspace is not an absolute normalized path.'); } const projectsRoot = Object.hasOwn(configRecord, 'projectsRoot') ? configRecord.projectsRoot : configRecord.workspaceDirectory; if (!isAbsoluteNormalizedPath(projectsRoot)) { throw new Error('Legacy controller projects root is not an absolute normalized path.'); } const migratedConfig: Record = { ...configRecord, projectsRoot }; delete migratedConfig.workspaceDirectory; const migrated: Record = { ...valueArg, config: migratedConfig }; assertRuntimeConfigDocument(migrated); return { document: migrated, migrated: true }; }; export class RuntimeConfigV23Migration { 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 = ControllerAuthModel.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 migration = migrateV23RuntimeConfigDocument( persistedBodyFromRawDocument(raw), ); 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) continue; const concurrent = await collection.findOne({ _id: raw._id }); if (!concurrent) { throw new AuthError( 'concurrent_change', 'The runtime configuration migration changed concurrently.', ); } const reconciled = migrateV23RuntimeConfigDocument( persistedBodyFromRawDocument(concurrent), ); if (reconciled.migrated) { throw new AuthError( 'concurrent_change', 'The runtime configuration migration changed concurrently.', ); } } const last = documents.at(-1); if (!last || documents.length < migrationPageLimit) break; lastId = last._id; } } }