@prisma-next/migration-tools 0.11.0-dev.6 → 0.11.0-dev.61
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-vFROOhCR.mjs} +46 -21
- package/dist/errors-vFROOhCR.mjs.map +1 -0
- package/dist/exports/aggregate.d.mts +302 -178
- package/dist/exports/aggregate.d.mts.map +1 -1
- package/dist/exports/aggregate.mjs +475 -238
- package/dist/exports/aggregate.mjs.map +1 -1
- 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 +3 -2
- 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 +3 -2
- package/dist/exports/spaces.d.mts +31 -132
- package/dist/exports/spaces.d.mts.map +1 -1
- package/dist/exports/spaces.mjs +13 -9
- package/dist/exports/spaces.mjs.map +1 -1
- package/dist/{graph-BrLXqoUc.d.mts → graph-3dLMZp5l.d.mts} +1 -2
- package/dist/graph-3dLMZp5l.d.mts.map +1 -0
- package/dist/graph-membership-BV23F1IV.mjs +15 -0
- package/dist/graph-membership-BV23F1IV.mjs.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-C23nXy1c.mjs} +2 -2
- package/dist/{invariants-0daYEzyo.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
- package/dist/{io-BPLfzvZe.mjs → io-BGlPOt9b.mjs} +100 -13
- package/dist/io-BGlPOt9b.mjs.map +1 -0
- package/dist/io-BH4G3F-i.d.mts +124 -0
- package/dist/io-BH4G3F-i.d.mts.map +1 -0
- package/dist/metadata-Bp9X04gM.d.mts +2 -0
- package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-BMAqSfv9.mjs} +6 -26
- package/dist/migration-graph-BMAqSfv9.mjs.map +1 -0
- package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-CWEM2SLR.d.mts} +6 -6
- package/dist/migration-graph-CWEM2SLR.d.mts.map +1 -0
- package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
- package/dist/{package-DZj8YvD0.d.mts → package-Ca-J_z_0.d.mts} +1 -1
- package/dist/package-Ca-J_z_0.d.mts.map +1 -0
- package/dist/{read-contract-space-contract-DRueB4Aa.mjs → read-contract-space-contract-TbeXuJXL.mjs} +32 -5
- package/dist/read-contract-space-contract-TbeXuJXL.mjs.map +1 -0
- package/dist/{refs-BDHo5l_g.mjs → refs-C-_WUrPw.mjs} +97 -4
- package/dist/refs-C-_WUrPw.mjs.map +1 -0
- package/dist/refs-C7wuYFqZ.d.mts +42 -0
- package/dist/refs-C7wuYFqZ.d.mts.map +1 -0
- package/dist/snapshot-38tKJ9o9.mjs +137 -0
- package/dist/snapshot-38tKJ9o9.mjs.map +1 -0
- package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
- package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
- package/package.json +6 -6
- package/src/aggregate/aggregate.ts +266 -0
- package/src/aggregate/check-integrity.ts +243 -0
- package/src/aggregate/loader.ts +161 -334
- package/src/aggregate/planner.ts +8 -6
- package/src/aggregate/project-schema-to-space.ts +3 -8
- package/src/aggregate/strategies/graph-walk.ts +12 -7
- package/src/aggregate/strategies/synth.ts +2 -2
- package/src/aggregate/types.ts +81 -62
- package/src/aggregate/verifier.ts +9 -6
- 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/errors.ts +58 -2
- package/src/exports/aggregate.ts +18 -8
- 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/refs.ts +11 -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/read-contract-space-head-ref.ts +5 -2
- package/src/refs/snapshot.ts +197 -0
- package/src/refs.ts +124 -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 +0 -16
- package/dist/refs-CDaNerhT.d.mts.map +0 -1
- package/src/aggregate/extract-storage-element-names.ts +0 -75
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { MigrationPlan } from '@prisma-next/framework-components/control';
|
|
2
2
|
import { EMPTY_CONTRACT_HASH } from '../../constants';
|
|
3
3
|
import { findPathWithDecision } from '../../migration-graph';
|
|
4
|
-
import type { MigrationOps } from '../../package';
|
|
4
|
+
import type { MigrationOps, OnDiskMigrationPackage } from '../../package';
|
|
5
|
+
import { requireHeadRef } from '../aggregate';
|
|
5
6
|
import type { ContractMarkerRecordLike } from '../marker-types';
|
|
6
7
|
import type { AggregatePerSpacePlan } from '../planner-types';
|
|
7
8
|
import type { ContractSpaceMember } from '../types';
|
|
@@ -9,7 +10,7 @@ import type { ContractSpaceMember } from '../types';
|
|
|
9
10
|
/**
|
|
10
11
|
* Outcome variants for the graph-walk strategy. Mirrors
|
|
11
12
|
* {@link import('../../compute-extension-space-apply-path').ExtensionSpaceApplyPathOutcome}
|
|
12
|
-
* but operates against the
|
|
13
|
+
* but operates against the member's lazily-reconstructed `graph()`
|
|
13
14
|
* instead of re-reading from disk. The aggregate planner converts
|
|
14
15
|
* these into {@link import('../planner-types').AggregatePlannerError}
|
|
15
16
|
* variants.
|
|
@@ -49,13 +50,17 @@ export interface GraphWalkStrategyInputs {
|
|
|
49
50
|
*/
|
|
50
51
|
export function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutcome {
|
|
51
52
|
const { aggregateTargetId, member, currentMarker, refName } = input;
|
|
52
|
-
const
|
|
53
|
+
const headRef = requireHeadRef(member);
|
|
54
|
+
const graph = member.graph();
|
|
55
|
+
const packagesByMigrationHash = new Map<string, OnDiskMigrationPackage>(
|
|
56
|
+
member.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]),
|
|
57
|
+
);
|
|
53
58
|
|
|
54
59
|
const fromHash = currentMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
|
|
55
60
|
const markerInvariants = new Set(currentMarker?.invariants ?? []);
|
|
56
|
-
const required = new Set(
|
|
61
|
+
const required = new Set(headRef.invariants.filter((id) => !markerInvariants.has(id)));
|
|
57
62
|
|
|
58
|
-
const outcome = findPathWithDecision(graph, fromHash,
|
|
63
|
+
const outcome = findPathWithDecision(graph, fromHash, headRef.hash, {
|
|
59
64
|
required,
|
|
60
65
|
...(refName !== undefined ? { refName } : {}),
|
|
61
66
|
});
|
|
@@ -98,7 +103,7 @@ export function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutc
|
|
|
98
103
|
targetId: aggregateTargetId,
|
|
99
104
|
spaceId: member.spaceId,
|
|
100
105
|
origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
|
|
101
|
-
destination: { storageHash:
|
|
106
|
+
destination: { storageHash: headRef.hash },
|
|
102
107
|
operations: pathOps,
|
|
103
108
|
providedInvariants: [...providedInvariantsSet].sort(),
|
|
104
109
|
};
|
|
@@ -108,7 +113,7 @@ export function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutc
|
|
|
108
113
|
result: {
|
|
109
114
|
plan,
|
|
110
115
|
displayOps: pathOps,
|
|
111
|
-
destinationContract: member.contract,
|
|
116
|
+
destinationContract: member.contract(),
|
|
112
117
|
strategy: 'graph-walk',
|
|
113
118
|
migrationEdges: edgeRefs,
|
|
114
119
|
pathDecision: outcome.decision,
|
|
@@ -73,7 +73,7 @@ export async function synthStrategy<TFamilyId extends string, TTargetId extends
|
|
|
73
73
|
|
|
74
74
|
const planner = input.migrations.createPlanner(input.familyInstance);
|
|
75
75
|
const plannerResult: MigrationPlannerResult = await (planner.plan({
|
|
76
|
-
contract: input.member.contract,
|
|
76
|
+
contract: input.member.contract(),
|
|
77
77
|
schema: projectedSchema,
|
|
78
78
|
policy: input.operationPolicy,
|
|
79
79
|
fromContract: null,
|
|
@@ -114,7 +114,7 @@ export async function synthStrategy<TFamilyId extends string, TTargetId extends
|
|
|
114
114
|
result: {
|
|
115
115
|
plan,
|
|
116
116
|
displayOps: synthedPlan.operations,
|
|
117
|
-
destinationContract: input.member.contract,
|
|
117
|
+
destinationContract: input.member.contract(),
|
|
118
118
|
strategy: 'synth',
|
|
119
119
|
},
|
|
120
120
|
};
|
package/src/aggregate/types.ts
CHANGED
|
@@ -1,89 +1,108 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import type { MigrationGraph } from '../graph';
|
|
3
|
+
import type { IntegrityQueryOptions, IntegrityViolation } from '../integrity-violation';
|
|
3
4
|
import type { OnDiskMigrationPackage } from '../package';
|
|
5
|
+
import type { Refs } from '../refs';
|
|
6
|
+
import type { ContractSpaceHeadRecord } from '../verify-contract-spaces';
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* `graph` is the structural shortest-path graph (forward / reverse chain,
|
|
9
|
-
* deterministic tie-break order) reconstructed from a set of on-disk
|
|
10
|
-
* migration packages. `packagesByMigrationHash` is the lookup table the
|
|
11
|
-
* graph-walk strategy uses to resolve a path's edge sequence back to the
|
|
12
|
-
* concrete `OnDiskMigrationPackage` (and therefore the operation list) for
|
|
13
|
-
* apply.
|
|
14
|
-
*
|
|
15
|
-
* Eagerly hydrated by the loader. Once a `ContractSpaceAggregate` exists,
|
|
16
|
-
* downstream consumers do **not** touch the filesystem to walk graphs or
|
|
17
|
-
* resolve packages — the aggregate is the boundary.
|
|
18
|
-
*/
|
|
19
|
-
export interface HydratedMigrationGraph {
|
|
20
|
-
readonly graph: MigrationGraph;
|
|
21
|
-
readonly packagesByMigrationHash: ReadonlyMap<string, OnDiskMigrationPackage>;
|
|
8
|
+
export interface ContractAtOptions {
|
|
9
|
+
readonly refName?: string;
|
|
22
10
|
}
|
|
23
11
|
|
|
12
|
+
export type ContractAtResult =
|
|
13
|
+
| {
|
|
14
|
+
readonly provenance: 'snapshot';
|
|
15
|
+
readonly hash: string;
|
|
16
|
+
readonly contractJson: unknown;
|
|
17
|
+
readonly contractDts: string;
|
|
18
|
+
readonly contract: Contract;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
readonly provenance: 'graph-node';
|
|
22
|
+
readonly sourceDir: string;
|
|
23
|
+
readonly hash: string;
|
|
24
|
+
readonly contractJson: unknown;
|
|
25
|
+
readonly contractDts: string;
|
|
26
|
+
readonly contract: Contract;
|
|
27
|
+
};
|
|
28
|
+
|
|
24
29
|
/**
|
|
25
30
|
* One contract space — app or extension — as a member of a
|
|
26
31
|
* {@link ContractSpaceAggregate}. Every member has the same shape.
|
|
27
32
|
*
|
|
33
|
+
* A member is a tolerant snapshot of one space's on-disk state, not a
|
|
34
|
+
* validated value: `packages` is the raw migration-package list as read
|
|
35
|
+
* from disk (a hash- or invariants-mismatched package is retained here;
|
|
36
|
+
* a genuinely unparseable one is omitted), and integrity is judged
|
|
37
|
+
* separately by {@link ContractSpaceAggregate.checkIntegrity}.
|
|
38
|
+
*
|
|
28
39
|
* - `spaceId`: `'app'` for the application, otherwise the extension's
|
|
29
40
|
* id (validated against `[a-z][a-z0-9_-]{0,63}`).
|
|
30
|
-
* - `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* - `packages`: raw on-disk migration packages, as read; never
|
|
42
|
+
* integrity-validated at load.
|
|
43
|
+
* - `refs`: the user-authored refs under `migrations/<spaceId>/refs/*.json`.
|
|
44
|
+
* - `headRef`: the system head ref read from
|
|
45
|
+
* `migrations/<spaceId>/refs/head.json`, or `null` when absent
|
|
46
|
+
* (represented as a `headRefMissing` violation, never fatal). The app
|
|
47
|
+
* member's head ref is always synthesised from its live contract's
|
|
48
|
+
* storage hash, so it is never `null`.
|
|
49
|
+
* - `graph()`: the migration graph this space's packages induce —
|
|
50
|
+
* lazily reconstructed on first call and memoised. Pure structure: a
|
|
51
|
+
* `from === to` self-edge is represented, not rejected.
|
|
52
|
+
* - `contract()`: the deserialized contract for this member — lazily
|
|
53
|
+
* produced on first call and memoised. For the app it is the live
|
|
54
|
+
* contract the caller supplied; for an extension it is the on-disk
|
|
55
|
+
* `migrations/<spaceId>/contract.json` run through the family's
|
|
56
|
+
* `deserializeContract`. Throws if the on-disk contract is missing or
|
|
57
|
+
* undeserializable (surfaced as `contractUnreadable` by `checkIntegrity`
|
|
58
|
+
* under `checkContracts`); callers gate before querying it.
|
|
59
|
+
* - `contractAt(hash, opts?)`: materializes the contract at an arbitrary
|
|
60
|
+
* graph node — when `opts.refName` is set, prefer the ref's paired
|
|
61
|
+
* snapshot; else find the package whose `metadata.to === hash` and read
|
|
62
|
+
* its `end-contract.*`. Lazy per `(hash, refName?)` memoisation; throws
|
|
63
|
+
* typed {@link MigrationToolsError} values compatible with CLI mappers.
|
|
44
64
|
*/
|
|
45
65
|
export interface ContractSpaceMember {
|
|
46
66
|
readonly spaceId: string;
|
|
47
|
-
readonly
|
|
48
|
-
readonly
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
readonly packages: readonly OnDiskMigrationPackage[];
|
|
68
|
+
readonly refs: Refs;
|
|
69
|
+
readonly headRef: ContractSpaceHeadRecord | null;
|
|
70
|
+
graph(): MigrationGraph;
|
|
71
|
+
contract(): Contract;
|
|
72
|
+
contractAt(hash: string, opts?: ContractAtOptions): Promise<ContractAtResult>;
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
76
|
+
* Tolerant, queryable snapshot of a project's on-disk migration state:
|
|
77
|
+
* the app contract space plus every extension contract space, each a
|
|
78
|
+
* {@link ContractSpaceMember}.
|
|
58
79
|
*
|
|
59
80
|
* Produced once per CLI invocation by `loadContractSpaceAggregate`.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* Invariants the loader enforces at construction:
|
|
64
|
-
*
|
|
65
|
-
* 1. `targetId` is consistent across every member (`contract.target`
|
|
66
|
-
* matches `aggregate.targetId`). The aggregate's `targetId` is the
|
|
67
|
-
* `Config.adapter.targetId` value the loader was told to use.
|
|
68
|
-
* 2. `aggregate.extensions` is sorted alphabetically by `spaceId`.
|
|
69
|
-
* Mirrors {@link import('../concatenate-space-apply-inputs').concatenateSpaceApplyInputs}'s
|
|
70
|
-
* extension ordering convention so downstream apply order matches
|
|
71
|
-
* today's behaviour byte-for-byte.
|
|
72
|
-
* 3. No two members claim the same storage element (table / type / etc.).
|
|
73
|
-
* 4. For each extension member: `member.headRef.hash` is reachable from
|
|
74
|
-
* the empty-contract sentinel in `member.migrations.graph` (or the
|
|
75
|
-
* graph is empty and `member.headRef.hash === EMPTY_CONTRACT_HASH`).
|
|
76
|
-
* 5. For the app member: `member.headRef.hash` equals
|
|
77
|
-
* `member.contract.storage.storageHash`. The app's `migrations`
|
|
78
|
-
* is hydrated from the user's authored `migrations/` (or empty if
|
|
79
|
-
* none).
|
|
81
|
+
* Building the aggregate never throws on disk content; every consumer
|
|
82
|
+
* obtains spaces / packages / refs / graphs from this one value rather
|
|
83
|
+
* than re-deriving them from disk.
|
|
80
84
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
85
|
+
* - `targetId`: the app contract's target; every member is expected to
|
|
86
|
+
* share it (a mismatch surfaces as a `targetMismatch` violation under
|
|
87
|
+
* `checkContracts`).
|
|
88
|
+
* - `app` / `extensions`: retained as fields for the existing planner /
|
|
89
|
+
* verifier / runner consumers. `extensions` is sorted alphabetically
|
|
90
|
+
* by `spaceId` (the apply-ordering convention).
|
|
91
|
+
* - `listSpaces()` / `hasSpace()` / `space()` / `spaces()`: the query
|
|
92
|
+
* surface the read commands consume — `app` first, then extension ids
|
|
93
|
+
* lex-ascending.
|
|
94
|
+
* - `checkIntegrity()`: judges the loaded model and returns every
|
|
95
|
+
* violation (never bailing at the first). Config/contract-dependent
|
|
96
|
+
* checks run only when the matching {@link IntegrityQueryOptions} opt
|
|
97
|
+
* is set.
|
|
84
98
|
*/
|
|
85
99
|
export interface ContractSpaceAggregate {
|
|
86
100
|
readonly targetId: string;
|
|
87
101
|
readonly app: ContractSpaceMember;
|
|
88
102
|
readonly extensions: readonly ContractSpaceMember[];
|
|
103
|
+
listSpaces(): readonly string[];
|
|
104
|
+
hasSpace(id: string): boolean;
|
|
105
|
+
space(id: string): ContractSpaceMember | undefined;
|
|
106
|
+
spaces(): readonly ContractSpaceMember[];
|
|
107
|
+
checkIntegrity(opts?: IntegrityQueryOptions): readonly IntegrityViolation[];
|
|
89
108
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { elementCoordinates } from '@prisma-next/framework-components/ir';
|
|
1
2
|
import type { Result } from '@prisma-next/utils/result';
|
|
2
3
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
3
|
-
import {
|
|
4
|
+
import { requireHeadRef } from './aggregate';
|
|
4
5
|
import type { ContractMarkerRecordLike } from './marker-types';
|
|
5
6
|
import { projectSchemaToSpace } from './project-schema-to-space';
|
|
6
7
|
import type { ContractSpaceAggregate, ContractSpaceMember } from './types';
|
|
@@ -145,16 +146,17 @@ function runVerifyAggregate<TSchemaResult>(
|
|
|
145
146
|
markerPerSpace.set(member.spaceId, { kind: 'absent' });
|
|
146
147
|
continue;
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
+
const headRef = requireHeadRef(member);
|
|
150
|
+
if (marker.storageHash !== headRef.hash) {
|
|
149
151
|
markerPerSpace.set(member.spaceId, {
|
|
150
152
|
kind: 'hashMismatch',
|
|
151
153
|
markerHash: marker.storageHash,
|
|
152
|
-
expected:
|
|
154
|
+
expected: headRef.hash,
|
|
153
155
|
});
|
|
154
156
|
continue;
|
|
155
157
|
}
|
|
156
158
|
const markerInvariants = new Set(marker.invariants);
|
|
157
|
-
const missing =
|
|
159
|
+
const missing = headRef.invariants.filter((id) => !markerInvariants.has(id));
|
|
158
160
|
if (missing.length > 0) {
|
|
159
161
|
markerPerSpace.set(member.spaceId, {
|
|
160
162
|
kind: 'missingInvariants',
|
|
@@ -211,8 +213,9 @@ function detectOrphanElements(
|
|
|
211
213
|
|
|
212
214
|
const claimedTables = new Set<string>();
|
|
213
215
|
for (const member of members) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
const contract = member.contract();
|
|
217
|
+
for (const { entityName } of elementCoordinates(contract.storage)) {
|
|
218
|
+
claimedTables.add(entityName);
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { PreserveEmptyPredicate, StorageSort } from '@prisma-next/contract/hashing';
|
|
1
2
|
import { computeStorageHash } from '@prisma-next/contract/hashing';
|
|
3
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
4
|
import { errorDescriptorHeadHashMismatch } from './errors';
|
|
3
5
|
|
|
4
6
|
function stripNamespaceKinds(storage: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -35,6 +37,8 @@ export interface DescriptorSelfConsistencyInputs {
|
|
|
35
37
|
*/
|
|
36
38
|
readonly storage: unknown;
|
|
37
39
|
readonly headRefHash: string;
|
|
40
|
+
readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
|
|
41
|
+
readonly sortStorage?: StorageSort;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
@@ -82,6 +86,8 @@ export function assertDescriptorSelfConsistency(inputs: DescriptorSelfConsistenc
|
|
|
82
86
|
target: inputs.target,
|
|
83
87
|
targetFamily: inputs.targetFamily,
|
|
84
88
|
storage: normalizedStorage,
|
|
89
|
+
...ifDefined('shouldPreserveEmpty', inputs.shouldPreserveEmpty),
|
|
90
|
+
...ifDefined('sortStorage', inputs.sortStorage),
|
|
85
91
|
});
|
|
86
92
|
if (recomputed !== inputs.headRefHash) {
|
|
87
93
|
throw errorDescriptorHeadHashMismatch({
|
|
@@ -97,7 +97,7 @@ export async function computeExtensionSpaceApplyPath(
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const spaceDir = spaceMigrationDirectory(projectMigrationsDir, spaceId);
|
|
100
|
-
const packages = await readMigrationsDir(spaceDir);
|
|
100
|
+
const { packages } = await readMigrationsDir(spaceDir);
|
|
101
101
|
const graph = reconstructGraph(packages);
|
|
102
102
|
|
|
103
103
|
// Live-marker layer encodes "no prior state" as EMPTY_CONTRACT_HASH;
|
|
@@ -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
|
}
|
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,58 @@ export function errorMigrationHashMismatch(
|
|
|
399
400
|
details: { dir, storedHash, computedHash },
|
|
400
401
|
});
|
|
401
402
|
}
|
|
403
|
+
|
|
404
|
+
export function errorSnapshotMissing(refName: string): MigrationToolsError {
|
|
405
|
+
return new MigrationToolsError(
|
|
406
|
+
'MIGRATION.SNAPSHOT_MISSING',
|
|
407
|
+
`Ref "${refName}" has no paired contract snapshot`,
|
|
408
|
+
{
|
|
409
|
+
why: `Ref "${refName}" exists but its paired snapshot files are missing.`,
|
|
410
|
+
fix: `Run "prisma-next db update --advance-ref ${refName}" to repopulate the snapshot, or "prisma-next ref delete ${refName}" to clear the orphan pointer.`,
|
|
411
|
+
details: { refName, identifier: refName, viaRef: true },
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function errorBundleNotFoundForGraphNode(
|
|
417
|
+
hash: string,
|
|
418
|
+
explicitLabel?: string,
|
|
419
|
+
): MigrationToolsError {
|
|
420
|
+
const summary = explicitLabel
|
|
421
|
+
? `No migration bundle found for reference "${explicitLabel}" (resolved hash: ${hash})`
|
|
422
|
+
: `No migration bundle found for graph node ${hash}`;
|
|
423
|
+
return new MigrationToolsError('MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE', summary, {
|
|
424
|
+
why: `The hash ${hash} is a graph node but no on-disk migration package has an end-contract hash matching it.`,
|
|
425
|
+
fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
|
|
426
|
+
details: { hash, ...(explicitLabel ? { explicitLabel } : {}) },
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function errorContractDeserializationFailed(
|
|
431
|
+
filePath: string,
|
|
432
|
+
message: string,
|
|
433
|
+
): MigrationToolsError {
|
|
434
|
+
return new MigrationToolsError(
|
|
435
|
+
'MIGRATION.CONTRACT_DESERIALIZATION_FAILED',
|
|
436
|
+
'Contract failed to deserialize',
|
|
437
|
+
{
|
|
438
|
+
why: `Contract at "${filePath}" failed to deserialize: ${message}`,
|
|
439
|
+
fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
|
|
440
|
+
details: { filePath, message },
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function errorHashNotInGraph(hash: string, graph: MigrationGraph): MigrationToolsError {
|
|
446
|
+
const reachableHashes = [...graph.nodes].sort();
|
|
447
|
+
const reachableList = reachableHashes.length > 0 ? reachableHashes.join(', ') : '(none)';
|
|
448
|
+
return new MigrationToolsError(
|
|
449
|
+
'MIGRATION.HASH_NOT_IN_GRAPH',
|
|
450
|
+
`Hash "${hash}" is not a node in the migration graph`,
|
|
451
|
+
{
|
|
452
|
+
why: `The migration graph contains nodes ${reachableList}; "${hash}" isn't one of them.`,
|
|
453
|
+
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.`,
|
|
454
|
+
details: { hash, reachableHashes },
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
}
|
package/src/exports/aggregate.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
createContractSpaceAggregate,
|
|
3
|
+
createContractSpaceMember,
|
|
4
|
+
requireHeadRef,
|
|
5
|
+
} from '../aggregate/aggregate';
|
|
6
|
+
export {
|
|
7
|
+
computeIntegrityViolations,
|
|
8
|
+
type IntegrityComputationInput,
|
|
9
|
+
type IntegritySpaceState,
|
|
10
|
+
loadProblemToViolation,
|
|
11
|
+
} from '../aggregate/check-integrity';
|
|
12
|
+
export { type LoadAggregateInput, loadContractSpaceAggregate } from '../aggregate/loader';
|
|
9
13
|
export type { ContractMarkerRecordLike } from '../aggregate/marker-types';
|
|
10
14
|
export {
|
|
11
15
|
type AggregateCurrentDBState,
|
|
@@ -25,9 +29,10 @@ export {
|
|
|
25
29
|
graphWalkStrategy,
|
|
26
30
|
} from '../aggregate/strategies/graph-walk';
|
|
27
31
|
export type {
|
|
32
|
+
ContractAtOptions,
|
|
33
|
+
ContractAtResult,
|
|
28
34
|
ContractSpaceAggregate,
|
|
29
35
|
ContractSpaceMember,
|
|
30
|
-
HydratedMigrationGraph,
|
|
31
36
|
} from '../aggregate/types';
|
|
32
37
|
export {
|
|
33
38
|
type AggregateVerifierError,
|
|
@@ -40,3 +45,8 @@ export {
|
|
|
40
45
|
type SchemaCheckSection,
|
|
41
46
|
verifyAggregate,
|
|
42
47
|
} from '../aggregate/verifier';
|
|
48
|
+
export type {
|
|
49
|
+
DeclaredExtensionEntry,
|
|
50
|
+
IntegrityQueryOptions,
|
|
51
|
+
IntegrityViolation,
|
|
52
|
+
} from '../integrity-violation';
|
package/src/exports/io.ts
CHANGED
package/src/exports/metadata.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { MigrationMetadata } from '../metadata';
|
package/src/exports/refs.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export type { RefEntry, Refs } from '../refs';
|
|
2
2
|
export {
|
|
3
3
|
deleteRef,
|
|
4
|
+
HEAD_REF_NAME,
|
|
4
5
|
readRef,
|
|
5
6
|
readRefs,
|
|
7
|
+
refsByContractHash,
|
|
6
8
|
resolveRef,
|
|
9
|
+
resolveRefsByContractHash,
|
|
7
10
|
validateRefName,
|
|
8
11
|
validateRefValue,
|
|
9
12
|
writeRef,
|
|
10
13
|
} from '../refs';
|
|
14
|
+
export type { ContractIR } from '../refs/snapshot';
|
|
15
|
+
export {
|
|
16
|
+
deleteRefPaired,
|
|
17
|
+
deleteRefSnapshot,
|
|
18
|
+
readRefSnapshot,
|
|
19
|
+
writeRefPaired,
|
|
20
|
+
writeRefSnapshot,
|
|
21
|
+
} 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);
|