Files
smartmigration/ts/classes.migrationcontext.ts

110 lines
3.4 KiB
TypeScript

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<T = unknown>(key: string): Promise<T | undefined> {
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<T = unknown>(key: string, value: T): Promise<void> {
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<void> {
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;
}