@prisma-next/migration-tools 0.4.0-dev.9 → 0.5.0-dev.1
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 +1 -1
- package/dist/{attestation-DnebS4XZ.mjs → attestation-DtF8tEOM.mjs} +24 -23
- package/dist/attestation-DtF8tEOM.mjs.map +1 -0
- package/dist/{errors-C_XuSbX7.mjs → errors-BKbRGCJM.mjs} +9 -2
- package/dist/errors-BKbRGCJM.mjs.map +1 -0
- package/dist/exports/attestation.d.mts +20 -6
- package/dist/exports/attestation.d.mts.map +1 -1
- package/dist/exports/attestation.mjs +3 -3
- package/dist/exports/dag.d.mts +8 -6
- package/dist/exports/dag.d.mts.map +1 -1
- package/dist/exports/dag.mjs +181 -107
- package/dist/exports/dag.mjs.map +1 -1
- package/dist/exports/io.d.mts +16 -13
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +2 -2
- package/dist/exports/migration-ts.d.mts +15 -21
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs +28 -36
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +48 -18
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +75 -85
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/refs.mjs +1 -1
- package/dist/exports/types.d.mts +2 -2
- package/dist/exports/types.mjs +2 -16
- package/dist/{io-Cun81AIZ.mjs → io-CCnYsUHU.mjs} +18 -22
- package/dist/io-CCnYsUHU.mjs.map +1 -0
- package/dist/types-DyGXcWWp.d.mts +71 -0
- package/dist/types-DyGXcWWp.d.mts.map +1 -0
- package/package.json +5 -4
- package/src/attestation.ts +34 -26
- package/src/dag.ts +140 -154
- package/src/errors.ts +8 -0
- package/src/exports/attestation.ts +2 -1
- package/src/exports/io.ts +1 -1
- package/src/exports/migration-ts.ts +1 -1
- package/src/exports/migration.ts +8 -1
- package/src/exports/types.ts +2 -8
- package/src/graph-ops.ts +65 -0
- package/src/io.ts +23 -24
- package/src/migration-base.ts +99 -101
- package/src/migration-ts.ts +28 -50
- package/src/queue.ts +37 -0
- package/src/types.ts +15 -55
- package/dist/attestation-DnebS4XZ.mjs.map +0 -1
- package/dist/errors-C_XuSbX7.mjs.map +0 -1
- package/dist/exports/types.mjs.map +0 -1
- package/dist/io-Cun81AIZ.mjs.map +0 -1
- package/dist/types-D2uX4ql7.d.mts +0 -100
- package/dist/types-D2uX4ql7.d.mts.map +0 -1
package/src/dag.ts
CHANGED
|
@@ -7,9 +7,30 @@ import {
|
|
|
7
7
|
errorNoTarget,
|
|
8
8
|
errorSameSourceAndTarget,
|
|
9
9
|
} from './errors';
|
|
10
|
-
import
|
|
10
|
+
import { bfs } from './graph-ops';
|
|
11
|
+
import type { MigrationBundle, MigrationChainEntry, MigrationGraph } from './types';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/** Forward-edge neighbours for BFS: edge `e` from `n` visits `e.to` next. */
|
|
14
|
+
function forwardNeighbours(graph: MigrationGraph, node: string) {
|
|
15
|
+
return (graph.forwardChain.get(node) ?? []).map((edge) => ({ next: edge.to, edge }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reverse-edge neighbours for BFS: edge `e` from `n` visits `e.from` next. */
|
|
19
|
+
function reverseNeighbours(graph: MigrationGraph, node: string) {
|
|
20
|
+
return (graph.reverseChain.get(node) ?? []).map((edge) => ({ next: edge.from, edge }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function appendEdge(
|
|
24
|
+
map: Map<string, MigrationChainEntry[]>,
|
|
25
|
+
key: string,
|
|
26
|
+
entry: MigrationChainEntry,
|
|
27
|
+
): void {
|
|
28
|
+
const bucket = map.get(key);
|
|
29
|
+
if (bucket) bucket.push(entry);
|
|
30
|
+
else map.set(key, [entry]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function reconstructGraph(packages: readonly MigrationBundle[]): MigrationGraph {
|
|
13
34
|
const nodes = new Set<string>();
|
|
14
35
|
const forwardChain = new Map<string, MigrationChainEntry[]>();
|
|
15
36
|
const reverseChain = new Map<string, MigrationChainEntry[]>();
|
|
@@ -34,31 +55,24 @@ export function reconstructGraph(packages: readonly AttestedMigrationBundle[]):
|
|
|
34
55
|
labels: pkg.manifest.labels,
|
|
35
56
|
};
|
|
36
57
|
|
|
37
|
-
if (migration.migrationId
|
|
38
|
-
|
|
39
|
-
throw errorDuplicateMigrationId(migration.migrationId);
|
|
40
|
-
}
|
|
41
|
-
migrationById.set(migration.migrationId, migration);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const fwd = forwardChain.get(from);
|
|
45
|
-
if (fwd) {
|
|
46
|
-
fwd.push(migration);
|
|
47
|
-
} else {
|
|
48
|
-
forwardChain.set(from, [migration]);
|
|
58
|
+
if (migrationById.has(migration.migrationId)) {
|
|
59
|
+
throw errorDuplicateMigrationId(migration.migrationId);
|
|
49
60
|
}
|
|
61
|
+
migrationById.set(migration.migrationId, migration);
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
rev.push(migration);
|
|
54
|
-
} else {
|
|
55
|
-
reverseChain.set(to, [migration]);
|
|
56
|
-
}
|
|
63
|
+
appendEdge(forwardChain, from, migration);
|
|
64
|
+
appendEdge(reverseChain, to, migration);
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
return { nodes, forwardChain, reverseChain, migrationById };
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Deterministic tie-breaking for BFS neighbour order.
|
|
72
|
+
// Used by `findPath` and `findPathWithDecision` only; not a general-purpose
|
|
73
|
+
// utility. Ordering: label priority → createdAt → to → migrationId.
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
62
76
|
const LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };
|
|
63
77
|
|
|
64
78
|
function labelPriority(labels: readonly string[]): number {
|
|
@@ -70,16 +84,25 @@ function labelPriority(labels: readonly string[]): number {
|
|
|
70
84
|
return best;
|
|
71
85
|
}
|
|
72
86
|
|
|
87
|
+
function compareTieBreak(a: MigrationChainEntry, b: MigrationChainEntry): number {
|
|
88
|
+
const lp = labelPriority(a.labels) - labelPriority(b.labels);
|
|
89
|
+
if (lp !== 0) return lp;
|
|
90
|
+
const ca = a.createdAt.localeCompare(b.createdAt);
|
|
91
|
+
if (ca !== 0) return ca;
|
|
92
|
+
const tc = a.to.localeCompare(b.to);
|
|
93
|
+
if (tc !== 0) return tc;
|
|
94
|
+
return a.migrationId.localeCompare(b.migrationId);
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
function sortedNeighbors(edges: readonly MigrationChainEntry[]): readonly MigrationChainEntry[] {
|
|
74
|
-
return [...edges].sort(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
98
|
+
return [...edges].sort(compareTieBreak);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Ordering adapter for `bfs` — sorts `{next, edge}` pairs by tie-break. */
|
|
102
|
+
function bfsOrdering(
|
|
103
|
+
items: readonly { next: string; edge: MigrationChainEntry }[],
|
|
104
|
+
): readonly { next: string; edge: MigrationChainEntry }[] {
|
|
105
|
+
return items.slice().sort((a, b) => compareTieBreak(a.edge, b.edge));
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
/**
|
|
@@ -97,44 +120,40 @@ export function findPath(
|
|
|
97
120
|
): readonly MigrationChainEntry[] | null {
|
|
98
121
|
if (fromHash === toHash) return [];
|
|
99
122
|
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const current = queue.shift();
|
|
107
|
-
if (current === undefined) break;
|
|
108
|
-
|
|
109
|
-
if (current === toHash) {
|
|
123
|
+
const parents = new Map<string, { parent: string; edge: MigrationChainEntry }>();
|
|
124
|
+
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n), bfsOrdering)) {
|
|
125
|
+
if (step.parent !== null && step.incomingEdge !== null) {
|
|
126
|
+
parents.set(step.node, { parent: step.parent, edge: step.incomingEdge });
|
|
127
|
+
}
|
|
128
|
+
if (step.node === toHash) {
|
|
110
129
|
const path: MigrationChainEntry[] = [];
|
|
111
|
-
let
|
|
112
|
-
let
|
|
113
|
-
while (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
entry = parent.get(node);
|
|
130
|
+
let cur = toHash;
|
|
131
|
+
let p = parents.get(cur);
|
|
132
|
+
while (p) {
|
|
133
|
+
path.push(p.edge);
|
|
134
|
+
cur = p.parent;
|
|
135
|
+
p = parents.get(cur);
|
|
118
136
|
}
|
|
119
137
|
path.reverse();
|
|
120
138
|
return path;
|
|
121
139
|
}
|
|
122
|
-
|
|
123
|
-
const outgoing = graph.forwardChain.get(current);
|
|
124
|
-
if (!outgoing) continue;
|
|
125
|
-
|
|
126
|
-
for (const edge of sortedNeighbors(outgoing)) {
|
|
127
|
-
if (!visited.has(edge.to)) {
|
|
128
|
-
visited.add(edge.to);
|
|
129
|
-
parent.set(edge.to, { node: current, edge });
|
|
130
|
-
queue.push(edge.to);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
return null;
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Reverse-BFS from `toHash` over `reverseChain` to collect every node from
|
|
147
|
+
* which `toHash` is reachable (inclusive of `toHash` itself).
|
|
148
|
+
*/
|
|
149
|
+
function collectNodesReachingTarget(graph: MigrationGraph, toHash: string): Set<string> {
|
|
150
|
+
const reached = new Set<string>();
|
|
151
|
+
for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) {
|
|
152
|
+
reached.add(step.node);
|
|
153
|
+
}
|
|
154
|
+
return reached;
|
|
155
|
+
}
|
|
156
|
+
|
|
138
157
|
export interface PathDecision {
|
|
139
158
|
readonly selectedPath: readonly MigrationChainEntry[];
|
|
140
159
|
readonly fromHash: string;
|
|
@@ -168,16 +187,18 @@ export function findPathWithDecision(
|
|
|
168
187
|
const path = findPath(graph, fromHash, toHash);
|
|
169
188
|
if (!path) return null;
|
|
170
189
|
|
|
190
|
+
// Single reverse BFS marks every node from which `toHash` is reachable.
|
|
191
|
+
// Replaces a per-edge `findPath(e.to, toHash)` call inside the loop below,
|
|
192
|
+
// which made the whole function O(|path| · (V + E)) instead of O(V + E).
|
|
193
|
+
const reachesTarget = collectNodesReachingTarget(graph, toHash);
|
|
194
|
+
|
|
171
195
|
const tieBreakReasons: string[] = [];
|
|
172
196
|
let alternativeCount = 0;
|
|
173
197
|
|
|
174
198
|
for (const edge of path) {
|
|
175
199
|
const outgoing = graph.forwardChain.get(edge.from);
|
|
176
200
|
if (outgoing && outgoing.length > 1) {
|
|
177
|
-
const reachable = outgoing.filter((e) =>
|
|
178
|
-
const pathFromE = findPath(graph, e.to, toHash);
|
|
179
|
-
return pathFromE !== null || e.to === toHash;
|
|
180
|
-
});
|
|
201
|
+
const reachable = outgoing.filter((e) => reachesTarget.has(e.to));
|
|
181
202
|
if (reachable.length > 1) {
|
|
182
203
|
alternativeCount += reachable.length - 1;
|
|
183
204
|
const sorted = sortedNeighbors(reachable);
|
|
@@ -213,17 +234,8 @@ function findDivergencePoint(
|
|
|
213
234
|
): string {
|
|
214
235
|
const ancestorSets = leaves.map((leaf) => {
|
|
215
236
|
const ancestors = new Set<string>();
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
const current = queue.shift() as string;
|
|
219
|
-
if (ancestors.has(current)) continue;
|
|
220
|
-
ancestors.add(current);
|
|
221
|
-
const incoming = graph.reverseChain.get(current);
|
|
222
|
-
if (incoming) {
|
|
223
|
-
for (const edge of incoming) {
|
|
224
|
-
queue.push(edge.from);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
237
|
+
for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) {
|
|
238
|
+
ancestors.add(step.node);
|
|
227
239
|
}
|
|
228
240
|
return ancestors;
|
|
229
241
|
});
|
|
@@ -250,40 +262,26 @@ function findDivergencePoint(
|
|
|
250
262
|
* `fromHash` via forward edges.
|
|
251
263
|
*/
|
|
252
264
|
export function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[] {
|
|
253
|
-
const visited = new Set<string>();
|
|
254
|
-
const queue: string[] = [fromHash];
|
|
255
|
-
visited.add(fromHash);
|
|
256
265
|
const leaves: string[] = [];
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (current === undefined) break;
|
|
261
|
-
const outgoing = graph.forwardChain.get(current);
|
|
262
|
-
|
|
263
|
-
if (!outgoing || outgoing.length === 0) {
|
|
264
|
-
leaves.push(current);
|
|
265
|
-
} else {
|
|
266
|
-
for (const edge of outgoing) {
|
|
267
|
-
if (!visited.has(edge.to)) {
|
|
268
|
-
visited.add(edge.to);
|
|
269
|
-
queue.push(edge.to);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
266
|
+
for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) {
|
|
267
|
+
if (!graph.forwardChain.get(step.node)?.length) {
|
|
268
|
+
leaves.push(step.node);
|
|
272
269
|
}
|
|
273
270
|
}
|
|
274
|
-
|
|
275
271
|
return leaves;
|
|
276
272
|
}
|
|
277
273
|
|
|
278
274
|
/**
|
|
279
275
|
* Find the target contract hash of the migration graph reachable from
|
|
280
|
-
* EMPTY_CONTRACT_HASH.
|
|
281
|
-
*
|
|
282
|
-
* Throws
|
|
276
|
+
* EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target
|
|
277
|
+
* state (either empty, or containing only the root with no outgoing
|
|
278
|
+
* edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none
|
|
279
|
+
* originate from the empty hash, and AMBIGUOUS_TARGET if multiple
|
|
280
|
+
* branch tips exist.
|
|
283
281
|
*/
|
|
284
|
-
export function findLeaf(graph: MigrationGraph): string {
|
|
282
|
+
export function findLeaf(graph: MigrationGraph): string | null {
|
|
285
283
|
if (graph.nodes.size === 0) {
|
|
286
|
-
return
|
|
284
|
+
return null;
|
|
287
285
|
}
|
|
288
286
|
|
|
289
287
|
if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) {
|
|
@@ -297,7 +295,7 @@ export function findLeaf(graph: MigrationGraph): string {
|
|
|
297
295
|
if (reachable.length > 0) {
|
|
298
296
|
throw errorNoTarget(reachable);
|
|
299
297
|
}
|
|
300
|
-
return
|
|
298
|
+
return null;
|
|
301
299
|
}
|
|
302
300
|
|
|
303
301
|
if (leaves.length > 1) {
|
|
@@ -312,8 +310,8 @@ export function findLeaf(graph: MigrationGraph): string {
|
|
|
312
310
|
throw errorAmbiguousTarget(leaves, { divergencePoint, branches });
|
|
313
311
|
}
|
|
314
312
|
|
|
315
|
-
|
|
316
|
-
return
|
|
313
|
+
// biome-ignore lint/style/noNonNullAssertion: leaves.length is neither 0 nor >1 per the branches above, so exactly one leaf remains
|
|
314
|
+
return leaves[0]!;
|
|
317
315
|
}
|
|
318
316
|
|
|
319
317
|
/**
|
|
@@ -322,21 +320,11 @@ export function findLeaf(graph: MigrationGraph): string {
|
|
|
322
320
|
* Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
|
|
323
321
|
*/
|
|
324
322
|
export function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null {
|
|
325
|
-
if (graph.nodes.size === 0) {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
323
|
const leafHash = findLeaf(graph);
|
|
330
|
-
if (leafHash ===
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
324
|
+
if (leafHash === null) return null;
|
|
333
325
|
|
|
334
326
|
const path = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);
|
|
335
|
-
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return path[path.length - 1] ?? null;
|
|
327
|
+
return path?.at(-1) ?? null;
|
|
340
328
|
}
|
|
341
329
|
|
|
342
330
|
export function detectCycles(graph: MigrationGraph): readonly string[][] {
|
|
@@ -352,37 +340,50 @@ export function detectCycles(graph: MigrationGraph): readonly string[][] {
|
|
|
352
340
|
color.set(node, WHITE);
|
|
353
341
|
}
|
|
354
342
|
|
|
355
|
-
|
|
343
|
+
// Iterative three-color DFS. A frame is (node, outgoing edges, next-index).
|
|
344
|
+
interface Frame {
|
|
345
|
+
node: string;
|
|
346
|
+
outgoing: readonly MigrationChainEntry[];
|
|
347
|
+
index: number;
|
|
348
|
+
}
|
|
349
|
+
const stack: Frame[] = [];
|
|
350
|
+
|
|
351
|
+
function pushFrame(u: string): void {
|
|
356
352
|
color.set(u, GRAY);
|
|
353
|
+
stack.push({ node: u, outgoing: graph.forwardChain.get(u) ?? [], index: 0 });
|
|
354
|
+
}
|
|
357
355
|
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
356
|
+
for (const root of graph.nodes) {
|
|
357
|
+
if (color.get(root) !== WHITE) continue;
|
|
358
|
+
parentMap.set(root, null);
|
|
359
|
+
pushFrame(root);
|
|
360
|
+
|
|
361
|
+
while (stack.length > 0) {
|
|
362
|
+
// biome-ignore lint/style/noNonNullAssertion: stack.length > 0 should guarantee that this cannot be undefined
|
|
363
|
+
const frame = stack[stack.length - 1]!;
|
|
364
|
+
if (frame.index >= frame.outgoing.length) {
|
|
365
|
+
color.set(frame.node, BLACK);
|
|
366
|
+
stack.pop();
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
// biome-ignore lint/style/noNonNullAssertion: the early-continue above guarantees frame.index < frame.outgoing.length here, so this is defined
|
|
370
|
+
const edge = frame.outgoing[frame.index++]!;
|
|
371
|
+
const v = edge.to;
|
|
372
|
+
const vColor = color.get(v);
|
|
373
|
+
if (vColor === GRAY) {
|
|
374
|
+
const cycle: string[] = [v];
|
|
375
|
+
let cur = frame.node;
|
|
376
|
+
while (cur !== v) {
|
|
377
|
+
cycle.push(cur);
|
|
378
|
+
cur = parentMap.get(cur) ?? v;
|
|
374
379
|
}
|
|
380
|
+
cycle.reverse();
|
|
381
|
+
cycles.push(cycle);
|
|
382
|
+
} else if (vColor === WHITE) {
|
|
383
|
+
parentMap.set(v, frame.node);
|
|
384
|
+
pushFrame(v);
|
|
375
385
|
}
|
|
376
386
|
}
|
|
377
|
-
|
|
378
|
-
color.set(u, BLACK);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
for (const node of graph.nodes) {
|
|
382
|
-
if (color.get(node) === WHITE) {
|
|
383
|
-
parentMap.set(node, null);
|
|
384
|
-
dfs(node);
|
|
385
|
-
}
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
return cycles;
|
|
@@ -410,23 +411,8 @@ export function detectOrphans(graph: MigrationGraph): readonly MigrationChainEnt
|
|
|
410
411
|
}
|
|
411
412
|
}
|
|
412
413
|
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
reachable.add(hash);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
while (queue.length > 0) {
|
|
419
|
-
const node = queue.shift();
|
|
420
|
-
if (node === undefined) break;
|
|
421
|
-
const outgoing = graph.forwardChain.get(node);
|
|
422
|
-
if (!outgoing) continue;
|
|
423
|
-
|
|
424
|
-
for (const migration of outgoing) {
|
|
425
|
-
if (!reachable.has(migration.to)) {
|
|
426
|
-
reachable.add(migration.to);
|
|
427
|
-
queue.push(migration.to);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
414
|
+
for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) {
|
|
415
|
+
reachable.add(step.node);
|
|
430
416
|
}
|
|
431
417
|
|
|
432
418
|
const orphans: MigrationChainEntry[] = [];
|
package/src/errors.ts
CHANGED
|
@@ -84,6 +84,14 @@ export function errorInvalidSlug(slug: string): MigrationToolsError {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export function errorInvalidDestName(destName: string): MigrationToolsError {
|
|
88
|
+
return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {
|
|
89
|
+
why: `The destination name "${destName}" must be a single path segment (no ".." or directory separators).`,
|
|
90
|
+
fix: 'Use a simple file name such as "contract.json" for each destination in the copy list.',
|
|
91
|
+
details: { destName },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
export function errorSameSourceAndTarget(dirName: string, hash: string): MigrationToolsError {
|
|
88
96
|
return new MigrationToolsError(
|
|
89
97
|
'MIGRATION.SAME_SOURCE_AND_TARGET',
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { VerifyResult } from '../attestation';
|
|
2
|
+
export { computeMigrationId, verifyMigration, verifyMigrationBundle } from '../attestation';
|
package/src/exports/io.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { hasMigrationTs, writeMigrationTs } from '../migration-ts';
|
|
2
2
|
export type { ScaffoldRuntime } from '../runtime-detection';
|
|
3
3
|
export { detectScaffoldRuntime, shebangLineFor } from '../runtime-detection';
|
package/src/exports/migration.ts
CHANGED
package/src/exports/types.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
export { MigrationToolsError } from '../errors';
|
|
2
2
|
export type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
BaseMigrationBundle,
|
|
6
|
-
BaseMigrationBundle as MigrationBundle,
|
|
7
|
-
BaseMigrationBundle as MigrationPackage,
|
|
8
|
-
DraftMigrationBundle,
|
|
9
|
-
DraftMigrationManifest,
|
|
3
|
+
MigrationBundle,
|
|
4
|
+
MigrationBundle as MigrationPackage,
|
|
10
5
|
MigrationChainEntry,
|
|
11
6
|
MigrationGraph,
|
|
12
7
|
MigrationHints,
|
|
13
8
|
MigrationManifest,
|
|
14
9
|
MigrationOps,
|
|
15
10
|
} from '../types';
|
|
16
|
-
export { isAttested, isDraft } from '../types';
|
package/src/graph-ops.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Queue } from './queue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One step of a BFS traversal.
|
|
5
|
+
*
|
|
6
|
+
* `parent` and `incomingEdge` are `null` for start nodes — they were not
|
|
7
|
+
* reached via any edge. For every other node they record the node and edge
|
|
8
|
+
* by which this node was first reached.
|
|
9
|
+
*/
|
|
10
|
+
export interface BfsStep<E> {
|
|
11
|
+
readonly node: string;
|
|
12
|
+
readonly parent: string | null;
|
|
13
|
+
readonly incomingEdge: E | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generic breadth-first traversal.
|
|
18
|
+
*
|
|
19
|
+
* Direction (forward/reverse) is expressed by the caller's `neighbours`
|
|
20
|
+
* closure: return `{ next, edge }` pairs where `next` is the node to visit
|
|
21
|
+
* next and `edge` is the edge that connects them. Callers that don't need
|
|
22
|
+
* path reconstruction can ignore the `parent`/`incomingEdge` fields of each
|
|
23
|
+
* yielded step.
|
|
24
|
+
*
|
|
25
|
+
* Stops are intrinsic — callers `break` out of the `for..of` loop when
|
|
26
|
+
* they've found what they're looking for.
|
|
27
|
+
*
|
|
28
|
+
* `ordering`, if provided, controls the order in which neighbours of each
|
|
29
|
+
* node are enqueued. Only matters for path-finding: a deterministic ordering
|
|
30
|
+
* makes BFS return a deterministic shortest path when multiple exist.
|
|
31
|
+
*/
|
|
32
|
+
export function* bfs<E>(
|
|
33
|
+
starts: Iterable<string>,
|
|
34
|
+
neighbours: (node: string) => Iterable<{ next: string; edge: E }>,
|
|
35
|
+
ordering?: (items: readonly { next: string; edge: E }[]) => readonly { next: string; edge: E }[],
|
|
36
|
+
): Generator<BfsStep<E>> {
|
|
37
|
+
const visited = new Set<string>();
|
|
38
|
+
const parentMap = new Map<string, { parent: string; edge: E }>();
|
|
39
|
+
const queue = new Queue<string>();
|
|
40
|
+
for (const start of starts) {
|
|
41
|
+
if (!visited.has(start)) {
|
|
42
|
+
visited.add(start);
|
|
43
|
+
queue.push(start);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
while (!queue.isEmpty) {
|
|
47
|
+
const current = queue.shift();
|
|
48
|
+
const parentInfo = parentMap.get(current);
|
|
49
|
+
yield {
|
|
50
|
+
node: current,
|
|
51
|
+
parent: parentInfo?.parent ?? null,
|
|
52
|
+
incomingEdge: parentInfo?.edge ?? null,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const items = neighbours(current);
|
|
56
|
+
const toVisit = ordering ? ordering([...items]) : items;
|
|
57
|
+
for (const { next, edge } of toVisit) {
|
|
58
|
+
if (!visited.has(next)) {
|
|
59
|
+
visited.add(next);
|
|
60
|
+
parentMap.set(next, { parent: current, edge });
|
|
61
|
+
queue.push(next);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/io.ts
CHANGED
|
@@ -3,12 +3,13 @@ import { type } from 'arktype';
|
|
|
3
3
|
import { basename, dirname, join } from 'pathe';
|
|
4
4
|
import {
|
|
5
5
|
errorDirectoryExists,
|
|
6
|
+
errorInvalidDestName,
|
|
6
7
|
errorInvalidJson,
|
|
7
8
|
errorInvalidManifest,
|
|
8
9
|
errorInvalidSlug,
|
|
9
10
|
errorMissingFile,
|
|
10
11
|
} from './errors';
|
|
11
|
-
import type {
|
|
12
|
+
import type { MigrationBundle, MigrationManifest, MigrationOps } from './types';
|
|
12
13
|
|
|
13
14
|
const MANIFEST_FILE = 'migration.json';
|
|
14
15
|
const OPS_FILE = 'ops.json';
|
|
@@ -22,13 +23,12 @@ const MigrationHintsSchema = type({
|
|
|
22
23
|
used: 'string[]',
|
|
23
24
|
applied: 'string[]',
|
|
24
25
|
plannerVersion: 'string',
|
|
25
|
-
planningStrategy: 'string',
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
const MigrationManifestSchema = type({
|
|
29
29
|
from: 'string',
|
|
30
30
|
to: 'string',
|
|
31
|
-
migrationId: 'string
|
|
31
|
+
migrationId: 'string',
|
|
32
32
|
kind: "'regular' | 'baseline'",
|
|
33
33
|
fromContract: 'object | null',
|
|
34
34
|
toContract: 'object',
|
|
@@ -75,27 +75,26 @@ export async function writeMigrationPackage(
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* Copy
|
|
79
|
-
* colocated `contract.d.ts`) into the migration package directory so
|
|
80
|
-
* authors of the scaffolded `migration.ts` can import the typed
|
|
81
|
-
* contract relative to the migration directory
|
|
82
|
-
* (`import type { Contract } from './contract'`).
|
|
78
|
+
* Copy a list of files into `destDir`, optionally renaming each one.
|
|
83
79
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
80
|
+
* The destination directory is created (with `recursive: true`) if it
|
|
81
|
+
* does not already exist. Each source path is copied byte-for-byte into
|
|
82
|
+
* `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
|
|
83
|
+
* intentionally generic: callers own the list of files (e.g. a contract
|
|
84
|
+
* emitter's emitted output) and the naming convention (e.g. renaming
|
|
85
|
+
* the destination contract to `end-contract.*` and the source contract
|
|
86
|
+
* to `start-contract.*`).
|
|
87
87
|
*/
|
|
88
|
-
export async function
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
export async function copyFilesWithRename(
|
|
89
|
+
destDir: string,
|
|
90
|
+
files: readonly { readonly sourcePath: string; readonly destName: string }[],
|
|
91
91
|
): Promise<void> {
|
|
92
|
-
await
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
throw error;
|
|
92
|
+
await mkdir(destDir, { recursive: true });
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
if (basename(file.destName) !== file.destName) {
|
|
95
|
+
throw errorInvalidDestName(file.destName);
|
|
96
|
+
}
|
|
97
|
+
await copyFile(file.sourcePath, join(destDir, file.destName));
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
|
|
@@ -110,7 +109,7 @@ export async function writeMigrationOps(dir: string, ops: MigrationOps): Promise
|
|
|
110
109
|
await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
export async function readMigrationPackage(dir: string): Promise<
|
|
112
|
+
export async function readMigrationPackage(dir: string): Promise<MigrationBundle> {
|
|
114
113
|
const manifestPath = join(dir, MANIFEST_FILE);
|
|
115
114
|
const opsPath = join(dir, OPS_FILE);
|
|
116
115
|
|
|
@@ -178,7 +177,7 @@ function validateOps(ops: unknown, filePath: string): asserts ops is MigrationOp
|
|
|
178
177
|
|
|
179
178
|
export async function readMigrationsDir(
|
|
180
179
|
migrationsRoot: string,
|
|
181
|
-
): Promise<readonly
|
|
180
|
+
): Promise<readonly MigrationBundle[]> {
|
|
182
181
|
let entries: string[];
|
|
183
182
|
try {
|
|
184
183
|
entries = await readdir(migrationsRoot);
|
|
@@ -189,7 +188,7 @@ export async function readMigrationsDir(
|
|
|
189
188
|
throw error;
|
|
190
189
|
}
|
|
191
190
|
|
|
192
|
-
const packages:
|
|
191
|
+
const packages: MigrationBundle[] = [];
|
|
193
192
|
|
|
194
193
|
for (const entry of entries.sort()) {
|
|
195
194
|
const entryPath = join(migrationsRoot, entry);
|