import * as plugins from '../ts/plugins.js'; import { assertControllerResourceDocument, ControllerResourceModel, } from '../ts/classes.authmodels.js'; import { AuthError } from '../ts/interfaces.auth.js'; import type { IControllerResourceDocument } from '../ts/interfaces.projects.js'; import { migrateV31ResourceDocument, type IV31LegacyResourceDocument, } from './v31_resourceattachmentset.js'; const migrationPageLimit = 128; const persistedBodyFromRawDocument = ( valueArg: Record, ): IV31LegacyResourceDocument => { const body = { ...valueArg }; delete body._id; delete body._smartdataRevision; return body as unknown as IV31LegacyResourceDocument; }; /** * Lifts every persisted resource from one attachment subject to the attachment set, so a resource * can be attached to several conversations at once. * * One-way: a controller older than this build rejects a lifted document, so the upgrade cannot be * rolled back once it has run. */ export class ResourceAttachmentSetV31Migration { 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 = ControllerResourceModel.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 = migrateV31ResourceDocument(persistedBodyFromRawDocument(raw)); if (!migration.migrated) continue; assertControllerResourceDocument(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 resource attachment-set migration changed concurrently.', ); } const remaining = migrateV31ResourceDocument( persistedBodyFromRawDocument(concurrent), ); if (remaining.migrated) { throw new AuthError( 'concurrent_change', 'The resource attachment-set migration changed concurrently.', ); } assertControllerResourceDocument(remaining.document); } } const last = documents.at(-1); if (!last || documents.length < migrationPageLimit) break; lastId = last._id; } } }