/// /** * dcrouter migration runner. * * Uses @push.rocks/smartmigration via dynamic import so smartmigration's type * chain (which pulls in mongodb 7.x and related types) doesn't leak into * compile-time type checking for this folder. */ /** Matches the subset of IMigrationRunResult we actually log. */ export interface IMigrationRunResult { stepsApplied: Array; wasFreshInstall: boolean; currentVersionBefore: string | null; currentVersionAfter: string; totalDurationMs: number; } export interface IMigrationRunner { run(): Promise; } async function migrateTargetProfileTargetHosts(ctx: { mongo?: { collection: (name: string) => any }; log: { log: (level: 'info', message: string) => void }; }): Promise { const collection = ctx.mongo!.collection('TargetProfileDoc'); const cursor = collection.find({ 'targets.host': { $exists: true } }); let migrated = 0; for await (const doc of cursor) { const targets = ((doc as any).targets || []).map((target: any) => { if (target && typeof target === 'object' && 'host' in target && !('ip' in target)) { const { host, ...rest } = target; return { ...rest, ip: host }; } return target; }); await collection.updateOne({ _id: (doc as any)._id }, { $set: { targets } }); migrated++; } ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`); } /** * Create a configured SmartMigration runner with all dcrouter migration steps registered. * * Call `.run()` on the returned instance at startup (after DcRouterDb is ready, * before any service that reads migrated collections). * * @param db - The initialized SmartdataDb instance from DcRouterDb.getDb() * @param targetVersion - The current app version (from commitinfo.version) */ export async function createMigrationRunner( db: unknown, targetVersion: string, ): Promise { const sm = await import('@push.rocks/smartmigration'); const migration = new sm.SmartMigration({ targetVersion, db: db as any, // Brand-new installs skip all migrations and stamp directly to the current version. freshInstallVersion: targetVersion, }); // Register steps in execution order. Each step's .from() must match the // previous step's .to() to form a contiguous chain. migration .step('rename-target-profile-host-to-ip') .from('13.0.11').to('13.1.0') .description('Rename ITargetProfileTarget.host → ip on all target profiles') .up(async (ctx) => migrateTargetProfileTargetHosts(ctx)) .step('rename-domain-source-manual-to-dcrouter') .from('13.1.0').to('13.8.1') .description('Rename DomainDoc.source value from "manual" to "dcrouter"') .up(async (ctx) => { const collection = ctx.mongo!.collection('domaindoc'); const result = await collection.updateMany( { source: 'manual' }, { $set: { source: 'dcrouter' } }, ); ctx.log.log( 'info', `rename-domain-source-manual-to-dcrouter: migrated ${result.modifiedCount} domain(s)`, ); }) .step('rename-record-source-manual-to-local') .from('13.8.1').to('13.8.2') .description('Rename DnsRecordDoc.source value from "manual" to "local"') .up(async (ctx) => { const collection = ctx.mongo!.collection('dnsrecorddoc'); const result = await collection.updateMany( { source: 'manual' }, { $set: { source: 'local' } }, ); ctx.log.log( 'info', `rename-record-source-manual-to-local: migrated ${result.modifiedCount} record(s)`, ); }) .step('unify-routes-rename-collection') .from('13.8.2').to('13.16.0') .description('Rename StoredRouteDoc → RouteDoc, add origin field, drop RouteOverrideDoc') .up(async (ctx) => { const db = ctx.mongo!; // 1. Rename StoredRouteDoc → RouteDoc (smartdata uses exact class names) const collections = await db.listCollections({ name: 'StoredRouteDoc' }).toArray(); if (collections.length > 0) { await db.renameCollection('StoredRouteDoc', 'RouteDoc'); ctx.log.log('info', 'Renamed StoredRouteDoc → RouteDoc'); } // 2. Set origin='api' on all migrated docs (they were API-created) const routeCol = db.collection('RouteDoc'); const result = await routeCol.updateMany( { origin: { $exists: false } }, { $set: { origin: 'api' } }, ); ctx.log.log('info', `Set origin='api' on ${result.modifiedCount} migrated route(s)`); // 3. Drop RouteOverrideDoc collection const overrideCollections = await db.listCollections({ name: 'RouteOverrideDoc' }).toArray(); if (overrideCollections.length > 0) { await db.collection('RouteOverrideDoc').drop(); ctx.log.log('info', 'Dropped RouteOverrideDoc collection'); } }) .step('repair-target-profile-ip-migration') .from('13.16.0').to('13.17.4') .description('Repair TargetProfileDoc.targets host→ip migration for already-upgraded installs') .up(async (ctx) => { await migrateTargetProfileTargetHosts(ctx); }); return migration; }