import * as plugins from './plugins.js'; import type { IMigrationCheckpoint, IMigrationContext, IMigrationStepDefinition, ISmartMigrationLedgerData, ISmartMigrationOptions, } from './interfaces.js'; import type { Ledger } from './ledgers/classes.ledger.js'; import { SmartMigrationError } from './classes.versionresolver.js'; /** * Per-step parameters needed to build a context. Kept separate from * `IMigrationContext` so the factory can stay framework-internal. */ export interface IBuildContextParams { step: IMigrationStepDefinition; options: ISmartMigrationOptions; ledger: Ledger; isDryRun: boolean; log: plugins.smartlog.Smartlog; } /** * A `IMigrationCheckpoint` implementation that round-trips through the ledger. * Each read fetches the latest ledger state; each write performs a * read-modify-write of the `checkpoints[stepId]` sub-object. */ class LedgerCheckpoint implements IMigrationCheckpoint { private ledger: Ledger; private stepId: string; constructor(ledger: Ledger, stepId: string) { this.ledger = ledger; this.stepId = stepId; } public async read(key: string): Promise { const data = await this.ledger.read(); const bag = data.checkpoints[this.stepId]; if (!bag) return undefined; return bag[key] as T | undefined; } public async write(key: string, value: T): Promise { const data = await this.ledger.read(); const bag = data.checkpoints[this.stepId] ?? {}; bag[key] = value as unknown; data.checkpoints[this.stepId] = bag; await this.ledger.write(data); } public async clear(): Promise { const data = await this.ledger.read(); delete data.checkpoints[this.stepId]; await this.ledger.write(data); } } /** * Build the `IMigrationContext` passed to a step's `up` handler. */ export function buildContext(params: IBuildContextParams): IMigrationContext { const { step, options, ledger, isDryRun, log } = params; const ctx: IMigrationContext = { db: options.db, bucket: options.bucket, mongo: options.db?.mongoDb, s3: extractS3Client(options.bucket), step: { id: step.id, fromVersion: step.fromVersion, toVersion: step.toVersion, description: step.description, isResumable: step.isResumable, }, log, isDryRun, checkpoint: step.isResumable ? new LedgerCheckpoint(ledger, step.id) : undefined, startSession: () => { if (!options.db) { throw new SmartMigrationError( 'NO_DB_CONFIGURED', `Step "${step.id}" called ctx.startSession(), but no SmartdataDb was passed to SmartMigration.`, { stepId: step.id }, ); } return options.db.startSession(); }, }; return ctx; } /** * Extract the underlying AWS SDK v3 S3Client from a smartbucket Bucket. * Uses `bucket.getStorageClient()` (added in @push.rocks/smartbucket 4.6.0) * which is the typed, documented escape hatch for advanced S3 operations. */ function extractS3Client( bucket: plugins.smartbucket.Bucket | undefined, ): plugins.TRawS3Client | undefined { if (!bucket) return undefined; // `getStorageClient` is typed as the actual S3Client, but it lives behind // an optional peer dep type so we cast through unknown to keep this file // compiling when smartbucket isn't installed by the consumer. return (bucket as any).getStorageClient() as plugins.TRawS3Client | undefined; }