110 lines
3.4 KiB
TypeScript
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;
|
|
}
|