@prisma-next/migration-tools 0.11.0 → 0.12.0
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 +328 -204
- package/dist/exports/aggregate.d.mts.map +1 -1
- package/dist/exports/aggregate.mjs +480 -243
- 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-Bazwo13S.mjs +137 -0
- package/dist/snapshot-Bazwo13S.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 +18 -9
- 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-types.ts +14 -14
- package/src/aggregate/planner.ts +20 -23
- package/src/aggregate/project-schema-to-space.ts +3 -8
- package/src/aggregate/strategies/graph-walk.ts +15 -10
- package/src/aggregate/strategies/synth.ts +4 -4
- package/src/aggregate/types.ts +81 -62
- package/src/aggregate/verifier.ts +23 -23
- 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 +29 -19
- 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 +199 -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
package/src/aggregate/planner.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
AggregatePlannerError,
|
|
5
|
-
AggregatePlannerInput,
|
|
6
|
-
AggregatePlannerOutput,
|
|
7
|
-
} from './planner-types';
|
|
2
|
+
import { requireHeadRef } from './aggregate';
|
|
3
|
+
import type { PerSpacePlan, PlannerError, PlannerInput, PlannerOutput } from './planner-types';
|
|
8
4
|
import { graphWalkStrategy } from './strategies/graph-walk';
|
|
9
5
|
import { synthStrategy } from './strategies/synth';
|
|
10
6
|
import type { ContractSpaceMember } from './types';
|
|
@@ -12,12 +8,12 @@ import type { ContractSpaceMember } from './types';
|
|
|
12
8
|
export type {
|
|
13
9
|
AggregateCurrentDBState,
|
|
14
10
|
AggregateMigrationEdgeRef,
|
|
15
|
-
AggregatePerSpacePlan,
|
|
16
|
-
AggregatePlannerError,
|
|
17
|
-
AggregatePlannerInput,
|
|
18
|
-
AggregatePlannerOutput,
|
|
19
|
-
AggregatePlannerSuccess,
|
|
20
11
|
CallerPolicy,
|
|
12
|
+
PerSpacePlan,
|
|
13
|
+
PlannerError,
|
|
14
|
+
PlannerInput,
|
|
15
|
+
PlannerOutput,
|
|
16
|
+
PlannerSuccess,
|
|
21
17
|
} from './planner-types';
|
|
22
18
|
|
|
23
19
|
/**
|
|
@@ -28,7 +24,7 @@ export type {
|
|
|
28
24
|
* 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
|
|
29
25
|
* - If `member.headRef.invariants` is empty → synth.
|
|
30
26
|
* - Else → `policyConflict` (synth cannot satisfy authored invariants).
|
|
31
|
-
* 2. Else if `member.
|
|
27
|
+
* 2. Else if `member.graph()` is non-empty AND graph-walk
|
|
32
28
|
* succeeds → graph-walk.
|
|
33
29
|
* 3. Else if `member.headRef.invariants` is empty → synth.
|
|
34
30
|
* 4. Else → graph-walk failure → `extensionPathUnreachable` /
|
|
@@ -37,18 +33,18 @@ export type {
|
|
|
37
33
|
* Output `applyOrder` is `[...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`
|
|
38
34
|
* — extensions alphabetical, then app — matching today's
|
|
39
35
|
* `concatenateSpaceApplyInputs` ordering. This preserves
|
|
40
|
-
* `
|
|
36
|
+
* `MigrationRunnerFailure.failingSpace` attribution byte-for-byte.
|
|
41
37
|
*
|
|
42
38
|
* Every emitted `MigrationPlan` has `targetId = aggregate.targetId`.
|
|
43
39
|
* No placeholder cast; no patch step.
|
|
44
40
|
*/
|
|
45
|
-
export async function
|
|
46
|
-
input:
|
|
47
|
-
): Promise<
|
|
41
|
+
export async function planMigration<TFamilyId extends string, TTargetId extends string>(
|
|
42
|
+
input: PlannerInput<TFamilyId, TTargetId>,
|
|
43
|
+
): Promise<PlannerOutput> {
|
|
48
44
|
const { aggregate, currentDBState, callerPolicy } = input;
|
|
49
45
|
const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
|
|
50
46
|
|
|
51
|
-
const perSpace = new Map<string,
|
|
47
|
+
const perSpace = new Map<string, PerSpacePlan>();
|
|
52
48
|
|
|
53
49
|
// Iterate in apply order so a per-member error short-circuits the
|
|
54
50
|
// walk in the same order the runner would walk inputs.
|
|
@@ -60,15 +56,16 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
|
|
|
60
56
|
for (const member of orderedMembers) {
|
|
61
57
|
const otherMembers = allMembers.filter((m) => m.spaceId !== member.spaceId);
|
|
62
58
|
const currentMarker = currentDBState.markersBySpaceId.get(member.spaceId) ?? null;
|
|
59
|
+
const headRef = requireHeadRef(member);
|
|
63
60
|
|
|
64
61
|
const ignoreGraph = callerPolicy.ignoreGraphFor.has(member.spaceId);
|
|
65
|
-
const invariantsRequired =
|
|
62
|
+
const invariantsRequired = headRef.invariants.length > 0;
|
|
66
63
|
|
|
67
64
|
if (ignoreGraph && invariantsRequired) {
|
|
68
|
-
const conflict:
|
|
65
|
+
const conflict: PlannerError = {
|
|
69
66
|
kind: 'policyConflict',
|
|
70
67
|
spaceId: member.spaceId,
|
|
71
|
-
detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${member.spaceId}", but the member declares non-empty head-ref invariants (${
|
|
68
|
+
detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${member.spaceId}", but the member declares non-empty head-ref invariants (${headRef.invariants.join(', ')}). Synthesising a plan from the contract IR cannot satisfy authored invariants — the graph must be walked. Either remove "${member.spaceId}" from \`ignoreGraphFor\` or amend the on-disk head ref to declare zero invariants.`,
|
|
72
69
|
};
|
|
73
70
|
return notOk(conflict);
|
|
74
71
|
}
|
|
@@ -97,7 +94,7 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
|
|
|
97
94
|
|
|
98
95
|
// Try graph-walk first when the graph has nodes; fall back to synth
|
|
99
96
|
// when the graph is empty AND no invariants are required.
|
|
100
|
-
if (member.
|
|
97
|
+
if (member.graph().nodes.size > 0) {
|
|
101
98
|
const walked = graphWalkStrategy({
|
|
102
99
|
aggregateTargetId: aggregate.targetId,
|
|
103
100
|
member,
|
|
@@ -111,7 +108,7 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
|
|
|
111
108
|
return notOk({
|
|
112
109
|
kind: 'extensionPathUnreachable',
|
|
113
110
|
spaceId: member.spaceId,
|
|
114
|
-
target:
|
|
111
|
+
target: headRef.hash,
|
|
115
112
|
});
|
|
116
113
|
}
|
|
117
114
|
// unsatisfiable — surface
|
|
@@ -128,7 +125,7 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
|
|
|
128
125
|
return notOk({
|
|
129
126
|
kind: 'extensionPathUnsatisfiable',
|
|
130
127
|
spaceId: member.spaceId,
|
|
131
|
-
missingInvariants: [...
|
|
128
|
+
missingInvariants: [...headRef.invariants].sort(),
|
|
132
129
|
});
|
|
133
130
|
}
|
|
134
131
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { elementCoordinates } from '@prisma-next/framework-components/ir';
|
|
2
2
|
import type { ContractSpaceMember } from './types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -90,11 +90,6 @@ export function projectSchemaToSpace(
|
|
|
90
90
|
return schema;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
* Collect the set of storage element names claimed by other members.
|
|
95
|
-
* Reuses the loader's `extractStorageElementNames` helper so the
|
|
96
|
-
* tables/collections walk lives in exactly one place.
|
|
97
|
-
*/
|
|
98
93
|
function collectOwnedNames(
|
|
99
94
|
member: ContractSpaceMember,
|
|
100
95
|
otherMembers: ReadonlyArray<ContractSpaceMember>,
|
|
@@ -102,8 +97,8 @@ function collectOwnedNames(
|
|
|
102
97
|
const owned = new Set<string>();
|
|
103
98
|
for (const other of otherMembers) {
|
|
104
99
|
if (other.spaceId === member.spaceId) continue;
|
|
105
|
-
for (const
|
|
106
|
-
owned.add(
|
|
100
|
+
for (const { entityName } of elementCoordinates(other.contract().storage)) {
|
|
101
|
+
owned.add(entityName);
|
|
107
102
|
}
|
|
108
103
|
}
|
|
109
104
|
return owned;
|
|
@@ -1,21 +1,22 @@
|
|
|
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
|
-
import type {
|
|
7
|
+
import type { PerSpacePlan } from '../planner-types';
|
|
7
8
|
import type { ContractSpaceMember } from '../types';
|
|
8
9
|
|
|
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
|
-
* these into {@link import('../planner-types').
|
|
15
|
+
* these into {@link import('../planner-types').PlannerError}
|
|
15
16
|
* variants.
|
|
16
17
|
*/
|
|
17
18
|
export type GraphWalkOutcome =
|
|
18
|
-
| { readonly kind: 'ok'; readonly result:
|
|
19
|
+
| { readonly kind: 'ok'; readonly result: PerSpacePlan }
|
|
19
20
|
| { readonly kind: 'unreachable' }
|
|
20
21
|
| { readonly kind: 'unsatisfiable'; readonly missing: readonly string[] };
|
|
21
22
|
|
|
@@ -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,
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
MigrationPlannerResult,
|
|
8
8
|
TargetMigrationsCapability,
|
|
9
9
|
} from '@prisma-next/framework-components/control';
|
|
10
|
-
import type {
|
|
10
|
+
import type { PerSpacePlan } from '../planner-types';
|
|
11
11
|
import { projectSchemaToSpace } from '../project-schema-to-space';
|
|
12
12
|
import type { ContractSpaceMember } from '../types';
|
|
13
13
|
|
|
@@ -27,7 +27,7 @@ export interface SynthStrategyInputs<TFamilyId extends string, TTargetId extends
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export type SynthStrategyOutcome =
|
|
30
|
-
| { readonly kind: 'ok'; readonly result:
|
|
30
|
+
| { readonly kind: 'ok'; readonly result: PerSpacePlan }
|
|
31
31
|
| { readonly kind: 'failure'; readonly conflicts: readonly MigrationPlannerConflict[] };
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -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,17 +1,18 @@
|
|
|
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';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Caller policy for the
|
|
10
|
+
* Caller policy for the verifier. Today's only knob is
|
|
10
11
|
* `mode`: `strict` treats orphan elements (live tables not claimed by
|
|
11
12
|
* any aggregate member) as errors; `lenient` treats them as
|
|
12
13
|
* informational. Maps directly to `db verify --strict`.
|
|
13
14
|
*/
|
|
14
|
-
export interface
|
|
15
|
+
export interface VerifierInput<TSchemaResult> {
|
|
15
16
|
readonly aggregate: ContractSpaceAggregate;
|
|
16
17
|
readonly markersBySpaceId: ReadonlyMap<string, ContractMarkerRecordLike | null>;
|
|
17
18
|
readonly schemaIntrospection: unknown;
|
|
@@ -19,7 +20,7 @@ export interface AggregateVerifierInput<TSchemaResult> {
|
|
|
19
20
|
/**
|
|
20
21
|
* Caller-supplied per-space schema verifier. The CLI wires this to
|
|
21
22
|
* the family's `verifySqlSchema` (SQL) / equivalent (other
|
|
22
|
-
* families). The
|
|
23
|
+
* families). The verifier projects the schema to the
|
|
23
24
|
* member's slice via {@link projectSchemaToSpace} before invoking
|
|
24
25
|
* the callback, so single-contract semantics are preserved.
|
|
25
26
|
*
|
|
@@ -61,7 +62,7 @@ export interface MarkerCheckSection {
|
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* A live storage element (today: a top-level table) not claimed by any
|
|
64
|
-
* member of the aggregate. The
|
|
65
|
+
* member of the aggregate. The verifier always reports these;
|
|
65
66
|
* the caller decides what to do — `db verify --strict` treats them as
|
|
66
67
|
* errors, the lenient default treats them as informational.
|
|
67
68
|
*
|
|
@@ -80,20 +81,17 @@ export interface SchemaCheckSection<TSchemaResult> {
|
|
|
80
81
|
readonly orphanElements: readonly OrphanElement[];
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
export interface
|
|
84
|
+
export interface VerifierSuccess<TSchemaResult> {
|
|
84
85
|
readonly markerCheck: MarkerCheckSection;
|
|
85
86
|
readonly schemaCheck: SchemaCheckSection<TSchemaResult>;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export type
|
|
89
|
+
export type VerifierError = {
|
|
89
90
|
readonly kind: 'introspectionFailure';
|
|
90
91
|
readonly detail: string;
|
|
91
92
|
};
|
|
92
93
|
|
|
93
|
-
export type
|
|
94
|
-
AggregateVerifierSuccess<TSchemaResult>,
|
|
95
|
-
AggregateVerifierError
|
|
96
|
-
>;
|
|
94
|
+
export type VerifierOutput<TSchemaResult> = Result<VerifierSuccess<TSchemaResult>, VerifierError>;
|
|
97
95
|
|
|
98
96
|
/**
|
|
99
97
|
* Verify a {@link ContractSpaceAggregate} against the live database
|
|
@@ -117,11 +115,11 @@ export type AggregateVerifierOutput<TSchemaResult> = Result<
|
|
|
117
115
|
* Pure synchronous function; no I/O. The caller (CLI) gathers
|
|
118
116
|
* `markersBySpaceId` and `schemaIntrospection` ahead of the call.
|
|
119
117
|
*/
|
|
120
|
-
export function
|
|
121
|
-
input:
|
|
122
|
-
):
|
|
118
|
+
export function verifyMigration<TSchemaResult>(
|
|
119
|
+
input: VerifierInput<TSchemaResult>,
|
|
120
|
+
): VerifierOutput<TSchemaResult> {
|
|
123
121
|
try {
|
|
124
|
-
return
|
|
122
|
+
return runVerifyMigration(input);
|
|
125
123
|
} catch (error) {
|
|
126
124
|
return notOk({
|
|
127
125
|
kind: 'introspectionFailure',
|
|
@@ -130,9 +128,9 @@ export function verifyAggregate<TSchemaResult>(
|
|
|
130
128
|
}
|
|
131
129
|
}
|
|
132
130
|
|
|
133
|
-
function
|
|
134
|
-
input:
|
|
135
|
-
):
|
|
131
|
+
function runVerifyMigration<TSchemaResult>(
|
|
132
|
+
input: VerifierInput<TSchemaResult>,
|
|
133
|
+
): VerifierOutput<TSchemaResult> {
|
|
136
134
|
const { aggregate, markersBySpaceId, schemaIntrospection, mode, verifySchemaForMember } = input;
|
|
137
135
|
const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
|
|
138
136
|
const memberSpaceIds = new Set(allMembers.map((m) => m.spaceId));
|
|
@@ -145,16 +143,17 @@ function runVerifyAggregate<TSchemaResult>(
|
|
|
145
143
|
markerPerSpace.set(member.spaceId, { kind: 'absent' });
|
|
146
144
|
continue;
|
|
147
145
|
}
|
|
148
|
-
|
|
146
|
+
const headRef = requireHeadRef(member);
|
|
147
|
+
if (marker.storageHash !== headRef.hash) {
|
|
149
148
|
markerPerSpace.set(member.spaceId, {
|
|
150
149
|
kind: 'hashMismatch',
|
|
151
150
|
markerHash: marker.storageHash,
|
|
152
|
-
expected:
|
|
151
|
+
expected: headRef.hash,
|
|
153
152
|
});
|
|
154
153
|
continue;
|
|
155
154
|
}
|
|
156
155
|
const markerInvariants = new Set(marker.invariants);
|
|
157
|
-
const missing =
|
|
156
|
+
const missing = headRef.invariants.filter((id) => !markerInvariants.has(id));
|
|
158
157
|
if (missing.length > 0) {
|
|
159
158
|
markerPerSpace.set(member.spaceId, {
|
|
160
159
|
kind: 'missingInvariants',
|
|
@@ -211,8 +210,9 @@ function detectOrphanElements(
|
|
|
211
210
|
|
|
212
211
|
const claimedTables = new Set<string>();
|
|
213
212
|
for (const member of members) {
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
const contract = member.contract();
|
|
214
|
+
for (const { entityName } of elementCoordinates(contract.storage)) {
|
|
215
|
+
claimedTables.add(entityName);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -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
|
}
|