@prisma-next/migration-tools 0.11.0-dev.4 → 0.11.0-dev.41
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 +4 -4
- package/dist/{errors-DGYwcwXs.mjs → errors-CoEN114u.mjs} +14 -2
- package/dist/errors-CoEN114u.mjs.map +1 -0
- package/dist/exports/aggregate.d.mts +4 -4
- package/dist/exports/aggregate.d.mts.map +1 -1
- package/dist/exports/aggregate.mjs +5 -4
- package/dist/exports/aggregate.mjs.map +1 -1
- package/dist/exports/enumerate-migration-spaces.d.mts +53 -0
- package/dist/exports/enumerate-migration-spaces.d.mts.map +1 -0
- package/dist/exports/enumerate-migration-spaces.mjs +107 -0
- package/dist/exports/enumerate-migration-spaces.mjs.map +1 -0
- package/dist/exports/errors.d.mts +2 -2
- package/dist/exports/errors.d.mts.map +1 -1
- package/dist/exports/errors.mjs +1 -1
- package/dist/exports/graph.d.mts +1 -1
- package/dist/exports/hash.d.mts +8 -9
- package/dist/exports/hash.d.mts.map +1 -1
- package/dist/exports/hash.mjs +1 -1
- package/dist/exports/invariants.d.mts +1 -1
- package/dist/exports/invariants.d.mts.map +1 -1
- package/dist/exports/invariants.mjs +1 -1
- package/dist/exports/io.d.mts +1 -1
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +1 -1
- package/dist/exports/metadata.d.mts +2 -2
- package/dist/exports/migration-graph.d.mts +9 -2
- package/dist/exports/migration-graph.d.mts.map +1 -0
- package/dist/exports/migration-graph.mjs +16 -2
- package/dist/exports/migration-graph.mjs.map +1 -0
- package/dist/exports/migration-list-types.d.mts +2 -0
- package/dist/exports/migration-list-types.mjs +1 -0
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +5 -6
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +14 -32
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +1 -1
- package/dist/exports/ref-resolution.d.mts +2 -2
- package/dist/exports/ref-resolution.d.mts.map +1 -1
- package/dist/exports/ref-resolution.mjs +1 -1
- package/dist/exports/ref-resolution.mjs.map +1 -1
- package/dist/exports/refs.d.mts +15 -2
- package/dist/exports/refs.d.mts.map +1 -0
- package/dist/exports/refs.mjs +137 -2
- package/dist/exports/refs.mjs.map +1 -0
- package/dist/exports/spaces.d.mts +27 -2
- package/dist/exports/spaces.d.mts.map +1 -1
- package/dist/exports/spaces.mjs +9 -7
- package/dist/exports/spaces.mjs.map +1 -1
- package/dist/{graph-BrLXqoUc.d.mts → graph-ZAIvH6p4.d.mts} +1 -2
- package/dist/graph-ZAIvH6p4.d.mts.map +1 -0
- package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
- package/dist/hash--Y7vCpN3.mjs.map +1 -0
- package/dist/{invariants-0daYEzyo.mjs → invariants-lbJddL-S.mjs} +2 -2
- package/dist/{invariants-0daYEzyo.mjs.map → invariants-lbJddL-S.mjs.map} +1 -1
- package/dist/{io-BPLfzvZe.mjs → io-Dc64lvaL.mjs} +4 -10
- package/dist/io-Dc64lvaL.mjs.map +1 -0
- package/dist/metadata-BCH1rNsk.d.mts +2 -0
- package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-D5JeadSE.mjs} +5 -21
- package/dist/migration-graph-D5JeadSE.mjs.map +1 -0
- package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-D97XMWVH.d.mts} +6 -6
- package/dist/migration-graph-D97XMWVH.d.mts.map +1 -0
- package/dist/migration-list-types-B-qimPet.d.mts +23 -0
- package/dist/migration-list-types-B-qimPet.d.mts.map +1 -0
- package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
- package/dist/{package-DZj8YvD0.d.mts → package-DIttKL7X.d.mts} +1 -1
- package/dist/package-DIttKL7X.d.mts.map +1 -0
- package/dist/read-contract-space-contract-_EvvV5Gl.mjs +82 -0
- package/dist/read-contract-space-contract-_EvvV5Gl.mjs.map +1 -0
- package/dist/{refs-CDaNerhT.d.mts → refs-BaygQaFD.d.mts} +1 -1
- package/dist/refs-BaygQaFD.d.mts.map +1 -0
- package/dist/{refs-BDHo5l_g.mjs → refs-HhOkD8BT.mjs} +3 -3
- package/dist/refs-HhOkD8BT.mjs.map +1 -0
- package/dist/{read-contract-space-contract-DRueB4Aa.mjs → verify-contract-spaces-DIdQLGo7.mjs} +32 -80
- package/dist/verify-contract-spaces-DIdQLGo7.mjs.map +1 -0
- package/package.json +14 -6
- package/src/emit-contract-space-artefacts.ts +4 -3
- package/src/enumerate-migration-spaces.ts +127 -0
- package/src/errors.ts +17 -2
- package/src/exports/enumerate-migration-spaces.ts +4 -0
- package/src/exports/metadata.ts +1 -1
- package/src/exports/migration-graph.ts +1 -0
- package/src/exports/migration-list-types.ts +5 -0
- package/src/exports/refs.ts +8 -0
- package/src/exports/spaces.ts +3 -0
- package/src/graph-membership.ts +17 -0
- package/src/graph.ts +0 -1
- package/src/hash.ts +7 -8
- package/src/io.ts +0 -8
- package/src/metadata.ts +1 -1
- package/src/migration-base.ts +10 -30
- package/src/migration-graph.ts +4 -18
- package/src/migration-list-types.ts +21 -0
- package/src/read-contract-space-head-ref.ts +5 -2
- package/src/refs/snapshot.ts +197 -0
- package/src/refs.ts +3 -1
- package/src/space-layout.ts +30 -0
- package/dist/errors-DGYwcwXs.mjs.map +0 -1
- package/dist/graph-BrLXqoUc.d.mts.map +0 -1
- package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
- package/dist/io-BPLfzvZe.mjs.map +0 -1
- package/dist/metadata-BFX0xdz8.d.mts +0 -2
- package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
- package/dist/migration-graph-nlS4TRpn.mjs.map +0 -1
- package/dist/package-DZj8YvD0.d.mts.map +0 -1
- package/dist/read-contract-space-contract-DRueB4Aa.mjs.map +0 -1
- package/dist/refs-BDHo5l_g.mjs.map +0 -1
- package/dist/refs-CDaNerhT.d.mts.map +0 -1
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/migration-tools",
|
|
3
|
-
"version": "0.11.0-dev.
|
|
3
|
+
"version": "0.11.0-dev.41",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "On-disk migration persistence, hash verification, and chain reconstruction for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/contract": "0.11.0-dev.
|
|
10
|
-
"@prisma-next/framework-components": "0.11.0-dev.
|
|
11
|
-
"@prisma-next/utils": "0.11.0-dev.
|
|
9
|
+
"@prisma-next/contract": "0.11.0-dev.41",
|
|
10
|
+
"@prisma-next/framework-components": "0.11.0-dev.41",
|
|
11
|
+
"@prisma-next/utils": "0.11.0-dev.41",
|
|
12
12
|
"arktype": "^2.2.0",
|
|
13
13
|
"pathe": "^2.0.3",
|
|
14
14
|
"prettier": "^3.8.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@prisma-next/tsconfig": "0.11.0-dev.
|
|
18
|
-
"@prisma-next/tsdown": "0.11.0-dev.
|
|
17
|
+
"@prisma-next/tsconfig": "0.11.0-dev.41",
|
|
18
|
+
"@prisma-next/tsdown": "0.11.0-dev.41",
|
|
19
19
|
"tsdown": "0.22.0",
|
|
20
20
|
"typescript": "5.9.3",
|
|
21
21
|
"vitest": "4.1.6"
|
|
@@ -60,6 +60,14 @@
|
|
|
60
60
|
"types": "./dist/exports/migration-graph.d.mts",
|
|
61
61
|
"import": "./dist/exports/migration-graph.mjs"
|
|
62
62
|
},
|
|
63
|
+
"./migration-list-types": {
|
|
64
|
+
"types": "./dist/exports/migration-list-types.d.mts",
|
|
65
|
+
"import": "./dist/exports/migration-list-types.mjs"
|
|
66
|
+
},
|
|
67
|
+
"./enumerate-migration-spaces": {
|
|
68
|
+
"types": "./dist/exports/enumerate-migration-spaces.d.mts",
|
|
69
|
+
"import": "./dist/exports/enumerate-migration-spaces.mjs"
|
|
70
|
+
},
|
|
63
71
|
"./refs": {
|
|
64
72
|
"types": "./dist/exports/refs.d.mts",
|
|
65
73
|
"import": "./dist/exports/refs.mjs"
|
|
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { canonicalizeJson } from '@prisma-next/framework-components/utils';
|
|
3
3
|
import { join } from 'pathe';
|
|
4
4
|
import type { ContractSpaceHeadRef } from './read-contract-space-head-ref';
|
|
5
|
-
import { assertValidSpaceId } from './space-layout';
|
|
5
|
+
import { assertValidSpaceId, spaceRefsDirectory } from './space-layout';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Inputs for {@link emitContractSpaceArtefacts}.
|
|
@@ -56,7 +56,8 @@ export async function emitContractSpaceArtefacts(
|
|
|
56
56
|
assertValidSpaceId(spaceId);
|
|
57
57
|
|
|
58
58
|
const dir = join(projectMigrationsDir, spaceId);
|
|
59
|
-
|
|
59
|
+
const refsDir = spaceRefsDirectory(dir);
|
|
60
|
+
await mkdir(refsDir, { recursive: true });
|
|
60
61
|
|
|
61
62
|
await writeFile(join(dir, 'contract.json'), `${canonicalizeJson(inputs.contract)}\n`);
|
|
62
63
|
await writeFile(join(dir, 'contract.d.ts'), inputs.contractDts);
|
|
@@ -66,5 +67,5 @@ export async function emitContractSpaceArtefacts(
|
|
|
66
67
|
hash: inputs.headRef.hash,
|
|
67
68
|
invariants: sortedInvariants,
|
|
68
69
|
});
|
|
69
|
-
await writeFile(join(
|
|
70
|
+
await writeFile(join(refsDir, 'head.json'), `${headJson}\n`);
|
|
70
71
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readMigrationsDir } from './io';
|
|
2
|
+
import type { MigrationListEntry, MigrationSpaceListEntry } from './migration-list-types';
|
|
3
|
+
import { readRefs } from './refs';
|
|
4
|
+
import {
|
|
5
|
+
APP_SPACE_ID,
|
|
6
|
+
isValidSpaceId,
|
|
7
|
+
RESERVED_SPACE_SUBDIR_NAMES,
|
|
8
|
+
spaceMigrationDirectory,
|
|
9
|
+
spaceRefsDirectory,
|
|
10
|
+
} from './space-layout';
|
|
11
|
+
import { listContractSpaceDirectories } from './verify-contract-spaces';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Index `migrations/<space>/refs/*.json` by the contract hash each ref
|
|
15
|
+
* points at, so callers can attach `(ref names)` decorations to every
|
|
16
|
+
* row whose destination contract hash matches.
|
|
17
|
+
*
|
|
18
|
+
* Each bucket is sorted lex-asc to keep rendered output deterministic
|
|
19
|
+
* (adjacent rows pointing at the same hash render their ref decorations
|
|
20
|
+
* in the same order).
|
|
21
|
+
*
|
|
22
|
+
* Refs whose hash matches no migration on disk are still indexed; the
|
|
23
|
+
* caller decides whether to surface them. Migration rows only carry
|
|
24
|
+
* `(refs)` decorations when a matching destination contract hash exists
|
|
25
|
+
* on disk — orphan refs are not rendered on any row.
|
|
26
|
+
*
|
|
27
|
+
* Returns an empty map when the refs directory does not exist
|
|
28
|
+
* ({@link readRefs} treats `ENOENT` as "no refs").
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveRefsByContractHash(
|
|
31
|
+
refsDir: string,
|
|
32
|
+
): Promise<ReadonlyMap<string, readonly string[]>> {
|
|
33
|
+
const refs = await readRefs(refsDir);
|
|
34
|
+
const byHash = new Map<string, string[]>();
|
|
35
|
+
for (const [name, entry] of Object.entries(refs)) {
|
|
36
|
+
const bucket = byHash.get(entry.hash);
|
|
37
|
+
if (bucket) bucket.push(name);
|
|
38
|
+
else byHash.set(entry.hash, [name]);
|
|
39
|
+
}
|
|
40
|
+
for (const bucket of byHash.values()) {
|
|
41
|
+
bucket.sort();
|
|
42
|
+
}
|
|
43
|
+
return byHash;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compare two contract-space IDs for the inter-space ordering rule:
|
|
48
|
+
* {@link APP_SPACE_ID} first if present, then lex-asc on the rest.
|
|
49
|
+
*/
|
|
50
|
+
function compareSpaceIds(a: string, b: string): number {
|
|
51
|
+
if (a === APP_SPACE_ID) return b === APP_SPACE_ID ? 0 : -1;
|
|
52
|
+
if (b === APP_SPACE_ID) return 1;
|
|
53
|
+
if (a < b) return -1;
|
|
54
|
+
if (a > b) return 1;
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sort `dirName` descending so the rendered output reads latest-first,
|
|
60
|
+
* matching the `git log` latest-first convention.
|
|
61
|
+
*/
|
|
62
|
+
function compareDirNamesDescending(a: MigrationListEntry, b: MigrationListEntry): number {
|
|
63
|
+
if (a.dirName < b.dirName) return 1;
|
|
64
|
+
if (a.dirName > b.dirName) return -1;
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enumerate every contract space's on-disk migrations under
|
|
70
|
+
* `<projectMigrationsDir>/`. For each valid space directory:
|
|
71
|
+
*
|
|
72
|
+
* - Loads on-disk packages via {@link readMigrationsDir}.
|
|
73
|
+
* - Attaches ref decorations: each migration's `refs[]` lists every ref
|
|
74
|
+
* name from `migrations/<spaceId>/refs/*.json` whose hash equals the
|
|
75
|
+
* migration's destination contract hash.
|
|
76
|
+
* - Sorts migrations within the space by `dirName` descending
|
|
77
|
+
* (reverse-filename, latest first).
|
|
78
|
+
*
|
|
79
|
+
* Contract spaces are returned with {@link APP_SPACE_ID} first when
|
|
80
|
+
* present, then the remaining ids lex-asc. A contract-space directory
|
|
81
|
+
* that contains no migrations becomes `{ spaceId, migrations: [] }` so
|
|
82
|
+
* the renderer's empty-state path can surface it.
|
|
83
|
+
*
|
|
84
|
+
* Directory entries that are not valid {@link isValidSpaceId} names are
|
|
85
|
+
* skipped (a stray non-space directory under `migrations/` does not
|
|
86
|
+
* spawn a phantom space entry). Entries whose name appears in
|
|
87
|
+
* {@link RESERVED_SPACE_SUBDIR_NAMES} are also skipped — the per-space
|
|
88
|
+
* `refs/` subdirectory name shape would otherwise satisfy
|
|
89
|
+
* {@link isValidSpaceId} and surface as a phantom contract space.
|
|
90
|
+
*
|
|
91
|
+
* Returns `[]` when `<projectMigrationsDir>` does not exist — a fresh
|
|
92
|
+
* project that has not authored any migration yet.
|
|
93
|
+
*/
|
|
94
|
+
export async function enumerateMigrationSpaces(args: {
|
|
95
|
+
readonly projectMigrationsDir: string;
|
|
96
|
+
}): Promise<readonly MigrationSpaceListEntry[]> {
|
|
97
|
+
const { projectMigrationsDir } = args;
|
|
98
|
+
const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
|
|
99
|
+
const spaceIds = candidateDirs
|
|
100
|
+
.filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name))
|
|
101
|
+
.filter(isValidSpaceId)
|
|
102
|
+
.sort(compareSpaceIds);
|
|
103
|
+
|
|
104
|
+
const spaces: MigrationSpaceListEntry[] = [];
|
|
105
|
+
for (const spaceId of spaceIds) {
|
|
106
|
+
const spaceDir = spaceMigrationDirectory(projectMigrationsDir, spaceId);
|
|
107
|
+
const packages = await readMigrationsDir(spaceDir);
|
|
108
|
+
const refsByHash = await resolveRefsByContractHash(spaceRefsDirectory(spaceDir));
|
|
109
|
+
|
|
110
|
+
const migrations: MigrationListEntry[] = packages
|
|
111
|
+
.map((pkg) => ({
|
|
112
|
+
dirName: pkg.dirName,
|
|
113
|
+
from: pkg.metadata.from,
|
|
114
|
+
to: pkg.metadata.to,
|
|
115
|
+
migrationHash: pkg.metadata.migrationHash,
|
|
116
|
+
operationCount: pkg.ops.length,
|
|
117
|
+
createdAt: pkg.metadata.createdAt,
|
|
118
|
+
refs: refsByHash.get(pkg.metadata.to) ?? [],
|
|
119
|
+
providedInvariants: pkg.metadata.providedInvariants,
|
|
120
|
+
}))
|
|
121
|
+
.sort(compareDirNamesDescending);
|
|
122
|
+
|
|
123
|
+
spaces.push({ spaceId, migrations });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return spaces;
|
|
127
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
2
|
import { basename, dirname, relative } from 'pathe';
|
|
3
|
+
import type { MigrationGraph } from './graph';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Build the canonical "re-emit this package" remediation hint.
|
|
@@ -319,8 +320,8 @@ export function errorProvidedInvariantsMismatch(
|
|
|
319
320
|
/**
|
|
320
321
|
* Wire-shape edge surfaced through the JSON envelope's
|
|
321
322
|
* `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —
|
|
322
|
-
* authoring metadata (`createdAt
|
|
323
|
-
*
|
|
323
|
+
* authoring metadata (`createdAt`) lives on `MigrationEdge` but is
|
|
324
|
+
* intentionally dropped here so the envelope stays stable across
|
|
324
325
|
* graph-internal refactors.
|
|
325
326
|
*
|
|
326
327
|
* Stability: any field added here is part of the public CLI JSON contract.
|
|
@@ -399,3 +400,17 @@ export function errorMigrationHashMismatch(
|
|
|
399
400
|
details: { dir, storedHash, computedHash },
|
|
400
401
|
});
|
|
401
402
|
}
|
|
403
|
+
|
|
404
|
+
export function errorHashNotInGraph(hash: string, graph: MigrationGraph): MigrationToolsError {
|
|
405
|
+
const reachableHashes = [...graph.nodes].sort();
|
|
406
|
+
const reachableList = reachableHashes.length > 0 ? reachableHashes.join(', ') : '(none)';
|
|
407
|
+
return new MigrationToolsError(
|
|
408
|
+
'MIGRATION.HASH_NOT_IN_GRAPH',
|
|
409
|
+
`Hash "${hash}" is not a node in the migration graph`,
|
|
410
|
+
{
|
|
411
|
+
why: `The migration graph contains nodes ${reachableList}; "${hash}" isn't one of them.`,
|
|
412
|
+
fix: `Pass a hash that's the from-or-to of an on-disk migration bundle, use --from with a graph-node hash, or run "prisma-next migration plan" to introduce it.`,
|
|
413
|
+
details: { hash, reachableHashes },
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
}
|
package/src/exports/metadata.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { MigrationMetadata } from '../metadata';
|
package/src/exports/refs.ts
CHANGED
|
@@ -8,3 +8,11 @@ export {
|
|
|
8
8
|
validateRefValue,
|
|
9
9
|
writeRef,
|
|
10
10
|
} from '../refs';
|
|
11
|
+
export type { ContractIR } from '../refs/snapshot';
|
|
12
|
+
export {
|
|
13
|
+
deleteRefPaired,
|
|
14
|
+
deleteRefSnapshot,
|
|
15
|
+
readRefSnapshot,
|
|
16
|
+
writeRefPaired,
|
|
17
|
+
writeRefSnapshot,
|
|
18
|
+
} from '../refs/snapshot';
|
package/src/exports/spaces.ts
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EMPTY_CONTRACT_HASH } from './constants';
|
|
2
|
+
import { errorHashNotInGraph } from './errors';
|
|
3
|
+
import type { MigrationGraph } from './graph';
|
|
4
|
+
|
|
5
|
+
export function isGraphNode(hash: string, graph: MigrationGraph): boolean {
|
|
6
|
+
if (hash === EMPTY_CONTRACT_HASH) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
return graph.nodes.has(hash);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function assertHashIsGraphNode(hash: string, graph: MigrationGraph): asserts hash is string {
|
|
13
|
+
if (isGraphNode(hash, graph)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
throw errorHashNotInGraph(hash, graph);
|
|
17
|
+
}
|
package/src/graph.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface MigrationEdge {
|
|
|
8
8
|
readonly migrationHash: string;
|
|
9
9
|
readonly dirName: string;
|
|
10
10
|
readonly createdAt: string;
|
|
11
|
-
readonly labels: readonly string[];
|
|
12
11
|
/**
|
|
13
12
|
* Sorted, deduplicated list of `invariantId`s this edge provides.
|
|
14
13
|
* An empty array means the migration declares no routing-visible
|
package/src/hash.ts
CHANGED
|
@@ -15,13 +15,12 @@ function sha256Hex(input: string): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Content-addressed migration hash over (metadata envelope
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* authored alongside the migration, never inlined here.
|
|
18
|
+
* Content-addressed migration hash over (metadata envelope, ops). See
|
|
19
|
+
* ADR 199 — Storage-only migration identity for the rationale: the
|
|
20
|
+
* storage-hash bookends (`from`, `to`) inside the envelope anchor the
|
|
21
|
+
* contract identity by hash. The full contract IRs are not part of the
|
|
22
|
+
* manifest — they live in sibling `*-contract.json` files authored
|
|
23
|
+
* alongside the migration, never inlined here.
|
|
25
24
|
*
|
|
26
25
|
* The integrity check is purely structural, not semantic. The function
|
|
27
26
|
* canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`
|
|
@@ -46,7 +45,7 @@ export function computeMigrationHash(
|
|
|
46
45
|
metadata: Omit<MigrationMetadata, 'migrationHash'> & { readonly migrationHash?: string },
|
|
47
46
|
ops: MigrationOps,
|
|
48
47
|
): string {
|
|
49
|
-
const { migrationHash: _migrationHash,
|
|
48
|
+
const { migrationHash: _migrationHash, ...strippedMeta } = metadata;
|
|
50
49
|
|
|
51
50
|
const canonicalMetadata = canonicalizeJson(strippedMeta);
|
|
52
51
|
const canonicalOps = canonicalizeJson(ops);
|
package/src/io.ts
CHANGED
|
@@ -28,19 +28,11 @@ function hasErrnoCode(error: unknown, code: string): boolean {
|
|
|
28
28
|
return error instanceof Error && (error as { code?: string }).code === code;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const MigrationHintsSchema = type({
|
|
32
|
-
used: 'string[]',
|
|
33
|
-
applied: 'string[]',
|
|
34
|
-
plannerVersion: 'string',
|
|
35
|
-
});
|
|
36
|
-
|
|
37
31
|
const MigrationMetadataSchema = type({
|
|
38
32
|
'+': 'reject',
|
|
39
33
|
from: 'string > 0 | null',
|
|
40
34
|
to: 'string',
|
|
41
35
|
migrationHash: 'string',
|
|
42
|
-
hints: MigrationHintsSchema,
|
|
43
|
-
labels: 'string[]',
|
|
44
36
|
providedInvariants: 'string[]',
|
|
45
37
|
createdAt: 'string',
|
|
46
38
|
});
|
package/src/metadata.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { MigrationMetadata } from '@prisma-next/framework-components/control';
|
package/src/migration-base.ts
CHANGED
|
@@ -9,14 +9,13 @@ import { type } from 'arktype';
|
|
|
9
9
|
import { errorInvalidOperationEntry } from './errors';
|
|
10
10
|
import { computeMigrationHash } from './hash';
|
|
11
11
|
import { deriveProvidedInvariants } from './invariants';
|
|
12
|
-
import type {
|
|
12
|
+
import type { MigrationMetadata } from './metadata';
|
|
13
13
|
import { MigrationOpSchema } from './op-schema';
|
|
14
14
|
import type { MigrationOps } from './package';
|
|
15
15
|
|
|
16
16
|
export interface MigrationMeta {
|
|
17
17
|
readonly from: string | null;
|
|
18
18
|
readonly to: string;
|
|
19
|
-
readonly labels?: readonly string[];
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
// `from` rejects empty strings to mirror `MigrationMetadataSchema` in
|
|
@@ -27,7 +26,6 @@ export interface MigrationMeta {
|
|
|
27
26
|
const MigrationMetaSchema = type({
|
|
28
27
|
from: 'string > 0 | null',
|
|
29
28
|
to: 'string',
|
|
30
|
-
'labels?': type('string').array(),
|
|
31
29
|
});
|
|
32
30
|
|
|
33
31
|
/**
|
|
@@ -127,12 +125,11 @@ export interface MigrationArtifacts {
|
|
|
127
125
|
* operations list, and the previously-scaffolded metadata (if any).
|
|
128
126
|
*
|
|
129
127
|
* When a `migration.json` already exists for this package (the common
|
|
130
|
-
* case: it was scaffolded by `migration plan`), preserve
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* `migration.ts` run from scratch), synthesize a minimal but
|
|
128
|
+
* case: it was scaffolded by `migration plan`), preserve `createdAt`
|
|
129
|
+
* set there — that field is owned by the CLI scaffolder, not the authored
|
|
130
|
+
* class. Only the `describe()`-derived fields (`from`, `to`) and the
|
|
131
|
+
* operations change as the author iterates. When no metadata exists yet
|
|
132
|
+
* (a bare `migration.ts` run from scratch), synthesize a minimal but
|
|
136
133
|
* schema-conformant record so the resulting package can still be read,
|
|
137
134
|
* verified, and applied.
|
|
138
135
|
*
|
|
@@ -147,39 +144,22 @@ function buildAttestedMetadata(
|
|
|
147
144
|
const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
|
|
148
145
|
from: meta.from,
|
|
149
146
|
to: meta.to,
|
|
150
|
-
labels: meta.labels ?? existing?.labels ?? [],
|
|
151
147
|
providedInvariants: deriveProvidedInvariants(ops),
|
|
152
148
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
153
|
-
hints: normalizeHints(existing?.hints),
|
|
154
149
|
};
|
|
155
150
|
|
|
156
151
|
const migrationHash = computeMigrationHash(baseMetadata, ops);
|
|
157
152
|
return { ...baseMetadata, migrationHash };
|
|
158
153
|
}
|
|
159
154
|
|
|
160
|
-
/**
|
|
161
|
-
* Project `existing.hints` down to the known `MigrationHints` shape, dropping
|
|
162
|
-
* any legacy keys that may linger in metadata scaffolded by older CLI
|
|
163
|
-
* versions (e.g. `planningStrategy`). Picking fields explicitly instead of
|
|
164
|
-
* spreading keeps refreshed `migration.json` files schema-clean regardless
|
|
165
|
-
* of what was on disk before.
|
|
166
|
-
*/
|
|
167
|
-
function normalizeHints(existing: MigrationHints | undefined): MigrationHints {
|
|
168
|
-
return {
|
|
169
|
-
used: existing?.used ?? [],
|
|
170
|
-
applied: existing?.applied ?? [],
|
|
171
|
-
plannerVersion: existing?.plannerVersion ?? '2.0.0',
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
155
|
/**
|
|
176
156
|
* Pure conversion from a `Migration` instance (plus the previously
|
|
177
157
|
* scaffolded metadata, when one exists on disk) to the in-memory
|
|
178
158
|
* artifacts that downstream tooling persists. Owns metadata validation,
|
|
179
|
-
* metadata synthesis/preservation,
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* `
|
|
159
|
+
* metadata synthesis/preservation, and the content-addressed
|
|
160
|
+
* `migrationHash` computation, but performs no file I/O — callers handle
|
|
161
|
+
* reads (to source `existing`) and writes (to persist `opsJson` /
|
|
162
|
+
* `metadataJson`).
|
|
183
163
|
*/
|
|
184
164
|
export function buildMigrationArtifacts(
|
|
185
165
|
instance: Migration,
|
package/src/migration-graph.ts
CHANGED
|
@@ -66,7 +66,6 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
66
66
|
migrationHash: pkg.metadata.migrationHash,
|
|
67
67
|
dirName: pkg.dirName,
|
|
68
68
|
createdAt: pkg.metadata.createdAt,
|
|
69
|
-
labels: pkg.metadata.labels,
|
|
70
69
|
invariants: pkg.metadata.providedInvariants,
|
|
71
70
|
};
|
|
72
71
|
|
|
@@ -85,23 +84,10 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
85
84
|
// ---------------------------------------------------------------------------
|
|
86
85
|
// Deterministic tie-breaking for BFS neighbour order.
|
|
87
86
|
// Used by path-finders only; not a general-purpose utility.
|
|
88
|
-
// Ordering:
|
|
87
|
+
// Ordering: createdAt → to → migrationHash.
|
|
89
88
|
// ---------------------------------------------------------------------------
|
|
90
89
|
|
|
91
|
-
const LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };
|
|
92
|
-
|
|
93
|
-
function labelPriority(labels: readonly string[]): number {
|
|
94
|
-
let best = 3;
|
|
95
|
-
for (const l of labels) {
|
|
96
|
-
const p = LABEL_PRIORITY[l];
|
|
97
|
-
if (p !== undefined && p < best) best = p;
|
|
98
|
-
}
|
|
99
|
-
return best;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
90
|
function compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {
|
|
103
|
-
const lp = labelPriority(a.labels) - labelPriority(b.labels);
|
|
104
|
-
if (lp !== 0) return lp;
|
|
105
91
|
const ca = a.createdAt.localeCompare(b.createdAt);
|
|
106
92
|
if (ca !== 0) return ca;
|
|
107
93
|
const tc = a.to.localeCompare(b.to);
|
|
@@ -119,7 +105,7 @@ function sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdg
|
|
|
119
105
|
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
120
106
|
*
|
|
121
107
|
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
122
|
-
*
|
|
108
|
+
* createdAt → to → migrationHash.
|
|
123
109
|
*/
|
|
124
110
|
export function findPath(
|
|
125
111
|
graph: MigrationGraph,
|
|
@@ -165,8 +151,8 @@ export function findPath(
|
|
|
165
151
|
* control chars at authoring time).
|
|
166
152
|
*
|
|
167
153
|
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
|
|
168
|
-
* invariant come first, with `
|
|
169
|
-
*
|
|
154
|
+
* invariant come first, with `createdAt → to → migrationHash` as the
|
|
155
|
+
* secondary key. The heuristic steers BFS toward the satisfying path;
|
|
170
156
|
* correctness (shortest, deterministic) does not depend on it.
|
|
171
157
|
*/
|
|
172
158
|
export function findPathWithInvariants(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MigrationListEntry {
|
|
2
|
+
readonly dirName: string;
|
|
3
|
+
readonly from: string | null;
|
|
4
|
+
readonly to: string;
|
|
5
|
+
readonly migrationHash: string;
|
|
6
|
+
readonly operationCount: number;
|
|
7
|
+
readonly createdAt: string;
|
|
8
|
+
readonly refs: readonly string[];
|
|
9
|
+
readonly providedInvariants: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MigrationSpaceListEntry {
|
|
13
|
+
readonly spaceId: string;
|
|
14
|
+
readonly migrations: readonly MigrationListEntry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MigrationListResult {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly spaces: readonly MigrationSpaceListEntry[];
|
|
20
|
+
readonly summary: string;
|
|
21
|
+
}
|
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import type { ContractSpaceHeadRef } from '@prisma-next/framework-components/control';
|
|
3
3
|
import { join } from 'pathe';
|
|
4
4
|
import { errorInvalidJson, errorInvalidRefFile } from './errors';
|
|
5
|
-
import { assertValidSpaceId } from './space-layout';
|
|
5
|
+
import { assertValidSpaceId, spaceMigrationDirectory, spaceRefsDirectory } from './space-layout';
|
|
6
6
|
|
|
7
7
|
export type { ContractSpaceHeadRef };
|
|
8
8
|
|
|
@@ -29,7 +29,10 @@ export async function readContractSpaceHeadRef(
|
|
|
29
29
|
): Promise<ContractSpaceHeadRef | null> {
|
|
30
30
|
assertValidSpaceId(spaceId);
|
|
31
31
|
|
|
32
|
-
const filePath = join(
|
|
32
|
+
const filePath = join(
|
|
33
|
+
spaceRefsDirectory(spaceMigrationDirectory(projectMigrationsDir, spaceId)),
|
|
34
|
+
'head.json',
|
|
35
|
+
);
|
|
33
36
|
|
|
34
37
|
let raw: string;
|
|
35
38
|
try {
|