/// /** * 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; } /** * 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) => { 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((t: any) => { if (t && typeof t === 'object' && 'host' in t && !('ip' in t)) { const { host, ...rest } = t; return { ...rest, ip: host }; } return t; }); 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)`); }) .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)`, ); }); return migration; }