@prisma-next/cli 0.5.0-dev.66 → 0.5.0-dev.68
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/dist/{cli-errors-By1iVE3z.mjs → cli-errors-D3_sMh2K.mjs} +2 -3
- package/dist/{cli-errors-By1iVE3z.mjs.map → cli-errors-D3_sMh2K.mjs.map} +1 -1
- package/dist/{cli-errors-DDeVsP2Y.d.mts → cli-errors-QH8kf-C2.d.mts} +0 -2
- package/dist/cli.mjs +12 -76
- package/dist/cli.mjs.map +1 -1
- package/dist/client-0ZX24FXF.mjs +1398 -0
- package/dist/client-0ZX24FXF.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -4
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +2 -4
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +11 -11
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +5 -7
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +8 -9
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +11 -11
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -321
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +16 -17
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts +0 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +10 -11
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -345
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.d.mts.map +1 -1
- package/dist/commands/migration-ref.mjs +5 -6
- package/dist/commands/migration-ref.mjs.map +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +13 -13
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -4
- package/dist/{config-loader-ih8ViDb_.mjs → config-loader-B6sJjXTv.mjs} +2 -4
- package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
- package/dist/config-loader.d.mts +0 -1
- package/dist/config-loader.d.mts.map +1 -1
- package/dist/config-loader.mjs +2 -3
- package/dist/{contract-emit-CnTXVVbF.mjs → contract-emit-B3ChISB_.mjs} +22 -13
- package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
- package/dist/{contract-emit-CcZr3HS9.mjs → contract-emit-DkMqO7f2.mjs} +8 -10
- package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
- package/dist/{contract-enrichment-xDeJBC-o.mjs → contract-enrichment-CF6ogEJ_.mjs} +2 -2
- package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
- package/dist/{contract-infer-sER84Le-.mjs → contract-infer-BDKAE0B0.mjs} +5 -7
- package/dist/{contract-infer-sER84Le-.mjs.map → contract-infer-BDKAE0B0.mjs.map} +1 -1
- package/dist/db-verify-B4TdDKOI.mjs +403 -0
- package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
- package/dist/exports/config-types.mjs +1 -2
- package/dist/exports/control-api.d.mts +202 -7
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +4 -6
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +28 -30
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +2 -4
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +2 -3
- package/dist/{framework-components-Bgcre3Z6.mjs → framework-components-gwAHl7ml.mjs} +3 -4
- package/dist/{framework-components-Bgcre3Z6.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
- package/dist/{init-DC4sL4Rp.mjs → init-Deo7U8_U.mjs} +13 -30
- package/dist/init-Deo7U8_U.mjs.map +1 -0
- package/dist/{inspect-live-schema-BQN21nNO.mjs → inspect-live-schema-BAgQMYpD.mjs} +7 -8
- package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
- package/dist/migration-cli.d.mts +0 -1
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +2 -3
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-DLmYGRug.mjs → migration-command-scaffold-B8J702Uh.mjs} +7 -8
- package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
- package/dist/migration-plan-BcKNnTM7.mjs +530 -0
- package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
- package/dist/{migration-status-CDW4RDsO.mjs → migration-status-CjwB2of-.mjs} +10 -14
- package/dist/migration-status-CjwB2of-.mjs.map +1 -0
- package/dist/{migrations-MEoKMiV5.mjs → migrations-CIK94AJf.mjs} +3 -4
- package/dist/migrations-CIK94AJf.mjs.map +1 -0
- package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
- package/dist/output-DnjfCC_u.mjs.map +1 -0
- package/dist/{progress-adapter-DgRGldpT.mjs → progress-adapter-xASh41wr.mjs} +2 -2
- package/dist/{progress-adapter-DgRGldpT.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
- package/dist/{result-handler-Ch6hVnOo.mjs → result-handler-DWb1rFS-.mjs} +20 -10
- package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
- package/dist/{terminal-ui-u2YgKghu.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
- package/dist/{terminal-ui-u2YgKghu.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
- package/dist/{verify-BT9tgCOH.mjs → verify-BEIa9638.mjs} +3 -4
- package/dist/verify-BEIa9638.mjs.map +1 -0
- package/package.json +24 -24
- package/src/commands/db-init.ts +13 -3
- package/src/commands/db-update.ts +7 -3
- package/src/commands/db-verify.ts +47 -15
- package/src/commands/init/index.ts +1 -1
- package/src/commands/init/init.ts +2 -2
- package/src/commands/migration-apply.ts +9 -9
- package/src/commands/migration-new.ts +4 -4
- package/src/commands/migration-plan.ts +66 -9
- package/src/commands/migration-show.ts +7 -5
- package/src/commands/migration-status.ts +3 -3
- package/src/control-api/client.ts +42 -0
- package/src/control-api/operations/db-apply-aggregate.ts +446 -0
- package/src/control-api/operations/db-init.ts +51 -258
- package/src/control-api/operations/db-update.ts +66 -188
- package/src/control-api/operations/db-verify.ts +342 -0
- package/src/control-api/types.ts +56 -0
- package/src/exports/control-api.ts +13 -2
- package/src/load-ts-contract.ts +28 -26
- package/src/utils/combine-schema-results.ts +84 -0
- package/src/utils/command-helpers.ts +24 -2
- package/src/utils/contract-space-aggregate-loader.ts +236 -0
- package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
- package/src/utils/contract-space-migrate-pass.ts +156 -0
- package/dist/client-hUCMXFE_.mjs +0 -1031
- package/dist/client-hUCMXFE_.mjs.map +0 -1
- package/dist/commands/db-verify.mjs.map +0 -1
- package/dist/commands/migration-plan.mjs.map +0 -1
- package/dist/config-loader-ih8ViDb_.mjs.map +0 -1
- package/dist/contract-emit-BkRH9lGt.mjs +0 -4
- package/dist/contract-emit-CcZr3HS9.mjs.map +0 -1
- package/dist/contract-emit-CnTXVVbF.mjs.map +0 -1
- package/dist/contract-enrichment-xDeJBC-o.mjs.map +0 -1
- package/dist/init-DC4sL4Rp.mjs.map +0 -1
- package/dist/inspect-live-schema-BQN21nNO.mjs.map +0 -1
- package/dist/migration-command-scaffold-DLmYGRug.mjs.map +0 -1
- package/dist/migration-status-CDW4RDsO.mjs.map +0 -1
- package/dist/migrations-MEoKMiV5.mjs.map +0 -1
- package/dist/output-BpcQrnnq.mjs.map +0 -1
- package/dist/result-handler-Ch6hVnOo.mjs.map +0 -1
- package/dist/verify-BT9tgCOH.mjs.map +0 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
|
+
import type {
|
|
4
|
+
ControlDriverInstance,
|
|
5
|
+
ControlExtensionDescriptor,
|
|
6
|
+
ControlFamilyInstance,
|
|
7
|
+
VerifyDatabaseSchemaResult,
|
|
8
|
+
} from '@prisma-next/framework-components/control';
|
|
9
|
+
import {
|
|
10
|
+
type AggregateVerifierOutput,
|
|
11
|
+
type ContractSpaceMember,
|
|
12
|
+
verifyAggregate,
|
|
13
|
+
} from '@prisma-next/migration-tools/aggregate';
|
|
14
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
15
|
+
import { CliStructuredError } from '../../utils/cli-errors';
|
|
16
|
+
import {
|
|
17
|
+
type BuildAggregateInputs,
|
|
18
|
+
buildContractSpaceAggregate,
|
|
19
|
+
} from '../../utils/contract-space-aggregate-loader';
|
|
20
|
+
import type { OnControlProgress } from '../types';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Span IDs emitted via `onProgress` during the aggregate verify flow.
|
|
24
|
+
* Mirrors the span identifiers used by the legacy precheck / marker-check
|
|
25
|
+
* helpers so structured-output renderers and progress tests keep working.
|
|
26
|
+
*/
|
|
27
|
+
const SPAN_IDS = {
|
|
28
|
+
introspect: 'introspect',
|
|
29
|
+
verify: 'verify',
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Inputs for the aggregate `db verify` operation.
|
|
34
|
+
*
|
|
35
|
+
* Loader → verifier pipeline. The loader (sole descriptor-import
|
|
36
|
+
* boundary) builds a {@link import('@prisma-next/migration-tools/aggregate').ContractSpaceAggregate};
|
|
37
|
+
* the aggregate verifier bundles `markerCheck` + per-space pre-projected
|
|
38
|
+
* `schemaCheck`. `mode: 'strict' | 'lenient'` maps directly to the user
|
|
39
|
+
* facing `--strict` flag.
|
|
40
|
+
*/
|
|
41
|
+
export interface ExecuteDbVerifyOptions<TFamilyId extends string, TTargetId extends string> {
|
|
42
|
+
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
43
|
+
readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
|
|
44
|
+
readonly contract: Contract;
|
|
45
|
+
readonly migrationsDir: string;
|
|
46
|
+
readonly targetId: TTargetId;
|
|
47
|
+
readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
|
|
48
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
|
|
49
|
+
readonly mode: 'strict' | 'lenient';
|
|
50
|
+
readonly skipSchema: boolean;
|
|
51
|
+
readonly skipMarker: boolean;
|
|
52
|
+
readonly onProgress?: OnControlProgress;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result of the aggregate verify operation.
|
|
57
|
+
*
|
|
58
|
+
* Marker-check failures are surfaced as a {@link CliStructuredError}
|
|
59
|
+
* (same envelope code `5002` the legacy `runContractSpaceVerifierMarkerCheck`
|
|
60
|
+
* emitted, so downstream tooling and integration tests assert on the
|
|
61
|
+
* same shape).
|
|
62
|
+
*
|
|
63
|
+
* On success, the per-space schema results are returned for the CLI to
|
|
64
|
+
* render. When `skipSchema` is true (`--marker-only`), the schema map
|
|
65
|
+
* is empty.
|
|
66
|
+
*/
|
|
67
|
+
export interface ExecuteDbVerifySuccess {
|
|
68
|
+
readonly schemaResults: ReadonlyMap<string, VerifyDatabaseSchemaResult>;
|
|
69
|
+
readonly memberOrder: readonly string[];
|
|
70
|
+
readonly appSpaceId: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ExecuteDbVerifyResult = Result<ExecuteDbVerifySuccess, CliStructuredError>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Loader → verifier pipeline shared by `db verify` modes (`full`,
|
|
77
|
+
* `marker-only`, `schema-only`).
|
|
78
|
+
*
|
|
79
|
+
* 1. **Load**: build a {@link import('@prisma-next/migration-tools/aggregate').ContractSpaceAggregate}
|
|
80
|
+
* from descriptors + on-disk on-disk artefacts. Layout / drift /
|
|
81
|
+
* integrity / disjointness violations short-circuit with a
|
|
82
|
+
* structured CLI error.
|
|
83
|
+
* 2. **Read DB state**: marker rows + (when `skipSchema` is `false`)
|
|
84
|
+
* schema introspection.
|
|
85
|
+
* 3. **Verify**: {@link verifyAggregate} returns per-space
|
|
86
|
+
* `markerCheck` + per-space pre-projected `schemaCheck` (closes F23).
|
|
87
|
+
* Marker mismatches map to `CliStructuredError` (code `5002`) so
|
|
88
|
+
* callers (CLI command) can render and exit. Schema results are
|
|
89
|
+
* returned to the caller verbatim.
|
|
90
|
+
*/
|
|
91
|
+
export async function executeDbVerify<TFamilyId extends string, TTargetId extends string>(
|
|
92
|
+
options: ExecuteDbVerifyOptions<TFamilyId, TTargetId>,
|
|
93
|
+
): Promise<ExecuteDbVerifyResult> {
|
|
94
|
+
const { driver, familyInstance, onProgress, skipSchema, skipMarker } = options;
|
|
95
|
+
const loaded = await buildContractSpaceAggregate(buildLoadInputs(options));
|
|
96
|
+
if (!loaded.ok) return notOk(loaded.failure);
|
|
97
|
+
const aggregate = loaded.value;
|
|
98
|
+
|
|
99
|
+
const markersBySpaceId = await familyInstance.readAllMarkers({ driver });
|
|
100
|
+
const schemaIntrospection = skipSchema
|
|
101
|
+
? null
|
|
102
|
+
: await runIntrospection({ driver, familyInstance, onProgress });
|
|
103
|
+
|
|
104
|
+
emitVerifySpan(onProgress, 'spanStart');
|
|
105
|
+
const verifyResult = verifyAggregate({
|
|
106
|
+
aggregate,
|
|
107
|
+
markersBySpaceId,
|
|
108
|
+
schemaIntrospection,
|
|
109
|
+
mode: options.mode,
|
|
110
|
+
verifySchemaForMember: createPerMemberVerifier(options),
|
|
111
|
+
});
|
|
112
|
+
return finaliseVerifyResult({ verifyResult, aggregate, skipMarker, onProgress });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildLoadInputs<TFamilyId extends string, TTargetId extends string>(
|
|
116
|
+
options: ExecuteDbVerifyOptions<TFamilyId, TTargetId>,
|
|
117
|
+
): BuildAggregateInputs<TFamilyId, TTargetId> {
|
|
118
|
+
return {
|
|
119
|
+
targetId: options.targetId,
|
|
120
|
+
migrationsDir: options.migrationsDir,
|
|
121
|
+
appContract: options.contract,
|
|
122
|
+
extensionPacks: options.extensionPacks,
|
|
123
|
+
validateContract: (json) => options.familyInstance.validateContract(json),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function runIntrospection<TFamilyId extends string, TTargetId extends string>(args: {
|
|
128
|
+
driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
129
|
+
familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
|
|
130
|
+
onProgress: OnControlProgress | undefined;
|
|
131
|
+
}): Promise<unknown> {
|
|
132
|
+
const { driver, familyInstance, onProgress } = args;
|
|
133
|
+
onProgress?.({
|
|
134
|
+
action: 'dbVerify',
|
|
135
|
+
kind: 'spanStart',
|
|
136
|
+
spanId: SPAN_IDS.introspect,
|
|
137
|
+
label: 'Introspecting database schema',
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
const result = await familyInstance.introspect({ driver });
|
|
141
|
+
onProgress?.({
|
|
142
|
+
action: 'dbVerify',
|
|
143
|
+
kind: 'spanEnd',
|
|
144
|
+
spanId: SPAN_IDS.introspect,
|
|
145
|
+
outcome: 'ok',
|
|
146
|
+
});
|
|
147
|
+
return result;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
onProgress?.({
|
|
150
|
+
action: 'dbVerify',
|
|
151
|
+
kind: 'spanEnd',
|
|
152
|
+
spanId: SPAN_IDS.introspect,
|
|
153
|
+
outcome: 'error',
|
|
154
|
+
});
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build the per-member schema callback handed to the aggregate verifier.
|
|
161
|
+
* When `skipSchema` is true the callback short-circuits with a synthetic
|
|
162
|
+
* `ok` result so the verifier still runs the (cheap) schemaCheck loop
|
|
163
|
+
* without invoking the family's verification path.
|
|
164
|
+
*/
|
|
165
|
+
function createPerMemberVerifier<TFamilyId extends string, TTargetId extends string>(
|
|
166
|
+
options: ExecuteDbVerifyOptions<TFamilyId, TTargetId>,
|
|
167
|
+
): (
|
|
168
|
+
projectedSchema: unknown,
|
|
169
|
+
member: ContractSpaceMember,
|
|
170
|
+
verifyMode: 'strict' | 'lenient',
|
|
171
|
+
) => VerifyDatabaseSchemaResult {
|
|
172
|
+
const { skipSchema, familyInstance, frameworkComponents } = options;
|
|
173
|
+
return (projectedSchema, member, verifyMode) => {
|
|
174
|
+
if (skipSchema) return buildSkippedSchemaResult(member);
|
|
175
|
+
return familyInstance.schemaVerifyAgainstSchema({
|
|
176
|
+
contract: member.contract,
|
|
177
|
+
// The family's `TSchemaIR` is opaque to migration-tools; the
|
|
178
|
+
// aggregate verifier passes through whatever we hand it. The
|
|
179
|
+
// family expects its own IR shape on the way back.
|
|
180
|
+
schema: projectedSchema as never,
|
|
181
|
+
strict: verifyMode === 'strict',
|
|
182
|
+
frameworkComponents,
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function emitVerifySpan(
|
|
188
|
+
onProgress: OnControlProgress | undefined,
|
|
189
|
+
kind: 'spanStart' | 'spanEndOk' | 'spanEndError',
|
|
190
|
+
): void {
|
|
191
|
+
if (kind === 'spanStart') {
|
|
192
|
+
onProgress?.({
|
|
193
|
+
action: 'dbVerify',
|
|
194
|
+
kind: 'spanStart',
|
|
195
|
+
spanId: SPAN_IDS.verify,
|
|
196
|
+
label: 'Verifying contract spaces',
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
onProgress?.({
|
|
201
|
+
action: 'dbVerify',
|
|
202
|
+
kind: 'spanEnd',
|
|
203
|
+
spanId: SPAN_IDS.verify,
|
|
204
|
+
outcome: kind === 'spanEndOk' ? 'ok' : 'error',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Map an {@link AggregateVerifierOutput} to the operation's
|
|
210
|
+
* {@link ExecuteDbVerifyResult}, applying the `skipMarker` policy used
|
|
211
|
+
* by the CLI's `--schema-only` mode.
|
|
212
|
+
*/
|
|
213
|
+
function finaliseVerifyResult(args: {
|
|
214
|
+
verifyResult: AggregateVerifierOutput<VerifyDatabaseSchemaResult>;
|
|
215
|
+
aggregate: {
|
|
216
|
+
readonly app: { readonly spaceId: string };
|
|
217
|
+
readonly extensions: ReadonlyArray<{ readonly spaceId: string }>;
|
|
218
|
+
};
|
|
219
|
+
skipMarker: boolean;
|
|
220
|
+
onProgress: OnControlProgress | undefined;
|
|
221
|
+
}): ExecuteDbVerifyResult {
|
|
222
|
+
const { verifyResult, aggregate, skipMarker, onProgress } = args;
|
|
223
|
+
if (!verifyResult.ok) {
|
|
224
|
+
emitVerifySpan(onProgress, 'spanEndError');
|
|
225
|
+
return notOk(
|
|
226
|
+
new CliStructuredError('5002', 'Aggregate verifier introspection failed', {
|
|
227
|
+
domain: 'MIG',
|
|
228
|
+
why: verifyResult.failure.detail,
|
|
229
|
+
fix: 'Check database connectivity and the introspection tooling.',
|
|
230
|
+
docsUrl: 'https://pris.ly/contract-spaces',
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const markerError = skipMarker
|
|
235
|
+
? null
|
|
236
|
+
: mapMarkerCheckFailures(aggregate.app.spaceId, verifyResult.value.markerCheck);
|
|
237
|
+
if (markerError !== null) {
|
|
238
|
+
emitVerifySpan(onProgress, 'spanEndError');
|
|
239
|
+
return notOk(markerError);
|
|
240
|
+
}
|
|
241
|
+
emitVerifySpan(onProgress, 'spanEndOk');
|
|
242
|
+
return ok({
|
|
243
|
+
schemaResults: verifyResult.value.schemaCheck.perSpace,
|
|
244
|
+
memberOrder: [aggregate.app.spaceId, ...aggregate.extensions.map((e) => e.spaceId)],
|
|
245
|
+
appSpaceId: aggregate.app.spaceId,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildSkippedSchemaResult(member: ContractSpaceMember): VerifyDatabaseSchemaResult {
|
|
250
|
+
const profileHash = (member.contract as { profileHash?: string }).profileHash;
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
summary: 'Schema verification skipped',
|
|
254
|
+
contract: {
|
|
255
|
+
storageHash: member.headRef.hash,
|
|
256
|
+
...(profileHash ? { profileHash } : {}),
|
|
257
|
+
},
|
|
258
|
+
target: { expected: member.contract.target },
|
|
259
|
+
schema: {
|
|
260
|
+
issues: [],
|
|
261
|
+
root: {
|
|
262
|
+
status: 'pass',
|
|
263
|
+
kind: 'skipped',
|
|
264
|
+
name: member.spaceId,
|
|
265
|
+
contractPath: '',
|
|
266
|
+
code: 'SKIPPED',
|
|
267
|
+
message: 'Schema verification skipped',
|
|
268
|
+
expected: undefined,
|
|
269
|
+
actual: undefined,
|
|
270
|
+
children: [],
|
|
271
|
+
},
|
|
272
|
+
counts: { pass: 0, warn: 0, fail: 0, totalNodes: 0 },
|
|
273
|
+
},
|
|
274
|
+
timings: { total: 0 },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Translate per-space marker check failures and orphan markers into a
|
|
280
|
+
* single CLI structured error envelope. Preserves the legacy code
|
|
281
|
+
* `5002` (was emitted by `runContractSpaceVerifierMarkerCheck`).
|
|
282
|
+
*/
|
|
283
|
+
function mapMarkerCheckFailures(
|
|
284
|
+
appSpaceId: string,
|
|
285
|
+
section: {
|
|
286
|
+
readonly perSpace: ReadonlyMap<
|
|
287
|
+
string,
|
|
288
|
+
| { readonly kind: 'ok' }
|
|
289
|
+
| { readonly kind: 'absent' }
|
|
290
|
+
| { readonly kind: 'hashMismatch'; readonly markerHash: string; readonly expected: string }
|
|
291
|
+
| { readonly kind: 'missingInvariants'; readonly missing: readonly string[] }
|
|
292
|
+
>;
|
|
293
|
+
readonly orphanMarkers: readonly { readonly spaceId: string; readonly row: unknown }[];
|
|
294
|
+
},
|
|
295
|
+
): CliStructuredError | null {
|
|
296
|
+
const violations: Array<{
|
|
297
|
+
kind: string;
|
|
298
|
+
spaceId: string;
|
|
299
|
+
remediation: string;
|
|
300
|
+
}> = [];
|
|
301
|
+
for (const [spaceId, result] of section.perSpace) {
|
|
302
|
+
if (result.kind === 'ok' || result.kind === 'absent') continue;
|
|
303
|
+
if (result.kind === 'hashMismatch') {
|
|
304
|
+
violations.push({
|
|
305
|
+
kind: 'hashMismatch',
|
|
306
|
+
spaceId,
|
|
307
|
+
remediation:
|
|
308
|
+
spaceId === appSpaceId
|
|
309
|
+
? 'Run `prisma-next db update` to advance the marker, or roll the database back to the recorded hash.'
|
|
310
|
+
: `Apply on-disk migrations under \`migrations/${spaceId}/\` to advance the marker, or remove the conflicting marker row.`,
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (result.kind === 'missingInvariants') {
|
|
315
|
+
violations.push({
|
|
316
|
+
kind: 'invariantsMismatch',
|
|
317
|
+
spaceId,
|
|
318
|
+
remediation: `Re-apply the migrations under \`migrations/${spaceId}/\` so the marker carries invariants: ${result.missing.join(', ')}.`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const orphan of section.orphanMarkers) {
|
|
323
|
+
violations.push({
|
|
324
|
+
kind: 'orphanMarker',
|
|
325
|
+
spaceId: orphan.spaceId,
|
|
326
|
+
remediation: `Add the corresponding extension to \`extensionPacks\` in \`prisma-next.config.ts\`, or delete the orphan marker row for "${orphan.spaceId}".`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (violations.length === 0) return null;
|
|
330
|
+
const lines = violations.map((v) => `- [${v.kind}] ${v.spaceId}: ${v.remediation}`);
|
|
331
|
+
const summary =
|
|
332
|
+
violations.length === 1
|
|
333
|
+
? 'Contract-space verifier found a violation'
|
|
334
|
+
: `Contract-space verifier found violations (${violations.length})`;
|
|
335
|
+
return new CliStructuredError('5002', summary, {
|
|
336
|
+
domain: 'MIG',
|
|
337
|
+
why: `The on-disk \`migrations/\` directory, the \`extensionPacks\` declaration, and the live database marker rows are not in agreement.\n${lines.join('\n')}`,
|
|
338
|
+
fix: violations[0]?.remediation ?? 'Review and reconcile the violations listed above.',
|
|
339
|
+
docsUrl: 'https://pris.ly/contract-spaces',
|
|
340
|
+
meta: { violations },
|
|
341
|
+
});
|
|
342
|
+
}
|
package/src/control-api/types.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from '@prisma-next/framework-components/control';
|
|
20
20
|
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
21
21
|
import type { Result } from '@prisma-next/utils/result';
|
|
22
|
+
import type { ExecuteDbVerifyResult } from './operations/db-verify';
|
|
22
23
|
|
|
23
24
|
// ============================================================================
|
|
24
25
|
// Client Options
|
|
@@ -65,6 +66,7 @@ export interface ControlClientOptions {
|
|
|
65
66
|
export type ControlActionName =
|
|
66
67
|
| 'dbInit'
|
|
67
68
|
| 'dbUpdate'
|
|
69
|
+
| 'dbVerify'
|
|
68
70
|
| 'migrationApply'
|
|
69
71
|
| 'verify'
|
|
70
72
|
| 'schemaVerify'
|
|
@@ -191,6 +193,13 @@ export interface DbInitOptions {
|
|
|
191
193
|
* The type is driver-specific (e.g., string URL for Postgres).
|
|
192
194
|
*/
|
|
193
195
|
readonly connection?: unknown;
|
|
196
|
+
/**
|
|
197
|
+
* On-disk migrations directory. Always required — every `db init`
|
|
198
|
+
* routes through the per-space flow, which reads on-disk
|
|
199
|
+
* `refs/head.json` and extension destination contracts from this
|
|
200
|
+
* root.
|
|
201
|
+
*/
|
|
202
|
+
readonly migrationsDir: string;
|
|
194
203
|
/** Optional progress callback for observing operation progress */
|
|
195
204
|
readonly onProgress?: OnControlProgress;
|
|
196
205
|
}
|
|
@@ -221,10 +230,35 @@ export interface DbUpdateOptions {
|
|
|
221
230
|
* or re-run with -y/--yes.
|
|
222
231
|
*/
|
|
223
232
|
readonly acceptDataLoss?: boolean;
|
|
233
|
+
/**
|
|
234
|
+
* On-disk migrations directory. Always required — every `db update`
|
|
235
|
+
* routes through the per-space flow, which reads on-disk
|
|
236
|
+
* `refs/head.json` and extension destination contracts from this
|
|
237
|
+
* root.
|
|
238
|
+
*/
|
|
239
|
+
readonly migrationsDir: string;
|
|
224
240
|
/** Optional progress callback for observing operation progress */
|
|
225
241
|
readonly onProgress?: OnControlProgress;
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Options for the dbVerify operation.
|
|
246
|
+
*
|
|
247
|
+
* Drives the loader → aggregate-verifier pipeline. `strict` maps to
|
|
248
|
+
* `verifyAggregate({ mode: 'strict' | 'lenient' })`; `skipSchema`
|
|
249
|
+
* mirrors the `--marker-only` CLI flag and short-circuits the schema
|
|
250
|
+
* portion of the verifier.
|
|
251
|
+
*/
|
|
252
|
+
export interface DbVerifyOptions {
|
|
253
|
+
readonly contract: unknown;
|
|
254
|
+
readonly migrationsDir: string;
|
|
255
|
+
readonly strict: boolean;
|
|
256
|
+
readonly skipSchema: boolean;
|
|
257
|
+
readonly skipMarker: boolean;
|
|
258
|
+
readonly connection?: unknown;
|
|
259
|
+
readonly onProgress?: OnControlProgress;
|
|
260
|
+
}
|
|
261
|
+
|
|
228
262
|
/**
|
|
229
263
|
* Options for the introspect operation.
|
|
230
264
|
*/
|
|
@@ -677,6 +711,21 @@ export interface ControlClient {
|
|
|
677
711
|
*/
|
|
678
712
|
dbUpdate(options: DbUpdateOptions): Promise<DbUpdateResult>;
|
|
679
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Verifies the database against every contract space (app + extensions).
|
|
716
|
+
*
|
|
717
|
+
* Loader → aggregate-verifier pipeline:
|
|
718
|
+
* - The loader catches layout / drift / disjointness violations.
|
|
719
|
+
* - The aggregate verifier surfaces marker-vs-on-disk drift and orphan
|
|
720
|
+
* markers, and (unless `skipSchema` is true) per-space schema
|
|
721
|
+
* verification with pre-projection (closes F23).
|
|
722
|
+
*
|
|
723
|
+
* @returns Result pattern: per-space schema results on success;
|
|
724
|
+
* structured CLI error on marker / loader failure.
|
|
725
|
+
* @throws If not connected or infrastructure failure
|
|
726
|
+
*/
|
|
727
|
+
dbVerify(options: DbVerifyOptions): Promise<ExecuteDbVerifyResult>;
|
|
728
|
+
|
|
680
729
|
/**
|
|
681
730
|
* Reads the contract marker from the database.
|
|
682
731
|
* Returns null if no marker exists (fresh database).
|
|
@@ -685,6 +734,13 @@ export interface ControlClient {
|
|
|
685
734
|
*/
|
|
686
735
|
readMarker(): Promise<ContractMarkerRecord | null>;
|
|
687
736
|
|
|
737
|
+
/**
|
|
738
|
+
* Reads every marker row (one per contract space). Used by the
|
|
739
|
+
* per-space verifier to detect orphan marker rows and marker-vs-on-disk
|
|
740
|
+
* drift after a database connection has been established.
|
|
741
|
+
*/
|
|
742
|
+
readAllMarkers(): Promise<ReadonlyMap<string, ContractMarkerRecord>>;
|
|
743
|
+
|
|
688
744
|
/**
|
|
689
745
|
* Applies pre-planned on-disk migrations to the database.
|
|
690
746
|
* Each migration runs in its own transaction with full execution checks.
|
|
@@ -20,9 +20,20 @@ export { createControlClient } from '../control-api/client';
|
|
|
20
20
|
|
|
21
21
|
// Contract enrichment (merges framework-derived capabilities and extension pack metadata)
|
|
22
22
|
export { enrichContract } from '../control-api/contract-enrichment';
|
|
23
|
-
|
|
24
|
-
// Standalone operations (for tooling that doesn't need full client)
|
|
25
23
|
export { executeContractEmit } from '../control-api/operations/contract-emit';
|
|
24
|
+
// Standalone operations (for tooling that doesn't need full client).
|
|
25
|
+
// These drive the aggregate-pipeline `db init` / `db update` / `db verify`
|
|
26
|
+
// flow against a loaded contract-space aggregate.
|
|
27
|
+
export { type ExecuteDbInitOptions, executeDbInit } from '../control-api/operations/db-init';
|
|
28
|
+
export {
|
|
29
|
+
type ExecuteDbUpdateOptions,
|
|
30
|
+
executeDbUpdate,
|
|
31
|
+
} from '../control-api/operations/db-update';
|
|
32
|
+
export {
|
|
33
|
+
type ExecuteDbVerifyOptions,
|
|
34
|
+
type ExecuteDbVerifyResult,
|
|
35
|
+
executeDbVerify,
|
|
36
|
+
} from '../control-api/operations/db-verify';
|
|
26
37
|
// CLI-specific types
|
|
27
38
|
export type {
|
|
28
39
|
ContractEmitOptions,
|
package/src/load-ts-contract.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import type { Contract } from '@prisma-next/contract/types';
|
|
5
5
|
import type { Plugin } from 'esbuild';
|
|
6
6
|
import { build } from 'esbuild';
|
|
7
|
-
import { join } from 'pathe';
|
|
7
|
+
import { join, resolve as resolvePath } from 'pathe';
|
|
8
8
|
|
|
9
9
|
export interface LoadTsContractOptions {
|
|
10
10
|
readonly allowlist?: ReadonlyArray<string>;
|
|
@@ -78,7 +78,21 @@ function validatePurity(value: unknown): void {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function createImportAllowlistPlugin(
|
|
81
|
+
function createImportAllowlistPlugin(
|
|
82
|
+
allowlist: ReadonlyArray<string>,
|
|
83
|
+
entryPath: string,
|
|
84
|
+
collected: Set<string>,
|
|
85
|
+
): Plugin {
|
|
86
|
+
// Match against several path forms that esbuild may use as the importer:
|
|
87
|
+
// the absolute resolved entry, the value the caller passed (which may be
|
|
88
|
+
// relative), and the conventional `<stdin>` placeholder. This is more
|
|
89
|
+
// forgiving than `===` against a single form, which broke when esbuild
|
|
90
|
+
// resolved the entry to an absolute path while the caller passed a
|
|
91
|
+
// relative one (or vice versa).
|
|
92
|
+
const entryAbs = resolvePath(entryPath);
|
|
93
|
+
function isFromEntry(importer: string): boolean {
|
|
94
|
+
return importer === entryAbs || importer === entryPath || importer === '<stdin>';
|
|
95
|
+
}
|
|
82
96
|
return {
|
|
83
97
|
name: 'import-allowlist',
|
|
84
98
|
setup(build) {
|
|
@@ -89,8 +103,8 @@ function createImportAllowlistPlugin(allowlist: ReadonlyArray<string>, entryPath
|
|
|
89
103
|
if (args.path.startsWith('.') || args.path.startsWith('/')) {
|
|
90
104
|
return undefined;
|
|
91
105
|
}
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
if (isFromEntry(args.importer) && !isAllowedImport(args.path, allowlist)) {
|
|
107
|
+
collected.add(args.path);
|
|
94
108
|
return {
|
|
95
109
|
path: args.path,
|
|
96
110
|
external: true,
|
|
@@ -132,6 +146,13 @@ export async function loadContractFromTs(
|
|
|
132
146
|
`prisma-next-contract-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`,
|
|
133
147
|
);
|
|
134
148
|
|
|
149
|
+
// Disallowed imports are collected by the allowlist resolver plugin itself,
|
|
150
|
+
// which has the `importer` context to distinguish entry-direct imports from
|
|
151
|
+
// transitive imports made inside allowlisted (`@prisma-next/*`) dependencies.
|
|
152
|
+
// The metafile is intentionally not re-walked: it would surface internal
|
|
153
|
+
// `node:*` imports inside framework code as false positives.
|
|
154
|
+
const disallowedFromEntry = new Set<string>();
|
|
155
|
+
|
|
135
156
|
try {
|
|
136
157
|
const result = await build({
|
|
137
158
|
entryPoints: [entryPath],
|
|
@@ -142,7 +163,7 @@ export async function loadContractFromTs(
|
|
|
142
163
|
outfile: tempFile,
|
|
143
164
|
write: false,
|
|
144
165
|
metafile: true,
|
|
145
|
-
plugins: [createImportAllowlistPlugin(allowlist, entryPath)],
|
|
166
|
+
plugins: [createImportAllowlistPlugin(allowlist, entryPath, disallowedFromEntry)],
|
|
146
167
|
logLevel: 'error',
|
|
147
168
|
});
|
|
148
169
|
|
|
@@ -155,28 +176,9 @@ export async function loadContractFromTs(
|
|
|
155
176
|
throw new Error('No output files generated from bundling');
|
|
156
177
|
}
|
|
157
178
|
|
|
158
|
-
|
|
159
|
-
if (result.metafile) {
|
|
160
|
-
const inputs = result.metafile.inputs;
|
|
161
|
-
for (const [, inputData] of Object.entries(inputs)) {
|
|
162
|
-
const imports =
|
|
163
|
-
(inputData as { imports?: Array<{ path: string; external?: boolean }> }).imports || [];
|
|
164
|
-
for (const imp of imports) {
|
|
165
|
-
if (
|
|
166
|
-
imp.external &&
|
|
167
|
-
!imp.path.startsWith('.') &&
|
|
168
|
-
!imp.path.startsWith('/') &&
|
|
169
|
-
!isAllowedImport(imp.path, allowlist)
|
|
170
|
-
) {
|
|
171
|
-
disallowedImports.push(imp.path);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (disallowedImports.length > 0) {
|
|
179
|
+
if (disallowedFromEntry.size > 0) {
|
|
178
180
|
throw new Error(
|
|
179
|
-
`Disallowed imports detected. Only imports matching the allowlist are permitted:\n Allowlist: ${allowlist.join(', ')}\n Disallowed imports: ${
|
|
181
|
+
`Disallowed imports detected. Only imports matching the allowlist are permitted:\n Allowlist: ${allowlist.join(', ')}\n Disallowed imports: ${[...disallowedFromEntry].join(', ')}`,
|
|
180
182
|
);
|
|
181
183
|
}
|
|
182
184
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { VerifyDatabaseSchemaResult } from '@prisma-next/framework-components/control';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collapse the aggregate verifier's per-space schema results into a
|
|
5
|
+
* single {@link VerifyDatabaseSchemaResult} for the existing CLI
|
|
6
|
+
* display surface. Concatenates issues across members; sums counts;
|
|
7
|
+
* uses the app member's result as the structural envelope (storage
|
|
8
|
+
* hash, target).
|
|
9
|
+
*
|
|
10
|
+
* **Summary policy.** Preserve the per-family phrasing whenever the
|
|
11
|
+
* combined `ok` flag agrees with the app member's `ok` flag — this is
|
|
12
|
+
* the common case (single-family deployments, single-app deployments)
|
|
13
|
+
* and the family's "satisfies / does not satisfy contract" phrasing
|
|
14
|
+
* stays user-visible. When the app passes but an extension fails (or
|
|
15
|
+
* vice versa) the app's summary contradicts the envelope, so fall back
|
|
16
|
+
* to the first failing member's summary. This keeps family phrasing
|
|
17
|
+
* intact and the envelope internally consistent (`ok: false` ↔ failure
|
|
18
|
+
* summary).
|
|
19
|
+
*/
|
|
20
|
+
export function combineSchemaResults(
|
|
21
|
+
perSpace: ReadonlyMap<string, VerifyDatabaseSchemaResult>,
|
|
22
|
+
appSpaceId: string,
|
|
23
|
+
strict: boolean,
|
|
24
|
+
): VerifyDatabaseSchemaResult {
|
|
25
|
+
const appResult = perSpace.get(appSpaceId) ?? perSpace.values().next().value;
|
|
26
|
+
if (appResult === undefined) {
|
|
27
|
+
throw new Error('Aggregate verifier returned no schema results — this is a wiring bug.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let okAll = true;
|
|
31
|
+
let firstFailure: VerifyDatabaseSchemaResult | undefined;
|
|
32
|
+
let issues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
|
|
33
|
+
const counts = { pass: 0, warn: 0, fail: 0, totalNodes: 0 };
|
|
34
|
+
const childRoots: Array<VerifyDatabaseSchemaResult['schema']['root']> = [];
|
|
35
|
+
for (const [, result] of perSpace) {
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
okAll = false;
|
|
38
|
+
if (firstFailure === undefined) firstFailure = result;
|
|
39
|
+
}
|
|
40
|
+
issues = [...issues, ...result.schema.issues];
|
|
41
|
+
counts.pass += result.schema.counts.pass;
|
|
42
|
+
counts.warn += result.schema.counts.warn;
|
|
43
|
+
counts.fail += result.schema.counts.fail;
|
|
44
|
+
counts.totalNodes += result.schema.counts.totalNodes;
|
|
45
|
+
childRoots.push(result.schema.root);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// When `okAll !== appResult.ok`, exactly one shape is reachable: app passes
|
|
49
|
+
// (`appResult.ok === true`) and at least one other member failed
|
|
50
|
+
// (`okAll === false`). In that shape the failure was assigned to
|
|
51
|
+
// `firstFailure` during iteration, so non-null assertion is safe. The mirror
|
|
52
|
+
// shape (app fails while every member passes) is impossible because
|
|
53
|
+
// `appResult` either *is* a member of `perSpace` or is the first iterator
|
|
54
|
+
// value; either way its `ok` flag participates in `okAll`.
|
|
55
|
+
const summary =
|
|
56
|
+
okAll === appResult.ok
|
|
57
|
+
? appResult.summary
|
|
58
|
+
: (firstFailure as VerifyDatabaseSchemaResult).summary;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
ok: okAll,
|
|
62
|
+
...(okAll ? {} : { code: appResult.code ?? 'PN-RUN-3010' }),
|
|
63
|
+
summary,
|
|
64
|
+
contract: appResult.contract,
|
|
65
|
+
target: appResult.target,
|
|
66
|
+
schema: {
|
|
67
|
+
issues,
|
|
68
|
+
root: {
|
|
69
|
+
status: okAll ? 'pass' : 'fail',
|
|
70
|
+
kind: 'aggregate',
|
|
71
|
+
name: 'aggregate',
|
|
72
|
+
contractPath: '',
|
|
73
|
+
code: 'AGGREGATE',
|
|
74
|
+
message: okAll ? 'Aggregate schema matches' : 'Aggregate schema mismatch',
|
|
75
|
+
expected: undefined,
|
|
76
|
+
actual: undefined,
|
|
77
|
+
children: childRoots,
|
|
78
|
+
},
|
|
79
|
+
counts,
|
|
80
|
+
},
|
|
81
|
+
meta: { strict },
|
|
82
|
+
timings: { total: 0 },
|
|
83
|
+
};
|
|
84
|
+
}
|