import * as plugins from '../ts/plugins.js'; import { assertControllerProjectDocument, ControllerProjectModel, } from '../ts/classes.authmodels.js'; import { CanonicalDirectoryError, resolveCanonicalDirectory, } from '../ts/functions.canonicaldirectory.js'; import { AuthError } from '../ts/interfaces.auth.js'; import type { IControllerProjectDocument } from '../ts/interfaces.projects.js'; const migrationPageLimit = 128; const persistedBodyFromRawDocument = ( valueArg: Record, ): Record => { const body = { ...valueArg }; delete body._id; delete body._smartdataRevision; return body; }; const hasIdentityState = (valueArg: Record): boolean => Object.hasOwn( valueArg, 'directoryIdentityState', ); export class ProtocolV22ProjectFilesystemIdentityMigration { constructor( private readonly database: plugins.smartdata.SmartdataDb | undefined, private readonly controllerId: string, ) {} public async run(): Promise { if (!this.database) { throw new AuthError('not_initialized', 'The authentication store database is unavailable.'); } const collection = ControllerProjectModel.collection.mongoDbCollection; let lastId: plugins.smartdata.TStoredDocument['_id'] | undefined; while (true) { const cursor = collection.find({ controllerId: this.controllerId, ...(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); if (body.directoryIdentityState === 'unbound') continue; if ( body.directoryIdentityState === 'bound' && Object.hasOwn(body, 'directoryIdentity') ) { try { assertControllerProjectDocument(body); continue; } catch { // Pre-release leaf-only or incomplete bindings are terminally unbound below. } } const trulyLegacy = !hasIdentityState(body); let identity: Awaited> | undefined; if (trulyLegacy) { try { identity = await resolveCanonicalDirectory(String(body.directory)); } catch (errorArg) { if (!(errorArg instanceof CanonicalDirectoryError)) throw errorArg; identity = undefined; } } const migratedBody = { ...body }; delete migratedBody.directoryDevice; delete migratedBody.directoryInode; delete migratedBody.directoryIdentity; const migrated = { ...migratedBody, directoryIdentityState: identity ? 'bound' as const : 'unbound' as const, ...(identity ? { directoryIdentity: { ancestry: identity.ancestry } } : {}), }; assertControllerProjectDocument(migrated); 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, ...migrated, _smartdataRevision: plugins.crypto.randomUUID() }, { returnDocument: 'after', includeResultMetadata: false, upsert: false }, ); if (!replaced) { const concurrent = await collection.findOne({ _id: raw._id }); const concurrentBody = concurrent ? persistedBodyFromRawDocument(concurrent) : undefined; let concurrentlyMigrated = false; if (concurrentBody && hasIdentityState(concurrentBody)) { try { assertControllerProjectDocument(concurrentBody); concurrentlyMigrated = true; } catch { concurrentlyMigrated = false; } } if (!concurrentlyMigrated) { throw new AuthError( 'concurrent_change', 'The project filesystem identity migration changed concurrently.', ); } } } const last = documents.at(-1); if (!last || documents.length < migrationPageLimit) break; lastId = last._id; } } }