feat(smartmigration): add initial smartmigration package with MongoDB and S3 migration runner
This commit is contained in:
109
ts/classes.migrationcontext.ts
Normal file
109
ts/classes.migrationcontext.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user