/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the Server Side Public License, v 1; you may not use this file except * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ import { ObjectMigrationDefinition, ObjectTransform, Version } from './types'; import { validateObj, validateVersion } from './utils'; /** * Extract versions metadata from an object migration definition * * @param migrationDefinition The object migration definition * @returns Metadata about the versions (list of available, last supported, latest) */ const getVersionsMeta = (migrationDefinition: ObjectMigrationDefinition) => { const versions = Object.keys(migrationDefinition) .map((version) => { const { result, value } = validateVersion(version); if (!result) { throw new Error(`Invalid version number [${version}].`); } return value as number; }) .sort((a, b) => a - b); const latestVersion = versions[versions.length - 1]; const lastSupportedVersion = versions[0]; return { versions, lastSupportedVersion, latestVersion, }; }; /** * Get a list of pure functions to transform an object from one version * to another. Either "up" or "down" according if the "to" is > than the "from" * * @param from The version to start from * @param to The version to end to * @param migrationDefinition The object migration definition * @returns An array of transform functions */ const getTransformFns = ( from: Version, to: Version, migrationDefinition: ObjectMigrationDefinition ): Array> => { const fns: Array> = []; let i = from; let fn: ObjectTransform | undefined; if (to > from) { while (i <= to) { fn = migrationDefinition[i].up; if (fn) { fns.push(fn); } i++; } } else if (to < from) { while (i >= to) { fn = migrationDefinition[i].down; if (fn) { fns.push(fn); } i--; } } return fns; }; /** * Initiate a transform for a specific request version. After we initiate the transforms * for a specific version we can then pass different `ObjectMigrationDefinition` to the provided * handler to start up/down transforming different object based on this request version. * * @example * * ```ts * const transforms = initTransform(2); // start from version "2" * const fooTransforms = transforms(fooMigrationDefinition); * const barTransforms = transforms(barMigrationDefinition); * * // Up transform the objects to the latest, starting from version "2" * const { value: fooOnLatest } = foo.up(); * const { value: barOnLatest } = bar.up(); * ``` * * @param requestVersion The starting version before up/down transforming * @returns A handler to pass an object migration definition */ export const initTransform = (requestVersion: Version) => (migrationDefinition: ObjectMigrationDefinition) => { const { latestVersion } = getVersionsMeta(migrationDefinition); const getVersion = (v: Version | 'latest'): Version => (v === 'latest' ? latestVersion : v); const validateFn = (value: unknown, version: number = requestVersion) => { const def = migrationDefinition[version]; if (!def) { throw new Error(`Invalid version number [${version}].`); } const { schema } = def; if (schema) { return validateObj(value, schema); } return null; }; return { up: ( obj: I, to: number | 'latest' = 'latest', { validate = true }: { validate?: boolean } = {} ) => { try { if (!migrationDefinition[requestVersion]) { return { error: new Error(`Unvalid version to up transform from [${requestVersion}].`), value: null, }; } if (validate) { const error = validateFn(obj, requestVersion); if (error) { return { error, value: null }; } } const targetVersion = getVersion(to); if (!migrationDefinition[targetVersion]) { return { error: new Error(`Unvalid version to up transform to [${to}].`), value: null, }; } const fns = getTransformFns(requestVersion, targetVersion, migrationDefinition); const value = fns.reduce((acc, fn) => { const res = fn(acc as unknown as I); return res; }, obj as unknown as O); return { value, error: null }; } catch (e) { return { value: null, error: new Error(`[Transform error] ${e.message ?? 'could not transform object'}.`), }; } }, down: ( obj: I, from: number | 'latest' = 'latest', { validate = true }: { validate?: boolean } = {} ) => { try { if (!migrationDefinition[requestVersion]) { return { error: new Error(`Unvalid version to down transform to [${requestVersion}].`), value: null, }; } const fromVersion = getVersion(from); if (!migrationDefinition[fromVersion]) { return { error: new Error(`Unvalid version to down transform from [${from}].`), value: null, }; } if (validate) { const error = validateFn(obj, fromVersion); if (error) { return { error, value: null }; } } const fns = getTransformFns(fromVersion, requestVersion, migrationDefinition); const value = fns.reduce((acc, fn) => { const res = fn(acc as unknown as I); return res; }, obj as unknown as O); return { value: value as any, error: null }; } catch (e) { return { value: null, error: new Error(`[Transform error] ${e.message ?? 'could not transform object'}.`), }; } }, validate: validateFn, }; };