UNPKG

object-migrations

Version:

Linear, in-memory migrations for versioned objects

222 lines (221 loc) 7.19 kB
"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;