prisma-next 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 +17 -17
- 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,1398 @@
|
|
|
1
|
+
import { p as errorRunnerFailed, t as CliStructuredError } from "./cli-errors-D3_sMh2K.mjs";
|
|
2
|
+
import { t as assertFrameworkComponentsCompatible } from "./framework-components-gwAHl7ml.mjs";
|
|
3
|
+
import { t as enrichContract } from "./contract-enrichment-CF6ogEJ_.mjs";
|
|
4
|
+
import { emit } from "@prisma-next/emitter";
|
|
5
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
6
|
+
import { notOk, ok } from "@prisma-next/utils/result";
|
|
7
|
+
import { APP_SPACE_ID, createControlStack, hasMigrations, hasMultiSpaceRunner, hasOperationPreview, hasPslContractInfer, hasSchemaView } from "@prisma-next/framework-components/control";
|
|
8
|
+
import { loadContractSpaceAggregate, planAggregate, verifyAggregate } from "@prisma-next/migration-tools/aggregate";
|
|
9
|
+
import { EMPTY_CONTRACT_HASH } from "@prisma-next/migration-tools/constants";
|
|
10
|
+
//#region src/control-api/errors.ts
|
|
11
|
+
var ContractValidationError = class extends Error {
|
|
12
|
+
cause;
|
|
13
|
+
constructor(message, cause) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ContractValidationError";
|
|
16
|
+
this.cause = cause;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/utils/contract-space-aggregate-loader.ts
|
|
21
|
+
/**
|
|
22
|
+
* Convert the CLI's `Config.extensionPacks` array into the loader's
|
|
23
|
+
* `DeclaredExtensionEntry[]` shape.
|
|
24
|
+
*
|
|
25
|
+
* The loader hashes `contractSpace.contractJson` to compare against the
|
|
26
|
+
* on-disk `refs/head.json.hash` (drift detection). Rather than re-running
|
|
27
|
+
* the canonical-JSON + SHA-256 pipeline at the CLI surface, we look up
|
|
28
|
+
* the descriptor's pre-computed `headRef.hash` via reference identity
|
|
29
|
+
* on the contract JSON value — the loader passes the same
|
|
30
|
+
* `entry.contractSpace.contractJson` reference through to the hasher,
|
|
31
|
+
* so identity-keyed lookup is safe.
|
|
32
|
+
*/
|
|
33
|
+
function toDeclaredExtensions(extensionPacks) {
|
|
34
|
+
const entries = [];
|
|
35
|
+
const hashByContractJson = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const pack of extensionPacks) {
|
|
37
|
+
const entry = pack.contractSpace ? {
|
|
38
|
+
id: pack.id,
|
|
39
|
+
targetId: pack.targetId,
|
|
40
|
+
contractSpace: { contractJson: pack.contractSpace.contractJson }
|
|
41
|
+
} : {
|
|
42
|
+
id: pack.id,
|
|
43
|
+
targetId: pack.targetId
|
|
44
|
+
};
|
|
45
|
+
entries.push(entry);
|
|
46
|
+
if (pack.contractSpace) hashByContractJson.set(pack.contractSpace.contractJson, pack.contractSpace.headRef.hash);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
entries,
|
|
50
|
+
hashByContractJson
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Render a {@link LoadAggregateError} into a CLI structured-error
|
|
55
|
+
* envelope. Preserves error codes `5001` (layout) and `5002` (marker /
|
|
56
|
+
* drift / disjointness / etc.) so existing integration tests and
|
|
57
|
+
* downstream tooling continue to assert on the same `meta.violations[]`
|
|
58
|
+
* shape they did under the old precheck/marker-check helpers.
|
|
59
|
+
*/
|
|
60
|
+
function mapLoadAggregateError(error) {
|
|
61
|
+
if (error.kind === "layoutViolation") {
|
|
62
|
+
const lines = error.violations.map((v) => `- [${v.kind}] ${v.spaceId}`);
|
|
63
|
+
return new CliStructuredError("5001", error.violations.length === 1 ? "Contract-space layout violation detected" : `Contract-space layout violations detected (${error.violations.length})`, {
|
|
64
|
+
domain: "MIG",
|
|
65
|
+
why: `The on-disk \`migrations/\` directory and your \`extensionPacks\` declaration are not in agreement.\n${lines.join("\n")}`,
|
|
66
|
+
fix: "Run `prisma-next migrate` to materialise on-disk artefacts for declared extensions, or remove the orphan directory.",
|
|
67
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
68
|
+
meta: { violations: error.violations.map((v) => ({
|
|
69
|
+
kind: v.kind,
|
|
70
|
+
spaceId: v.spaceId
|
|
71
|
+
})) }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (error.kind === "driftViolation") return new CliStructuredError("5002", `Contract-space drift detected for "${error.spaceId}"`, {
|
|
75
|
+
domain: "MIG",
|
|
76
|
+
why: `The on-disk contract for space "${error.spaceId}" (hash ${error.priorHeadHash}) does not match the live extension descriptor (hash ${error.liveHash}).`,
|
|
77
|
+
fix: "Run `prisma-next migrate` to refresh the on-disk artefacts to match the live descriptor.",
|
|
78
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
79
|
+
meta: { violations: [{
|
|
80
|
+
kind: "drift",
|
|
81
|
+
spaceId: error.spaceId,
|
|
82
|
+
priorHeadHash: error.priorHeadHash,
|
|
83
|
+
liveHash: error.liveHash
|
|
84
|
+
}] }
|
|
85
|
+
});
|
|
86
|
+
if (error.kind === "disjointnessViolation") return new CliStructuredError("5002", `Contract-space disjointness violation: storage element "${error.element}" claimed by multiple spaces`, {
|
|
87
|
+
domain: "MIG",
|
|
88
|
+
why: `Spaces ${error.claimedBy.map((s) => `"${s}"`).join(", ")} all claim the storage element "${error.element}". Each storage element must be owned by exactly one contract space.`,
|
|
89
|
+
fix: "Update the conflicting contracts so each storage element is claimed by exactly one space.",
|
|
90
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
91
|
+
meta: { violations: [{
|
|
92
|
+
kind: "disjointness",
|
|
93
|
+
spaceId: error.claimedBy.join(","),
|
|
94
|
+
element: error.element,
|
|
95
|
+
claimedBy: error.claimedBy
|
|
96
|
+
}] }
|
|
97
|
+
});
|
|
98
|
+
if (error.kind === "integrityFailure") return new CliStructuredError("5002", `Contract-space integrity failure for "${error.spaceId}"`, {
|
|
99
|
+
domain: "MIG",
|
|
100
|
+
why: error.detail,
|
|
101
|
+
fix: "Run `prisma-next migrate` to refresh on-disk artefacts, or restore the on-disk `migrations/` directory from version control.",
|
|
102
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
103
|
+
meta: { violations: [{
|
|
104
|
+
kind: "integrity",
|
|
105
|
+
spaceId: error.spaceId,
|
|
106
|
+
detail: error.detail
|
|
107
|
+
}] }
|
|
108
|
+
});
|
|
109
|
+
if (error.kind === "validationFailure") return new CliStructuredError("5002", `Contract-space contract validation failed for "${error.spaceId}"`, {
|
|
110
|
+
domain: "MIG",
|
|
111
|
+
why: error.detail,
|
|
112
|
+
fix: "Run `prisma-next migrate` to refresh on-disk artefacts, or fix the extension descriptor producing the invalid contract.",
|
|
113
|
+
meta: { violations: [{
|
|
114
|
+
kind: "validation",
|
|
115
|
+
spaceId: error.spaceId,
|
|
116
|
+
detail: error.detail
|
|
117
|
+
}] }
|
|
118
|
+
});
|
|
119
|
+
return new CliStructuredError("5002", `Contract-space target mismatch for "${error.spaceId}"`, {
|
|
120
|
+
domain: "MIG",
|
|
121
|
+
why: `Space "${error.spaceId}" targets "${error.actual}" but the project's adapter targets "${error.expected}".`,
|
|
122
|
+
fix: "Update the extension descriptor to target the configured database, or change the project adapter.",
|
|
123
|
+
meta: { violations: [{
|
|
124
|
+
kind: "targetMismatch",
|
|
125
|
+
spaceId: error.spaceId,
|
|
126
|
+
expected: error.expected,
|
|
127
|
+
actual: error.actual
|
|
128
|
+
}] }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run the aggregate loader at the CLI surface, mapping any
|
|
133
|
+
* {@link LoadAggregateError} into a {@link CliStructuredError} envelope.
|
|
134
|
+
*
|
|
135
|
+
* App-side migration packages are intentionally not threaded through:
|
|
136
|
+
* `db init` / `db update` go through the planner's `synth` strategy for
|
|
137
|
+
* the app member (driven by `callerPolicy.ignoreGraphFor`), so the
|
|
138
|
+
* app's authored `migrations/` graph is not walked.
|
|
139
|
+
*
|
|
140
|
+
* @see specs/contract-space-aggregate-spec.md § Loader.
|
|
141
|
+
*/
|
|
142
|
+
async function buildContractSpaceAggregate(inputs) {
|
|
143
|
+
const { entries, hashByContractJson } = toDeclaredExtensions(inputs.extensionPacks);
|
|
144
|
+
const result = await loadContractSpaceAggregate({
|
|
145
|
+
targetId: inputs.targetId,
|
|
146
|
+
migrationsDir: inputs.migrationsDir,
|
|
147
|
+
appContract: inputs.appContract,
|
|
148
|
+
declaredExtensions: entries,
|
|
149
|
+
validateContract: inputs.validateContract,
|
|
150
|
+
hashContract: (contractJson) => {
|
|
151
|
+
const precomputed = hashByContractJson.get(contractJson);
|
|
152
|
+
if (precomputed === void 0) throw new Error("CLI aggregate loader: encountered an extension contract without a pre-computed descriptor hash. This is a wiring bug.");
|
|
153
|
+
return precomputed;
|
|
154
|
+
},
|
|
155
|
+
appMigrationPackages: []
|
|
156
|
+
});
|
|
157
|
+
if (!result.ok) return notOk(mapLoadAggregateError(result.failure));
|
|
158
|
+
return ok(result.value.aggregate);
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/control-api/operations/migration-helpers.ts
|
|
162
|
+
/**
|
|
163
|
+
* Strips operation objects to their public shape (id, label, operationClass).
|
|
164
|
+
* Used at the API boundary to avoid leaking internal fields (precheck, execute, postcheck, etc.).
|
|
165
|
+
*/
|
|
166
|
+
function stripOperations(operations) {
|
|
167
|
+
return operations.map((op) => ({
|
|
168
|
+
id: op.id,
|
|
169
|
+
label: op.label,
|
|
170
|
+
operationClass: op.operationClass
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/control-api/operations/db-apply-aggregate.ts
|
|
175
|
+
/**
|
|
176
|
+
* Span IDs emitted via `onProgress` during the aggregate apply flow.
|
|
177
|
+
* Stable identifiers consumed by the structured-output renderer and by
|
|
178
|
+
* tests asserting on span ids.
|
|
179
|
+
*/
|
|
180
|
+
const SPAN_IDS$1 = {
|
|
181
|
+
introspect: "introspect",
|
|
182
|
+
plan: "plan",
|
|
183
|
+
apply: "apply"
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Loader → planner → runner pipeline shared by `db init` and `db update`.
|
|
187
|
+
*
|
|
188
|
+
* The pipeline:
|
|
189
|
+
*
|
|
190
|
+
* 1. **Load**: build a {@link ContractSpaceAggregate} from the descriptor
|
|
191
|
+
* set + on-disk on-disk artefacts. Any layout / drift / disjointness /
|
|
192
|
+
* integrity violation short-circuits with a structured error.
|
|
193
|
+
* 2. **Read DB state**: marker rows (`familyInstance.readAllMarkers`)
|
|
194
|
+
* + introspected schema (`familyInstance.introspect`).
|
|
195
|
+
* 3. **Plan**: {@link planAggregate} chooses graph-walk vs synth per
|
|
196
|
+
* member according to `callerPolicy.ignoreGraphFor`. The app member
|
|
197
|
+
* is forced through synth (today's daily-driver behaviour); every
|
|
198
|
+
* extension member walks its on-disk graph.
|
|
199
|
+
* 4. **Apply** (when `mode === 'apply'`): every per-space `MigrationPlan`
|
|
200
|
+
* feeds into the runner's `executeAcrossSpaces` — one outer
|
|
201
|
+
* transaction across every space; failure on any space rolls back
|
|
202
|
+
* every space's writes.
|
|
203
|
+
*/
|
|
204
|
+
async function executeAggregateApply(options) {
|
|
205
|
+
const { driver, familyInstance, contract, mode, migrations, frameworkComponents, migrationsDir, extensionPacks, targetId, policy, action, onProgress } = options;
|
|
206
|
+
const loaded = await buildContractSpaceAggregate({
|
|
207
|
+
targetId,
|
|
208
|
+
migrationsDir,
|
|
209
|
+
appContract: contract,
|
|
210
|
+
extensionPacks,
|
|
211
|
+
validateContract: (json) => familyInstance.validateContract(json)
|
|
212
|
+
});
|
|
213
|
+
if (!loaded.ok) throw loaded.failure;
|
|
214
|
+
const aggregate = loaded.value;
|
|
215
|
+
const markerRows = await familyInstance.readAllMarkers({ driver });
|
|
216
|
+
const orphanMarkerError = detectOrphanMarkers(aggregate, markerRows);
|
|
217
|
+
if (orphanMarkerError !== null) throw orphanMarkerError;
|
|
218
|
+
onProgress?.({
|
|
219
|
+
action,
|
|
220
|
+
kind: "spanStart",
|
|
221
|
+
spanId: SPAN_IDS$1.introspect,
|
|
222
|
+
label: "Introspecting database schema"
|
|
223
|
+
});
|
|
224
|
+
const schemaIR = await familyInstance.introspect({ driver });
|
|
225
|
+
onProgress?.({
|
|
226
|
+
action,
|
|
227
|
+
kind: "spanEnd",
|
|
228
|
+
spanId: SPAN_IDS$1.introspect,
|
|
229
|
+
outcome: "ok"
|
|
230
|
+
});
|
|
231
|
+
onProgress?.({
|
|
232
|
+
action,
|
|
233
|
+
kind: "spanStart",
|
|
234
|
+
spanId: SPAN_IDS$1.plan,
|
|
235
|
+
label: "Planning migration"
|
|
236
|
+
});
|
|
237
|
+
const planResult = await planAggregate({
|
|
238
|
+
aggregate,
|
|
239
|
+
currentDBState: {
|
|
240
|
+
markersBySpaceId: markerRows,
|
|
241
|
+
schemaIntrospection: schemaIR
|
|
242
|
+
},
|
|
243
|
+
familyInstance,
|
|
244
|
+
migrations,
|
|
245
|
+
frameworkComponents,
|
|
246
|
+
callerPolicy: { ignoreGraphFor: new Set([aggregate.app.spaceId]) },
|
|
247
|
+
operationPolicy: policy
|
|
248
|
+
});
|
|
249
|
+
if (!planResult.ok) {
|
|
250
|
+
onProgress?.({
|
|
251
|
+
action,
|
|
252
|
+
kind: "spanEnd",
|
|
253
|
+
spanId: SPAN_IDS$1.plan,
|
|
254
|
+
outcome: "error"
|
|
255
|
+
});
|
|
256
|
+
return mapPlannerError(planResult.failure);
|
|
257
|
+
}
|
|
258
|
+
onProgress?.({
|
|
259
|
+
action,
|
|
260
|
+
kind: "spanEnd",
|
|
261
|
+
spanId: SPAN_IDS$1.plan,
|
|
262
|
+
outcome: "ok"
|
|
263
|
+
});
|
|
264
|
+
const orderedResolutions = collectOrdered(planResult.value.applyOrder, planResult.value.perSpace);
|
|
265
|
+
const appResolution = orderedResolutions.find((r) => r.spaceId === aggregate.app.spaceId);
|
|
266
|
+
if (!appResolution) throw new Error("Aggregate planner returned no plan for the app member — the planner is supposed to always emit one.");
|
|
267
|
+
const appPlan = appResolution.entry.plan;
|
|
268
|
+
if (mode === "plan") {
|
|
269
|
+
const aggregateOps = orderedResolutions.flatMap((r) => r.entry.displayOps);
|
|
270
|
+
const preview = hasOperationPreview(familyInstance) ? familyInstance.toOperationPreview(aggregateOps) : void 0;
|
|
271
|
+
const summary = `Planned ${aggregateOps.length} operation(s) across ${orderedResolutions.length} space(s)`;
|
|
272
|
+
return wrapPlanResult({
|
|
273
|
+
operations: aggregateOps,
|
|
274
|
+
destination: appPlan.destination,
|
|
275
|
+
preview,
|
|
276
|
+
summary
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const runner = migrations.createRunner(familyInstance);
|
|
280
|
+
if (!hasMultiSpaceRunner(runner)) throw errorRunnerFailed(`Runner for target "${aggregate.targetId}" does not implement \`executeAcrossSpaces\``, { why: `${action === "dbInit" ? "db init" : "db update"} requires multi-space-capable runners (today: every SQL family runner).` });
|
|
281
|
+
onProgress?.({
|
|
282
|
+
action,
|
|
283
|
+
kind: "spanStart",
|
|
284
|
+
spanId: SPAN_IDS$1.apply,
|
|
285
|
+
label: "Applying migration plan across spaces"
|
|
286
|
+
});
|
|
287
|
+
const perSpaceOptions = orderedResolutions.map((r) => ({
|
|
288
|
+
space: r.spaceId,
|
|
289
|
+
plan: r.entry.plan,
|
|
290
|
+
driver,
|
|
291
|
+
destinationContract: r.entry.destinationContract,
|
|
292
|
+
policy,
|
|
293
|
+
executionChecks: {
|
|
294
|
+
prechecks: false,
|
|
295
|
+
postchecks: false,
|
|
296
|
+
idempotencyChecks: false
|
|
297
|
+
},
|
|
298
|
+
frameworkComponents,
|
|
299
|
+
strictVerification: false
|
|
300
|
+
}));
|
|
301
|
+
const runnerResult = await runner.executeAcrossSpaces({
|
|
302
|
+
driver,
|
|
303
|
+
perSpaceOptions
|
|
304
|
+
});
|
|
305
|
+
if (!runnerResult.ok) {
|
|
306
|
+
onProgress?.({
|
|
307
|
+
action,
|
|
308
|
+
kind: "spanEnd",
|
|
309
|
+
spanId: SPAN_IDS$1.apply,
|
|
310
|
+
outcome: "error"
|
|
311
|
+
});
|
|
312
|
+
return buildRunnerFailure({
|
|
313
|
+
summary: runnerResult.failure.summary,
|
|
314
|
+
...ifDefined("why", runnerResult.failure.why),
|
|
315
|
+
meta: {
|
|
316
|
+
...runnerResult.failure.meta ?? {},
|
|
317
|
+
failingSpace: runnerResult.failure.failingSpace
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
onProgress?.({
|
|
322
|
+
action,
|
|
323
|
+
kind: "spanEnd",
|
|
324
|
+
spanId: SPAN_IDS$1.apply,
|
|
325
|
+
outcome: "ok"
|
|
326
|
+
});
|
|
327
|
+
const totalOpsPlanned = runnerResult.value.perSpaceResults.reduce((sum, r) => sum + r.value.operationsPlanned, 0);
|
|
328
|
+
const totalOpsExecuted = runnerResult.value.perSpaceResults.reduce((sum, r) => sum + r.value.operationsExecuted, 0);
|
|
329
|
+
const aggregateOps = orderedResolutions.flatMap((r) => r.entry.displayOps);
|
|
330
|
+
const summary = action === "dbInit" ? `Applied ${totalOpsExecuted} operation(s) across ${orderedResolutions.length} space(s), database signed` : totalOpsExecuted === 0 ? `Database already matches contract across ${orderedResolutions.length} space(s), signature updated` : `Applied ${totalOpsExecuted} operation(s) across ${orderedResolutions.length} space(s), signature updated`;
|
|
331
|
+
return wrapApplyResult({
|
|
332
|
+
operations: aggregateOps,
|
|
333
|
+
destination: appPlan.destination,
|
|
334
|
+
operationsPlanned: totalOpsPlanned,
|
|
335
|
+
operationsExecuted: totalOpsExecuted,
|
|
336
|
+
summary
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Compare the live `_prisma_marker` rows against the aggregate's
|
|
341
|
+
* declared members. Any marker row whose `space` is not a member of
|
|
342
|
+
* the aggregate is an "orphan" — typically a marker left behind by
|
|
343
|
+
* an extension that was removed from `extensionPacks` without first
|
|
344
|
+
* cleaning up its on-disk migrations / database tables.
|
|
345
|
+
*
|
|
346
|
+
* Returns a {@link CliStructuredError} envelope (code `5002`,
|
|
347
|
+
* `kind: 'orphanMarker'`) for the first orphan it finds, or `null`
|
|
348
|
+
* when every marker row maps to a declared member. Mirrors the M2
|
|
349
|
+
* `runContractSpaceVerifierMarkerCheck` envelope so downstream
|
|
350
|
+
* tooling (integration tests, JSON consumers) keeps asserting on the
|
|
351
|
+
* same shape.
|
|
352
|
+
*/
|
|
353
|
+
function detectOrphanMarkers(aggregate, markerRows) {
|
|
354
|
+
const memberSpaceIds = new Set([aggregate.app.spaceId, ...aggregate.extensions.map((m) => m.spaceId)]);
|
|
355
|
+
const orphans = [];
|
|
356
|
+
for (const [spaceId, row] of markerRows) if (row !== null && row !== void 0 && !memberSpaceIds.has(spaceId)) orphans.push(spaceId);
|
|
357
|
+
if (orphans.length === 0) return null;
|
|
358
|
+
orphans.sort((a, b) => a.localeCompare(b));
|
|
359
|
+
return new CliStructuredError("5002", orphans.length === 1 ? `Orphan contract-space marker detected for "${orphans[0]}"` : `Orphan contract-space markers detected for ${orphans.length} spaces`, {
|
|
360
|
+
domain: "MIG",
|
|
361
|
+
why: `The database has \`_prisma_marker\` rows for spaces (${orphans.map((s) => `"${s}"`).join(", ")}) that are not declared in the project's \`extensionPacks\`. The aggregate pipeline refuses to advance markers it cannot account for.`,
|
|
362
|
+
fix: "Either re-declare the missing extension(s) in `extensionPacks` (so the aggregate owns them again), or remove the orphan marker row(s) from `_prisma_marker` once you have confirmed the corresponding tables can be safely retired.",
|
|
363
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
364
|
+
meta: { violations: orphans.map((spaceId) => ({
|
|
365
|
+
kind: "orphanMarker",
|
|
366
|
+
spaceId
|
|
367
|
+
})) }
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function collectOrdered(applyOrder, perSpace) {
|
|
371
|
+
return applyOrder.map((spaceId) => {
|
|
372
|
+
const entry = perSpace.get(spaceId);
|
|
373
|
+
if (!entry) throw new Error(`Aggregate planner output missing per-space plan for "${spaceId}"`);
|
|
374
|
+
return {
|
|
375
|
+
spaceId,
|
|
376
|
+
entry
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function mapPlannerError(error) {
|
|
381
|
+
if (error.kind === "appSynthFailure") return notOk({
|
|
382
|
+
code: "PLANNING_FAILED",
|
|
383
|
+
summary: "Migration planning failed due to conflicts",
|
|
384
|
+
conflicts: error.conflicts,
|
|
385
|
+
why: void 0,
|
|
386
|
+
meta: void 0
|
|
387
|
+
});
|
|
388
|
+
if (error.kind === "extensionPathUnreachable") return buildRunnerFailure({
|
|
389
|
+
summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
|
|
390
|
+
why: `No path in the on-disk migration graph for extension space "${error.spaceId}" reaches the on-disk head ref hash "${error.target}".`,
|
|
391
|
+
meta: {
|
|
392
|
+
spaceId: error.spaceId,
|
|
393
|
+
target: error.target
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
if (error.kind === "extensionPathUnsatisfiable") return buildRunnerFailure({
|
|
397
|
+
summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
|
|
398
|
+
why: `On-disk migration graph for extension space "${error.spaceId}" reaches the on-disk head ref but does not cover required invariants: ${error.missingInvariants.join(", ")}.`,
|
|
399
|
+
meta: {
|
|
400
|
+
spaceId: error.spaceId,
|
|
401
|
+
missingInvariants: error.missingInvariants
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
return buildRunnerFailure({
|
|
405
|
+
summary: `Aggregate planner policy conflict for space "${error.spaceId}"`,
|
|
406
|
+
why: error.detail,
|
|
407
|
+
meta: { spaceId: error.spaceId }
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function wrapPlanResult(args) {
|
|
411
|
+
return ok({
|
|
412
|
+
mode: "plan",
|
|
413
|
+
plan: {
|
|
414
|
+
operations: stripOperations(args.operations),
|
|
415
|
+
...ifDefined("preview", args.preview)
|
|
416
|
+
},
|
|
417
|
+
destination: {
|
|
418
|
+
storageHash: args.destination.storageHash,
|
|
419
|
+
...ifDefined("profileHash", args.destination.profileHash)
|
|
420
|
+
},
|
|
421
|
+
summary: args.summary
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function wrapApplyResult(args) {
|
|
425
|
+
return ok({
|
|
426
|
+
mode: "apply",
|
|
427
|
+
plan: { operations: stripOperations(args.operations) },
|
|
428
|
+
destination: {
|
|
429
|
+
storageHash: args.destination.storageHash,
|
|
430
|
+
...ifDefined("profileHash", args.destination.profileHash)
|
|
431
|
+
},
|
|
432
|
+
execution: {
|
|
433
|
+
operationsPlanned: args.operationsPlanned,
|
|
434
|
+
operationsExecuted: args.operationsExecuted
|
|
435
|
+
},
|
|
436
|
+
marker: args.destination.profileHash ? {
|
|
437
|
+
storageHash: args.destination.storageHash,
|
|
438
|
+
profileHash: args.destination.profileHash
|
|
439
|
+
} : { storageHash: args.destination.storageHash },
|
|
440
|
+
summary: args.summary
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
function buildRunnerFailure(args) {
|
|
444
|
+
return notOk({
|
|
445
|
+
code: "RUNNER_FAILED",
|
|
446
|
+
summary: args.summary,
|
|
447
|
+
why: args.why,
|
|
448
|
+
meta: args.meta,
|
|
449
|
+
conflicts: void 0
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/control-api/operations/db-init.ts
|
|
454
|
+
/**
|
|
455
|
+
* Execute `db init` against the configured contract.
|
|
456
|
+
*
|
|
457
|
+
* Routes through the loader → planner → runner pipeline (sub-spec
|
|
458
|
+
* "Commit-by-commit § Commit 4"). Always additive-only; destructive
|
|
459
|
+
* changes belong to `db update`.
|
|
460
|
+
*/
|
|
461
|
+
async function executeDbInit(options) {
|
|
462
|
+
return await executeAggregateApply({
|
|
463
|
+
driver: options.driver,
|
|
464
|
+
familyInstance: options.familyInstance,
|
|
465
|
+
contract: options.contract,
|
|
466
|
+
mode: options.mode,
|
|
467
|
+
migrations: options.migrations,
|
|
468
|
+
frameworkComponents: options.frameworkComponents,
|
|
469
|
+
migrationsDir: options.migrationsDir,
|
|
470
|
+
targetId: options.targetId,
|
|
471
|
+
extensionPacks: options.extensionPacks ?? [],
|
|
472
|
+
policy: { allowedOperationClasses: ["additive"] },
|
|
473
|
+
action: "dbInit",
|
|
474
|
+
...ifDefined("onProgress", options.onProgress)
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/control-api/operations/db-update.ts
|
|
479
|
+
const DB_UPDATE_POLICY = { allowedOperationClasses: [
|
|
480
|
+
"additive",
|
|
481
|
+
"widening",
|
|
482
|
+
"destructive"
|
|
483
|
+
] };
|
|
484
|
+
/**
|
|
485
|
+
* Execute `db update` against the configured contract.
|
|
486
|
+
*
|
|
487
|
+
* Routes through the loader → planner → runner pipeline. Destructive
|
|
488
|
+
* operations require either `acceptDataLoss: true` or a prior
|
|
489
|
+
* `mode: 'plan'` invocation that surfaces the destructive ops; the
|
|
490
|
+
* confirmation gate is implemented here so the lower-level applier
|
|
491
|
+
* remains policy-agnostic.
|
|
492
|
+
*/
|
|
493
|
+
async function executeDbUpdate(options) {
|
|
494
|
+
const sharedInputs = {
|
|
495
|
+
driver: options.driver,
|
|
496
|
+
familyInstance: options.familyInstance,
|
|
497
|
+
contract: options.contract,
|
|
498
|
+
migrations: options.migrations,
|
|
499
|
+
frameworkComponents: options.frameworkComponents,
|
|
500
|
+
migrationsDir: options.migrationsDir,
|
|
501
|
+
targetId: options.targetId,
|
|
502
|
+
extensionPacks: options.extensionPacks ?? [],
|
|
503
|
+
policy: DB_UPDATE_POLICY,
|
|
504
|
+
action: "dbUpdate",
|
|
505
|
+
...ifDefined("onProgress", options.onProgress)
|
|
506
|
+
};
|
|
507
|
+
if (options.mode === "apply" && !options.acceptDataLoss) {
|
|
508
|
+
const gate = await guardDestructiveChanges(sharedInputs);
|
|
509
|
+
if (gate !== null) return gate;
|
|
510
|
+
}
|
|
511
|
+
return await executeAggregateApply({
|
|
512
|
+
...sharedInputs,
|
|
513
|
+
mode: options.mode
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Pre-plan once when running `db update apply` without `acceptDataLoss`.
|
|
518
|
+
* Surfaces destructive operations across every space; if any are
|
|
519
|
+
* planned, returns a `DESTRUCTIVE_CHANGES` failure that the CLI shows
|
|
520
|
+
* as a confirmation prompt. Returns `null` when the apply is safe to
|
|
521
|
+
* run.
|
|
522
|
+
*/
|
|
523
|
+
async function guardDestructiveChanges(sharedInputs) {
|
|
524
|
+
const planResult = await executeAggregateApply({
|
|
525
|
+
...sharedInputs,
|
|
526
|
+
mode: "plan"
|
|
527
|
+
});
|
|
528
|
+
if (!planResult.ok) return planResult;
|
|
529
|
+
const destructiveOps = planResult.value.plan.operations.filter((op) => op.operationClass === "destructive").map((op) => ({
|
|
530
|
+
id: op.id,
|
|
531
|
+
label: op.label
|
|
532
|
+
}));
|
|
533
|
+
if (destructiveOps.length === 0) return null;
|
|
534
|
+
return notOk({
|
|
535
|
+
code: "DESTRUCTIVE_CHANGES",
|
|
536
|
+
summary: `Planned ${destructiveOps.length} destructive operation(s) that require confirmation`,
|
|
537
|
+
why: "Destructive operations require confirmation — re-run with -y to accept",
|
|
538
|
+
conflicts: void 0,
|
|
539
|
+
meta: { destructiveOperations: destructiveOps }
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/control-api/operations/db-verify.ts
|
|
544
|
+
/**
|
|
545
|
+
* Span IDs emitted via `onProgress` during the aggregate verify flow.
|
|
546
|
+
* Mirrors the span identifiers used by the legacy precheck / marker-check
|
|
547
|
+
* helpers so structured-output renderers and progress tests keep working.
|
|
548
|
+
*/
|
|
549
|
+
const SPAN_IDS = {
|
|
550
|
+
introspect: "introspect",
|
|
551
|
+
verify: "verify"
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* Loader → verifier pipeline shared by `db verify` modes (`full`,
|
|
555
|
+
* `marker-only`, `schema-only`).
|
|
556
|
+
*
|
|
557
|
+
* 1. **Load**: build a {@link import('@prisma-next/migration-tools/aggregate').ContractSpaceAggregate}
|
|
558
|
+
* from descriptors + on-disk on-disk artefacts. Layout / drift /
|
|
559
|
+
* integrity / disjointness violations short-circuit with a
|
|
560
|
+
* structured CLI error.
|
|
561
|
+
* 2. **Read DB state**: marker rows + (when `skipSchema` is `false`)
|
|
562
|
+
* schema introspection.
|
|
563
|
+
* 3. **Verify**: {@link verifyAggregate} returns per-space
|
|
564
|
+
* `markerCheck` + per-space pre-projected `schemaCheck` (closes F23).
|
|
565
|
+
* Marker mismatches map to `CliStructuredError` (code `5002`) so
|
|
566
|
+
* callers (CLI command) can render and exit. Schema results are
|
|
567
|
+
* returned to the caller verbatim.
|
|
568
|
+
*/
|
|
569
|
+
async function executeDbVerify(options) {
|
|
570
|
+
const { driver, familyInstance, onProgress, skipSchema, skipMarker } = options;
|
|
571
|
+
const loaded = await buildContractSpaceAggregate(buildLoadInputs(options));
|
|
572
|
+
if (!loaded.ok) return notOk(loaded.failure);
|
|
573
|
+
const aggregate = loaded.value;
|
|
574
|
+
const markersBySpaceId = await familyInstance.readAllMarkers({ driver });
|
|
575
|
+
const schemaIntrospection = skipSchema ? null : await runIntrospection({
|
|
576
|
+
driver,
|
|
577
|
+
familyInstance,
|
|
578
|
+
onProgress
|
|
579
|
+
});
|
|
580
|
+
emitVerifySpan(onProgress, "spanStart");
|
|
581
|
+
return finaliseVerifyResult({
|
|
582
|
+
verifyResult: verifyAggregate({
|
|
583
|
+
aggregate,
|
|
584
|
+
markersBySpaceId,
|
|
585
|
+
schemaIntrospection,
|
|
586
|
+
mode: options.mode,
|
|
587
|
+
verifySchemaForMember: createPerMemberVerifier(options)
|
|
588
|
+
}),
|
|
589
|
+
aggregate,
|
|
590
|
+
skipMarker,
|
|
591
|
+
onProgress
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function buildLoadInputs(options) {
|
|
595
|
+
return {
|
|
596
|
+
targetId: options.targetId,
|
|
597
|
+
migrationsDir: options.migrationsDir,
|
|
598
|
+
appContract: options.contract,
|
|
599
|
+
extensionPacks: options.extensionPacks,
|
|
600
|
+
validateContract: (json) => options.familyInstance.validateContract(json)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
async function runIntrospection(args) {
|
|
604
|
+
const { driver, familyInstance, onProgress } = args;
|
|
605
|
+
onProgress?.({
|
|
606
|
+
action: "dbVerify",
|
|
607
|
+
kind: "spanStart",
|
|
608
|
+
spanId: SPAN_IDS.introspect,
|
|
609
|
+
label: "Introspecting database schema"
|
|
610
|
+
});
|
|
611
|
+
try {
|
|
612
|
+
const result = await familyInstance.introspect({ driver });
|
|
613
|
+
onProgress?.({
|
|
614
|
+
action: "dbVerify",
|
|
615
|
+
kind: "spanEnd",
|
|
616
|
+
spanId: SPAN_IDS.introspect,
|
|
617
|
+
outcome: "ok"
|
|
618
|
+
});
|
|
619
|
+
return result;
|
|
620
|
+
} catch (error) {
|
|
621
|
+
onProgress?.({
|
|
622
|
+
action: "dbVerify",
|
|
623
|
+
kind: "spanEnd",
|
|
624
|
+
spanId: SPAN_IDS.introspect,
|
|
625
|
+
outcome: "error"
|
|
626
|
+
});
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Build the per-member schema callback handed to the aggregate verifier.
|
|
632
|
+
* When `skipSchema` is true the callback short-circuits with a synthetic
|
|
633
|
+
* `ok` result so the verifier still runs the (cheap) schemaCheck loop
|
|
634
|
+
* without invoking the family's verification path.
|
|
635
|
+
*/
|
|
636
|
+
function createPerMemberVerifier(options) {
|
|
637
|
+
const { skipSchema, familyInstance, frameworkComponents } = options;
|
|
638
|
+
return (projectedSchema, member, verifyMode) => {
|
|
639
|
+
if (skipSchema) return buildSkippedSchemaResult(member);
|
|
640
|
+
return familyInstance.schemaVerifyAgainstSchema({
|
|
641
|
+
contract: member.contract,
|
|
642
|
+
schema: projectedSchema,
|
|
643
|
+
strict: verifyMode === "strict",
|
|
644
|
+
frameworkComponents
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function emitVerifySpan(onProgress, kind) {
|
|
649
|
+
if (kind === "spanStart") {
|
|
650
|
+
onProgress?.({
|
|
651
|
+
action: "dbVerify",
|
|
652
|
+
kind: "spanStart",
|
|
653
|
+
spanId: SPAN_IDS.verify,
|
|
654
|
+
label: "Verifying contract spaces"
|
|
655
|
+
});
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
onProgress?.({
|
|
659
|
+
action: "dbVerify",
|
|
660
|
+
kind: "spanEnd",
|
|
661
|
+
spanId: SPAN_IDS.verify,
|
|
662
|
+
outcome: kind === "spanEndOk" ? "ok" : "error"
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Map an {@link AggregateVerifierOutput} to the operation's
|
|
667
|
+
* {@link ExecuteDbVerifyResult}, applying the `skipMarker` policy used
|
|
668
|
+
* by the CLI's `--schema-only` mode.
|
|
669
|
+
*/
|
|
670
|
+
function finaliseVerifyResult(args) {
|
|
671
|
+
const { verifyResult, aggregate, skipMarker, onProgress } = args;
|
|
672
|
+
if (!verifyResult.ok) {
|
|
673
|
+
emitVerifySpan(onProgress, "spanEndError");
|
|
674
|
+
return notOk(new CliStructuredError("5002", "Aggregate verifier introspection failed", {
|
|
675
|
+
domain: "MIG",
|
|
676
|
+
why: verifyResult.failure.detail,
|
|
677
|
+
fix: "Check database connectivity and the introspection tooling.",
|
|
678
|
+
docsUrl: "https://pris.ly/contract-spaces"
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
const markerError = skipMarker ? null : mapMarkerCheckFailures(aggregate.app.spaceId, verifyResult.value.markerCheck);
|
|
682
|
+
if (markerError !== null) {
|
|
683
|
+
emitVerifySpan(onProgress, "spanEndError");
|
|
684
|
+
return notOk(markerError);
|
|
685
|
+
}
|
|
686
|
+
emitVerifySpan(onProgress, "spanEndOk");
|
|
687
|
+
return ok({
|
|
688
|
+
schemaResults: verifyResult.value.schemaCheck.perSpace,
|
|
689
|
+
memberOrder: [aggregate.app.spaceId, ...aggregate.extensions.map((e) => e.spaceId)],
|
|
690
|
+
appSpaceId: aggregate.app.spaceId
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function buildSkippedSchemaResult(member) {
|
|
694
|
+
const profileHash = member.contract.profileHash;
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
summary: "Schema verification skipped",
|
|
698
|
+
contract: {
|
|
699
|
+
storageHash: member.headRef.hash,
|
|
700
|
+
...profileHash ? { profileHash } : {}
|
|
701
|
+
},
|
|
702
|
+
target: { expected: member.contract.target },
|
|
703
|
+
schema: {
|
|
704
|
+
issues: [],
|
|
705
|
+
root: {
|
|
706
|
+
status: "pass",
|
|
707
|
+
kind: "skipped",
|
|
708
|
+
name: member.spaceId,
|
|
709
|
+
contractPath: "",
|
|
710
|
+
code: "SKIPPED",
|
|
711
|
+
message: "Schema verification skipped",
|
|
712
|
+
expected: void 0,
|
|
713
|
+
actual: void 0,
|
|
714
|
+
children: []
|
|
715
|
+
},
|
|
716
|
+
counts: {
|
|
717
|
+
pass: 0,
|
|
718
|
+
warn: 0,
|
|
719
|
+
fail: 0,
|
|
720
|
+
totalNodes: 0
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
timings: { total: 0 }
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Translate per-space marker check failures and orphan markers into a
|
|
728
|
+
* single CLI structured error envelope. Preserves the legacy code
|
|
729
|
+
* `5002` (was emitted by `runContractSpaceVerifierMarkerCheck`).
|
|
730
|
+
*/
|
|
731
|
+
function mapMarkerCheckFailures(appSpaceId, section) {
|
|
732
|
+
const violations = [];
|
|
733
|
+
for (const [spaceId, result] of section.perSpace) {
|
|
734
|
+
if (result.kind === "ok" || result.kind === "absent") continue;
|
|
735
|
+
if (result.kind === "hashMismatch") {
|
|
736
|
+
violations.push({
|
|
737
|
+
kind: "hashMismatch",
|
|
738
|
+
spaceId,
|
|
739
|
+
remediation: spaceId === appSpaceId ? "Run `prisma-next db update` to advance the marker, or roll the database back to the recorded hash." : `Apply on-disk migrations under \`migrations/${spaceId}/\` to advance the marker, or remove the conflicting marker row.`
|
|
740
|
+
});
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (result.kind === "missingInvariants") violations.push({
|
|
744
|
+
kind: "invariantsMismatch",
|
|
745
|
+
spaceId,
|
|
746
|
+
remediation: `Re-apply the migrations under \`migrations/${spaceId}/\` so the marker carries invariants: ${result.missing.join(", ")}.`
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
for (const orphan of section.orphanMarkers) violations.push({
|
|
750
|
+
kind: "orphanMarker",
|
|
751
|
+
spaceId: orphan.spaceId,
|
|
752
|
+
remediation: `Add the corresponding extension to \`extensionPacks\` in \`prisma-next.config.ts\`, or delete the orphan marker row for "${orphan.spaceId}".`
|
|
753
|
+
});
|
|
754
|
+
if (violations.length === 0) return null;
|
|
755
|
+
const lines = violations.map((v) => `- [${v.kind}] ${v.spaceId}: ${v.remediation}`);
|
|
756
|
+
return new CliStructuredError("5002", violations.length === 1 ? "Contract-space verifier found a violation" : `Contract-space verifier found violations (${violations.length})`, {
|
|
757
|
+
domain: "MIG",
|
|
758
|
+
why: `The on-disk \`migrations/\` directory, the \`extensionPacks\` declaration, and the live database marker rows are not in agreement.\n${lines.join("\n")}`,
|
|
759
|
+
fix: violations[0]?.remediation ?? "Review and reconcile the violations listed above.",
|
|
760
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
761
|
+
meta: { violations }
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/control-api/operations/migration-apply.ts
|
|
766
|
+
/**
|
|
767
|
+
* Apply a sequence of migration packages against the configured driver.
|
|
768
|
+
*
|
|
769
|
+
* Validates the path's continuity (origin → ... → destination, no gaps),
|
|
770
|
+
* then drives the family/target's migration runner over each package's
|
|
771
|
+
* operations in order, surfacing per-migration progress through `onProgress`.
|
|
772
|
+
*
|
|
773
|
+
* The `pendingMigrations` parameter is trusted input. Callers are responsible
|
|
774
|
+
* for upstream verification of the originating migration packages — typically
|
|
775
|
+
* by loading them via `readMigrationPackage` from
|
|
776
|
+
* `@prisma-next/migration-tools/io`, which performs hash-integrity checks at
|
|
777
|
+
* the load boundary. This operation does not re-verify the packages and
|
|
778
|
+
* assumes the `(metadata, ops)` pairs on disk have not been tampered with
|
|
779
|
+
* since emit.
|
|
780
|
+
*/
|
|
781
|
+
async function executeMigrationApply(options) {
|
|
782
|
+
const { driver, familyInstance, originHash, destinationHash, pendingMigrations, migrations, frameworkComponents, targetId, onProgress } = options;
|
|
783
|
+
if (pendingMigrations.length === 0) {
|
|
784
|
+
if (originHash !== destinationHash) return notOk({
|
|
785
|
+
code: "MIGRATION_PATH_NOT_FOUND",
|
|
786
|
+
summary: "No migrations provided for requested origin and destination",
|
|
787
|
+
why: `Requested ${originHash} -> ${destinationHash} but pendingMigrations is empty`,
|
|
788
|
+
meta: {
|
|
789
|
+
originHash,
|
|
790
|
+
destinationHash
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
return ok({
|
|
794
|
+
migrationsApplied: 0,
|
|
795
|
+
markerHash: originHash,
|
|
796
|
+
applied: [],
|
|
797
|
+
summary: "Already up to date"
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
const firstMigration = pendingMigrations[0];
|
|
801
|
+
const lastMigration = pendingMigrations[pendingMigrations.length - 1];
|
|
802
|
+
const firstFromMarker = firstMigration.from ?? EMPTY_CONTRACT_HASH;
|
|
803
|
+
if (firstFromMarker !== originHash || lastMigration.to !== destinationHash) return notOk({
|
|
804
|
+
code: "MIGRATION_PATH_NOT_FOUND",
|
|
805
|
+
summary: "Migration apply path does not match requested origin and destination",
|
|
806
|
+
why: `Path resolved as ${firstFromMarker} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
|
|
807
|
+
meta: {
|
|
808
|
+
originHash,
|
|
809
|
+
destinationHash,
|
|
810
|
+
pathOrigin: firstFromMarker,
|
|
811
|
+
pathDestination: lastMigration.to
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
for (let i = 1; i < pendingMigrations.length; i++) {
|
|
815
|
+
const previous = pendingMigrations[i - 1];
|
|
816
|
+
const current = pendingMigrations[i];
|
|
817
|
+
const currentFromMarker = current.from ?? EMPTY_CONTRACT_HASH;
|
|
818
|
+
if (previous.to !== currentFromMarker) return notOk({
|
|
819
|
+
code: "MIGRATION_PATH_NOT_FOUND",
|
|
820
|
+
summary: "Migration apply path contains a discontinuity between adjacent migrations",
|
|
821
|
+
why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${currentFromMarker}`,
|
|
822
|
+
meta: {
|
|
823
|
+
originHash,
|
|
824
|
+
destinationHash,
|
|
825
|
+
previousDirName: previous.dirName,
|
|
826
|
+
previousTo: previous.to,
|
|
827
|
+
currentDirName: current.dirName,
|
|
828
|
+
currentFrom: currentFromMarker,
|
|
829
|
+
discontinuityIndex: i
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
const runner = migrations.createRunner(familyInstance);
|
|
834
|
+
const applied = [];
|
|
835
|
+
for (const migration of pendingMigrations) {
|
|
836
|
+
const migrationSpanId = `migration:${migration.dirName}`;
|
|
837
|
+
onProgress?.({
|
|
838
|
+
action: "migrationApply",
|
|
839
|
+
kind: "spanStart",
|
|
840
|
+
spanId: migrationSpanId,
|
|
841
|
+
label: `Applying ${migration.dirName}`
|
|
842
|
+
});
|
|
843
|
+
const { operations } = migration;
|
|
844
|
+
const policy = { allowedOperationClasses: [
|
|
845
|
+
"additive",
|
|
846
|
+
"widening",
|
|
847
|
+
"destructive",
|
|
848
|
+
"data"
|
|
849
|
+
] };
|
|
850
|
+
const plan = {
|
|
851
|
+
targetId,
|
|
852
|
+
spaceId: APP_SPACE_ID,
|
|
853
|
+
origin: migration.from === null ? null : { storageHash: migration.from },
|
|
854
|
+
destination: { storageHash: migration.to },
|
|
855
|
+
operations,
|
|
856
|
+
providedInvariants: migration.providedInvariants
|
|
857
|
+
};
|
|
858
|
+
const destinationContract = familyInstance.validateContract(migration.toContract);
|
|
859
|
+
const runnerResult = await runner.execute({
|
|
860
|
+
plan,
|
|
861
|
+
driver,
|
|
862
|
+
destinationContract,
|
|
863
|
+
policy,
|
|
864
|
+
executionChecks: {
|
|
865
|
+
prechecks: true,
|
|
866
|
+
postchecks: true,
|
|
867
|
+
idempotencyChecks: true
|
|
868
|
+
},
|
|
869
|
+
frameworkComponents
|
|
870
|
+
});
|
|
871
|
+
if (!runnerResult.ok) {
|
|
872
|
+
onProgress?.({
|
|
873
|
+
action: "migrationApply",
|
|
874
|
+
kind: "spanEnd",
|
|
875
|
+
spanId: migrationSpanId,
|
|
876
|
+
outcome: "error"
|
|
877
|
+
});
|
|
878
|
+
return notOk({
|
|
879
|
+
code: "RUNNER_FAILED",
|
|
880
|
+
summary: runnerResult.failure.summary,
|
|
881
|
+
why: runnerResult.failure.why,
|
|
882
|
+
meta: {
|
|
883
|
+
migration: migration.dirName,
|
|
884
|
+
from: migration.from,
|
|
885
|
+
to: migration.to,
|
|
886
|
+
...runnerResult.failure.meta ?? {}
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
onProgress?.({
|
|
891
|
+
action: "migrationApply",
|
|
892
|
+
kind: "spanEnd",
|
|
893
|
+
spanId: migrationSpanId,
|
|
894
|
+
outcome: "ok"
|
|
895
|
+
});
|
|
896
|
+
applied.push({
|
|
897
|
+
dirName: migration.dirName,
|
|
898
|
+
from: migration.from,
|
|
899
|
+
to: migration.to,
|
|
900
|
+
operationsExecuted: runnerResult.value.operationsExecuted
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const finalHash = pendingMigrations[pendingMigrations.length - 1].to;
|
|
904
|
+
const totalOps = applied.reduce((sum, a) => sum + a.operationsExecuted, 0);
|
|
905
|
+
return ok({
|
|
906
|
+
migrationsApplied: applied.length,
|
|
907
|
+
markerHash: finalHash,
|
|
908
|
+
applied,
|
|
909
|
+
summary: `Applied ${applied.length} migration(s) (${totalOps} operation(s)), marker at ${finalHash}`
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/control-api/client.ts
|
|
914
|
+
/**
|
|
915
|
+
* Creates a programmatic control client for Prisma Next operations.
|
|
916
|
+
*
|
|
917
|
+
* The client accepts framework component descriptors at creation time,
|
|
918
|
+
* manages driver lifecycle via connect()/close(), and exposes domain
|
|
919
|
+
* operations that delegate to the existing family instance methods.
|
|
920
|
+
*
|
|
921
|
+
* @see {@link ControlClient} for the client interface
|
|
922
|
+
* @see README.md "Programmatic Control API" section for usage examples
|
|
923
|
+
*/
|
|
924
|
+
function createControlClient(options) {
|
|
925
|
+
return new ControlClientImpl(options);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Implementation of ControlClient.
|
|
929
|
+
* Manages initialization and connection state, delegates operations to family instance.
|
|
930
|
+
*/
|
|
931
|
+
var ControlClientImpl = class {
|
|
932
|
+
options;
|
|
933
|
+
stack = null;
|
|
934
|
+
driver = null;
|
|
935
|
+
familyInstance = null;
|
|
936
|
+
frameworkComponents = null;
|
|
937
|
+
initialized = false;
|
|
938
|
+
defaultConnection;
|
|
939
|
+
constructor(options) {
|
|
940
|
+
this.options = options;
|
|
941
|
+
this.defaultConnection = options.connection;
|
|
942
|
+
}
|
|
943
|
+
init() {
|
|
944
|
+
if (this.initialized) return;
|
|
945
|
+
this.stack = createControlStack({
|
|
946
|
+
family: this.options.family,
|
|
947
|
+
target: this.options.target,
|
|
948
|
+
adapter: this.options.adapter,
|
|
949
|
+
driver: this.options.driver,
|
|
950
|
+
extensionPacks: this.options.extensionPacks
|
|
951
|
+
});
|
|
952
|
+
this.familyInstance = this.options.family.create(this.stack);
|
|
953
|
+
const rawComponents = [
|
|
954
|
+
this.options.target,
|
|
955
|
+
this.options.adapter,
|
|
956
|
+
...this.options.extensionPacks ?? []
|
|
957
|
+
];
|
|
958
|
+
this.frameworkComponents = assertFrameworkComponentsCompatible(this.options.family.familyId, this.options.target.targetId, rawComponents);
|
|
959
|
+
this.initialized = true;
|
|
960
|
+
}
|
|
961
|
+
async connect(connection) {
|
|
962
|
+
this.init();
|
|
963
|
+
if (this.driver) throw new Error("Already connected. Call close() before reconnecting.");
|
|
964
|
+
const resolvedConnection = connection ?? this.defaultConnection;
|
|
965
|
+
if (resolvedConnection === void 0) throw new Error("No connection provided. Pass a connection to connect() or provide a default connection when creating the client.");
|
|
966
|
+
if (!this.stack?.driver) throw new Error("Driver is not configured. Pass a driver descriptor when creating the control client to enable database operations.");
|
|
967
|
+
this.driver = await this.stack.driver.create(resolvedConnection);
|
|
968
|
+
}
|
|
969
|
+
async close() {
|
|
970
|
+
if (this.driver) {
|
|
971
|
+
await this.driver.close();
|
|
972
|
+
this.driver = null;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async ensureConnected() {
|
|
976
|
+
this.init();
|
|
977
|
+
if (!this.driver && this.defaultConnection !== void 0) await this.connect(this.defaultConnection);
|
|
978
|
+
if (!this.driver || !this.familyInstance || !this.frameworkComponents) throw new Error("Not connected. Call connect(connection) first.");
|
|
979
|
+
return {
|
|
980
|
+
driver: this.driver,
|
|
981
|
+
familyInstance: this.familyInstance,
|
|
982
|
+
frameworkComponents: this.frameworkComponents
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
async connectWithProgress(connection, action, onProgress) {
|
|
986
|
+
if (connection === void 0) return;
|
|
987
|
+
onProgress?.({
|
|
988
|
+
action,
|
|
989
|
+
kind: "spanStart",
|
|
990
|
+
spanId: "connect",
|
|
991
|
+
label: "Connecting to database..."
|
|
992
|
+
});
|
|
993
|
+
try {
|
|
994
|
+
await this.connect(connection);
|
|
995
|
+
onProgress?.({
|
|
996
|
+
action,
|
|
997
|
+
kind: "spanEnd",
|
|
998
|
+
spanId: "connect",
|
|
999
|
+
outcome: "ok"
|
|
1000
|
+
});
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
onProgress?.({
|
|
1003
|
+
action,
|
|
1004
|
+
kind: "spanEnd",
|
|
1005
|
+
spanId: "connect",
|
|
1006
|
+
outcome: "error"
|
|
1007
|
+
});
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async verify(options) {
|
|
1012
|
+
const { onProgress } = options;
|
|
1013
|
+
await this.connectWithProgress(options.connection, "verify", onProgress);
|
|
1014
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
1015
|
+
let contract;
|
|
1016
|
+
try {
|
|
1017
|
+
contract = familyInstance.validateContract(options.contract);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
throw new ContractValidationError(error instanceof Error ? error.message : String(error), error);
|
|
1020
|
+
}
|
|
1021
|
+
onProgress?.({
|
|
1022
|
+
action: "verify",
|
|
1023
|
+
kind: "spanStart",
|
|
1024
|
+
spanId: "verify",
|
|
1025
|
+
label: "Verifying database marker..."
|
|
1026
|
+
});
|
|
1027
|
+
try {
|
|
1028
|
+
const result = await familyInstance.verify({
|
|
1029
|
+
driver,
|
|
1030
|
+
contract,
|
|
1031
|
+
expectedTargetId: this.options.target.targetId,
|
|
1032
|
+
contractPath: ""
|
|
1033
|
+
});
|
|
1034
|
+
onProgress?.({
|
|
1035
|
+
action: "verify",
|
|
1036
|
+
kind: "spanEnd",
|
|
1037
|
+
spanId: "verify",
|
|
1038
|
+
outcome: result.ok ? "ok" : "error"
|
|
1039
|
+
});
|
|
1040
|
+
return result;
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
onProgress?.({
|
|
1043
|
+
action: "verify",
|
|
1044
|
+
kind: "spanEnd",
|
|
1045
|
+
spanId: "verify",
|
|
1046
|
+
outcome: "error"
|
|
1047
|
+
});
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async schemaVerify(options) {
|
|
1052
|
+
const { onProgress } = options;
|
|
1053
|
+
await this.connectWithProgress(options.connection, "schemaVerify", onProgress);
|
|
1054
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
1055
|
+
let contract;
|
|
1056
|
+
try {
|
|
1057
|
+
contract = familyInstance.validateContract(options.contract);
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
throw new ContractValidationError(error instanceof Error ? error.message : String(error), error);
|
|
1060
|
+
}
|
|
1061
|
+
onProgress?.({
|
|
1062
|
+
action: "schemaVerify",
|
|
1063
|
+
kind: "spanStart",
|
|
1064
|
+
spanId: "schemaVerify",
|
|
1065
|
+
label: "Verifying database schema..."
|
|
1066
|
+
});
|
|
1067
|
+
try {
|
|
1068
|
+
const result = await familyInstance.schemaVerify({
|
|
1069
|
+
driver,
|
|
1070
|
+
contract,
|
|
1071
|
+
strict: options.strict ?? false,
|
|
1072
|
+
contractPath: "",
|
|
1073
|
+
frameworkComponents
|
|
1074
|
+
});
|
|
1075
|
+
onProgress?.({
|
|
1076
|
+
action: "schemaVerify",
|
|
1077
|
+
kind: "spanEnd",
|
|
1078
|
+
spanId: "schemaVerify",
|
|
1079
|
+
outcome: result.ok ? "ok" : "error"
|
|
1080
|
+
});
|
|
1081
|
+
return result;
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
onProgress?.({
|
|
1084
|
+
action: "schemaVerify",
|
|
1085
|
+
kind: "spanEnd",
|
|
1086
|
+
spanId: "schemaVerify",
|
|
1087
|
+
outcome: "error"
|
|
1088
|
+
});
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async sign(options) {
|
|
1093
|
+
const { onProgress } = options;
|
|
1094
|
+
await this.connectWithProgress(options.connection, "sign", onProgress);
|
|
1095
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
1096
|
+
let contract;
|
|
1097
|
+
try {
|
|
1098
|
+
contract = familyInstance.validateContract(options.contract);
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
throw new ContractValidationError(error instanceof Error ? error.message : String(error), error);
|
|
1101
|
+
}
|
|
1102
|
+
onProgress?.({
|
|
1103
|
+
action: "sign",
|
|
1104
|
+
kind: "spanStart",
|
|
1105
|
+
spanId: "sign",
|
|
1106
|
+
label: "Signing database..."
|
|
1107
|
+
});
|
|
1108
|
+
try {
|
|
1109
|
+
const result = await familyInstance.sign({
|
|
1110
|
+
driver,
|
|
1111
|
+
contract,
|
|
1112
|
+
contractPath: options.contractPath ?? "",
|
|
1113
|
+
...ifDefined("configPath", options.configPath)
|
|
1114
|
+
});
|
|
1115
|
+
onProgress?.({
|
|
1116
|
+
action: "sign",
|
|
1117
|
+
kind: "spanEnd",
|
|
1118
|
+
spanId: "sign",
|
|
1119
|
+
outcome: "ok"
|
|
1120
|
+
});
|
|
1121
|
+
return result;
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
onProgress?.({
|
|
1124
|
+
action: "sign",
|
|
1125
|
+
kind: "spanEnd",
|
|
1126
|
+
spanId: "sign",
|
|
1127
|
+
outcome: "error"
|
|
1128
|
+
});
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async dbInit(options) {
|
|
1133
|
+
const { onProgress } = options;
|
|
1134
|
+
await this.connectWithProgress(options.connection, "dbInit", onProgress);
|
|
1135
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
1136
|
+
if (!hasMigrations(this.options.target)) throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
|
|
1137
|
+
let contract;
|
|
1138
|
+
try {
|
|
1139
|
+
contract = familyInstance.validateContract(options.contract);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
throw new ContractValidationError(error instanceof Error ? error.message : String(error), error);
|
|
1142
|
+
}
|
|
1143
|
+
return executeDbInit({
|
|
1144
|
+
driver,
|
|
1145
|
+
familyInstance,
|
|
1146
|
+
contract,
|
|
1147
|
+
mode: options.mode,
|
|
1148
|
+
migrations: this.options.target.migrations,
|
|
1149
|
+
frameworkComponents,
|
|
1150
|
+
migrationsDir: options.migrationsDir,
|
|
1151
|
+
targetId: this.options.target.targetId,
|
|
1152
|
+
extensionPacks: this.options.extensionPacks ?? [],
|
|
1153
|
+
...ifDefined("onProgress", onProgress)
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
async dbUpdate(options) {
|
|
1157
|
+
const { onProgress } = options;
|
|
1158
|
+
await this.connectWithProgress(options.connection, "dbUpdate", onProgress);
|
|
1159
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
1160
|
+
if (!hasMigrations(this.options.target)) throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
|
|
1161
|
+
let contract;
|
|
1162
|
+
try {
|
|
1163
|
+
contract = familyInstance.validateContract(options.contract);
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
throw new ContractValidationError(error instanceof Error ? error.message : String(error), error);
|
|
1166
|
+
}
|
|
1167
|
+
return executeDbUpdate({
|
|
1168
|
+
driver,
|
|
1169
|
+
familyInstance,
|
|
1170
|
+
contract,
|
|
1171
|
+
mode: options.mode,
|
|
1172
|
+
migrations: this.options.target.migrations,
|
|
1173
|
+
frameworkComponents,
|
|
1174
|
+
migrationsDir: options.migrationsDir,
|
|
1175
|
+
targetId: this.options.target.targetId,
|
|
1176
|
+
extensionPacks: this.options.extensionPacks ?? [],
|
|
1177
|
+
...ifDefined("acceptDataLoss", options.acceptDataLoss),
|
|
1178
|
+
...ifDefined("onProgress", onProgress)
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
async dbVerify(options) {
|
|
1182
|
+
const { onProgress } = options;
|
|
1183
|
+
await this.connectWithProgress(options.connection, "dbVerify", onProgress);
|
|
1184
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
1185
|
+
let contract;
|
|
1186
|
+
try {
|
|
1187
|
+
contract = familyInstance.validateContract(options.contract);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
throw new ContractValidationError(error instanceof Error ? error.message : String(error), error);
|
|
1190
|
+
}
|
|
1191
|
+
return executeDbVerify({
|
|
1192
|
+
driver,
|
|
1193
|
+
familyInstance,
|
|
1194
|
+
contract,
|
|
1195
|
+
migrationsDir: options.migrationsDir,
|
|
1196
|
+
targetId: this.options.target.targetId,
|
|
1197
|
+
extensionPacks: this.options.extensionPacks ?? [],
|
|
1198
|
+
frameworkComponents,
|
|
1199
|
+
mode: options.strict ? "strict" : "lenient",
|
|
1200
|
+
skipSchema: options.skipSchema,
|
|
1201
|
+
skipMarker: options.skipMarker,
|
|
1202
|
+
...ifDefined("onProgress", onProgress)
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
async readMarker() {
|
|
1206
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
1207
|
+
return familyInstance.readMarker({
|
|
1208
|
+
driver,
|
|
1209
|
+
space: APP_SPACE_ID
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
async readAllMarkers() {
|
|
1213
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
1214
|
+
return familyInstance.readAllMarkers({ driver });
|
|
1215
|
+
}
|
|
1216
|
+
async migrationApply(options) {
|
|
1217
|
+
const { onProgress } = options;
|
|
1218
|
+
await this.connectWithProgress(options.connection, "migrationApply", onProgress);
|
|
1219
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
1220
|
+
if (!hasMigrations(this.options.target)) throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
|
|
1221
|
+
return executeMigrationApply({
|
|
1222
|
+
driver,
|
|
1223
|
+
familyInstance,
|
|
1224
|
+
originHash: options.originHash,
|
|
1225
|
+
destinationHash: options.destinationHash,
|
|
1226
|
+
pendingMigrations: options.pendingMigrations,
|
|
1227
|
+
migrations: this.options.target.migrations,
|
|
1228
|
+
frameworkComponents,
|
|
1229
|
+
targetId: this.options.target.targetId,
|
|
1230
|
+
...onProgress ? { onProgress } : {}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
async introspect(options) {
|
|
1234
|
+
const onProgress = options?.onProgress;
|
|
1235
|
+
await this.connectWithProgress(options?.connection, "introspect", onProgress);
|
|
1236
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
1237
|
+
options?.schema;
|
|
1238
|
+
onProgress?.({
|
|
1239
|
+
action: "introspect",
|
|
1240
|
+
kind: "spanStart",
|
|
1241
|
+
spanId: "introspect",
|
|
1242
|
+
label: "Introspecting database schema..."
|
|
1243
|
+
});
|
|
1244
|
+
try {
|
|
1245
|
+
const result = await familyInstance.introspect({ driver });
|
|
1246
|
+
onProgress?.({
|
|
1247
|
+
action: "introspect",
|
|
1248
|
+
kind: "spanEnd",
|
|
1249
|
+
spanId: "introspect",
|
|
1250
|
+
outcome: "ok"
|
|
1251
|
+
});
|
|
1252
|
+
return result;
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
onProgress?.({
|
|
1255
|
+
action: "introspect",
|
|
1256
|
+
kind: "spanEnd",
|
|
1257
|
+
spanId: "introspect",
|
|
1258
|
+
outcome: "error"
|
|
1259
|
+
});
|
|
1260
|
+
throw error;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
toSchemaView(schemaIR) {
|
|
1264
|
+
this.init();
|
|
1265
|
+
if (this.familyInstance && hasSchemaView(this.familyInstance)) return this.familyInstance.toSchemaView(schemaIR);
|
|
1266
|
+
}
|
|
1267
|
+
inferPslContract(schemaIR) {
|
|
1268
|
+
this.init();
|
|
1269
|
+
if (this.familyInstance && hasPslContractInfer(this.familyInstance)) return this.familyInstance.inferPslContract(schemaIR);
|
|
1270
|
+
}
|
|
1271
|
+
toOperationPreview(operations) {
|
|
1272
|
+
this.init();
|
|
1273
|
+
if (this.familyInstance && hasOperationPreview(this.familyInstance)) return this.familyInstance.toOperationPreview(operations);
|
|
1274
|
+
}
|
|
1275
|
+
async emit(options) {
|
|
1276
|
+
const { onProgress, contractConfig } = options;
|
|
1277
|
+
this.init();
|
|
1278
|
+
if (!this.familyInstance) throw new Error("Family instance was not initialized. This is a bug.");
|
|
1279
|
+
let contractRaw;
|
|
1280
|
+
onProgress?.({
|
|
1281
|
+
action: "emit",
|
|
1282
|
+
kind: "spanStart",
|
|
1283
|
+
spanId: "resolveSource",
|
|
1284
|
+
label: "Resolving contract source..."
|
|
1285
|
+
});
|
|
1286
|
+
try {
|
|
1287
|
+
const stack = this.stack;
|
|
1288
|
+
const sourceContext = {
|
|
1289
|
+
composedExtensionPacks: stack.extensionPacks.map((p) => p.id),
|
|
1290
|
+
scalarTypeDescriptors: stack.scalarTypeDescriptors,
|
|
1291
|
+
authoringContributions: stack.authoringContributions,
|
|
1292
|
+
codecLookup: stack.codecLookup,
|
|
1293
|
+
controlMutationDefaults: stack.controlMutationDefaults,
|
|
1294
|
+
resolvedInputs: contractConfig.source.inputs ?? []
|
|
1295
|
+
};
|
|
1296
|
+
const providerResult = await contractConfig.source.load(sourceContext);
|
|
1297
|
+
if (!providerResult.ok) {
|
|
1298
|
+
onProgress?.({
|
|
1299
|
+
action: "emit",
|
|
1300
|
+
kind: "spanEnd",
|
|
1301
|
+
spanId: "resolveSource",
|
|
1302
|
+
outcome: "error"
|
|
1303
|
+
});
|
|
1304
|
+
return notOk({
|
|
1305
|
+
code: "CONTRACT_SOURCE_INVALID",
|
|
1306
|
+
summary: providerResult.failure.summary,
|
|
1307
|
+
why: providerResult.failure.summary,
|
|
1308
|
+
meta: providerResult.failure.meta,
|
|
1309
|
+
diagnostics: providerResult.failure
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
contractRaw = providerResult.value;
|
|
1313
|
+
onProgress?.({
|
|
1314
|
+
action: "emit",
|
|
1315
|
+
kind: "spanEnd",
|
|
1316
|
+
spanId: "resolveSource",
|
|
1317
|
+
outcome: "ok"
|
|
1318
|
+
});
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
onProgress?.({
|
|
1321
|
+
action: "emit",
|
|
1322
|
+
kind: "spanEnd",
|
|
1323
|
+
spanId: "resolveSource",
|
|
1324
|
+
outcome: "error"
|
|
1325
|
+
});
|
|
1326
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1327
|
+
return notOk({
|
|
1328
|
+
code: "CONTRACT_SOURCE_INVALID",
|
|
1329
|
+
summary: "Failed to resolve contract source",
|
|
1330
|
+
why: message,
|
|
1331
|
+
diagnostics: {
|
|
1332
|
+
summary: "Contract source provider threw an exception",
|
|
1333
|
+
diagnostics: [{
|
|
1334
|
+
code: "PROVIDER_THROW",
|
|
1335
|
+
message
|
|
1336
|
+
}]
|
|
1337
|
+
},
|
|
1338
|
+
meta: void 0
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
onProgress?.({
|
|
1342
|
+
action: "emit",
|
|
1343
|
+
kind: "spanStart",
|
|
1344
|
+
spanId: "emit",
|
|
1345
|
+
label: "Emitting contract..."
|
|
1346
|
+
});
|
|
1347
|
+
try {
|
|
1348
|
+
const enrichedIR = enrichContract(contractRaw, this.frameworkComponents ?? []);
|
|
1349
|
+
try {
|
|
1350
|
+
this.familyInstance.validateContract(enrichedIR);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
onProgress?.({
|
|
1353
|
+
action: "emit",
|
|
1354
|
+
kind: "spanEnd",
|
|
1355
|
+
spanId: "emit",
|
|
1356
|
+
outcome: "error"
|
|
1357
|
+
});
|
|
1358
|
+
return notOk({
|
|
1359
|
+
code: "CONTRACT_VALIDATION_FAILED",
|
|
1360
|
+
summary: "Contract validation failed",
|
|
1361
|
+
why: error instanceof Error ? error.message : String(error),
|
|
1362
|
+
meta: void 0
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
const result = await emit(enrichedIR, this.stack, this.options.family.emission);
|
|
1366
|
+
onProgress?.({
|
|
1367
|
+
action: "emit",
|
|
1368
|
+
kind: "spanEnd",
|
|
1369
|
+
spanId: "emit",
|
|
1370
|
+
outcome: "ok"
|
|
1371
|
+
});
|
|
1372
|
+
return ok({
|
|
1373
|
+
storageHash: result.storageHash,
|
|
1374
|
+
...ifDefined("executionHash", result.executionHash),
|
|
1375
|
+
profileHash: result.profileHash,
|
|
1376
|
+
contractJson: result.contractJson,
|
|
1377
|
+
contractDts: result.contractDts
|
|
1378
|
+
});
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
onProgress?.({
|
|
1381
|
+
action: "emit",
|
|
1382
|
+
kind: "spanEnd",
|
|
1383
|
+
spanId: "emit",
|
|
1384
|
+
outcome: "error"
|
|
1385
|
+
});
|
|
1386
|
+
return notOk({
|
|
1387
|
+
code: "EMIT_FAILED",
|
|
1388
|
+
summary: "Failed to emit contract",
|
|
1389
|
+
why: error instanceof Error ? error.message : String(error),
|
|
1390
|
+
meta: void 0
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
//#endregion
|
|
1396
|
+
export { ContractValidationError as a, executeDbInit as i, executeDbVerify as n, executeDbUpdate as r, createControlClient as t };
|
|
1397
|
+
|
|
1398
|
+
//# sourceMappingURL=client-0ZX24FXF.mjs.map
|