object-migrations
Version:
Linear, in-memory migrations for versioned objects
222 lines (221 loc) • 7.19 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Migrator = void 0;
const errors_js_1 = require("./errors.js");
/**
* Allows registering migrations between object versions and then migrating objects forward and
* backward accordingly.
*
* @remarks
*
* - First register migrations with {@linkcode register}. Then, migrate objects forward
* with {@linkcode forward}/{@linkcode forwardAsync} and backward with
* {@linkcode backward}/{@linkcode backwardAsync}.
*
* ```typescript
* const m = new Migrator();
*
* m.register(1, 2, ...);
* m.register(2, 3, ...);
*
* const v2 = m.forward<V2>(v1Object, 1, 2);
* ```
*
* - A record of version-type mappings can be optionally specified, so that subsequent
* {@linkcode register}, {@linkcode forward}, {@linkcode forwardAsync}, {@linkcode backward}, and
* {@linkcode backwardAsync} calls are typed accordingly.
*
* ```typescript
* const m = new Migrator<{
* 1: V1,
* 2: V2,
* }>();
*
* m.register(1, 2, ...);
*
* const v2: V2 = m.forward(v1Object, 1, 2);
* ```
*/
class Migrator {
constructor() {
/** @internal */
this.forwardStep = new Map();
/** @internal */
this.backwardStep = new Map();
/** @internal */
this.cache = new Map();
}
register(from, to, forward, backward) {
this.forwardStep.set(from, {
to,
migration: forward,
});
if (backward !== undefined) {
this.backwardStep.set(to, {
to: from,
migration: backward,
});
}
}
forward(obj, fromVersionOrToClass, toVersion) {
if (toVersion !== undefined) {
const fromVersion = fromVersionOrToClass;
return this.migrate(obj, fromVersion, toVersion, this.forwardStep);
}
else {
const fromClass = obj.constructor;
const toClass = fromVersionOrToClass;
return this.migrate(obj, fromClass, toClass, this.forwardStep);
}
}
async forwardAsync(obj, fromVersionOrToClass, toVersion) {
if (toVersion !== undefined) {
const fromVersion = fromVersionOrToClass;
return await this.migrateAsync(obj, fromVersion, toVersion, this.forwardStep);
}
else {
const fromClass = obj.constructor;
const toClass = fromVersionOrToClass;
return await this.migrateAsync(obj, fromClass, toClass, this.forwardStep);
}
}
backward(obj, fromVersionOrToClass, toVersion) {
const versions = this.resolveOverloadVersions(obj, fromVersionOrToClass, toVersion);
return this.migrate(obj, versions.from, versions.to, this.backwardStep);
}
async backwardAsync(obj, fromVersionOrToClass, toVersion) {
const versions = this.resolveOverloadVersions(obj, fromVersionOrToClass, toVersion);
return await this.migrateAsync(obj, versions.from, versions.to, this.backwardStep);
}
/** @internal */
resolveOverloadVersions(obj, fromVersionOrToClass, toVersion) {
if (toVersion !== undefined) {
// 3 args: (plainObject, v1, v2)
return { from: fromVersionOrToClass, to: toVersion };
}
else {
// 2 args: (new V1(), V2)
return { from: obj.constructor, to: fromVersionOrToClass };
}
}
/** @internal */
migrate(obj, from, to, nextStep) {
// Same versions? Return immediately.
if (from === to) {
return {
value: obj,
changed: false,
};
}
// Get steps.
let steps = this.tryGetCachedSteps(from, to);
if (steps === undefined) {
steps = this.computeSteps(from, to, nextStep);
this.cacheSteps(from, to, steps);
}
// Migrate.
let migratedObj = obj;
for (const step of steps) {
try {
const migrate = step.migration;
migratedObj = migrate(migratedObj);
}
catch (e) {
throw new errors_js_1.MigrationError(from, to, e);
}
}
return {
value: migratedObj,
changed: true,
};
}
/** @internal */
async migrateAsync(obj, from, to, nextStep) {
// Same versions? Return immediately.
if (from === to) {
return {
value: obj,
changed: false,
};
}
// Get steps.
let steps = this.tryGetCachedSteps(from, to);
if (steps === undefined) {
steps = this.computeSteps(from, to, nextStep);
this.cacheSteps(from, to, steps);
}
// Migrate.
let migratedObj = obj;
for (const step of steps) {
try {
const migrateAsync = step.migration;
migratedObj = await Promise.resolve(migrateAsync(migratedObj));
}
catch (e) {
throw new errors_js_1.MigrationError(from, to, e);
}
}
return {
value: migratedObj,
changed: true,
};
}
/**
* Computes steps to migrate an object from between two versions.
*
* @remarks
* - Iterate the steps in order and apply their migrations to migrate the object.
* - Throws {@linkcode NoMigrationStepsError}.
*
* @internal
*/
computeSteps(from, to, nextStep) {
const steps = [];
// Repeatedly append the next step, starting at version `from`, until (and including) the
// step that results in version `to`.
//
// Assuming versions 1 through 4 are registered, and an object is being migrated from 1 to 4,
// the computed steps would be [1to2, 2to3, 3to4].
let step = nextStep.get(from);
if (step === undefined) {
throw new errors_js_1.NoMigrationStepsError(from, to);
}
while (step !== undefined) {
steps.push(step);
if (step.to === to) {
break;
}
step = nextStep.get(step.to);
}
if (steps.length === 0 || steps[steps.length - 1].to !== to) {
throw new errors_js_1.NoMigrationStepsError(from, to);
}
return steps;
}
/**
* Gets the previously cached steps to migrate an object between two versions, or `undefined`
* if no steps have been cached yet.
*
* @internal
*/
tryGetCachedSteps(from, to) {
return this.cache.get(from)?.get(to);
}
/**
* Caches steps to migrate an object between two versions.
*
* @internal
*/
cacheSteps(from, to, steps) {
if (steps.length === 0) {
throw new RangeError('There must be at least 1 step to be cached.');
}
let toMap = this.cache.get(from);
if (toMap === undefined) {
toMap = new Map();
this.cache.set(from, toMap);
}
toMap.set(to, steps);
}
}
exports.Migrator = Migrator;