@prisma-next/migration-tools 0.5.0-dev.1 → 0.5.0-dev.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -22
- package/dist/{constants-BRi0X7B_.mjs → constants-WVGVMOdu.mjs} +1 -1
- package/dist/{constants-BRi0X7B_.mjs.map → constants-WVGVMOdu.mjs.map} +1 -1
- package/dist/{errors-BKbRGCJM.mjs → errors-CZ9JD4sd.mjs} +50 -21
- package/dist/errors-CZ9JD4sd.mjs.map +1 -0
- package/dist/exports/constants.mjs +1 -1
- package/dist/exports/{types.d.mts → errors.d.mts} +6 -8
- package/dist/exports/errors.d.mts.map +1 -0
- package/dist/exports/errors.mjs +3 -0
- package/dist/exports/graph.d.mts +2 -0
- package/dist/exports/graph.mjs +1 -0
- package/dist/exports/hash.d.mts +52 -0
- package/dist/exports/hash.d.mts.map +1 -0
- package/dist/exports/hash.mjs +3 -0
- package/dist/exports/io.d.mts +7 -6
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +156 -2
- package/dist/exports/io.mjs.map +1 -0
- package/dist/exports/metadata.d.mts +2 -0
- package/dist/exports/metadata.mjs +1 -0
- package/dist/exports/{dag.d.mts → migration-graph.d.mts} +10 -9
- package/dist/exports/migration-graph.d.mts.map +1 -0
- package/dist/exports/{dag.mjs → migration-graph.mjs} +17 -17
- package/dist/exports/migration-graph.mjs.map +1 -0
- package/dist/exports/migration-ts.mjs +1 -1
- package/dist/exports/migration.d.mts +13 -10
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +20 -21
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +2 -0
- package/dist/exports/package.mjs +1 -0
- package/dist/exports/refs.d.mts +11 -5
- package/dist/exports/refs.d.mts.map +1 -1
- package/dist/exports/refs.mjs +106 -30
- package/dist/exports/refs.mjs.map +1 -1
- package/dist/graph-B5wbCSna.d.mts +22 -0
- package/dist/graph-B5wbCSna.d.mts.map +1 -0
- package/dist/hash-BNWumjn7.mjs +76 -0
- package/dist/hash-BNWumjn7.mjs.map +1 -0
- package/dist/metadata-DDa5L-uD.d.mts +45 -0
- package/dist/metadata-DDa5L-uD.d.mts.map +1 -0
- package/dist/package-BJ5KAEcD.d.mts +21 -0
- package/dist/package-BJ5KAEcD.d.mts.map +1 -0
- package/package.json +26 -14
- package/src/errors.ts +57 -15
- package/src/exports/errors.ts +1 -0
- package/src/exports/graph.ts +1 -0
- package/src/exports/hash.ts +2 -0
- package/src/exports/io.ts +1 -1
- package/src/exports/metadata.ts +1 -0
- package/src/exports/{dag.ts → migration-graph.ts} +2 -2
- package/src/exports/package.ts +1 -0
- package/src/exports/refs.ts +10 -2
- package/src/graph.ts +19 -0
- package/src/hash.ts +91 -0
- package/src/io.ts +32 -20
- package/src/metadata.ts +36 -0
- package/src/migration-base.ts +32 -28
- package/src/{dag.ts → migration-graph.ts} +35 -38
- package/src/package.ts +18 -0
- package/src/refs.ts +148 -37
- package/dist/attestation-DtF8tEOM.mjs +0 -65
- package/dist/attestation-DtF8tEOM.mjs.map +0 -1
- package/dist/errors-BKbRGCJM.mjs.map +0 -1
- package/dist/exports/attestation.d.mts +0 -37
- package/dist/exports/attestation.d.mts.map +0 -1
- package/dist/exports/attestation.mjs +0 -4
- package/dist/exports/dag.d.mts.map +0 -1
- package/dist/exports/dag.mjs.map +0 -1
- package/dist/exports/types.d.mts.map +0 -1
- package/dist/exports/types.mjs +0 -3
- package/dist/io-CCnYsUHU.mjs +0 -153
- package/dist/io-CCnYsUHU.mjs.map +0 -1
- package/dist/types-DyGXcWWp.d.mts +0 -71
- package/dist/types-DyGXcWWp.d.mts.map +0 -1
- package/src/attestation.ts +0 -81
- package/src/exports/attestation.ts +0 -2
- package/src/exports/types.ts +0 -10
- package/src/types.ts +0 -66
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { n as MigrationGraph, t as MigrationEdge } from "../graph-B5wbCSna.mjs";
|
|
2
|
+
import { n as MigrationPackage } from "../package-BJ5KAEcD.mjs";
|
|
2
3
|
|
|
3
|
-
//#region src/
|
|
4
|
-
declare function reconstructGraph(packages: readonly
|
|
4
|
+
//#region src/migration-graph.d.ts
|
|
5
|
+
declare function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph;
|
|
5
6
|
/**
|
|
6
7
|
* Find the shortest path from `fromHash` to `toHash` using BFS over the
|
|
7
8
|
* contract-hash graph. Returns the ordered list of edges, or null if no path
|
|
8
9
|
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
9
10
|
*
|
|
10
11
|
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
11
|
-
* label priority → createdAt → to →
|
|
12
|
+
* label priority → createdAt → to → migrationHash.
|
|
12
13
|
*/
|
|
13
|
-
declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly
|
|
14
|
+
declare function findPath(graph: MigrationGraph, fromHash: string, toHash: string): readonly MigrationEdge[] | null;
|
|
14
15
|
interface PathDecision {
|
|
15
|
-
readonly selectedPath: readonly
|
|
16
|
+
readonly selectedPath: readonly MigrationEdge[];
|
|
16
17
|
readonly fromHash: string;
|
|
17
18
|
readonly toHash: string;
|
|
18
19
|
readonly alternativeCount: number;
|
|
@@ -43,9 +44,9 @@ declare function findLeaf(graph: MigrationGraph): string | null;
|
|
|
43
44
|
* to the single target. Returns null for an empty graph.
|
|
44
45
|
* Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
|
|
45
46
|
*/
|
|
46
|
-
declare function findLatestMigration(graph: MigrationGraph):
|
|
47
|
+
declare function findLatestMigration(graph: MigrationGraph): MigrationEdge | null;
|
|
47
48
|
declare function detectCycles(graph: MigrationGraph): readonly string[][];
|
|
48
|
-
declare function detectOrphans(graph: MigrationGraph): readonly
|
|
49
|
+
declare function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[];
|
|
49
50
|
//#endregion
|
|
50
51
|
export { type PathDecision, detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
51
|
-
//# sourceMappingURL=
|
|
52
|
+
//# sourceMappingURL=migration-graph.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/migration-graph.ts"],"sourcesContent":[],"mappings":";;;;iBA6BgB,gBAAA,oBAAoC,qBAAqB;;AAAzE;AAmFA;AAyCA;AAaA;AA8FA;AAkBA;AAwCA;AAQgB,iBAtNA,QAAA,CAsNoB,KAAA,EArN3B,cAqNyC,EAAA,QAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAAA,SAlNtC,aAkNsC,EAAA,GAAA,IAAA;AA8DlC,UA3OC,YAAA,CA2OY;kCA1OK;;;;;;;;;;;iBAYlB,oBAAA,QACP,qEAIN;;;;;iBAyFa,mBAAA,QAA2B;;;;;;;;;iBAkB3B,QAAA,QAAgB;;;;;;iBAwChB,mBAAA,QAA2B,iBAAiB;iBAQ5C,YAAA,QAAoB;iBA8DpB,aAAA,QAAqB,0BAA0B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as EMPTY_CONTRACT_HASH } from "../constants-
|
|
1
|
+
import { g as errorSameSourceAndTarget, h as errorNoTarget, i as errorDuplicateMigrationHash, m as errorNoInitialMigration, n as errorAmbiguousTarget } from "../errors-CZ9JD4sd.mjs";
|
|
2
|
+
import { t as EMPTY_CONTRACT_HASH } from "../constants-WVGVMOdu.mjs";
|
|
3
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
4
|
|
|
5
5
|
//#region src/queue.ts
|
|
@@ -82,7 +82,7 @@ function* bfs(starts, neighbours, ordering) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
//#endregion
|
|
85
|
-
//#region src/
|
|
85
|
+
//#region src/migration-graph.ts
|
|
86
86
|
/** Forward-edge neighbours for BFS: edge `e` from `n` visits `e.to` next. */
|
|
87
87
|
function forwardNeighbours(graph, node) {
|
|
88
88
|
return (graph.forwardChain.get(node) ?? []).map((edge) => ({
|
|
@@ -106,22 +106,22 @@ function reconstructGraph(packages) {
|
|
|
106
106
|
const nodes = /* @__PURE__ */ new Set();
|
|
107
107
|
const forwardChain = /* @__PURE__ */ new Map();
|
|
108
108
|
const reverseChain = /* @__PURE__ */ new Map();
|
|
109
|
-
const
|
|
109
|
+
const migrationByHash = /* @__PURE__ */ new Map();
|
|
110
110
|
for (const pkg of packages) {
|
|
111
|
-
const { from, to } = pkg.
|
|
112
|
-
if (from === to) throw errorSameSourceAndTarget(pkg.
|
|
111
|
+
const { from, to } = pkg.metadata;
|
|
112
|
+
if (from === to) throw errorSameSourceAndTarget(pkg.dirPath, from);
|
|
113
113
|
nodes.add(from);
|
|
114
114
|
nodes.add(to);
|
|
115
115
|
const migration = {
|
|
116
116
|
from,
|
|
117
117
|
to,
|
|
118
|
-
|
|
118
|
+
migrationHash: pkg.metadata.migrationHash,
|
|
119
119
|
dirName: pkg.dirName,
|
|
120
|
-
createdAt: pkg.
|
|
121
|
-
labels: pkg.
|
|
120
|
+
createdAt: pkg.metadata.createdAt,
|
|
121
|
+
labels: pkg.metadata.labels
|
|
122
122
|
};
|
|
123
|
-
if (
|
|
124
|
-
|
|
123
|
+
if (migrationByHash.has(migration.migrationHash)) throw errorDuplicateMigrationHash(migration.migrationHash);
|
|
124
|
+
migrationByHash.set(migration.migrationHash, migration);
|
|
125
125
|
appendEdge(forwardChain, from, migration);
|
|
126
126
|
appendEdge(reverseChain, to, migration);
|
|
127
127
|
}
|
|
@@ -129,7 +129,7 @@ function reconstructGraph(packages) {
|
|
|
129
129
|
nodes,
|
|
130
130
|
forwardChain,
|
|
131
131
|
reverseChain,
|
|
132
|
-
|
|
132
|
+
migrationByHash
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
const LABEL_PRIORITY = {
|
|
@@ -152,7 +152,7 @@ function compareTieBreak(a, b) {
|
|
|
152
152
|
if (ca !== 0) return ca;
|
|
153
153
|
const tc = a.to.localeCompare(b.to);
|
|
154
154
|
if (tc !== 0) return tc;
|
|
155
|
-
return a.
|
|
155
|
+
return a.migrationHash.localeCompare(b.migrationHash);
|
|
156
156
|
}
|
|
157
157
|
function sortedNeighbors(edges) {
|
|
158
158
|
return [...edges].sort(compareTieBreak);
|
|
@@ -167,7 +167,7 @@ function bfsOrdering(items) {
|
|
|
167
167
|
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
168
168
|
*
|
|
169
169
|
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
170
|
-
* label priority → createdAt → to →
|
|
170
|
+
* label priority → createdAt → to → migrationHash.
|
|
171
171
|
*/
|
|
172
172
|
function findPath(graph, fromHash, toHash) {
|
|
173
173
|
if (fromHash === toHash) return [];
|
|
@@ -226,8 +226,8 @@ function findPathWithDecision(graph, fromHash, toHash, refName) {
|
|
|
226
226
|
if (reachable.length > 1) {
|
|
227
227
|
alternativeCount += reachable.length - 1;
|
|
228
228
|
const sorted = sortedNeighbors(reachable);
|
|
229
|
-
if (sorted[0] && sorted[0].
|
|
230
|
-
if (reachable.some((e) => e.
|
|
229
|
+
if (sorted[0] && sorted[0].migrationHash === edge.migrationHash) {
|
|
230
|
+
if (reachable.some((e) => e.migrationHash !== edge.migrationHash)) tieBreakReasons.push(`at ${edge.from}: ${reachable.length} candidates, selected by tie-break`);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
@@ -383,4 +383,4 @@ function detectOrphans(graph) {
|
|
|
383
383
|
|
|
384
384
|
//#endregion
|
|
385
385
|
export { detectCycles, detectOrphans, findLatestMigration, findLeaf, findPath, findPathWithDecision, findReachableLeaves, reconstructGraph };
|
|
386
|
-
//# sourceMappingURL=
|
|
386
|
+
//# sourceMappingURL=migration-graph.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-graph.mjs","names":["migration: MigrationEdge","LABEL_PRIORITY: Record<string, number>","path: MigrationEdge[]","tieBreakReasons: string[]","leaves: string[]","cycles: string[][]","stack: Frame[]","cycle: string[]","startNodes: string[]","orphans: MigrationEdge[]"],"sources":["../../src/queue.ts","../../src/graph-ops.ts","../../src/migration-graph.ts"],"sourcesContent":["/**\n * FIFO queue with amortised O(1) push and shift.\n *\n * Uses a head-index cursor over a backing array rather than\n * `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped\n * traversals where the queue is drained in a single pass — it does not\n * reclaim memory for already-shifted items, so it is not suitable for\n * long-lived queues with many push/shift cycles.\n */\nexport class Queue<T> {\n private readonly items: T[];\n private head = 0;\n\n constructor(initial: Iterable<T> = []) {\n this.items = [...initial];\n }\n\n push(item: T): void {\n this.items.push(item);\n }\n\n /**\n * Remove and return the next item. Caller must check `isEmpty` first —\n * shifting an empty queue throws.\n */\n shift(): T {\n if (this.head >= this.items.length) {\n throw new Error('Queue.shift called on empty queue');\n }\n // biome-ignore lint/style/noNonNullAssertion: bounds-checked on the line above\n return this.items[this.head++]!;\n }\n\n get isEmpty(): boolean {\n return this.head >= this.items.length;\n }\n}\n","import { Queue } from './queue';\n\n/**\n * One step of a BFS traversal.\n *\n * `parent` and `incomingEdge` are `null` for start nodes — they were not\n * reached via any edge. For every other node they record the node and edge\n * by which this node was first reached.\n */\nexport interface BfsStep<E> {\n readonly node: string;\n readonly parent: string | null;\n readonly incomingEdge: E | null;\n}\n\n/**\n * Generic breadth-first traversal.\n *\n * Direction (forward/reverse) is expressed by the caller's `neighbours`\n * closure: return `{ next, edge }` pairs where `next` is the node to visit\n * next and `edge` is the edge that connects them. Callers that don't need\n * path reconstruction can ignore the `parent`/`incomingEdge` fields of each\n * yielded step.\n *\n * Stops are intrinsic — callers `break` out of the `for..of` loop when\n * they've found what they're looking for.\n *\n * `ordering`, if provided, controls the order in which neighbours of each\n * node are enqueued. Only matters for path-finding: a deterministic ordering\n * makes BFS return a deterministic shortest path when multiple exist.\n */\nexport function* bfs<E>(\n starts: Iterable<string>,\n neighbours: (node: string) => Iterable<{ next: string; edge: E }>,\n ordering?: (items: readonly { next: string; edge: E }[]) => readonly { next: string; edge: E }[],\n): Generator<BfsStep<E>> {\n const visited = new Set<string>();\n const parentMap = new Map<string, { parent: string; edge: E }>();\n const queue = new Queue<string>();\n for (const start of starts) {\n if (!visited.has(start)) {\n visited.add(start);\n queue.push(start);\n }\n }\n while (!queue.isEmpty) {\n const current = queue.shift();\n const parentInfo = parentMap.get(current);\n yield {\n node: current,\n parent: parentInfo?.parent ?? null,\n incomingEdge: parentInfo?.edge ?? null,\n };\n\n const items = neighbours(current);\n const toVisit = ordering ? ordering([...items]) : items;\n for (const { next, edge } of toVisit) {\n if (!visited.has(next)) {\n visited.add(next);\n parentMap.set(next, { parent: current, edge });\n queue.push(next);\n }\n }\n }\n}\n","import { ifDefined } from '@prisma-next/utils/defined';\nimport { EMPTY_CONTRACT_HASH } from './constants';\nimport {\n errorAmbiguousTarget,\n errorDuplicateMigrationHash,\n errorNoInitialMigration,\n errorNoTarget,\n errorSameSourceAndTarget,\n} from './errors';\nimport type { MigrationEdge, MigrationGraph } from './graph';\nimport { bfs } from './graph-ops';\nimport type { MigrationPackage } from './package';\n\n/** Forward-edge neighbours for BFS: edge `e` from `n` visits `e.to` next. */\nfunction forwardNeighbours(graph: MigrationGraph, node: string) {\n return (graph.forwardChain.get(node) ?? []).map((edge) => ({ next: edge.to, edge }));\n}\n\n/** Reverse-edge neighbours for BFS: edge `e` from `n` visits `e.from` next. */\nfunction reverseNeighbours(graph: MigrationGraph, node: string) {\n return (graph.reverseChain.get(node) ?? []).map((edge) => ({ next: edge.from, edge }));\n}\n\nfunction appendEdge(map: Map<string, MigrationEdge[]>, key: string, entry: MigrationEdge): void {\n const bucket = map.get(key);\n if (bucket) bucket.push(entry);\n else map.set(key, [entry]);\n}\n\nexport function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph {\n const nodes = new Set<string>();\n const forwardChain = new Map<string, MigrationEdge[]>();\n const reverseChain = new Map<string, MigrationEdge[]>();\n const migrationByHash = new Map<string, MigrationEdge>();\n\n for (const pkg of packages) {\n const { from, to } = pkg.metadata;\n\n if (from === to) {\n throw errorSameSourceAndTarget(pkg.dirPath, from);\n }\n\n nodes.add(from);\n nodes.add(to);\n\n const migration: MigrationEdge = {\n from,\n to,\n migrationHash: pkg.metadata.migrationHash,\n dirName: pkg.dirName,\n createdAt: pkg.metadata.createdAt,\n labels: pkg.metadata.labels,\n };\n\n if (migrationByHash.has(migration.migrationHash)) {\n throw errorDuplicateMigrationHash(migration.migrationHash);\n }\n migrationByHash.set(migration.migrationHash, migration);\n\n appendEdge(forwardChain, from, migration);\n appendEdge(reverseChain, to, migration);\n }\n\n return { nodes, forwardChain, reverseChain, migrationByHash };\n}\n\n// ---------------------------------------------------------------------------\n// Deterministic tie-breaking for BFS neighbour order.\n// Used by `findPath` and `findPathWithDecision` only; not a general-purpose\n// utility. Ordering: label priority → createdAt → to → migrationHash.\n// ---------------------------------------------------------------------------\n\nconst LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };\n\nfunction labelPriority(labels: readonly string[]): number {\n let best = 3;\n for (const l of labels) {\n const p = LABEL_PRIORITY[l];\n if (p !== undefined && p < best) best = p;\n }\n return best;\n}\n\nfunction compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {\n const lp = labelPriority(a.labels) - labelPriority(b.labels);\n if (lp !== 0) return lp;\n const ca = a.createdAt.localeCompare(b.createdAt);\n if (ca !== 0) return ca;\n const tc = a.to.localeCompare(b.to);\n if (tc !== 0) return tc;\n return a.migrationHash.localeCompare(b.migrationHash);\n}\n\nfunction sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdge[] {\n return [...edges].sort(compareTieBreak);\n}\n\n/** Ordering adapter for `bfs` — sorts `{next, edge}` pairs by tie-break. */\nfunction bfsOrdering(\n items: readonly { next: string; edge: MigrationEdge }[],\n): readonly { next: string; edge: MigrationEdge }[] {\n return items.slice().sort((a, b) => compareTieBreak(a.edge, b.edge));\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` using BFS over the\n * contract-hash graph. Returns the ordered list of edges, or null if no path\n * exists. Returns an empty array when `fromHash === toHash` (no-op).\n *\n * Neighbor ordering is deterministic via the tie-break sort key:\n * label priority → createdAt → to → migrationHash.\n */\nexport function findPath(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n): readonly MigrationEdge[] | null {\n if (fromHash === toHash) return [];\n\n const parents = new Map<string, { parent: string; edge: MigrationEdge }>();\n for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n), bfsOrdering)) {\n if (step.parent !== null && step.incomingEdge !== null) {\n parents.set(step.node, { parent: step.parent, edge: step.incomingEdge });\n }\n if (step.node === toHash) {\n const path: MigrationEdge[] = [];\n let cur = toHash;\n let p = parents.get(cur);\n while (p) {\n path.push(p.edge);\n cur = p.parent;\n p = parents.get(cur);\n }\n path.reverse();\n return path;\n }\n }\n\n return null;\n}\n\n/**\n * Reverse-BFS from `toHash` over `reverseChain` to collect every node from\n * which `toHash` is reachable (inclusive of `toHash` itself).\n */\nfunction collectNodesReachingTarget(graph: MigrationGraph, toHash: string): Set<string> {\n const reached = new Set<string>();\n for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) {\n reached.add(step.node);\n }\n return reached;\n}\n\nexport interface PathDecision {\n readonly selectedPath: readonly MigrationEdge[];\n readonly fromHash: string;\n readonly toHash: string;\n readonly alternativeCount: number;\n readonly tieBreakReasons: readonly string[];\n readonly refName?: string;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` and return structured\n * path-decision metadata for machine-readable output.\n */\nexport function findPathWithDecision(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n refName?: string,\n): PathDecision | null {\n if (fromHash === toHash) {\n return {\n selectedPath: [],\n fromHash,\n toHash,\n alternativeCount: 0,\n tieBreakReasons: [],\n ...ifDefined('refName', refName),\n };\n }\n\n const path = findPath(graph, fromHash, toHash);\n if (!path) return null;\n\n // Single reverse BFS marks every node from which `toHash` is reachable.\n // Replaces a per-edge `findPath(e.to, toHash)` call inside the loop below,\n // which made the whole function O(|path| · (V + E)) instead of O(V + E).\n const reachesTarget = collectNodesReachingTarget(graph, toHash);\n\n const tieBreakReasons: string[] = [];\n let alternativeCount = 0;\n\n for (const edge of path) {\n const outgoing = graph.forwardChain.get(edge.from);\n if (outgoing && outgoing.length > 1) {\n const reachable = outgoing.filter((e) => reachesTarget.has(e.to));\n if (reachable.length > 1) {\n alternativeCount += reachable.length - 1;\n const sorted = sortedNeighbors(reachable);\n if (sorted[0] && sorted[0].migrationHash === edge.migrationHash) {\n if (reachable.some((e) => e.migrationHash !== edge.migrationHash)) {\n tieBreakReasons.push(\n `at ${edge.from}: ${reachable.length} candidates, selected by tie-break`,\n );\n }\n }\n }\n }\n }\n\n return {\n selectedPath: path,\n fromHash,\n toHash,\n alternativeCount,\n tieBreakReasons,\n ...ifDefined('refName', refName),\n };\n}\n\n/**\n * Walk ancestors of each branch tip back to find the last node\n * that appears on all paths. Returns `fromHash` if no shared ancestor is found.\n */\nfunction findDivergencePoint(\n graph: MigrationGraph,\n fromHash: string,\n leaves: readonly string[],\n): string {\n const ancestorSets = leaves.map((leaf) => {\n const ancestors = new Set<string>();\n for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) {\n ancestors.add(step.node);\n }\n return ancestors;\n });\n\n const commonAncestors = [...(ancestorSets[0] ?? [])].filter((node) =>\n ancestorSets.every((s) => s.has(node)),\n );\n\n let deepest = fromHash;\n let deepestDepth = -1;\n for (const ancestor of commonAncestors) {\n const path = findPath(graph, fromHash, ancestor);\n const depth = path ? path.length : 0;\n if (depth > deepestDepth) {\n deepestDepth = depth;\n deepest = ancestor;\n }\n }\n return deepest;\n}\n\n/**\n * Find all branch tips (nodes with no outgoing edges) reachable from\n * `fromHash` via forward edges.\n */\nexport function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[] {\n const leaves: string[] = [];\n for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) {\n if (!graph.forwardChain.get(step.node)?.length) {\n leaves.push(step.node);\n }\n }\n return leaves;\n}\n\n/**\n * Find the target contract hash of the migration graph reachable from\n * EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target\n * state (either empty, or containing only the root with no outgoing\n * edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none\n * originate from the empty hash, and AMBIGUOUS_TARGET if multiple\n * branch tips exist.\n */\nexport function findLeaf(graph: MigrationGraph): string | null {\n if (graph.nodes.size === 0) {\n return null;\n }\n\n if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) {\n throw errorNoInitialMigration([...graph.nodes]);\n }\n\n const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);\n\n if (leaves.length === 0) {\n const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);\n if (reachable.length > 0) {\n throw errorNoTarget(reachable);\n }\n return null;\n }\n\n if (leaves.length > 1) {\n const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);\n const branches = leaves.map((tip) => {\n const path = findPath(graph, divergencePoint, tip);\n return {\n tip,\n edges: (path ?? []).map((e) => ({ dirName: e.dirName, from: e.from, to: e.to })),\n };\n });\n throw errorAmbiguousTarget(leaves, { divergencePoint, branches });\n }\n\n // biome-ignore lint/style/noNonNullAssertion: leaves.length is neither 0 nor >1 per the branches above, so exactly one leaf remains\n return leaves[0]!;\n}\n\n/**\n * Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH\n * to the single target. Returns null for an empty graph.\n * Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.\n */\nexport function findLatestMigration(graph: MigrationGraph): MigrationEdge | null {\n const leafHash = findLeaf(graph);\n if (leafHash === null) return null;\n\n const path = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);\n return path?.at(-1) ?? null;\n}\n\nexport function detectCycles(graph: MigrationGraph): readonly string[][] {\n const WHITE = 0;\n const GRAY = 1;\n const BLACK = 2;\n\n const color = new Map<string, number>();\n const parentMap = new Map<string, string | null>();\n const cycles: string[][] = [];\n\n for (const node of graph.nodes) {\n color.set(node, WHITE);\n }\n\n // Iterative three-color DFS. A frame is (node, outgoing edges, next-index).\n interface Frame {\n node: string;\n outgoing: readonly MigrationEdge[];\n index: number;\n }\n const stack: Frame[] = [];\n\n function pushFrame(u: string): void {\n color.set(u, GRAY);\n stack.push({ node: u, outgoing: graph.forwardChain.get(u) ?? [], index: 0 });\n }\n\n for (const root of graph.nodes) {\n if (color.get(root) !== WHITE) continue;\n parentMap.set(root, null);\n pushFrame(root);\n\n while (stack.length > 0) {\n // biome-ignore lint/style/noNonNullAssertion: stack.length > 0 should guarantee that this cannot be undefined\n const frame = stack[stack.length - 1]!;\n if (frame.index >= frame.outgoing.length) {\n color.set(frame.node, BLACK);\n stack.pop();\n continue;\n }\n // biome-ignore lint/style/noNonNullAssertion: the early-continue above guarantees frame.index < frame.outgoing.length here, so this is defined\n const edge = frame.outgoing[frame.index++]!;\n const v = edge.to;\n const vColor = color.get(v);\n if (vColor === GRAY) {\n const cycle: string[] = [v];\n let cur = frame.node;\n while (cur !== v) {\n cycle.push(cur);\n cur = parentMap.get(cur) ?? v;\n }\n cycle.reverse();\n cycles.push(cycle);\n } else if (vColor === WHITE) {\n parentMap.set(v, frame.node);\n pushFrame(v);\n }\n }\n }\n\n return cycles;\n}\n\nexport function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[] {\n if (graph.nodes.size === 0) return [];\n\n const reachable = new Set<string>();\n const startNodes: string[] = [];\n\n if (graph.forwardChain.has(EMPTY_CONTRACT_HASH)) {\n startNodes.push(EMPTY_CONTRACT_HASH);\n } else {\n const allTargets = new Set<string>();\n for (const edges of graph.forwardChain.values()) {\n for (const edge of edges) {\n allTargets.add(edge.to);\n }\n }\n for (const node of graph.nodes) {\n if (!allTargets.has(node)) {\n startNodes.push(node);\n }\n }\n }\n\n for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) {\n reachable.add(step.node);\n }\n\n const orphans: MigrationEdge[] = [];\n for (const [from, migrations] of graph.forwardChain) {\n if (!reachable.has(from)) {\n orphans.push(...migrations);\n }\n }\n\n return orphans;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as MigrationMetadata } from "../metadata-DDa5L-uD.mjs";
|
|
2
2
|
import { ControlStack, MigrationPlan, MigrationPlanOperation } from "@prisma-next/framework-components/control";
|
|
3
3
|
|
|
4
4
|
//#region src/migration-base.d.ts
|
|
@@ -13,7 +13,7 @@ interface MigrationMeta {
|
|
|
13
13
|
*
|
|
14
14
|
* A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
|
|
15
15
|
* runner can consume it directly via `targetId`, `operations`, `origin`, and
|
|
16
|
-
* `destination`. The
|
|
16
|
+
* `destination`. The metadata-shaped inputs come from `describe()`, which
|
|
17
17
|
* every migration must implement — `migration.json` is required for a
|
|
18
18
|
* migration to be valid.
|
|
19
19
|
*/
|
|
@@ -61,27 +61,30 @@ declare function isDirectEntrypoint(importMetaUrl: string): boolean;
|
|
|
61
61
|
declare function printMigrationHelp(): void;
|
|
62
62
|
/**
|
|
63
63
|
* In-memory artifacts produced from a `Migration` instance: the
|
|
64
|
-
* serialized `ops.json` body, the `migration.json`
|
|
64
|
+
* serialized `ops.json` body, the `migration.json` metadata object, and
|
|
65
65
|
* its serialized form. Returned by `buildMigrationArtifacts` so callers
|
|
66
66
|
* (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can
|
|
67
67
|
* decide how to persist them — write to disk, print in dry-run, ship
|
|
68
68
|
* over the wire — without coupling artifact construction to file I/O.
|
|
69
|
+
*
|
|
70
|
+
* `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical
|
|
71
|
+
* on-disk shape that the arktype loader-schema in `./io` validates.
|
|
69
72
|
*/
|
|
70
73
|
interface MigrationArtifacts {
|
|
71
74
|
readonly opsJson: string;
|
|
72
|
-
readonly
|
|
73
|
-
readonly
|
|
75
|
+
readonly metadata: MigrationMetadata;
|
|
76
|
+
readonly metadataJson: string;
|
|
74
77
|
}
|
|
75
78
|
/**
|
|
76
79
|
* Pure conversion from a `Migration` instance (plus the previously
|
|
77
|
-
* scaffolded
|
|
80
|
+
* scaffolded metadata, when one exists on disk) to the in-memory
|
|
78
81
|
* artifacts that downstream tooling persists. Owns metadata validation,
|
|
79
|
-
*
|
|
80
|
-
* content-addressed `
|
|
82
|
+
* metadata synthesis/preservation, hint normalization, and the
|
|
83
|
+
* content-addressed `migrationHash` computation, but performs no file I/O
|
|
81
84
|
* — callers handle reads (to source `existing`) and writes (to persist
|
|
82
|
-
* `opsJson` / `
|
|
85
|
+
* `opsJson` / `metadataJson`).
|
|
83
86
|
*/
|
|
84
|
-
declare function buildMigrationArtifacts(instance: Migration, existing: Partial<
|
|
87
|
+
declare function buildMigrationArtifacts(instance: Migration, existing: Partial<MigrationMetadata> | null): MigrationArtifacts;
|
|
85
88
|
//#endregion
|
|
86
89
|
export { Migration, type MigrationArtifacts, type MigrationMeta, buildMigrationArtifacts, isDirectEntrypoint, printMigrationHelp };
|
|
87
90
|
//# sourceMappingURL=migration.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"sourcesContent":[],"mappings":";;;;UAciB,aAAA;;EAAA,SAAA,EAAA,EAAA,MAAa;EAuBR,SAAA,IAAS,CAAA,EAAA,SAAA,GAAA,UAAA;EACV,SAAA,MAAA,CAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;;;;AAGK,uBAJJ,SAII,CAAA,mBAHL,sBAGK,GAHoB,sBAGpB,EAAA,kBAAA,MAAA,GAAA,MAAA,EAAA,kBAAA,MAAA,GAAA,MAAA,CAAA,YAAb,aAAa,CAAA;EAuDV,kBAAA,QAAkB,EAAA,MAAA;EAWlB;AA4BhB;AAyEA;;;;;;;4BA1J4B,aAAa,WAAW;sBAE9B,aAAa,WAAW;;;;;;;sCAUR;;;;;;uBAOf;;;;;;;;;;;;;;;iBAuBP,kBAAA;iBAWA,kBAAA,CAAA;;;;;;;;;;;;UA4BC,kBAAA;;qBAEI;;;;;;;;;;;;iBAuEL,uBAAA,WACJ,qBACA,QAAQ,4BACjB"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import "../
|
|
2
|
-
import { t as computeMigrationId } from "../attestation-DtF8tEOM.mjs";
|
|
1
|
+
import { t as computeMigrationHash } from "../hash-BNWumjn7.mjs";
|
|
3
2
|
import { type } from "arktype";
|
|
4
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
5
4
|
import { realpathSync } from "node:fs";
|
|
@@ -17,7 +16,7 @@ const MigrationMetaSchema = type({
|
|
|
17
16
|
*
|
|
18
17
|
* A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
|
|
19
18
|
* runner can consume it directly via `targetId`, `operations`, `origin`, and
|
|
20
|
-
* `destination`. The
|
|
19
|
+
* `destination`. The metadata-shaped inputs come from `describe()`, which
|
|
21
20
|
* every migration must implement — `migration.json` is required for a
|
|
22
21
|
* migration to be valid.
|
|
23
22
|
*/
|
|
@@ -74,24 +73,24 @@ function printHelp() {
|
|
|
74
73
|
].join("\n"));
|
|
75
74
|
}
|
|
76
75
|
/**
|
|
77
|
-
* Build the attested
|
|
78
|
-
* operations list, and the previously-scaffolded
|
|
76
|
+
* Build the attested metadata from `describe()`-derived metadata, the
|
|
77
|
+
* operations list, and the previously-scaffolded metadata (if any).
|
|
79
78
|
*
|
|
80
79
|
* When a `migration.json` already exists for this package (the common
|
|
81
80
|
* case: it was scaffolded by `migration plan`), preserve the contract
|
|
82
81
|
* bookends, hints, labels, and `createdAt` set there — those fields are
|
|
83
82
|
* owned by the CLI scaffolder, not the authored class. Only the
|
|
84
83
|
* `describe()`-derived fields (`from`, `to`, `kind`) and the operations
|
|
85
|
-
* change as the author iterates. When no
|
|
84
|
+
* change as the author iterates. When no metadata exists yet (a bare
|
|
86
85
|
* `migration.ts` run from scratch), synthesize a minimal but
|
|
87
|
-
* schema-conformant
|
|
86
|
+
* schema-conformant record so the resulting package can still be read,
|
|
88
87
|
* verified, and applied.
|
|
89
88
|
*
|
|
90
|
-
* The `
|
|
89
|
+
* The `migrationHash` is recomputed against the current metadata + ops so
|
|
91
90
|
* the on-disk artifacts are always fully attested.
|
|
92
91
|
*/
|
|
93
|
-
function
|
|
94
|
-
const
|
|
92
|
+
function buildAttestedMetadata(meta, ops, existing) {
|
|
93
|
+
const baseMetadata = {
|
|
95
94
|
from: meta.from,
|
|
96
95
|
to: meta.to,
|
|
97
96
|
kind: meta.kind ?? "regular",
|
|
@@ -102,15 +101,15 @@ function buildAttestedManifest(meta, ops, existing) {
|
|
|
102
101
|
hints: normalizeHints(existing?.hints),
|
|
103
102
|
...ifDefined("authorship", existing?.authorship)
|
|
104
103
|
};
|
|
105
|
-
const
|
|
104
|
+
const migrationHash = computeMigrationHash(baseMetadata, ops);
|
|
106
105
|
return {
|
|
107
|
-
...
|
|
108
|
-
|
|
106
|
+
...baseMetadata,
|
|
107
|
+
migrationHash
|
|
109
108
|
};
|
|
110
109
|
}
|
|
111
110
|
/**
|
|
112
111
|
* Project `existing.hints` down to the known `MigrationHints` shape, dropping
|
|
113
|
-
* any legacy keys that may linger in
|
|
112
|
+
* any legacy keys that may linger in metadata scaffolded by older CLI
|
|
114
113
|
* versions (e.g. `planningStrategy`). Picking fields explicitly instead of
|
|
115
114
|
* spreading keeps refreshed `migration.json` files schema-clean regardless
|
|
116
115
|
* of what was on disk before.
|
|
@@ -124,23 +123,23 @@ function normalizeHints(existing) {
|
|
|
124
123
|
}
|
|
125
124
|
/**
|
|
126
125
|
* Pure conversion from a `Migration` instance (plus the previously
|
|
127
|
-
* scaffolded
|
|
126
|
+
* scaffolded metadata, when one exists on disk) to the in-memory
|
|
128
127
|
* artifacts that downstream tooling persists. Owns metadata validation,
|
|
129
|
-
*
|
|
130
|
-
* content-addressed `
|
|
128
|
+
* metadata synthesis/preservation, hint normalization, and the
|
|
129
|
+
* content-addressed `migrationHash` computation, but performs no file I/O
|
|
131
130
|
* — callers handle reads (to source `existing`) and writes (to persist
|
|
132
|
-
* `opsJson` / `
|
|
131
|
+
* `opsJson` / `metadataJson`).
|
|
133
132
|
*/
|
|
134
133
|
function buildMigrationArtifacts(instance, existing) {
|
|
135
134
|
const ops = instance.operations;
|
|
136
135
|
if (!Array.isArray(ops)) throw new Error("operations must be an array");
|
|
137
136
|
const parsed = MigrationMetaSchema(instance.describe());
|
|
138
137
|
if (parsed instanceof type.errors) throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
|
|
139
|
-
const
|
|
138
|
+
const metadata = buildAttestedMetadata(parsed, ops, existing);
|
|
140
139
|
return {
|
|
141
140
|
opsJson: JSON.stringify(ops, null, 2),
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
metadata,
|
|
142
|
+
metadataJson: JSON.stringify(metadata, null, 2)
|
|
144
143
|
};
|
|
145
144
|
}
|
|
146
145
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.mjs","names":["baseManifest: Omit<MigrationManifest, 'migrationId'>"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n ControlStack,\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { type } from 'arktype';\nimport { computeMigrationId } from './attestation';\nimport type { MigrationHints, MigrationManifest, MigrationOps } from './types';\n\nexport interface MigrationMeta {\n readonly from: string;\n readonly to: string;\n readonly kind?: 'regular' | 'baseline';\n readonly labels?: readonly string[];\n}\n\nconst MigrationMetaSchema = type({\n from: 'string',\n to: 'string',\n 'kind?': \"'regular' | 'baseline'\",\n 'labels?': type('string').array(),\n});\n\n/**\n * Base class for migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The manifest-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<\n TOperation extends MigrationPlanOperation = MigrationPlanOperation,\n TFamilyId extends string = string,\n TTargetId extends string = string,\n> implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Assembled `ControlStack` injected by the orchestrator (`runMigration`).\n *\n * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their\n * adapter once per instance. Optional at the abstract level so unit tests can\n * construct `Migration` instances purely for `operations` / `describe`\n * assertions without needing a real stack; concrete subclasses that need the\n * stack at runtime should narrow the parameter to required.\n */\n protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;\n\n constructor(stack?: ControlStack<TFamilyId, TTargetId>) {\n this.stack = stack;\n }\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access.\n */\n abstract get operations(): readonly TOperation[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n // An empty `from` represents a migration with no prior origin (e.g.\n // initial baseline, or an in-process plan that was never persisted).\n // Surface that as a null origin so runners treat the plan as\n // origin-less rather than matching against an empty storage hash.\n return from === '' ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n}\n\n/**\n * Returns true when `import.meta.url` resolves to the same file that was\n * invoked as the node entrypoint (`process.argv[1]`). Used by\n * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when\n * the migration module is being imported (e.g. by another script) rather\n * than executed directly.\n */\nexport function isDirectEntrypoint(importMetaUrl: string): boolean {\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return false;\n }\n}\n\nexport function printMigrationHelp(): void {\n printHelp();\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'Usage: node <migration-file> [options]',\n '',\n 'Options:',\n ' --dry-run Print operations to stdout without writing files',\n ' --help Show this help message',\n '',\n ].join('\\n'),\n );\n}\n\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` manifest object, and\n * its serialized form. Returned by `buildMigrationArtifacts` so callers\n * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can\n * decide how to persist them — write to disk, print in dry-run, ship\n * over the wire — without coupling artifact construction to file I/O.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly manifest: MigrationManifest;\n readonly manifestJson: string;\n}\n\n/**\n * Build the attested manifest from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded manifest (if any).\n *\n * When a `migration.json` already exists for this package (the common\n * case: it was scaffolded by `migration plan`), preserve the contract\n * bookends, hints, labels, and `createdAt` set there — those fields are\n * owned by the CLI scaffolder, not the authored class. Only the\n * `describe()`-derived fields (`from`, `to`, `kind`) and the operations\n * change as the author iterates. When no manifest exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant manifest so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationId` is recomputed against the current manifest + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedManifest(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationManifest> | null,\n): MigrationManifest {\n const baseManifest: Omit<MigrationManifest, 'migrationId'> = {\n from: meta.from,\n to: meta.to,\n kind: meta.kind ?? 'regular',\n labels: meta.labels ?? existing?.labels ?? [],\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded manifest exists we synthesize a minimal contract\n // stub so the package is still readable end-to-end. The cast is\n // intentional: only the storage bookend matters for hash computation\n // (everything else is stripped by `computeMigrationId`), and a real\n // contract bookend would only be available after `migration plan`.\n toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),\n hints: normalizeHints(existing?.hints),\n ...ifDefined('authorship', existing?.authorship),\n };\n\n const migrationId = computeMigrationId(baseManifest, ops);\n return { ...baseManifest, migrationId };\n}\n\n/**\n * Project `existing.hints` down to the known `MigrationHints` shape, dropping\n * any legacy keys that may linger in manifests scaffolded by older CLI\n * versions (e.g. `planningStrategy`). Picking fields explicitly instead of\n * spreading keeps refreshed `migration.json` files schema-clean regardless\n * of what was on disk before.\n */\nfunction normalizeHints(existing: MigrationHints | undefined): MigrationHints {\n return {\n used: existing?.used ?? [],\n applied: existing?.applied ?? [],\n plannerVersion: existing?.plannerVersion ?? '2.0.0',\n };\n}\n\n/**\n * Pure conversion from a `Migration` instance (plus the previously\n * scaffolded manifest, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * manifest synthesis/preservation, hint normalization, and the\n * content-addressed `migrationId` computation, but performs no file I/O\n * — callers handle reads (to source `existing`) and writes (to persist\n * `opsJson` / `manifestJson`).\n */\nexport function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationManifest> | null,\n): MigrationArtifacts {\n const ops = instance.operations;\n if (!Array.isArray(ops)) {\n throw new Error('operations must be an array');\n }\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const manifest = buildAttestedManifest(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n manifest,\n manifestJson: JSON.stringify(manifest, null, 2),\n };\n}\n"],"mappings":";;;;;;;;AAoBA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,SAAS;CACT,WAAW,KAAK,SAAS,CAAC,OAAO;CAClC,CAAC;;;;;;;;;;AAWF,IAAsB,YAAtB,MAKA;;;;;;;;;;CAYE,AAAmB;CAEnB,YAAY,OAA4C;AACtD,OAAK,QAAQ;;CAkBf,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,UAAU,CAAC;AAK7B,SAAO,SAAS,KAAK,OAAO,EAAE,aAAa,MAAM;;CAGnD,IAAI,cAAgD;AAClD,SAAO,EAAE,aAAa,KAAK,UAAU,CAAC,IAAI;;;;;;;;;;AAW9C,SAAgB,mBAAmB,eAAgC;CACjE,MAAM,eAAe,cAAc,cAAc;CACjD,MAAM,QAAQ,QAAQ,KAAK;AAC3B,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI;AACF,SAAO,aAAa,aAAa,KAAK,aAAa,MAAM;SACnD;AACN,SAAO;;;AAIX,SAAgB,qBAA2B;AACzC,YAAW;;AAGb,SAAS,YAAkB;AACzB,SAAQ,OAAO,MACb;EACE;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,CACb;;;;;;;;;;;;;;;;;;;AAkCH,SAAS,sBACP,MACA,KACA,UACmB;CACnB,MAAMA,eAAuD;EAC3D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,UAAU,UAAU,EAAE;EAC7C,WAAW,UAAU,8BAAa,IAAI,MAAM,EAAC,aAAa;EAC1D,cAAc,UAAU,gBAAgB;EAMxC,YAAY,UAAU,cAAe,EAAE,SAAS,EAAE,aAAa,KAAK,IAAI,EAAE;EAC1E,OAAO,eAAe,UAAU,MAAM;EACtC,GAAG,UAAU,cAAc,UAAU,WAAW;EACjD;CAED,MAAM,cAAc,mBAAmB,cAAc,IAAI;AACzD,QAAO;EAAE,GAAG;EAAc;EAAa;;;;;;;;;AAUzC,SAAS,eAAe,UAAsD;AAC5E,QAAO;EACL,MAAM,UAAU,QAAQ,EAAE;EAC1B,SAAS,UAAU,WAAW,EAAE;EAChC,gBAAgB,UAAU,kBAAkB;EAC7C;;;;;;;;;;;AAYH,SAAgB,wBACd,UACA,UACoB;CACpB,MAAM,MAAM,SAAS;AACrB,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,8BAA8B;CAIhD,MAAM,SAAS,oBADU,SAAS,UAAU,CACD;AAC3C,KAAI,kBAAkB,KAAK,OACzB,OAAM,IAAI,MAAM,yCAAyC,OAAO,UAAU;CAG5E,MAAM,WAAW,sBAAsB,QAAQ,KAAK,SAAS;AAE7D,QAAO;EACL,SAAS,KAAK,UAAU,KAAK,MAAM,EAAE;EACrC;EACA,cAAc,KAAK,UAAU,UAAU,MAAM,EAAE;EAChD"}
|
|
1
|
+
{"version":3,"file":"migration.mjs","names":["baseMetadata: Omit<MigrationMetadata, 'migrationHash'>"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n ControlStack,\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { type } from 'arktype';\nimport { computeMigrationHash } from './hash';\nimport type { MigrationHints, MigrationMetadata } from './metadata';\nimport type { MigrationOps } from './package';\n\nexport interface MigrationMeta {\n readonly from: string;\n readonly to: string;\n readonly kind?: 'regular' | 'baseline';\n readonly labels?: readonly string[];\n}\n\nconst MigrationMetaSchema = type({\n from: 'string',\n to: 'string',\n 'kind?': \"'regular' | 'baseline'\",\n 'labels?': type('string').array(),\n});\n\n/**\n * Base class for migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The metadata-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<\n TOperation extends MigrationPlanOperation = MigrationPlanOperation,\n TFamilyId extends string = string,\n TTargetId extends string = string,\n> implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Assembled `ControlStack` injected by the orchestrator (`runMigration`).\n *\n * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their\n * adapter once per instance. Optional at the abstract level so unit tests can\n * construct `Migration` instances purely for `operations` / `describe`\n * assertions without needing a real stack; concrete subclasses that need the\n * stack at runtime should narrow the parameter to required.\n */\n protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;\n\n constructor(stack?: ControlStack<TFamilyId, TTargetId>) {\n this.stack = stack;\n }\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access.\n */\n abstract get operations(): readonly TOperation[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n // An empty `from` represents a migration with no prior origin (e.g.\n // initial baseline, or an in-process plan that was never persisted).\n // Surface that as a null origin so runners treat the plan as\n // origin-less rather than matching against an empty storage hash.\n return from === '' ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n}\n\n/**\n * Returns true when `import.meta.url` resolves to the same file that was\n * invoked as the node entrypoint (`process.argv[1]`). Used by\n * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when\n * the migration module is being imported (e.g. by another script) rather\n * than executed directly.\n */\nexport function isDirectEntrypoint(importMetaUrl: string): boolean {\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return false;\n }\n}\n\nexport function printMigrationHelp(): void {\n printHelp();\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'Usage: node <migration-file> [options]',\n '',\n 'Options:',\n ' --dry-run Print operations to stdout without writing files',\n ' --help Show this help message',\n '',\n ].join('\\n'),\n );\n}\n\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` metadata object, and\n * its serialized form. Returned by `buildMigrationArtifacts` so callers\n * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can\n * decide how to persist them — write to disk, print in dry-run, ship\n * over the wire — without coupling artifact construction to file I/O.\n *\n * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical\n * on-disk shape that the arktype loader-schema in `./io` validates.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly metadata: MigrationMetadata;\n readonly metadataJson: string;\n}\n\n/**\n * Build the attested metadata from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded metadata (if any).\n *\n * When a `migration.json` already exists for this package (the common\n * case: it was scaffolded by `migration plan`), preserve the contract\n * bookends, hints, labels, and `createdAt` set there — those fields are\n * owned by the CLI scaffolder, not the authored class. Only the\n * `describe()`-derived fields (`from`, `to`, `kind`) and the operations\n * change as the author iterates. When no metadata exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant record so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationHash` is recomputed against the current metadata + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedMetadata(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationMetadata> | null,\n): MigrationMetadata {\n const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {\n from: meta.from,\n to: meta.to,\n kind: meta.kind ?? 'regular',\n labels: meta.labels ?? existing?.labels ?? [],\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded metadata exists we synthesize a minimal contract\n // stub so the package is still readable end-to-end. The cast is\n // intentional: only the storage bookend matters for hash computation\n // (everything else is stripped by `computeMigrationHash`), and a real\n // contract bookend would only be available after `migration plan`.\n toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),\n hints: normalizeHints(existing?.hints),\n ...ifDefined('authorship', existing?.authorship),\n };\n\n const migrationHash = computeMigrationHash(baseMetadata, ops);\n return { ...baseMetadata, migrationHash };\n}\n\n/**\n * Project `existing.hints` down to the known `MigrationHints` shape, dropping\n * any legacy keys that may linger in metadata scaffolded by older CLI\n * versions (e.g. `planningStrategy`). Picking fields explicitly instead of\n * spreading keeps refreshed `migration.json` files schema-clean regardless\n * of what was on disk before.\n */\nfunction normalizeHints(existing: MigrationHints | undefined): MigrationHints {\n return {\n used: existing?.used ?? [],\n applied: existing?.applied ?? [],\n plannerVersion: existing?.plannerVersion ?? '2.0.0',\n };\n}\n\n/**\n * Pure conversion from a `Migration` instance (plus the previously\n * scaffolded metadata, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * metadata synthesis/preservation, hint normalization, and the\n * content-addressed `migrationHash` computation, but performs no file I/O\n * — callers handle reads (to source `existing`) and writes (to persist\n * `opsJson` / `metadataJson`).\n */\nexport function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationMetadata> | null,\n): MigrationArtifacts {\n const ops = instance.operations;\n if (!Array.isArray(ops)) {\n throw new Error('operations must be an array');\n }\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const metadata = buildAttestedMetadata(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n metadata,\n metadataJson: JSON.stringify(metadata, null, 2),\n };\n}\n"],"mappings":";;;;;;;AAqBA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,SAAS;CACT,WAAW,KAAK,SAAS,CAAC,OAAO;CAClC,CAAC;;;;;;;;;;AAWF,IAAsB,YAAtB,MAKA;;;;;;;;;;CAYE,AAAmB;CAEnB,YAAY,OAA4C;AACtD,OAAK,QAAQ;;CAkBf,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,UAAU,CAAC;AAK7B,SAAO,SAAS,KAAK,OAAO,EAAE,aAAa,MAAM;;CAGnD,IAAI,cAAgD;AAClD,SAAO,EAAE,aAAa,KAAK,UAAU,CAAC,IAAI;;;;;;;;;;AAW9C,SAAgB,mBAAmB,eAAgC;CACjE,MAAM,eAAe,cAAc,cAAc;CACjD,MAAM,QAAQ,QAAQ,KAAK;AAC3B,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI;AACF,SAAO,aAAa,aAAa,KAAK,aAAa,MAAM;SACnD;AACN,SAAO;;;AAIX,SAAgB,qBAA2B;AACzC,YAAW;;AAGb,SAAS,YAAkB;AACzB,SAAQ,OAAO,MACb;EACE;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,CACb;;;;;;;;;;;;;;;;;;;AAqCH,SAAS,sBACP,MACA,KACA,UACmB;CACnB,MAAMA,eAAyD;EAC7D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,UAAU,UAAU,EAAE;EAC7C,WAAW,UAAU,8BAAa,IAAI,MAAM,EAAC,aAAa;EAC1D,cAAc,UAAU,gBAAgB;EAMxC,YAAY,UAAU,cAAe,EAAE,SAAS,EAAE,aAAa,KAAK,IAAI,EAAE;EAC1E,OAAO,eAAe,UAAU,MAAM;EACtC,GAAG,UAAU,cAAc,UAAU,WAAW;EACjD;CAED,MAAM,gBAAgB,qBAAqB,cAAc,IAAI;AAC7D,QAAO;EAAE,GAAG;EAAc;EAAe;;;;;;;;;AAU3C,SAAS,eAAe,UAAsD;AAC5E,QAAO;EACL,MAAM,UAAU,QAAQ,EAAE;EAC1B,SAAS,UAAU,WAAW,EAAE;EAChC,gBAAgB,UAAU,kBAAkB;EAC7C;;;;;;;;;;;AAYH,SAAgB,wBACd,UACA,UACoB;CACpB,MAAM,MAAM,SAAS;AACrB,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,8BAA8B;CAIhD,MAAM,SAAS,oBADU,SAAS,UAAU,CACD;AAC3C,KAAI,kBAAkB,KAAK,OACzB,OAAM,IAAI,MAAM,yCAAyC,OAAO,UAAU;CAG5E,MAAM,WAAW,sBAAsB,QAAQ,KAAK,SAAS;AAE7D,QAAO;EACL,SAAS,KAAK,UAAU,KAAK,MAAM,EAAE;EACrC;EACA,cAAc,KAAK,UAAU,UAAU,MAAM,EAAE;EAChD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/exports/refs.d.mts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
//#region src/refs.d.ts
|
|
2
|
-
|
|
2
|
+
interface RefEntry {
|
|
3
|
+
readonly hash: string;
|
|
4
|
+
readonly invariants: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
type Refs = Readonly<Record<string, RefEntry>>;
|
|
3
7
|
declare function validateRefName(name: string): boolean;
|
|
4
8
|
declare function validateRefValue(value: string): boolean;
|
|
5
|
-
declare function
|
|
6
|
-
declare function
|
|
7
|
-
declare function
|
|
9
|
+
declare function readRef(refsDir: string, name: string): Promise<RefEntry>;
|
|
10
|
+
declare function readRefs(refsDir: string): Promise<Refs>;
|
|
11
|
+
declare function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void>;
|
|
12
|
+
declare function deleteRef(refsDir: string, name: string): Promise<void>;
|
|
13
|
+
declare function resolveRef(refs: Refs, name: string): RefEntry;
|
|
8
14
|
//#endregion
|
|
9
|
-
export { type Refs, readRefs, resolveRef, validateRefName, validateRefValue,
|
|
15
|
+
export { type RefEntry, type Refs, deleteRef, readRef, readRefs, resolveRef, validateRefName, validateRefValue, writeRef };
|
|
10
16
|
//# sourceMappingURL=refs.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refs.d.mts","names":[],"sources":["../../src/refs.ts"],"sourcesContent":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"refs.d.mts","names":[],"sources":["../../src/refs.ts"],"sourcesContent":[],"mappings":";UAUiB,QAAA;EAAA,SAAA,IAAQ,EAAA,MAAA;EAKb,SAAI,UAAA,EAAA,SAAA,MAAA,EAAA;;AAAY,KAAhB,IAAA,GAAO,QAAS,CAAA,MAAA,CAAA,MAAA,EAAe,QAAf,CAAA,CAAA;AAAT,iBAKH,eAAA,CALG,IAAA,EAAA,MAAA,CAAA,EAAA,OAAA;AAAQ,iBAaX,gBAAA,CAbW,KAAA,EAAA,MAAA,CAAA,EAAA,OAAA;AAKX,iBA8BM,OAAA,CA9BS,OAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,CAAA,EA8B+B,OA9B/B,CA8BuC,QA9BvC,CAAA;AAQf,iBAyDM,QAAA,CAzDU,OAAA,EAAA,MAAA,CAAA,EAyDiB,OAzDjB,CAyDyB,IAzDzB,CAAA;AAsBV,iBAsFA,QAAA,CAtFgD,OAAR,EAAA,MAAO,EAAA,IAAA,EAAA,MAAA,EAAA,KAAA,EAsFA,QAtFA,CAAA,EAsFW,OAtFX,CAAA,IAAA,CAAA;AAmC/C,iBAuEA,SAAA,CAvE2B,OAAO,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,CAAA,EAuEQ,OAvER,CAAA,IAAA,CAAA;AAmDlC,iBA0DN,UAAA,CA1DqD,IAAW,EA0D/C,IA1DsD,EAAA,IAAA,EAAA,MAAA,CAAA,EA0DjC,QA1DiC"}
|
package/dist/exports/refs.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import {
|
|
1
|
+
import { c as errorInvalidRefFile, l as errorInvalidRefName, t as MigrationToolsError, u as errorInvalidRefValue } from "../errors-CZ9JD4sd.mjs";
|
|
2
|
+
import { dirname, join, relative } from "pathe";
|
|
3
|
+
import { mkdir, readFile, readdir, rename, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
3
4
|
import { type } from "arktype";
|
|
4
|
-
import { dirname, join } from "pathe";
|
|
5
5
|
|
|
6
6
|
//#region src/refs.ts
|
|
7
7
|
const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
|
|
@@ -16,58 +16,134 @@ function validateRefName(name) {
|
|
|
16
16
|
function validateRefValue(value) {
|
|
17
17
|
return REF_VALUE_PATTERN.test(value);
|
|
18
18
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
19
|
+
const RefEntrySchema = type({
|
|
20
|
+
hash: "string",
|
|
21
|
+
invariants: "string[]"
|
|
22
|
+
}).narrow((entry, ctx) => {
|
|
23
|
+
if (!validateRefValue(entry.hash)) return ctx.mustBe(`a valid contract hash (got "${entry.hash}")`);
|
|
24
24
|
return true;
|
|
25
25
|
});
|
|
26
|
-
|
|
26
|
+
function refFilePath(refsDir, name) {
|
|
27
|
+
return join(refsDir, `${name}.json`);
|
|
28
|
+
}
|
|
29
|
+
function refNameFromPath(refsDir, filePath) {
|
|
30
|
+
return relative(refsDir, filePath).replace(/\.json$/, "");
|
|
31
|
+
}
|
|
32
|
+
async function readRef(refsDir, name) {
|
|
33
|
+
if (!validateRefName(name)) throw errorInvalidRefName(name);
|
|
34
|
+
const filePath = refFilePath(refsDir, name);
|
|
27
35
|
let raw;
|
|
28
36
|
try {
|
|
29
|
-
raw = await readFile(
|
|
37
|
+
raw = await readFile(filePath, "utf-8");
|
|
30
38
|
} catch (error) {
|
|
31
|
-
if (error instanceof Error && error.code === "ENOENT")
|
|
39
|
+
if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
|
|
40
|
+
why: `No ref file found at "${filePath}".`,
|
|
41
|
+
fix: `Create the ref with: prisma-next migration ref set ${name} <hash>`,
|
|
42
|
+
details: {
|
|
43
|
+
refName: name,
|
|
44
|
+
filePath
|
|
45
|
+
}
|
|
46
|
+
});
|
|
32
47
|
throw error;
|
|
33
48
|
}
|
|
34
49
|
let parsed;
|
|
35
50
|
try {
|
|
36
51
|
parsed = JSON.parse(raw);
|
|
37
52
|
} catch {
|
|
38
|
-
throw
|
|
53
|
+
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
|
|
39
54
|
}
|
|
40
|
-
const result =
|
|
41
|
-
if (result instanceof type.errors) throw
|
|
55
|
+
const result = RefEntrySchema(parsed);
|
|
56
|
+
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
|
|
42
57
|
return result;
|
|
43
58
|
}
|
|
44
|
-
async function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
async function readRefs(refsDir) {
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = await readdir(refsDir, {
|
|
63
|
+
recursive: true,
|
|
64
|
+
encoding: "utf-8"
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof Error && error.code === "ENOENT") return {};
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const jsonFiles = entries.filter((entry) => entry.endsWith(".json"));
|
|
71
|
+
const refs = {};
|
|
72
|
+
for (const jsonFile of jsonFiles) {
|
|
73
|
+
const filePath = join(refsDir, jsonFile);
|
|
74
|
+
const name = refNameFromPath(refsDir, filePath);
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = await readFile(filePath, "utf-8");
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
80
|
+
if (code === "ENOENT" || code === "EISDIR") continue;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(raw);
|
|
86
|
+
} catch {
|
|
87
|
+
throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
|
|
88
|
+
}
|
|
89
|
+
const result = RefEntrySchema(parsed);
|
|
90
|
+
if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
|
|
91
|
+
refs[name] = result;
|
|
48
92
|
}
|
|
49
|
-
|
|
50
|
-
|
|
93
|
+
return refs;
|
|
94
|
+
}
|
|
95
|
+
async function writeRef(refsDir, name, entry) {
|
|
96
|
+
if (!validateRefName(name)) throw errorInvalidRefName(name);
|
|
97
|
+
if (!validateRefValue(entry.hash)) throw errorInvalidRefValue(entry.hash);
|
|
98
|
+
const filePath = refFilePath(refsDir, name);
|
|
99
|
+
const dir = dirname(filePath);
|
|
51
100
|
await mkdir(dir, { recursive: true });
|
|
52
|
-
const tmpPath = join(dir,
|
|
53
|
-
await writeFile(tmpPath, `${JSON.stringify(
|
|
54
|
-
|
|
101
|
+
const tmpPath = join(dir, `.${name.split("/").pop()}.json.${Date.now()}.tmp`);
|
|
102
|
+
await writeFile(tmpPath, `${JSON.stringify({
|
|
103
|
+
hash: entry.hash,
|
|
104
|
+
invariants: [...entry.invariants]
|
|
105
|
+
}, null, 2)}\n`);
|
|
106
|
+
await rename(tmpPath, filePath);
|
|
107
|
+
}
|
|
108
|
+
async function deleteRef(refsDir, name) {
|
|
109
|
+
if (!validateRefName(name)) throw errorInvalidRefName(name);
|
|
110
|
+
const filePath = refFilePath(refsDir, name);
|
|
111
|
+
try {
|
|
112
|
+
await unlink(filePath);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
|
|
115
|
+
why: `No ref file found at "${filePath}".`,
|
|
116
|
+
fix: "Run `prisma-next migration ref list` to see available refs.",
|
|
117
|
+
details: {
|
|
118
|
+
refName: name,
|
|
119
|
+
filePath
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
let dir = dirname(filePath);
|
|
125
|
+
while (dir !== refsDir && dir.startsWith(refsDir)) try {
|
|
126
|
+
await rmdir(dir);
|
|
127
|
+
dir = dirname(dir);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
130
|
+
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT") break;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
55
133
|
}
|
|
56
134
|
function resolveRef(refs, name) {
|
|
57
135
|
if (!validateRefName(name)) throw errorInvalidRefName(name);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: set the "${name}" key in migrations/refs.json.`,
|
|
136
|
+
if (!Object.hasOwn(refs, name)) throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
|
|
137
|
+
why: `No ref named "${name}" exists.`,
|
|
138
|
+
fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: prisma-next migration ref set ${name} <hash>`,
|
|
62
139
|
details: {
|
|
63
140
|
refName: name,
|
|
64
141
|
availableRefs: Object.keys(refs)
|
|
65
142
|
}
|
|
66
143
|
});
|
|
67
|
-
|
|
68
|
-
return hash;
|
|
144
|
+
return refs[name];
|
|
69
145
|
}
|
|
70
146
|
|
|
71
147
|
//#endregion
|
|
72
|
-
export { readRefs, resolveRef, validateRefName, validateRefValue,
|
|
148
|
+
export { deleteRef, readRef, readRefs, resolveRef, validateRefName, validateRefValue, writeRef };
|
|
73
149
|
//# sourceMappingURL=refs.mjs.map
|