import { ExtendedVersion, VersionRange } from "../exver" import * as T from "../types" import { Graph, Vertex } from "../util/graph" import { once } from "../util/once" import { IMPOSSIBLE, VersionInfo } from "./VersionInfo" export class VersionGraph { private readonly graph: () => Graph< ExtendedVersion | VersionRange, ((opts: { effects: T.Effects }) => Promise) | undefined > private constructor( readonly current: VersionInfo, versions: Array>, ) { this.graph = once(() => { const graph = new Graph< ExtendedVersion | VersionRange, ((opts: { effects: T.Effects }) => Promise) | undefined >() const flavorMap: Record< string, [ ExtendedVersion, VersionInfo, Vertex< ExtendedVersion | VersionRange, ((opts: { effects: T.Effects }) => Promise) | undefined >, ][] > = {} for (let version of [current, ...versions]) { const v = ExtendedVersion.parse(version.options.version) const vertex = graph.addVertex(v, [], []) const flavor = v.flavor || "" if (!flavorMap[flavor]) { flavorMap[flavor] = [] } flavorMap[flavor].push([v, version, vertex]) } for (let flavor in flavorMap) { flavorMap[flavor].sort((a, b) => a[0].compareForSort(b[0])) let prev: | [ ExtendedVersion, VersionInfo, Vertex< ExtendedVersion | VersionRange, (opts: { effects: T.Effects }) => Promise >, ] | undefined = undefined for (let [v, version, vertex] of flavorMap[flavor]) { if (version.options.migrations.up !== IMPOSSIBLE) { let range if (prev) { graph.addEdge(version.options.migrations.up, prev[2], vertex) range = VersionRange.anchor(">=", prev[0]).and( VersionRange.anchor("<", v), ) } else { range = VersionRange.anchor("<", v) } const vRange = graph.addVertex(range, [], []) graph.addEdge(version.options.migrations.up, vRange, vertex) } if (version.options.migrations.down !== IMPOSSIBLE) { let range if (prev) { graph.addEdge(version.options.migrations.down, vertex, prev[2]) range = VersionRange.anchor(">=", prev[0]).and( VersionRange.anchor("<", v), ) } else { range = VersionRange.anchor("<", v) } const vRange = graph.addVertex(range, [], []) graph.addEdge(version.options.migrations.down, vertex, vRange) } if (version.options.migrations.other) { for (let rangeStr in version.options.migrations.other) { const range = VersionRange.parse(rangeStr) const vRange = graph.addVertex(range, [], []) graph.addEdge( version.options.migrations.other[rangeStr], vRange, vertex, ) for (let matching of graph.findVertex( (v) => v.metadata instanceof ExtendedVersion && v.metadata.satisfies(range), )) { graph.addEdge( version.options.migrations.other[rangeStr], matching, vertex, ) } } } } } return graph }) } currentVersion = once(() => ExtendedVersion.parse(this.current.options.version), ) static of< CurrentVersion extends string, OtherVersions extends Array>, >( currentVersion: VersionInfo, ...other: EnsureUniqueId ) { return new VersionGraph(currentVersion, other as Array>) } async migrate({ effects, from, to, }: { effects: T.Effects from: ExtendedVersion to: ExtendedVersion }) { const graph = this.graph() if (from && to) { const path = graph.shortestPath( (v) => (v.metadata instanceof VersionRange && v.metadata.satisfiedBy(from)) || (v.metadata instanceof ExtendedVersion && v.metadata.equals(from)), (v) => (v.metadata instanceof VersionRange && v.metadata.satisfiedBy(to)) || (v.metadata instanceof ExtendedVersion && v.metadata.equals(to)), ) if (path) { for (let edge of path) { if (edge.metadata) { await edge.metadata({ effects }) } await effects.setDataVersion({ version: edge.to.metadata.toString() }) } return } } throw new Error() } canMigrateFrom = once(() => Array.from( this.graph().reverseBreadthFirstSearch( (v) => (v.metadata instanceof VersionRange && v.metadata.satisfiedBy(this.currentVersion())) || (v.metadata instanceof ExtendedVersion && v.metadata.equals(this.currentVersion())), ), ).reduce( (acc, x) => acc.or( x.metadata instanceof VersionRange ? x.metadata : VersionRange.anchor("=", x.metadata), ), VersionRange.none(), ), ) canMigrateTo = once(() => Array.from( this.graph().breadthFirstSearch( (v) => (v.metadata instanceof VersionRange && v.metadata.satisfiedBy(this.currentVersion())) || (v.metadata instanceof ExtendedVersion && v.metadata.equals(this.currentVersion())), ), ).reduce( (acc, x) => acc.or( x.metadata instanceof VersionRange ? x.metadata : VersionRange.anchor("=", x.metadata), ), VersionRange.none(), ), ) } export function setupVersionGraph< CurrentVersion extends string, OtherVersions extends Array>, >( current: VersionInfo, ...other: EnsureUniqueId ) { return VersionGraph.of(current, ...other) } // prettier-ignore export type EnsureUniqueId = B extends [] ? A : B extends [VersionInfo, ...infer Rest] ? ( Version extends OtherVersions ? "One or more versions are not unique"[] : EnsureUniqueId ) : "There exists a migration that is not a Migration"[]