@prisma-next/migration-tools 0.11.0-dev.5 → 0.11.0-dev.50
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-4YabujxZ.mjs} +15 -21
- package/dist/errors-4YabujxZ.mjs.map +1 -0
- package/dist/exports/aggregate.d.mts +275 -179
- package/dist/exports/aggregate.d.mts.map +1 -1
- package/dist/exports/aggregate.mjs +363 -184
- 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 +2 -83
- 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-graph-topology.d.mts +13 -0
- package/dist/exports/migration-list-graph-topology.d.mts.map +1 -0
- package/dist/exports/migration-list-graph-topology.mjs +105 -0
- package/dist/exports/migration-list-graph-topology.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 +31 -132
- package/dist/exports/spaces.d.mts.map +1 -1
- package/dist/exports/spaces.mjs +14 -9
- package/dist/exports/spaces.mjs.map +1 -1
- package/dist/{graph-BrLXqoUc.d.mts → graph-BUZuUeBC.d.mts} +1 -2
- package/dist/graph-BUZuUeBC.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-CCOAyg6c.mjs} +2 -2
- package/dist/{invariants-0daYEzyo.mjs.map → invariants-CCOAyg6c.mjs.map} +1 -1
- package/dist/{io-BPLfzvZe.mjs → io-BHl0amF0.mjs} +100 -13
- package/dist/io-BHl0amF0.mjs.map +1 -0
- package/dist/io-nqFXSSTN.d.mts +124 -0
- package/dist/io-nqFXSSTN.d.mts.map +1 -0
- package/dist/metadata-Bp9X04gM.d.mts +2 -0
- package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-DtNT-cqc.d.mts} +6 -6
- package/dist/migration-graph-DtNT-cqc.d.mts.map +1 -0
- package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-kGBkIZDa.mjs} +6 -26
- package/dist/migration-graph-kGBkIZDa.mjs.map +1 -0
- package/dist/migration-list-types-BRTuXR8i.d.mts +23 -0
- package/dist/migration-list-types-BRTuXR8i.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-BS5Oxbgw.mjs +82 -0
- package/dist/read-contract-space-contract-BS5Oxbgw.mjs.map +1 -0
- package/dist/{refs-BDHo5l_g.mjs → refs-BBKNL45K.mjs} +76 -4
- package/dist/refs-BBKNL45K.mjs.map +1 -0
- package/dist/{refs-CDaNerhT.d.mts → refs-C8f2IGM8.d.mts} +12 -2
- package/dist/refs-C8f2IGM8.d.mts.map +1 -0
- package/dist/{read-contract-space-contract-DRueB4Aa.mjs → verify-contract-spaces-BJX5gqtD.mjs} +32 -80
- package/dist/verify-contract-spaces-BJX5gqtD.mjs.map +1 -0
- package/dist/verify-contract-spaces-T0aiJlBS.d.mts +132 -0
- package/dist/verify-contract-spaces-T0aiJlBS.d.mts.map +1 -0
- package/package.json +18 -6
- package/src/aggregate/aggregate.ts +90 -0
- package/src/aggregate/check-integrity.ts +243 -0
- package/src/aggregate/loader.ts +156 -334
- package/src/aggregate/planner.ts +8 -6
- package/src/aggregate/project-schema-to-space.ts +1 -1
- package/src/aggregate/strategies/graph-walk.ts +12 -7
- package/src/aggregate/strategies/synth.ts +2 -2
- package/src/aggregate/types.ts +56 -64
- package/src/aggregate/verifier.ts +6 -4
- package/src/assert-descriptor-self-consistency.ts +6 -0
- package/src/compute-extension-space-apply-path.ts +1 -1
- 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/aggregate.ts +17 -12
- package/src/exports/enumerate-migration-spaces.ts +4 -0
- package/src/exports/io.ts +2 -0
- package/src/exports/metadata.ts +1 -1
- package/src/exports/migration-graph.ts +1 -0
- package/src/exports/migration-list-graph-topology.ts +5 -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/integrity-violation.ts +114 -0
- package/src/io.ts +139 -14
- package/src/metadata.ts +1 -1
- package/src/migration-base.ts +10 -30
- package/src/migration-graph.ts +7 -35
- package/src/migration-list-graph-topology.ts +158 -0
- 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 +97 -1
- package/src/space-layout.ts +30 -0
- package/dist/errors-DGYwcwXs.mjs.map +0 -1
- package/dist/exports/io.d.mts.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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every structural problem the migration model can carry.
|
|
3
|
+
*
|
|
4
|
+
* Violations come in three groups:
|
|
5
|
+
*
|
|
6
|
+
* - **Recoverable**: the package or space is retained in the model;
|
|
7
|
+
* the violation is surfaced for policy (report, refuse, or ignore
|
|
8
|
+
* depending on the command).
|
|
9
|
+
* - **Config/contract-dependent**: produced only when the matching
|
|
10
|
+
* `IntegrityQueryOptions` opt is set (declaredExtensions /
|
|
11
|
+
* checkContracts). The model is built without them; they surface
|
|
12
|
+
* when the caller explicitly asks for the broader integrity view.
|
|
13
|
+
* - **Unloadable**: the package is omitted from the model entirely
|
|
14
|
+
* (its on-disk content cannot be parsed into an `OnDiskMigrationPackage`).
|
|
15
|
+
*
|
|
16
|
+
* `checkIntegrity()` on `ContractSpaceAggregate` returns the full set —
|
|
17
|
+
* all violations across all spaces — never bailing at the first hit.
|
|
18
|
+
*/
|
|
19
|
+
export type IntegrityViolation =
|
|
20
|
+
// recoverable — package/space retained, surfaced for policy
|
|
21
|
+
| {
|
|
22
|
+
readonly kind: 'sameSourceAndTarget';
|
|
23
|
+
readonly spaceId: string;
|
|
24
|
+
readonly dirName: string;
|
|
25
|
+
readonly hash: string;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
readonly kind: 'hashMismatch';
|
|
29
|
+
readonly spaceId: string;
|
|
30
|
+
readonly dirName: string;
|
|
31
|
+
readonly stored: string;
|
|
32
|
+
readonly computed: string;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
readonly kind: 'providedInvariantsMismatch';
|
|
36
|
+
readonly spaceId: string;
|
|
37
|
+
readonly dirName: string;
|
|
38
|
+
}
|
|
39
|
+
| { readonly kind: 'headRefMissing'; readonly spaceId: string }
|
|
40
|
+
| { readonly kind: 'headRefNotInGraph'; readonly spaceId: string; readonly hash: string }
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: 'duplicateMigrationHash';
|
|
43
|
+
readonly spaceId: string;
|
|
44
|
+
readonly migrationHash: string;
|
|
45
|
+
readonly dirNames: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
readonly kind: 'refUnreadable';
|
|
49
|
+
readonly spaceId: string;
|
|
50
|
+
readonly refName: string;
|
|
51
|
+
readonly detail: string;
|
|
52
|
+
}
|
|
53
|
+
// config/contract-dependent — produced only when the matching opt is set
|
|
54
|
+
| { readonly kind: 'orphanSpaceDir'; readonly spaceId: string }
|
|
55
|
+
| { readonly kind: 'declaredButUnmigrated'; readonly spaceId: string }
|
|
56
|
+
| {
|
|
57
|
+
readonly kind: 'targetMismatch';
|
|
58
|
+
readonly spaceId: string;
|
|
59
|
+
readonly expected: string;
|
|
60
|
+
readonly actual: string;
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
readonly kind: 'disjointness';
|
|
64
|
+
readonly element: string;
|
|
65
|
+
readonly claimedBy: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
| { readonly kind: 'contractUnreadable'; readonly spaceId: string; readonly detail: string }
|
|
68
|
+
// genuinely unloadable — package omitted from member.packages
|
|
69
|
+
| {
|
|
70
|
+
readonly kind: 'packageUnloadable';
|
|
71
|
+
readonly spaceId: string;
|
|
72
|
+
readonly dirName: string;
|
|
73
|
+
readonly detail: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One declared extension entry, drawn from `Config.extensionPacks`.
|
|
78
|
+
*
|
|
79
|
+
* The integrity layer needs only:
|
|
80
|
+
*
|
|
81
|
+
* - `id` — the space id (also the directory name under `migrations/`),
|
|
82
|
+
* used for the layout-drift checks (`orphanSpaceDir` /
|
|
83
|
+
* `declaredButUnmigrated`).
|
|
84
|
+
* - `targetId` — the target the declaring extension was configured for.
|
|
85
|
+
*
|
|
86
|
+
* Typed structurally so the migration-tools layer stays framework-neutral.
|
|
87
|
+
*/
|
|
88
|
+
export interface DeclaredExtensionEntry {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly targetId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Options controlling which config/contract-dependent violation checks
|
|
95
|
+
* `checkIntegrity()` runs.
|
|
96
|
+
*
|
|
97
|
+
* Both opts default to disabled: a caller without the app contract or
|
|
98
|
+
* declared extensions still gets the structurally-derivable violations
|
|
99
|
+
* (hashMismatch, providedInvariantsMismatch, headRefMissing,
|
|
100
|
+
* headRefNotInGraph, refUnreadable, sameSourceAndTarget, packageUnloadable).
|
|
101
|
+
*/
|
|
102
|
+
export interface IntegrityQueryOptions {
|
|
103
|
+
/**
|
|
104
|
+
* When provided, enables layout-drift checks: `orphanSpaceDir`
|
|
105
|
+
* (a directory exists on disk for an extension not in the list) and
|
|
106
|
+
* `declaredButUnmigrated` (an extension in the list has no on-disk dir).
|
|
107
|
+
*/
|
|
108
|
+
readonly declaredExtensions?: readonly DeclaredExtensionEntry[];
|
|
109
|
+
/**
|
|
110
|
+
* When true, enables contract/disjointness/target checks:
|
|
111
|
+
* `contractUnreadable`, `targetMismatch`, `disjointness`.
|
|
112
|
+
*/
|
|
113
|
+
readonly checkContracts?: boolean;
|
|
114
|
+
}
|
package/src/io.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
errorMigrationHashMismatch,
|
|
15
15
|
errorMissingFile,
|
|
16
16
|
errorProvidedInvariantsMismatch,
|
|
17
|
+
MigrationToolsError,
|
|
17
18
|
} from './errors';
|
|
18
19
|
import { verifyMigrationHash } from './hash';
|
|
19
20
|
import { deriveProvidedInvariants } from './invariants';
|
|
@@ -28,19 +29,11 @@ function hasErrnoCode(error: unknown, code: string): boolean {
|
|
|
28
29
|
return error instanceof Error && (error as { code?: string }).code === code;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const MigrationHintsSchema = type({
|
|
32
|
-
used: 'string[]',
|
|
33
|
-
applied: 'string[]',
|
|
34
|
-
plannerVersion: 'string',
|
|
35
|
-
});
|
|
36
|
-
|
|
37
32
|
const MigrationMetadataSchema = type({
|
|
38
33
|
'+': 'reject',
|
|
39
34
|
from: 'string > 0 | null',
|
|
40
35
|
to: 'string',
|
|
41
36
|
migrationHash: 'string',
|
|
42
|
-
hints: MigrationHintsSchema,
|
|
43
|
-
labels: 'string[]',
|
|
44
37
|
providedInvariants: 'string[]',
|
|
45
38
|
createdAt: 'string',
|
|
46
39
|
});
|
|
@@ -255,6 +248,60 @@ export async function readMigrationPackage(dir: string): Promise<OnDiskMigration
|
|
|
255
248
|
return pkg;
|
|
256
249
|
}
|
|
257
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Reads a migration package's manifest and ops without running hash or
|
|
253
|
+
* invariants verification. Returns `null` when the files cannot be read or
|
|
254
|
+
* parsed (i.e. when the package is genuinely unloadable).
|
|
255
|
+
*
|
|
256
|
+
* Used by {@link readMigrationsDir} to retain a package whose hash or
|
|
257
|
+
* invariants diverge from what is stored on disk — the raw content is still
|
|
258
|
+
* useful for display / querying; only integrity is in question.
|
|
259
|
+
*/
|
|
260
|
+
async function readMigrationPackageRaw(dir: string): Promise<OnDiskMigrationPackage | null> {
|
|
261
|
+
const absoluteDir = resolve(dir);
|
|
262
|
+
const manifestPath = join(absoluteDir, MANIFEST_FILE);
|
|
263
|
+
const opsPath = join(absoluteDir, OPS_FILE);
|
|
264
|
+
|
|
265
|
+
let manifestRaw: string;
|
|
266
|
+
try {
|
|
267
|
+
manifestRaw = await readFile(manifestPath, 'utf-8');
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
let opsRaw: string;
|
|
272
|
+
try {
|
|
273
|
+
opsRaw = await readFile(opsPath, 'utf-8');
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let metadata: MigrationMetadata;
|
|
279
|
+
try {
|
|
280
|
+
metadata = JSON.parse(manifestRaw);
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
let ops: MigrationOps;
|
|
285
|
+
try {
|
|
286
|
+
ops = JSON.parse(opsRaw);
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const result = MigrationMetadataSchema(metadata);
|
|
292
|
+
if (result instanceof type.errors) return null;
|
|
293
|
+
|
|
294
|
+
const opsResult = MigrationOpsSchema(ops);
|
|
295
|
+
if (opsResult instanceof type.errors) return null;
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
dirName: basename(absoluteDir),
|
|
299
|
+
dirPath: absoluteDir,
|
|
300
|
+
metadata,
|
|
301
|
+
ops,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
258
305
|
function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
259
306
|
if (a.length !== b.length) return false;
|
|
260
307
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -280,20 +327,64 @@ function validateOps(ops: unknown, filePath: string): asserts ops is MigrationOp
|
|
|
280
327
|
}
|
|
281
328
|
}
|
|
282
329
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
330
|
+
/**
|
|
331
|
+
* A per-package load-time problem returned by {@link readMigrationsDir}.
|
|
332
|
+
*
|
|
333
|
+
* Three variants, matching the relocated throws from the load path:
|
|
334
|
+
*
|
|
335
|
+
* - `hashMismatch` — stored `migrationHash` differs from the recomputed value.
|
|
336
|
+
* The package is **retained** in the returned `packages` array.
|
|
337
|
+
* - `providedInvariantsMismatch` — `migration.json` declares different
|
|
338
|
+
* `providedInvariants` than `ops.json` implies. The package is **retained**.
|
|
339
|
+
* - `packageUnloadable` — the manifest is missing, unparseable, or schema-
|
|
340
|
+
* invalid. The package is **omitted** from `packages`.
|
|
341
|
+
*
|
|
342
|
+
* Callers that need the `spaceId` context (e.g. the aggregate loader) attach
|
|
343
|
+
* it when converting to {@link import('./integrity-violation').IntegrityViolation}.
|
|
344
|
+
*/
|
|
345
|
+
export type PackageLoadProblem =
|
|
346
|
+
| {
|
|
347
|
+
readonly kind: 'hashMismatch';
|
|
348
|
+
readonly dirName: string;
|
|
349
|
+
readonly stored: string;
|
|
350
|
+
readonly computed: string;
|
|
351
|
+
}
|
|
352
|
+
| { readonly kind: 'providedInvariantsMismatch'; readonly dirName: string }
|
|
353
|
+
| { readonly kind: 'packageUnloadable'; readonly dirName: string; readonly detail: string };
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Result returned by {@link readMigrationsDir}.
|
|
357
|
+
*
|
|
358
|
+
* - `packages` — every package that could be read; hash-mismatched and
|
|
359
|
+
* invariants-mismatched packages are included here (the problem is
|
|
360
|
+
* represented rather than fatal).
|
|
361
|
+
* - `problems` — one entry per package that had a load-time issue.
|
|
362
|
+
* `packageUnloadable` entries are **not** in `packages`.
|
|
363
|
+
*/
|
|
364
|
+
export interface ReadMigrationsDirResult {
|
|
365
|
+
readonly packages: readonly OnDiskMigrationPackage[];
|
|
366
|
+
readonly problems: readonly PackageLoadProblem[];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function packageLoadProblemDetailFromError(error: unknown): string {
|
|
370
|
+
if (MigrationToolsError.is(error)) return error.why;
|
|
371
|
+
if (error instanceof Error) return error.message;
|
|
372
|
+
return String(error);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function readMigrationsDir(migrationsRoot: string): Promise<ReadMigrationsDirResult> {
|
|
286
376
|
let entries: string[];
|
|
287
377
|
try {
|
|
288
378
|
entries = await readdir(migrationsRoot);
|
|
289
379
|
} catch (error) {
|
|
290
380
|
if (hasErrnoCode(error, 'ENOENT')) {
|
|
291
|
-
return [];
|
|
381
|
+
return { packages: [], problems: [] };
|
|
292
382
|
}
|
|
293
383
|
throw error;
|
|
294
384
|
}
|
|
295
385
|
|
|
296
386
|
const packages: OnDiskMigrationPackage[] = [];
|
|
387
|
+
const problems: PackageLoadProblem[] = [];
|
|
297
388
|
|
|
298
389
|
for (const entry of entries.sort()) {
|
|
299
390
|
const entryPath = join(migrationsRoot, entry);
|
|
@@ -307,10 +398,44 @@ export async function readMigrationsDir(
|
|
|
307
398
|
continue; // skip non-migration directories
|
|
308
399
|
}
|
|
309
400
|
|
|
310
|
-
|
|
401
|
+
let pkg: OnDiskMigrationPackage;
|
|
402
|
+
try {
|
|
403
|
+
pkg = await readMigrationPackage(entryPath);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const dirName = entry;
|
|
406
|
+
if (MigrationToolsError.is(error)) {
|
|
407
|
+
if (error.code === 'MIGRATION.HASH_MISMATCH') {
|
|
408
|
+
const details = error.details;
|
|
409
|
+
const rawPkg = await readMigrationPackageRaw(entryPath);
|
|
410
|
+
if (rawPkg !== null) packages.push(rawPkg);
|
|
411
|
+
problems.push({
|
|
412
|
+
kind: 'hashMismatch',
|
|
413
|
+
dirName,
|
|
414
|
+
stored: typeof details?.['storedHash'] === 'string' ? details['storedHash'] : '',
|
|
415
|
+
computed: typeof details?.['computedHash'] === 'string' ? details['computedHash'] : '',
|
|
416
|
+
});
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (error.code === 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH') {
|
|
420
|
+
const rawPkg = await readMigrationPackageRaw(entryPath);
|
|
421
|
+
if (rawPkg !== null) packages.push(rawPkg);
|
|
422
|
+
problems.push({ kind: 'providedInvariantsMismatch', dirName });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Any other error (missing file, invalid JSON, invalid manifest schema) →
|
|
427
|
+
// package unloadable; omit from packages.
|
|
428
|
+
problems.push({
|
|
429
|
+
kind: 'packageUnloadable',
|
|
430
|
+
dirName,
|
|
431
|
+
detail: packageLoadProblemDetailFromError(error),
|
|
432
|
+
});
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
packages.push(pkg);
|
|
311
436
|
}
|
|
312
437
|
|
|
313
|
-
return packages;
|
|
438
|
+
return { packages, problems };
|
|
314
439
|
}
|
|
315
440
|
|
|
316
441
|
export function formatMigrationDirName(timestamp: Date, slug: string): string {
|
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
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
2
|
import { EMPTY_CONTRACT_HASH } from './constants';
|
|
3
|
-
import {
|
|
4
|
-
errorAmbiguousTarget,
|
|
5
|
-
errorDuplicateMigrationHash,
|
|
6
|
-
errorNoInitialMigration,
|
|
7
|
-
errorNoTarget,
|
|
8
|
-
errorSameSourceAndTarget,
|
|
9
|
-
} from './errors';
|
|
3
|
+
import { errorAmbiguousTarget, errorNoInitialMigration, errorNoTarget } from './errors';
|
|
10
4
|
import type { MigrationEdge, MigrationGraph } from './graph';
|
|
11
5
|
import { bfs } from './graph-ops';
|
|
12
6
|
import type { OnDiskMigrationPackage } from './package';
|
|
@@ -50,13 +44,6 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
50
44
|
const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;
|
|
51
45
|
const { to } = pkg.metadata;
|
|
52
46
|
|
|
53
|
-
if (from === to) {
|
|
54
|
-
const hasDataOp = pkg.ops.some((op) => op.operationClass === 'data');
|
|
55
|
-
if (!hasDataOp) {
|
|
56
|
-
throw errorSameSourceAndTarget(pkg.dirPath, from);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
47
|
nodes.add(from);
|
|
61
48
|
nodes.add(to);
|
|
62
49
|
|
|
@@ -66,14 +53,12 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
66
53
|
migrationHash: pkg.metadata.migrationHash,
|
|
67
54
|
dirName: pkg.dirName,
|
|
68
55
|
createdAt: pkg.metadata.createdAt,
|
|
69
|
-
labels: pkg.metadata.labels,
|
|
70
56
|
invariants: pkg.metadata.providedInvariants,
|
|
71
57
|
};
|
|
72
58
|
|
|
73
|
-
if (migrationByHash.has(migration.migrationHash)) {
|
|
74
|
-
|
|
59
|
+
if (!migrationByHash.has(migration.migrationHash)) {
|
|
60
|
+
migrationByHash.set(migration.migrationHash, migration);
|
|
75
61
|
}
|
|
76
|
-
migrationByHash.set(migration.migrationHash, migration);
|
|
77
62
|
|
|
78
63
|
appendEdge(forwardChain, from, migration);
|
|
79
64
|
appendEdge(reverseChain, to, migration);
|
|
@@ -85,23 +70,10 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
85
70
|
// ---------------------------------------------------------------------------
|
|
86
71
|
// Deterministic tie-breaking for BFS neighbour order.
|
|
87
72
|
// Used by path-finders only; not a general-purpose utility.
|
|
88
|
-
// Ordering:
|
|
73
|
+
// Ordering: createdAt → to → migrationHash.
|
|
89
74
|
// ---------------------------------------------------------------------------
|
|
90
75
|
|
|
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
76
|
function compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {
|
|
103
|
-
const lp = labelPriority(a.labels) - labelPriority(b.labels);
|
|
104
|
-
if (lp !== 0) return lp;
|
|
105
77
|
const ca = a.createdAt.localeCompare(b.createdAt);
|
|
106
78
|
if (ca !== 0) return ca;
|
|
107
79
|
const tc = a.to.localeCompare(b.to);
|
|
@@ -119,7 +91,7 @@ function sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdg
|
|
|
119
91
|
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
120
92
|
*
|
|
121
93
|
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
122
|
-
*
|
|
94
|
+
* createdAt → to → migrationHash.
|
|
123
95
|
*/
|
|
124
96
|
export function findPath(
|
|
125
97
|
graph: MigrationGraph,
|
|
@@ -165,8 +137,8 @@ export function findPath(
|
|
|
165
137
|
* control chars at authoring time).
|
|
166
138
|
*
|
|
167
139
|
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
|
|
168
|
-
* invariant come first, with `
|
|
169
|
-
*
|
|
140
|
+
* invariant come first, with `createdAt → to → migrationHash` as the
|
|
141
|
+
* secondary key. The heuristic steers BFS toward the satisfying path;
|
|
170
142
|
* correctness (shortest, deterministic) does not depend on it.
|
|
171
143
|
*/
|
|
172
144
|
export function findPathWithInvariants(
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { EMPTY_CONTRACT_HASH } from './constants';
|
|
2
|
+
import type { MigrationListEntry } from './migration-list-types';
|
|
3
|
+
|
|
4
|
+
export type MigrationEdgeKind = 'forward' | 'rollback' | 'self';
|
|
5
|
+
|
|
6
|
+
export interface MigrationListGraphTopology {
|
|
7
|
+
readonly kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>;
|
|
8
|
+
readonly forwardInDegree: ReadonlyMap<string, number>;
|
|
9
|
+
readonly forwardOutDegree: ReadonlyMap<string, number>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface NonSelfEdge {
|
|
13
|
+
readonly entry: MigrationListEntry;
|
|
14
|
+
readonly from: string;
|
|
15
|
+
readonly to: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function canonicalFrom(from: string | null): string {
|
|
19
|
+
return from ?? EMPTY_CONTRACT_HASH;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compareDirNameDesc(a: MigrationListEntry, b: MigrationListEntry): number {
|
|
23
|
+
return b.dirName.localeCompare(a.dirName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function bumpDegree(map: Map<string, number>, key: string): void {
|
|
27
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function classifyMigrationListGraphTopology(
|
|
31
|
+
entries: readonly MigrationListEntry[],
|
|
32
|
+
): MigrationListGraphTopology {
|
|
33
|
+
const nodes = new Set<string>();
|
|
34
|
+
const kindByMigrationHash = new Map<string, MigrationEdgeKind>();
|
|
35
|
+
const outgoingByFrom = new Map<string, NonSelfEdge[]>();
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const from = canonicalFrom(entry.from);
|
|
39
|
+
const to = entry.to;
|
|
40
|
+
nodes.add(from);
|
|
41
|
+
nodes.add(to);
|
|
42
|
+
|
|
43
|
+
if (from === to) {
|
|
44
|
+
kindByMigrationHash.set(entry.migrationHash, 'self');
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const bucket = outgoingByFrom.get(from);
|
|
49
|
+
const edge: NonSelfEdge = { entry, from, to };
|
|
50
|
+
if (bucket) bucket.push(edge);
|
|
51
|
+
else outgoingByFrom.set(from, [edge]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const bucket of outgoingByFrom.values()) {
|
|
55
|
+
bucket.sort((a, b) => compareDirNameDesc(a.entry, b.entry));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const nonSelfInDegree = new Map<string, number>();
|
|
59
|
+
for (const node of nodes) {
|
|
60
|
+
nonSelfInDegree.set(node, 0);
|
|
61
|
+
}
|
|
62
|
+
for (const bucket of outgoingByFrom.values()) {
|
|
63
|
+
for (const edge of bucket) {
|
|
64
|
+
bumpDegree(nonSelfInDegree, edge.to);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const dfsRoots: string[] = [];
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
if ((nonSelfInDegree.get(node) ?? 0) === 0) {
|
|
71
|
+
dfsRoots.push(node);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
dfsRoots.sort((a, b) => {
|
|
75
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
76
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
77
|
+
return a.localeCompare(b);
|
|
78
|
+
});
|
|
79
|
+
if (dfsRoots.length === 0) {
|
|
80
|
+
dfsRoots.push(...[...nodes].sort((a, b) => a.localeCompare(b)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const WHITE = 0;
|
|
84
|
+
const GRAY = 1;
|
|
85
|
+
const BLACK = 2;
|
|
86
|
+
const color = new Map<string, number>();
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
color.set(node, WHITE);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Frame {
|
|
92
|
+
node: string;
|
|
93
|
+
outgoing: readonly NonSelfEdge[];
|
|
94
|
+
index: number;
|
|
95
|
+
}
|
|
96
|
+
const stack: Frame[] = [];
|
|
97
|
+
|
|
98
|
+
function pushFrame(node: string): void {
|
|
99
|
+
color.set(node, GRAY);
|
|
100
|
+
stack.push({ node, outgoing: outgoingByFrom.get(node) ?? [], index: 0 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runDfsFrom(root: string): void {
|
|
104
|
+
if (color.get(root) !== WHITE) return;
|
|
105
|
+
pushFrame(root);
|
|
106
|
+
|
|
107
|
+
while (stack.length > 0) {
|
|
108
|
+
const frame = stack[stack.length - 1];
|
|
109
|
+
if (frame === undefined) break;
|
|
110
|
+
if (frame.index >= frame.outgoing.length) {
|
|
111
|
+
color.set(frame.node, BLACK);
|
|
112
|
+
stack.pop();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const edge = frame.outgoing[frame.index];
|
|
117
|
+
frame.index += 1;
|
|
118
|
+
if (edge === undefined) continue;
|
|
119
|
+
|
|
120
|
+
const v = edge.to;
|
|
121
|
+
const vColor = color.get(v);
|
|
122
|
+
if (vColor === GRAY) {
|
|
123
|
+
kindByMigrationHash.set(edge.entry.migrationHash, 'rollback');
|
|
124
|
+
} else {
|
|
125
|
+
kindByMigrationHash.set(edge.entry.migrationHash, 'forward');
|
|
126
|
+
if (vColor === WHITE) {
|
|
127
|
+
pushFrame(v);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const root of dfsRoots) {
|
|
134
|
+
runDfsFrom(root);
|
|
135
|
+
}
|
|
136
|
+
const remainingWhite = [...nodes].filter((node) => color.get(node) === WHITE);
|
|
137
|
+
remainingWhite.sort((a, b) => a.localeCompare(b));
|
|
138
|
+
for (const root of remainingWhite) {
|
|
139
|
+
runDfsFrom(root);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const forwardInDegree = new Map<string, number>();
|
|
143
|
+
const forwardOutDegree = new Map<string, number>();
|
|
144
|
+
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (kindByMigrationHash.get(entry.migrationHash) !== 'forward') continue;
|
|
147
|
+
const from = canonicalFrom(entry.from);
|
|
148
|
+
const to = entry.to;
|
|
149
|
+
bumpDegree(forwardOutDegree, from);
|
|
150
|
+
bumpDegree(forwardInDegree, to);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
kindByMigrationHash,
|
|
155
|
+
forwardInDegree,
|
|
156
|
+
forwardOutDegree,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -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
|
+
}
|