@prisma-next/cli 0.3.0-dev.9 → 0.3.0-pr.100.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -27
- package/dist/{chunk-BZMBKEEQ.js → chunk-AGOTG4L3.js} +44 -76
- package/dist/chunk-AGOTG4L3.js.map +1 -0
- package/dist/chunk-HLLI4YL7.js +180 -0
- package/dist/chunk-HLLI4YL7.js.map +1 -0
- package/dist/chunk-VG2R7DGF.js +735 -0
- package/dist/chunk-VG2R7DGF.js.map +1 -0
- package/dist/cli.js +1502 -942
- package/dist/cli.js.map +1 -1
- package/dist/commands/contract-emit.d.ts.map +1 -1
- package/dist/commands/contract-emit.js +3 -2
- package/dist/commands/db-init.d.ts.map +1 -1
- package/dist/commands/db-init.js +205 -285
- package/dist/commands/db-init.js.map +1 -1
- package/dist/commands/db-introspect.d.ts.map +1 -1
- package/dist/commands/db-introspect.js +108 -139
- package/dist/commands/db-introspect.js.map +1 -1
- package/dist/commands/db-schema-verify.d.ts.map +1 -1
- package/dist/commands/db-schema-verify.js +120 -109
- package/dist/commands/db-schema-verify.js.map +1 -1
- package/dist/commands/db-sign.d.ts.map +1 -1
- package/dist/commands/db-sign.js +152 -152
- package/dist/commands/db-sign.js.map +1 -1
- package/dist/commands/db-verify.d.ts.map +1 -1
- package/dist/commands/db-verify.js +142 -118
- package/dist/commands/db-verify.js.map +1 -1
- package/dist/control-api/client.d.ts +13 -0
- package/dist/control-api/client.d.ts.map +1 -0
- package/dist/control-api/operations/db-init.d.ts +29 -0
- package/dist/control-api/operations/db-init.d.ts.map +1 -0
- package/dist/control-api/types.d.ts +387 -0
- package/dist/control-api/types.d.ts.map +1 -0
- package/dist/exports/control-api.d.ts +13 -0
- package/dist/exports/control-api.d.ts.map +1 -0
- package/dist/exports/control-api.js +7 -0
- package/dist/exports/control-api.js.map +1 -0
- package/dist/exports/index.js +3 -2
- package/dist/exports/index.js.map +1 -1
- package/dist/utils/cli-errors.d.ts +1 -1
- package/dist/utils/cli-errors.d.ts.map +1 -1
- package/dist/utils/progress-adapter.d.ts +26 -0
- package/dist/utils/progress-adapter.d.ts.map +1 -0
- package/package.json +20 -16
- package/src/commands/contract-emit.ts +179 -102
- package/src/commands/db-init.ts +263 -355
- package/src/commands/db-introspect.ts +151 -180
- package/src/commands/db-schema-verify.ts +151 -145
- package/src/commands/db-sign.ts +202 -196
- package/src/commands/db-verify.ts +180 -151
- package/src/control-api/client.ts +589 -0
- package/src/control-api/operations/db-init.ts +281 -0
- package/src/control-api/types.ts +461 -0
- package/src/exports/control-api.ts +46 -0
- package/src/utils/cli-errors.ts +1 -1
- package/src/utils/progress-adapter.ts +86 -0
- package/dist/chunk-BZMBKEEQ.js.map +0 -1
- package/dist/chunk-CVNWLFXO.js +0 -91
- package/dist/chunk-CVNWLFXO.js.map +0 -1
- package/dist/chunk-QUPBU4KV.js +0 -131
- package/dist/chunk-QUPBU4KV.js.map +0 -1
- package/dist/utils/action.d.ts +0 -16
- package/dist/utils/action.d.ts.map +0 -1
- package/dist/utils/spinner.d.ts +0 -29
- package/dist/utils/spinner.d.ts.map +0 -1
- package/src/utils/action.ts +0 -43
- package/src/utils/spinner.ts +0 -67
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Command as Command7 } from "commander";
|
|
|
7
7
|
import { mkdirSync, writeFileSync } from "fs";
|
|
8
8
|
import { dirname as dirname2, relative as relative2, resolve as resolve2 } from "path";
|
|
9
9
|
import { errorContractConfigMissing as errorContractConfigMissing2 } from "@prisma-next/core-control-plane/errors";
|
|
10
|
-
import {
|
|
10
|
+
import { notOk as notOk3, ok as ok3 } from "@prisma-next/utils/result";
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
|
|
13
13
|
// src/config-loader.ts
|
|
@@ -53,8 +53,14 @@ async function loadConfig(configPath) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// src/
|
|
57
|
-
import {
|
|
56
|
+
// src/control-api/client.ts
|
|
57
|
+
import { createControlPlaneStack } from "@prisma-next/core-control-plane/stack";
|
|
58
|
+
import { notOk as notOk2, ok as ok2 } from "@prisma-next/utils/result";
|
|
59
|
+
|
|
60
|
+
// src/utils/framework-components.ts
|
|
61
|
+
import {
|
|
62
|
+
checkContractComponentRequirements
|
|
63
|
+
} from "@prisma-next/contract/framework-components";
|
|
58
64
|
|
|
59
65
|
// src/utils/cli-errors.ts
|
|
60
66
|
import {
|
|
@@ -64,7 +70,7 @@ import {
|
|
|
64
70
|
errorContractConfigMissing,
|
|
65
71
|
errorContractMissingExtensionPacks,
|
|
66
72
|
errorContractValidationFailed,
|
|
67
|
-
|
|
73
|
+
errorDatabaseConnectionRequired,
|
|
68
74
|
errorDriverRequired,
|
|
69
75
|
errorFamilyReadMarkerSqlRequired,
|
|
70
76
|
errorFileNotFound,
|
|
@@ -79,18 +85,693 @@ import {
|
|
|
79
85
|
errorUnexpected as errorUnexpected2
|
|
80
86
|
} from "@prisma-next/core-control-plane/errors";
|
|
81
87
|
|
|
82
|
-
// src/utils/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// src/utils/framework-components.ts
|
|
89
|
+
function assertFrameworkComponentsCompatible(expectedFamilyId, expectedTargetId, frameworkComponents) {
|
|
90
|
+
for (let i = 0; i < frameworkComponents.length; i++) {
|
|
91
|
+
const component = frameworkComponents[i];
|
|
92
|
+
if (typeof component !== "object" || component === null) {
|
|
93
|
+
throw errorConfigValidation("frameworkComponents[]", {
|
|
94
|
+
why: `Framework component at index ${i} must be an object`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const record = component;
|
|
98
|
+
if (!Object.hasOwn(record, "kind")) {
|
|
99
|
+
throw errorConfigValidation("frameworkComponents[].kind", {
|
|
100
|
+
why: `Framework component at index ${i} must have 'kind' property`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const kind = record["kind"];
|
|
104
|
+
if (kind !== "target" && kind !== "adapter" && kind !== "extension" && kind !== "driver") {
|
|
105
|
+
throw errorConfigValidation("frameworkComponents[].kind", {
|
|
106
|
+
why: `Framework component at index ${i} has invalid kind '${String(kind)}' (must be 'target', 'adapter', 'extension', or 'driver')`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (!Object.hasOwn(record, "familyId")) {
|
|
110
|
+
throw errorConfigValidation("frameworkComponents[].familyId", {
|
|
111
|
+
why: `Framework component at index ${i} (kind: ${String(kind)}) must have 'familyId' property`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const familyId = record["familyId"];
|
|
115
|
+
if (familyId !== expectedFamilyId) {
|
|
116
|
+
throw errorConfigValidation("frameworkComponents[].familyId", {
|
|
117
|
+
why: `Framework component at index ${i} (kind: ${String(kind)}) has familyId '${String(familyId)}' but expected '${expectedFamilyId}'`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (!Object.hasOwn(record, "targetId")) {
|
|
121
|
+
throw errorConfigValidation("frameworkComponents[].targetId", {
|
|
122
|
+
why: `Framework component at index ${i} (kind: ${String(kind)}) must have 'targetId' property`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const targetId = record["targetId"];
|
|
126
|
+
if (targetId !== expectedTargetId) {
|
|
127
|
+
throw errorConfigValidation("frameworkComponents[].targetId", {
|
|
128
|
+
why: `Framework component at index ${i} (kind: ${String(kind)}) has targetId '${String(targetId)}' but expected '${expectedTargetId}'`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return frameworkComponents;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/control-api/operations/db-init.ts
|
|
136
|
+
import { notOk, ok } from "@prisma-next/utils/result";
|
|
137
|
+
async function executeDbInit(options) {
|
|
138
|
+
const { driver, familyInstance, contractIR, mode, migrations, frameworkComponents, onProgress } = options;
|
|
139
|
+
const planner = migrations.createPlanner(familyInstance);
|
|
140
|
+
const runner = migrations.createRunner(familyInstance);
|
|
141
|
+
const introspectSpanId = "introspect";
|
|
142
|
+
onProgress?.({
|
|
143
|
+
action: "dbInit",
|
|
144
|
+
kind: "spanStart",
|
|
145
|
+
spanId: introspectSpanId,
|
|
146
|
+
label: "Introspecting database schema"
|
|
147
|
+
});
|
|
148
|
+
const schemaIR = await familyInstance.introspect({ driver });
|
|
149
|
+
onProgress?.({
|
|
150
|
+
action: "dbInit",
|
|
151
|
+
kind: "spanEnd",
|
|
152
|
+
spanId: introspectSpanId,
|
|
153
|
+
outcome: "ok"
|
|
154
|
+
});
|
|
155
|
+
const policy = { allowedOperationClasses: ["additive"] };
|
|
156
|
+
const planSpanId = "plan";
|
|
157
|
+
onProgress?.({
|
|
158
|
+
action: "dbInit",
|
|
159
|
+
kind: "spanStart",
|
|
160
|
+
spanId: planSpanId,
|
|
161
|
+
label: "Planning migration"
|
|
162
|
+
});
|
|
163
|
+
const plannerResult = await planner.plan({
|
|
164
|
+
contract: contractIR,
|
|
165
|
+
schema: schemaIR,
|
|
166
|
+
policy,
|
|
167
|
+
frameworkComponents
|
|
168
|
+
});
|
|
169
|
+
if (plannerResult.kind === "failure") {
|
|
170
|
+
onProgress?.({
|
|
171
|
+
action: "dbInit",
|
|
172
|
+
kind: "spanEnd",
|
|
173
|
+
spanId: planSpanId,
|
|
174
|
+
outcome: "error"
|
|
175
|
+
});
|
|
176
|
+
return notOk({
|
|
177
|
+
code: "PLANNING_FAILED",
|
|
178
|
+
summary: "Migration planning failed due to conflicts",
|
|
179
|
+
conflicts: plannerResult.conflicts,
|
|
180
|
+
why: void 0,
|
|
181
|
+
meta: void 0
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const migrationPlan = plannerResult.plan;
|
|
185
|
+
onProgress?.({
|
|
186
|
+
action: "dbInit",
|
|
187
|
+
kind: "spanEnd",
|
|
188
|
+
spanId: planSpanId,
|
|
189
|
+
outcome: "ok"
|
|
190
|
+
});
|
|
191
|
+
const checkMarkerSpanId = "checkMarker";
|
|
192
|
+
onProgress?.({
|
|
193
|
+
action: "dbInit",
|
|
194
|
+
kind: "spanStart",
|
|
195
|
+
spanId: checkMarkerSpanId,
|
|
196
|
+
label: "Checking contract marker"
|
|
197
|
+
});
|
|
198
|
+
const existingMarker = await familyInstance.readMarker({ driver });
|
|
199
|
+
if (existingMarker) {
|
|
200
|
+
const markerMatchesDestination = existingMarker.coreHash === migrationPlan.destination.coreHash && (!migrationPlan.destination.profileHash || existingMarker.profileHash === migrationPlan.destination.profileHash);
|
|
201
|
+
if (markerMatchesDestination) {
|
|
202
|
+
onProgress?.({
|
|
203
|
+
action: "dbInit",
|
|
204
|
+
kind: "spanEnd",
|
|
205
|
+
spanId: checkMarkerSpanId,
|
|
206
|
+
outcome: "skipped"
|
|
207
|
+
});
|
|
208
|
+
const result2 = {
|
|
209
|
+
mode,
|
|
210
|
+
plan: { operations: [] },
|
|
211
|
+
...mode === "apply" ? {
|
|
212
|
+
execution: { operationsPlanned: 0, operationsExecuted: 0 },
|
|
213
|
+
marker: {
|
|
214
|
+
coreHash: existingMarker.coreHash,
|
|
215
|
+
profileHash: existingMarker.profileHash
|
|
216
|
+
}
|
|
217
|
+
} : {},
|
|
218
|
+
summary: "Database already at target contract state"
|
|
219
|
+
};
|
|
220
|
+
return ok(result2);
|
|
221
|
+
}
|
|
222
|
+
onProgress?.({
|
|
223
|
+
action: "dbInit",
|
|
224
|
+
kind: "spanEnd",
|
|
225
|
+
spanId: checkMarkerSpanId,
|
|
226
|
+
outcome: "error"
|
|
227
|
+
});
|
|
228
|
+
return notOk({
|
|
229
|
+
code: "MARKER_ORIGIN_MISMATCH",
|
|
230
|
+
summary: "Existing contract marker does not match plan destination",
|
|
231
|
+
marker: {
|
|
232
|
+
coreHash: existingMarker.coreHash,
|
|
233
|
+
profileHash: existingMarker.profileHash
|
|
234
|
+
},
|
|
235
|
+
destination: {
|
|
236
|
+
coreHash: migrationPlan.destination.coreHash,
|
|
237
|
+
profileHash: migrationPlan.destination.profileHash
|
|
238
|
+
},
|
|
239
|
+
why: void 0,
|
|
240
|
+
conflicts: void 0,
|
|
241
|
+
meta: void 0
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
onProgress?.({
|
|
245
|
+
action: "dbInit",
|
|
246
|
+
kind: "spanEnd",
|
|
247
|
+
spanId: checkMarkerSpanId,
|
|
248
|
+
outcome: "ok"
|
|
249
|
+
});
|
|
250
|
+
if (mode === "plan") {
|
|
251
|
+
const result2 = {
|
|
252
|
+
mode: "plan",
|
|
253
|
+
plan: { operations: migrationPlan.operations },
|
|
254
|
+
summary: `Planned ${migrationPlan.operations.length} operation(s)`
|
|
255
|
+
};
|
|
256
|
+
return ok(result2);
|
|
257
|
+
}
|
|
258
|
+
const applySpanId = "apply";
|
|
259
|
+
onProgress?.({
|
|
260
|
+
action: "dbInit",
|
|
261
|
+
kind: "spanStart",
|
|
262
|
+
spanId: applySpanId,
|
|
263
|
+
label: "Applying migration plan"
|
|
264
|
+
});
|
|
265
|
+
const callbacks = onProgress ? {
|
|
266
|
+
onOperationStart: (op) => {
|
|
267
|
+
onProgress({
|
|
268
|
+
action: "dbInit",
|
|
269
|
+
kind: "spanStart",
|
|
270
|
+
spanId: `operation:${op.id}`,
|
|
271
|
+
parentSpanId: applySpanId,
|
|
272
|
+
label: op.label
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
onOperationComplete: (op) => {
|
|
276
|
+
onProgress({
|
|
277
|
+
action: "dbInit",
|
|
278
|
+
kind: "spanEnd",
|
|
279
|
+
spanId: `operation:${op.id}`,
|
|
280
|
+
outcome: "ok"
|
|
281
|
+
});
|
|
90
282
|
}
|
|
91
|
-
|
|
283
|
+
} : void 0;
|
|
284
|
+
const runnerResult = await runner.execute({
|
|
285
|
+
plan: migrationPlan,
|
|
286
|
+
driver,
|
|
287
|
+
destinationContract: contractIR,
|
|
288
|
+
policy,
|
|
289
|
+
...callbacks ? { callbacks } : {},
|
|
290
|
+
// db init plans and applies back-to-back from a fresh introspection, so per-operation
|
|
291
|
+
// pre/postchecks and the idempotency probe are usually redundant overhead. We still
|
|
292
|
+
// enforce marker/origin compatibility and a full schema verification after apply.
|
|
293
|
+
executionChecks: {
|
|
294
|
+
prechecks: false,
|
|
295
|
+
postchecks: false,
|
|
296
|
+
idempotencyChecks: false
|
|
297
|
+
},
|
|
298
|
+
frameworkComponents
|
|
299
|
+
});
|
|
300
|
+
if (!runnerResult.ok) {
|
|
301
|
+
onProgress?.({
|
|
302
|
+
action: "dbInit",
|
|
303
|
+
kind: "spanEnd",
|
|
304
|
+
spanId: applySpanId,
|
|
305
|
+
outcome: "error"
|
|
306
|
+
});
|
|
307
|
+
return notOk({
|
|
308
|
+
code: "RUNNER_FAILED",
|
|
309
|
+
summary: runnerResult.failure.summary,
|
|
310
|
+
why: runnerResult.failure.why,
|
|
311
|
+
meta: runnerResult.failure.meta,
|
|
312
|
+
conflicts: void 0
|
|
313
|
+
});
|
|
92
314
|
}
|
|
315
|
+
const execution = runnerResult.value;
|
|
316
|
+
onProgress?.({
|
|
317
|
+
action: "dbInit",
|
|
318
|
+
kind: "spanEnd",
|
|
319
|
+
spanId: applySpanId,
|
|
320
|
+
outcome: "ok"
|
|
321
|
+
});
|
|
322
|
+
const result = {
|
|
323
|
+
mode: "apply",
|
|
324
|
+
plan: { operations: migrationPlan.operations },
|
|
325
|
+
execution: {
|
|
326
|
+
operationsPlanned: execution.operationsPlanned,
|
|
327
|
+
operationsExecuted: execution.operationsExecuted
|
|
328
|
+
},
|
|
329
|
+
marker: migrationPlan.destination.profileHash ? {
|
|
330
|
+
coreHash: migrationPlan.destination.coreHash,
|
|
331
|
+
profileHash: migrationPlan.destination.profileHash
|
|
332
|
+
} : { coreHash: migrationPlan.destination.coreHash },
|
|
333
|
+
summary: `Applied ${execution.operationsExecuted} operation(s), marker written`
|
|
334
|
+
};
|
|
335
|
+
return ok(result);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/control-api/client.ts
|
|
339
|
+
function createControlClient(options) {
|
|
340
|
+
return new ControlClientImpl(options);
|
|
93
341
|
}
|
|
342
|
+
var ControlClientImpl = class {
|
|
343
|
+
options;
|
|
344
|
+
stack = null;
|
|
345
|
+
driver = null;
|
|
346
|
+
familyInstance = null;
|
|
347
|
+
frameworkComponents = null;
|
|
348
|
+
initialized = false;
|
|
349
|
+
defaultConnection;
|
|
350
|
+
constructor(options) {
|
|
351
|
+
this.options = options;
|
|
352
|
+
this.defaultConnection = options.connection;
|
|
353
|
+
}
|
|
354
|
+
init() {
|
|
355
|
+
if (this.initialized) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.stack = createControlPlaneStack({
|
|
359
|
+
target: this.options.target,
|
|
360
|
+
adapter: this.options.adapter,
|
|
361
|
+
driver: this.options.driver,
|
|
362
|
+
extensionPacks: this.options.extensionPacks
|
|
363
|
+
});
|
|
364
|
+
this.familyInstance = this.options.family.create(this.stack);
|
|
365
|
+
const rawComponents = [
|
|
366
|
+
this.options.target,
|
|
367
|
+
this.options.adapter,
|
|
368
|
+
...this.options.extensionPacks ?? []
|
|
369
|
+
];
|
|
370
|
+
this.frameworkComponents = assertFrameworkComponentsCompatible(
|
|
371
|
+
this.options.family.familyId,
|
|
372
|
+
this.options.target.targetId,
|
|
373
|
+
rawComponents
|
|
374
|
+
);
|
|
375
|
+
this.initialized = true;
|
|
376
|
+
}
|
|
377
|
+
async connect(connection) {
|
|
378
|
+
this.init();
|
|
379
|
+
if (this.driver) {
|
|
380
|
+
throw new Error("Already connected. Call close() before reconnecting.");
|
|
381
|
+
}
|
|
382
|
+
const resolvedConnection = connection ?? this.defaultConnection;
|
|
383
|
+
if (resolvedConnection === void 0) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
"No connection provided. Pass a connection to connect() or provide a default connection when creating the client."
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (!this.stack?.driver) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
"Driver is not configured. Pass a driver descriptor when creating the control client to enable database operations."
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
this.driver = await this.stack?.driver.create(resolvedConnection);
|
|
394
|
+
}
|
|
395
|
+
async close() {
|
|
396
|
+
if (this.driver) {
|
|
397
|
+
await this.driver.close();
|
|
398
|
+
this.driver = null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async ensureConnected() {
|
|
402
|
+
this.init();
|
|
403
|
+
if (!this.driver && this.defaultConnection !== void 0) {
|
|
404
|
+
await this.connect(this.defaultConnection);
|
|
405
|
+
}
|
|
406
|
+
if (!this.driver || !this.familyInstance || !this.frameworkComponents) {
|
|
407
|
+
throw new Error("Not connected. Call connect(connection) first.");
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
driver: this.driver,
|
|
411
|
+
familyInstance: this.familyInstance,
|
|
412
|
+
frameworkComponents: this.frameworkComponents
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async verify(options) {
|
|
416
|
+
const { onProgress } = options;
|
|
417
|
+
if (options.connection !== void 0) {
|
|
418
|
+
onProgress?.({
|
|
419
|
+
action: "verify",
|
|
420
|
+
kind: "spanStart",
|
|
421
|
+
spanId: "connect",
|
|
422
|
+
label: "Connecting to database..."
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
await this.connect(options.connection);
|
|
426
|
+
onProgress?.({
|
|
427
|
+
action: "verify",
|
|
428
|
+
kind: "spanEnd",
|
|
429
|
+
spanId: "connect",
|
|
430
|
+
outcome: "ok"
|
|
431
|
+
});
|
|
432
|
+
} catch (error) {
|
|
433
|
+
onProgress?.({
|
|
434
|
+
action: "verify",
|
|
435
|
+
kind: "spanEnd",
|
|
436
|
+
spanId: "connect",
|
|
437
|
+
outcome: "error"
|
|
438
|
+
});
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
443
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
444
|
+
onProgress?.({
|
|
445
|
+
action: "verify",
|
|
446
|
+
kind: "spanStart",
|
|
447
|
+
spanId: "verify",
|
|
448
|
+
label: "Verifying contract marker..."
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
const result = await familyInstance.verify({
|
|
452
|
+
driver,
|
|
453
|
+
contractIR,
|
|
454
|
+
expectedTargetId: this.options.target.targetId,
|
|
455
|
+
contractPath: ""
|
|
456
|
+
});
|
|
457
|
+
onProgress?.({
|
|
458
|
+
action: "verify",
|
|
459
|
+
kind: "spanEnd",
|
|
460
|
+
spanId: "verify",
|
|
461
|
+
outcome: result.ok ? "ok" : "error"
|
|
462
|
+
});
|
|
463
|
+
return result;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
onProgress?.({
|
|
466
|
+
action: "verify",
|
|
467
|
+
kind: "spanEnd",
|
|
468
|
+
spanId: "verify",
|
|
469
|
+
outcome: "error"
|
|
470
|
+
});
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async schemaVerify(options) {
|
|
475
|
+
const { onProgress } = options;
|
|
476
|
+
if (options.connection !== void 0) {
|
|
477
|
+
onProgress?.({
|
|
478
|
+
action: "schemaVerify",
|
|
479
|
+
kind: "spanStart",
|
|
480
|
+
spanId: "connect",
|
|
481
|
+
label: "Connecting to database..."
|
|
482
|
+
});
|
|
483
|
+
try {
|
|
484
|
+
await this.connect(options.connection);
|
|
485
|
+
onProgress?.({
|
|
486
|
+
action: "schemaVerify",
|
|
487
|
+
kind: "spanEnd",
|
|
488
|
+
spanId: "connect",
|
|
489
|
+
outcome: "ok"
|
|
490
|
+
});
|
|
491
|
+
} catch (error) {
|
|
492
|
+
onProgress?.({
|
|
493
|
+
action: "schemaVerify",
|
|
494
|
+
kind: "spanEnd",
|
|
495
|
+
spanId: "connect",
|
|
496
|
+
outcome: "error"
|
|
497
|
+
});
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
502
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
503
|
+
onProgress?.({
|
|
504
|
+
action: "schemaVerify",
|
|
505
|
+
kind: "spanStart",
|
|
506
|
+
spanId: "schemaVerify",
|
|
507
|
+
label: "Verifying database schema..."
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
const result = await familyInstance.schemaVerify({
|
|
511
|
+
driver,
|
|
512
|
+
contractIR,
|
|
513
|
+
strict: options.strict ?? false,
|
|
514
|
+
contractPath: "",
|
|
515
|
+
frameworkComponents
|
|
516
|
+
});
|
|
517
|
+
onProgress?.({
|
|
518
|
+
action: "schemaVerify",
|
|
519
|
+
kind: "spanEnd",
|
|
520
|
+
spanId: "schemaVerify",
|
|
521
|
+
outcome: result.ok ? "ok" : "error"
|
|
522
|
+
});
|
|
523
|
+
return result;
|
|
524
|
+
} catch (error) {
|
|
525
|
+
onProgress?.({
|
|
526
|
+
action: "schemaVerify",
|
|
527
|
+
kind: "spanEnd",
|
|
528
|
+
spanId: "schemaVerify",
|
|
529
|
+
outcome: "error"
|
|
530
|
+
});
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async sign(options) {
|
|
535
|
+
const { onProgress } = options;
|
|
536
|
+
if (options.connection !== void 0) {
|
|
537
|
+
onProgress?.({
|
|
538
|
+
action: "sign",
|
|
539
|
+
kind: "spanStart",
|
|
540
|
+
spanId: "connect",
|
|
541
|
+
label: "Connecting to database..."
|
|
542
|
+
});
|
|
543
|
+
try {
|
|
544
|
+
await this.connect(options.connection);
|
|
545
|
+
onProgress?.({
|
|
546
|
+
action: "sign",
|
|
547
|
+
kind: "spanEnd",
|
|
548
|
+
spanId: "connect",
|
|
549
|
+
outcome: "ok"
|
|
550
|
+
});
|
|
551
|
+
} catch (error) {
|
|
552
|
+
onProgress?.({
|
|
553
|
+
action: "sign",
|
|
554
|
+
kind: "spanEnd",
|
|
555
|
+
spanId: "connect",
|
|
556
|
+
outcome: "error"
|
|
557
|
+
});
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
562
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
563
|
+
onProgress?.({
|
|
564
|
+
action: "sign",
|
|
565
|
+
kind: "spanStart",
|
|
566
|
+
spanId: "sign",
|
|
567
|
+
label: "Signing database..."
|
|
568
|
+
});
|
|
569
|
+
try {
|
|
570
|
+
const result = await familyInstance.sign({
|
|
571
|
+
driver,
|
|
572
|
+
contractIR,
|
|
573
|
+
contractPath: options.contractPath ?? "",
|
|
574
|
+
...options.configPath ? { configPath: options.configPath } : {}
|
|
575
|
+
});
|
|
576
|
+
onProgress?.({
|
|
577
|
+
action: "sign",
|
|
578
|
+
kind: "spanEnd",
|
|
579
|
+
spanId: "sign",
|
|
580
|
+
outcome: "ok"
|
|
581
|
+
});
|
|
582
|
+
return result;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
onProgress?.({
|
|
585
|
+
action: "sign",
|
|
586
|
+
kind: "spanEnd",
|
|
587
|
+
spanId: "sign",
|
|
588
|
+
outcome: "error"
|
|
589
|
+
});
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async dbInit(options) {
|
|
594
|
+
const { onProgress } = options;
|
|
595
|
+
if (options.connection !== void 0) {
|
|
596
|
+
onProgress?.({
|
|
597
|
+
action: "dbInit",
|
|
598
|
+
kind: "spanStart",
|
|
599
|
+
spanId: "connect",
|
|
600
|
+
label: "Connecting to database..."
|
|
601
|
+
});
|
|
602
|
+
try {
|
|
603
|
+
await this.connect(options.connection);
|
|
604
|
+
onProgress?.({
|
|
605
|
+
action: "dbInit",
|
|
606
|
+
kind: "spanEnd",
|
|
607
|
+
spanId: "connect",
|
|
608
|
+
outcome: "ok"
|
|
609
|
+
});
|
|
610
|
+
} catch (error) {
|
|
611
|
+
onProgress?.({
|
|
612
|
+
action: "dbInit",
|
|
613
|
+
kind: "spanEnd",
|
|
614
|
+
spanId: "connect",
|
|
615
|
+
outcome: "error"
|
|
616
|
+
});
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
621
|
+
if (!this.options.target.migrations) {
|
|
622
|
+
throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
|
|
623
|
+
}
|
|
624
|
+
const contractIR = familyInstance.validateContractIR(options.contractIR);
|
|
625
|
+
return executeDbInit({
|
|
626
|
+
driver,
|
|
627
|
+
familyInstance,
|
|
628
|
+
contractIR,
|
|
629
|
+
mode: options.mode,
|
|
630
|
+
migrations: this.options.target.migrations,
|
|
631
|
+
frameworkComponents,
|
|
632
|
+
...onProgress ? { onProgress } : {}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
async introspect(options) {
|
|
636
|
+
const onProgress = options?.onProgress;
|
|
637
|
+
if (options?.connection !== void 0) {
|
|
638
|
+
onProgress?.({
|
|
639
|
+
action: "introspect",
|
|
640
|
+
kind: "spanStart",
|
|
641
|
+
spanId: "connect",
|
|
642
|
+
label: "Connecting to database..."
|
|
643
|
+
});
|
|
644
|
+
try {
|
|
645
|
+
await this.connect(options.connection);
|
|
646
|
+
onProgress?.({
|
|
647
|
+
action: "introspect",
|
|
648
|
+
kind: "spanEnd",
|
|
649
|
+
spanId: "connect",
|
|
650
|
+
outcome: "ok"
|
|
651
|
+
});
|
|
652
|
+
} catch (error) {
|
|
653
|
+
onProgress?.({
|
|
654
|
+
action: "introspect",
|
|
655
|
+
kind: "spanEnd",
|
|
656
|
+
spanId: "connect",
|
|
657
|
+
outcome: "error"
|
|
658
|
+
});
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
663
|
+
const _schema = options?.schema;
|
|
664
|
+
void _schema;
|
|
665
|
+
onProgress?.({
|
|
666
|
+
action: "introspect",
|
|
667
|
+
kind: "spanStart",
|
|
668
|
+
spanId: "introspect",
|
|
669
|
+
label: "Introspecting database schema..."
|
|
670
|
+
});
|
|
671
|
+
try {
|
|
672
|
+
const result = await familyInstance.introspect({ driver });
|
|
673
|
+
onProgress?.({
|
|
674
|
+
action: "introspect",
|
|
675
|
+
kind: "spanEnd",
|
|
676
|
+
spanId: "introspect",
|
|
677
|
+
outcome: "ok"
|
|
678
|
+
});
|
|
679
|
+
return result;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
onProgress?.({
|
|
682
|
+
action: "introspect",
|
|
683
|
+
kind: "spanEnd",
|
|
684
|
+
spanId: "introspect",
|
|
685
|
+
outcome: "error"
|
|
686
|
+
});
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
toSchemaView(schemaIR) {
|
|
691
|
+
this.init();
|
|
692
|
+
if (this.familyInstance?.toSchemaView) {
|
|
693
|
+
return this.familyInstance.toSchemaView(schemaIR);
|
|
694
|
+
}
|
|
695
|
+
return void 0;
|
|
696
|
+
}
|
|
697
|
+
async emit(options) {
|
|
698
|
+
const { onProgress, contractConfig } = options;
|
|
699
|
+
this.init();
|
|
700
|
+
if (!this.familyInstance) {
|
|
701
|
+
throw new Error("Family instance was not initialized. This is a bug.");
|
|
702
|
+
}
|
|
703
|
+
let contractRaw;
|
|
704
|
+
onProgress?.({
|
|
705
|
+
action: "emit",
|
|
706
|
+
kind: "spanStart",
|
|
707
|
+
spanId: "resolveSource",
|
|
708
|
+
label: "Resolving contract source..."
|
|
709
|
+
});
|
|
710
|
+
try {
|
|
711
|
+
switch (contractConfig.source.kind) {
|
|
712
|
+
case "loader":
|
|
713
|
+
contractRaw = await contractConfig.source.load();
|
|
714
|
+
break;
|
|
715
|
+
case "value":
|
|
716
|
+
contractRaw = contractConfig.source.value;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
onProgress?.({
|
|
720
|
+
action: "emit",
|
|
721
|
+
kind: "spanEnd",
|
|
722
|
+
spanId: "resolveSource",
|
|
723
|
+
outcome: "ok"
|
|
724
|
+
});
|
|
725
|
+
} catch (error) {
|
|
726
|
+
onProgress?.({
|
|
727
|
+
action: "emit",
|
|
728
|
+
kind: "spanEnd",
|
|
729
|
+
spanId: "resolveSource",
|
|
730
|
+
outcome: "error"
|
|
731
|
+
});
|
|
732
|
+
return notOk2({
|
|
733
|
+
code: "CONTRACT_SOURCE_INVALID",
|
|
734
|
+
summary: "Failed to resolve contract source",
|
|
735
|
+
why: error instanceof Error ? error.message : String(error),
|
|
736
|
+
meta: void 0
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
onProgress?.({
|
|
740
|
+
action: "emit",
|
|
741
|
+
kind: "spanStart",
|
|
742
|
+
spanId: "emit",
|
|
743
|
+
label: "Emitting contract..."
|
|
744
|
+
});
|
|
745
|
+
try {
|
|
746
|
+
const emitResult = await this.familyInstance.emitContract({ contractIR: contractRaw });
|
|
747
|
+
onProgress?.({
|
|
748
|
+
action: "emit",
|
|
749
|
+
kind: "spanEnd",
|
|
750
|
+
spanId: "emit",
|
|
751
|
+
outcome: "ok"
|
|
752
|
+
});
|
|
753
|
+
return ok2({
|
|
754
|
+
coreHash: emitResult.coreHash,
|
|
755
|
+
profileHash: emitResult.profileHash,
|
|
756
|
+
contractJson: emitResult.contractJson,
|
|
757
|
+
contractDts: emitResult.contractDts
|
|
758
|
+
});
|
|
759
|
+
} catch (error) {
|
|
760
|
+
onProgress?.({
|
|
761
|
+
action: "emit",
|
|
762
|
+
kind: "spanEnd",
|
|
763
|
+
spanId: "emit",
|
|
764
|
+
outcome: "error"
|
|
765
|
+
});
|
|
766
|
+
return notOk2({
|
|
767
|
+
code: "EMIT_FAILED",
|
|
768
|
+
summary: "Failed to emit contract",
|
|
769
|
+
why: error instanceof Error ? error.message : String(error),
|
|
770
|
+
meta: void 0
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
};
|
|
94
775
|
|
|
95
776
|
// src/utils/command-helpers.ts
|
|
96
777
|
function setCommandDescriptions(command, shortDescription, longDescription) {
|
|
@@ -1041,6 +1722,47 @@ function formatRootHelp(options) {
|
|
|
1041
1722
|
`;
|
|
1042
1723
|
}
|
|
1043
1724
|
|
|
1725
|
+
// src/utils/progress-adapter.ts
|
|
1726
|
+
import ora from "ora";
|
|
1727
|
+
function createProgressAdapter(options) {
|
|
1728
|
+
const { flags } = options;
|
|
1729
|
+
const shouldShowProgress = !flags.quiet && flags.json !== "object" && process.stdout.isTTY;
|
|
1730
|
+
if (!shouldShowProgress) {
|
|
1731
|
+
return () => {
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
const activeSpans = /* @__PURE__ */ new Map();
|
|
1735
|
+
return (event) => {
|
|
1736
|
+
if (event.kind === "spanStart") {
|
|
1737
|
+
if (event.parentSpanId) {
|
|
1738
|
+
console.log(` \u2192 ${event.label}...`);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const spinner = ora({
|
|
1742
|
+
text: event.label,
|
|
1743
|
+
color: flags.color !== false ? "cyan" : false
|
|
1744
|
+
}).start();
|
|
1745
|
+
activeSpans.set(event.spanId, {
|
|
1746
|
+
spinner,
|
|
1747
|
+
startTime: Date.now()
|
|
1748
|
+
});
|
|
1749
|
+
} else if (event.kind === "spanEnd") {
|
|
1750
|
+
const spanState = activeSpans.get(event.spanId);
|
|
1751
|
+
if (spanState) {
|
|
1752
|
+
const elapsed = Date.now() - spanState.startTime;
|
|
1753
|
+
if (event.outcome === "error") {
|
|
1754
|
+
spanState.spinner.fail(`${spanState.spinner.text} (failed)`);
|
|
1755
|
+
} else if (event.outcome === "skipped") {
|
|
1756
|
+
spanState.spinner.info(`${spanState.spinner.text} (skipped)`);
|
|
1757
|
+
} else {
|
|
1758
|
+
spanState.spinner.succeed(`${spanState.spinner.text} (${elapsed}ms)`);
|
|
1759
|
+
}
|
|
1760
|
+
activeSpans.delete(event.spanId);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1044
1766
|
// src/utils/result-handler.ts
|
|
1045
1767
|
function handleResult(result, flags, onSuccess) {
|
|
1046
1768
|
if (result.ok) {
|
|
@@ -1059,31 +1781,122 @@ function handleResult(result, flags, onSuccess) {
|
|
|
1059
1781
|
return exitCode;
|
|
1060
1782
|
}
|
|
1061
1783
|
|
|
1062
|
-
// src/
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1784
|
+
// src/commands/contract-emit.ts
|
|
1785
|
+
function mapEmitFailure(failure) {
|
|
1786
|
+
if (failure.code === "CONTRACT_SOURCE_INVALID") {
|
|
1787
|
+
return errorRuntime(failure.summary, {
|
|
1788
|
+
why: failure.why ?? "Contract source is invalid",
|
|
1789
|
+
fix: "Check your contract source configuration in prisma-next.config.ts"
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
if (failure.code === "EMIT_FAILED") {
|
|
1793
|
+
return errorRuntime(failure.summary, {
|
|
1794
|
+
why: failure.why ?? "Failed to emit contract",
|
|
1795
|
+
fix: "Check your contract configuration and ensure the source is valid"
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
const exhaustive = failure.code;
|
|
1799
|
+
throw new Error(`Unhandled EmitFailure code: ${exhaustive}`);
|
|
1800
|
+
}
|
|
1801
|
+
async function executeContractEmitCommand(options, flags, startTime) {
|
|
1802
|
+
let config;
|
|
1803
|
+
try {
|
|
1804
|
+
config = await loadConfig(options.config);
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
if (error instanceof CliStructuredError) {
|
|
1807
|
+
return notOk3(error);
|
|
1808
|
+
}
|
|
1809
|
+
return notOk3(
|
|
1810
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
1811
|
+
why: "Failed to load config"
|
|
1812
|
+
})
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
if (!config.contract) {
|
|
1816
|
+
return notOk3(
|
|
1817
|
+
errorContractConfigMissing2({
|
|
1818
|
+
why: "Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ..., types: ... }"
|
|
1819
|
+
})
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
const contractConfig = config.contract;
|
|
1823
|
+
if (!contractConfig.output || !contractConfig.types) {
|
|
1824
|
+
return notOk3(
|
|
1825
|
+
errorContractConfigMissing2({
|
|
1826
|
+
why: "Contract config must have output and types paths. This should not happen if defineConfig() was used."
|
|
1827
|
+
})
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
const outputJsonPath = resolve2(contractConfig.output);
|
|
1831
|
+
const outputDtsPath = resolve2(contractConfig.types);
|
|
1832
|
+
if (flags.json !== "object" && !flags.quiet) {
|
|
1833
|
+
const configPath = options.config ? relative2(process.cwd(), resolve2(options.config)) : "prisma-next.config.ts";
|
|
1834
|
+
const contractPath = relative2(process.cwd(), outputJsonPath);
|
|
1835
|
+
const typesPath = relative2(process.cwd(), outputDtsPath);
|
|
1836
|
+
const header = formatStyledHeader({
|
|
1837
|
+
command: "contract emit",
|
|
1838
|
+
description: "Write your contract to JSON and sign it",
|
|
1839
|
+
url: "https://pris.ly/contract-emit",
|
|
1840
|
+
details: [
|
|
1841
|
+
{ label: "config", value: configPath },
|
|
1842
|
+
{ label: "contract", value: contractPath },
|
|
1843
|
+
{ label: "types", value: typesPath }
|
|
1844
|
+
],
|
|
1845
|
+
flags
|
|
1846
|
+
});
|
|
1847
|
+
console.log(header);
|
|
1848
|
+
}
|
|
1849
|
+
const client = createControlClient({
|
|
1850
|
+
family: config.family,
|
|
1851
|
+
target: config.target,
|
|
1852
|
+
adapter: config.adapter,
|
|
1853
|
+
extensionPacks: config.extensionPacks ?? []
|
|
1854
|
+
});
|
|
1855
|
+
const onProgress = createProgressAdapter({ flags });
|
|
1075
1856
|
try {
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1857
|
+
const source = typeof contractConfig.source === "function" ? { kind: "loader", load: contractConfig.source } : { kind: "value", value: contractConfig.source };
|
|
1858
|
+
const result = await client.emit({
|
|
1859
|
+
contractConfig: {
|
|
1860
|
+
source,
|
|
1861
|
+
output: outputJsonPath,
|
|
1862
|
+
types: outputDtsPath
|
|
1863
|
+
},
|
|
1864
|
+
onProgress
|
|
1865
|
+
});
|
|
1866
|
+
if (!result.ok) {
|
|
1867
|
+
return notOk3(mapEmitFailure(result.failure));
|
|
1868
|
+
}
|
|
1869
|
+
mkdirSync(dirname2(outputJsonPath), { recursive: true });
|
|
1870
|
+
mkdirSync(dirname2(outputDtsPath), { recursive: true });
|
|
1871
|
+
writeFileSync(outputJsonPath, result.value.contractJson, "utf-8");
|
|
1872
|
+
writeFileSync(outputDtsPath, result.value.contractDts, "utf-8");
|
|
1873
|
+
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
1874
|
+
console.log("");
|
|
1875
|
+
}
|
|
1876
|
+
const emitResult = {
|
|
1877
|
+
coreHash: result.value.coreHash,
|
|
1878
|
+
profileHash: result.value.profileHash,
|
|
1879
|
+
outDir: dirname2(outputJsonPath),
|
|
1880
|
+
files: {
|
|
1881
|
+
json: outputJsonPath,
|
|
1882
|
+
dts: outputDtsPath
|
|
1883
|
+
},
|
|
1884
|
+
timings: { total: Date.now() - startTime }
|
|
1885
|
+
};
|
|
1886
|
+
return ok3(emitResult);
|
|
1080
1887
|
} catch (error) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1888
|
+
if (CliStructuredError.is(error)) {
|
|
1889
|
+
return notOk3(error);
|
|
1890
|
+
}
|
|
1891
|
+
return notOk3(
|
|
1892
|
+
errorUnexpected2("Unexpected error during contract emit", {
|
|
1893
|
+
why: error instanceof Error ? error.message : String(error)
|
|
1894
|
+
})
|
|
1895
|
+
);
|
|
1896
|
+
} finally {
|
|
1897
|
+
await client.close();
|
|
1083
1898
|
}
|
|
1084
1899
|
}
|
|
1085
|
-
|
|
1086
|
-
// src/commands/contract-emit.ts
|
|
1087
1900
|
function createContractEmitCommand() {
|
|
1088
1901
|
const command = new Command("emit");
|
|
1089
1902
|
setCommandDescriptions(
|
|
@@ -1098,79 +1911,8 @@ function createContractEmitCommand() {
|
|
|
1098
1911
|
}
|
|
1099
1912
|
}).option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object or ndjson)", false).option("-q, --quiet", "Quiet mode: errors only").option("-v, --verbose", "Verbose output: debug info, timings").option("-vv, --trace", "Trace output: deep internals, stack traces").option("--timestamps", "Add timestamps to output").option("--color", "Force color output").option("--no-color", "Disable color output").action(async (options) => {
|
|
1100
1913
|
const flags = parseGlobalFlags(options);
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
if (!config.contract) {
|
|
1104
|
-
throw errorContractConfigMissing2({
|
|
1105
|
-
why: "Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ..., types: ... }"
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
const contractConfig = config.contract;
|
|
1109
|
-
if (!contractConfig.output || !contractConfig.types) {
|
|
1110
|
-
throw errorContractConfigMissing2({
|
|
1111
|
-
why: "Contract config must have output and types paths. This should not happen if defineConfig() was used."
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
const outputJsonPath = resolve2(contractConfig.output);
|
|
1115
|
-
const outputDtsPath = resolve2(contractConfig.types);
|
|
1116
|
-
if (flags.json !== "object" && !flags.quiet) {
|
|
1117
|
-
const configPath = options.config ? relative2(process.cwd(), resolve2(options.config)) : "prisma-next.config.ts";
|
|
1118
|
-
const contractPath = relative2(process.cwd(), outputJsonPath);
|
|
1119
|
-
const typesPath = relative2(process.cwd(), outputDtsPath);
|
|
1120
|
-
const header = formatStyledHeader({
|
|
1121
|
-
command: "contract emit",
|
|
1122
|
-
description: "Write your contract to JSON and sign it",
|
|
1123
|
-
url: "https://pris.ly/contract-emit",
|
|
1124
|
-
details: [
|
|
1125
|
-
{ label: "config", value: configPath },
|
|
1126
|
-
{ label: "contract", value: contractPath },
|
|
1127
|
-
{ label: "types", value: typesPath }
|
|
1128
|
-
],
|
|
1129
|
-
flags
|
|
1130
|
-
});
|
|
1131
|
-
console.log(header);
|
|
1132
|
-
}
|
|
1133
|
-
const stack = createControlPlaneStack({
|
|
1134
|
-
target: config.target,
|
|
1135
|
-
adapter: config.adapter,
|
|
1136
|
-
driver: config.driver,
|
|
1137
|
-
extensionPacks: config.extensionPacks
|
|
1138
|
-
});
|
|
1139
|
-
const familyInstance = config.family.create(stack);
|
|
1140
|
-
let contractRaw;
|
|
1141
|
-
if (typeof contractConfig.source === "function") {
|
|
1142
|
-
contractRaw = await contractConfig.source();
|
|
1143
|
-
} else {
|
|
1144
|
-
contractRaw = contractConfig.source;
|
|
1145
|
-
}
|
|
1146
|
-
const emitResult = await withSpinner(
|
|
1147
|
-
() => familyInstance.emitContract({ contractIR: contractRaw }),
|
|
1148
|
-
{
|
|
1149
|
-
message: "Emitting contract...",
|
|
1150
|
-
flags
|
|
1151
|
-
}
|
|
1152
|
-
);
|
|
1153
|
-
mkdirSync(dirname2(outputJsonPath), { recursive: true });
|
|
1154
|
-
mkdirSync(dirname2(outputDtsPath), { recursive: true });
|
|
1155
|
-
writeFileSync(outputJsonPath, emitResult.contractJson, "utf-8");
|
|
1156
|
-
writeFileSync(outputDtsPath, emitResult.contractDts, "utf-8");
|
|
1157
|
-
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
1158
|
-
console.log("");
|
|
1159
|
-
}
|
|
1160
|
-
return {
|
|
1161
|
-
coreHash: emitResult.coreHash,
|
|
1162
|
-
profileHash: emitResult.profileHash,
|
|
1163
|
-
outDir: dirname2(outputJsonPath),
|
|
1164
|
-
files: {
|
|
1165
|
-
json: outputJsonPath,
|
|
1166
|
-
dts: outputDtsPath
|
|
1167
|
-
},
|
|
1168
|
-
timings: {
|
|
1169
|
-
total: 0
|
|
1170
|
-
// Timing is handled by emitContract internally if needed
|
|
1171
|
-
}
|
|
1172
|
-
};
|
|
1173
|
-
});
|
|
1914
|
+
const startTime = Date.now();
|
|
1915
|
+
const result = await executeContractEmitCommand(options, flags, startTime);
|
|
1174
1916
|
const exitCode = handleResult(result, flags, (emitResult) => {
|
|
1175
1917
|
if (flags.json === "object") {
|
|
1176
1918
|
console.log(formatEmitJson(emitResult));
|
|
@@ -1192,92 +1934,187 @@ function createContractEmitCommand() {
|
|
|
1192
1934
|
// src/commands/db-init.ts
|
|
1193
1935
|
import { readFile } from "fs/promises";
|
|
1194
1936
|
import { relative as relative3, resolve as resolve3 } from "path";
|
|
1195
|
-
import {
|
|
1196
|
-
import { redactDatabaseUrl } from "@prisma-next/utils/redact-db-url";
|
|
1937
|
+
import { notOk as notOk4, ok as ok4 } from "@prisma-next/utils/result";
|
|
1197
1938
|
import { Command as Command2 } from "commander";
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
why: `Framework component at index ${i} must be an object`
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
const record = component;
|
|
1212
|
-
if (!Object.hasOwn(record, "kind")) {
|
|
1213
|
-
throw errorConfigValidation("frameworkComponents[].kind", {
|
|
1214
|
-
why: `Framework component at index ${i} must have 'kind' property`
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
const kind = record["kind"];
|
|
1218
|
-
if (kind !== "target" && kind !== "adapter" && kind !== "extension" && kind !== "driver") {
|
|
1219
|
-
throw errorConfigValidation("frameworkComponents[].kind", {
|
|
1220
|
-
why: `Framework component at index ${i} has invalid kind '${String(kind)}' (must be 'target', 'adapter', 'extension', or 'driver')`
|
|
1221
|
-
});
|
|
1939
|
+
function mapDbInitFailure(failure) {
|
|
1940
|
+
if (failure.code === "PLANNING_FAILED") {
|
|
1941
|
+
return errorMigrationPlanningFailed({ conflicts: failure.conflicts ?? [] });
|
|
1942
|
+
}
|
|
1943
|
+
if (failure.code === "MARKER_ORIGIN_MISMATCH") {
|
|
1944
|
+
const mismatchParts = [];
|
|
1945
|
+
if (failure.marker?.coreHash !== failure.destination?.coreHash && failure.marker?.coreHash && failure.destination?.coreHash) {
|
|
1946
|
+
mismatchParts.push(
|
|
1947
|
+
`coreHash (marker: ${failure.marker.coreHash}, destination: ${failure.destination.coreHash})`
|
|
1948
|
+
);
|
|
1222
1949
|
}
|
|
1223
|
-
if (
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1950
|
+
if (failure.marker?.profileHash !== failure.destination?.profileHash && failure.marker?.profileHash && failure.destination?.profileHash) {
|
|
1951
|
+
mismatchParts.push(
|
|
1952
|
+
`profileHash (marker: ${failure.marker.profileHash}, destination: ${failure.destination.profileHash})`
|
|
1953
|
+
);
|
|
1227
1954
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
why:
|
|
1232
|
-
|
|
1955
|
+
return errorRuntime(
|
|
1956
|
+
`Existing contract marker does not match plan destination.${mismatchParts.length > 0 ? ` Mismatch in ${mismatchParts.join(" and ")}.` : ""}`,
|
|
1957
|
+
{
|
|
1958
|
+
why: "Database has an existing contract marker that does not match the target contract",
|
|
1959
|
+
fix: "If bootstrapping, drop/reset the database then re-run `prisma-next db init`; otherwise reconcile schema/marker using your migration workflow",
|
|
1960
|
+
meta: {
|
|
1961
|
+
code: "MARKER_ORIGIN_MISMATCH",
|
|
1962
|
+
...failure.marker?.coreHash ? { markerCoreHash: failure.marker.coreHash } : {},
|
|
1963
|
+
...failure.destination?.coreHash ? { destinationCoreHash: failure.destination.coreHash } : {},
|
|
1964
|
+
...failure.marker?.profileHash ? { markerProfileHash: failure.marker.profileHash } : {},
|
|
1965
|
+
...failure.destination?.profileHash ? { destinationProfileHash: failure.destination.profileHash } : {}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
if (failure.code === "RUNNER_FAILED") {
|
|
1971
|
+
return errorRuntime(failure.summary, {
|
|
1972
|
+
why: failure.why ?? "Migration runner failed",
|
|
1973
|
+
fix: "Fix the schema mismatch (db init is additive-only), or drop/reset the database and re-run `prisma-next db init`",
|
|
1974
|
+
meta: {
|
|
1975
|
+
code: "RUNNER_FAILED",
|
|
1976
|
+
...failure.meta ?? {}
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
const exhaustive = failure.code;
|
|
1981
|
+
throw new Error(`Unhandled DbInitFailure code: ${exhaustive}`);
|
|
1982
|
+
}
|
|
1983
|
+
async function executeDbInitCommand(options, flags, startTime) {
|
|
1984
|
+
const config = await loadConfig(options.config);
|
|
1985
|
+
const configPath = options.config ? relative3(process.cwd(), resolve3(options.config)) : "prisma-next.config.ts";
|
|
1986
|
+
const contractPathAbsolute = config.contract?.output ? resolve3(config.contract.output) : resolve3("src/prisma/contract.json");
|
|
1987
|
+
const contractPath = relative3(process.cwd(), contractPathAbsolute);
|
|
1988
|
+
if (flags.json !== "object" && !flags.quiet) {
|
|
1989
|
+
const details = [
|
|
1990
|
+
{ label: "config", value: configPath },
|
|
1991
|
+
{ label: "contract", value: contractPath }
|
|
1992
|
+
];
|
|
1993
|
+
if (options.db) {
|
|
1994
|
+
details.push({ label: "database", value: options.db });
|
|
1233
1995
|
}
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
why: `Framework component at index ${i} (kind: ${String(kind)}) must have 'targetId' property`
|
|
1237
|
-
});
|
|
1996
|
+
if (options.plan) {
|
|
1997
|
+
details.push({ label: "mode", value: "plan (dry run)" });
|
|
1238
1998
|
}
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1999
|
+
const header = formatStyledHeader({
|
|
2000
|
+
command: "db init",
|
|
2001
|
+
description: "Bootstrap a database to match the current contract",
|
|
2002
|
+
url: "https://pris.ly/db-init",
|
|
2003
|
+
details,
|
|
2004
|
+
flags
|
|
2005
|
+
});
|
|
2006
|
+
console.log(header);
|
|
2007
|
+
}
|
|
2008
|
+
let contractJsonContent;
|
|
2009
|
+
try {
|
|
2010
|
+
contractJsonContent = await readFile(contractPathAbsolute, "utf-8");
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
2013
|
+
return notOk4(
|
|
2014
|
+
errorFileNotFound(contractPathAbsolute, {
|
|
2015
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
2016
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
2017
|
+
})
|
|
2018
|
+
);
|
|
1244
2019
|
}
|
|
2020
|
+
return notOk4(
|
|
2021
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2022
|
+
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
2023
|
+
})
|
|
2024
|
+
);
|
|
1245
2025
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
const result = checkContractComponentRequirements({
|
|
1257
|
-
contract,
|
|
1258
|
-
expectedTargetFamily: stack.target.familyId,
|
|
1259
|
-
expectedTargetId: stack.target.targetId,
|
|
1260
|
-
providedComponentIds
|
|
1261
|
-
});
|
|
1262
|
-
if (result.familyMismatch) {
|
|
1263
|
-
throw errorConfigValidation("contract.targetFamily", {
|
|
1264
|
-
why: `Contract was emitted for family '${result.familyMismatch.actual}' but CLI config is wired to '${result.familyMismatch.expected}'.`
|
|
1265
|
-
});
|
|
2026
|
+
let contractJson;
|
|
2027
|
+
try {
|
|
2028
|
+
contractJson = JSON.parse(contractJsonContent);
|
|
2029
|
+
} catch (error) {
|
|
2030
|
+
return notOk4(
|
|
2031
|
+
errorContractValidationFailed(
|
|
2032
|
+
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2033
|
+
{ where: { path: contractPathAbsolute } }
|
|
2034
|
+
)
|
|
2035
|
+
);
|
|
1266
2036
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
2037
|
+
const dbConnection = options.db ?? config.db?.connection;
|
|
2038
|
+
if (!dbConnection) {
|
|
2039
|
+
return notOk4(
|
|
2040
|
+
errorDatabaseConnectionRequired({
|
|
2041
|
+
why: `Database connection is required for db init (set db.connection in ${configPath}, or pass --db <url>)`
|
|
2042
|
+
})
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
if (!config.driver) {
|
|
2046
|
+
return notOk4(errorDriverRequired({ why: "Config.driver is required for db init" }));
|
|
1271
2047
|
}
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
2048
|
+
if (!config.target.migrations) {
|
|
2049
|
+
return notOk4(
|
|
2050
|
+
errorTargetMigrationNotSupported({
|
|
2051
|
+
why: `Target "${config.target.id}" does not support migrations`
|
|
2052
|
+
})
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
const client = createControlClient({
|
|
2056
|
+
family: config.family,
|
|
2057
|
+
target: config.target,
|
|
2058
|
+
adapter: config.adapter,
|
|
2059
|
+
driver: config.driver,
|
|
2060
|
+
extensionPacks: config.extensionPacks ?? []
|
|
2061
|
+
});
|
|
2062
|
+
const onProgress = createProgressAdapter({ flags });
|
|
2063
|
+
try {
|
|
2064
|
+
const result = await client.dbInit({
|
|
2065
|
+
contractIR: contractJson,
|
|
2066
|
+
mode: options.plan ? "plan" : "apply",
|
|
2067
|
+
connection: dbConnection,
|
|
2068
|
+
onProgress
|
|
1276
2069
|
});
|
|
2070
|
+
if (!result.ok) {
|
|
2071
|
+
return notOk4(mapDbInitFailure(result.failure));
|
|
2072
|
+
}
|
|
2073
|
+
const profileHash = result.value.marker?.profileHash;
|
|
2074
|
+
const dbInitResult = {
|
|
2075
|
+
ok: true,
|
|
2076
|
+
mode: result.value.mode,
|
|
2077
|
+
plan: {
|
|
2078
|
+
targetId: config.target.targetId,
|
|
2079
|
+
destination: {
|
|
2080
|
+
coreHash: result.value.marker?.coreHash ?? "",
|
|
2081
|
+
...profileHash ? { profileHash } : {}
|
|
2082
|
+
},
|
|
2083
|
+
operations: result.value.plan.operations.map((op) => ({
|
|
2084
|
+
id: op.id,
|
|
2085
|
+
label: op.label,
|
|
2086
|
+
operationClass: op.operationClass
|
|
2087
|
+
}))
|
|
2088
|
+
},
|
|
2089
|
+
...result.value.execution ? {
|
|
2090
|
+
execution: {
|
|
2091
|
+
operationsPlanned: result.value.execution.operationsPlanned,
|
|
2092
|
+
operationsExecuted: result.value.execution.operationsExecuted
|
|
2093
|
+
}
|
|
2094
|
+
} : {},
|
|
2095
|
+
...result.value.marker ? {
|
|
2096
|
+
marker: {
|
|
2097
|
+
coreHash: result.value.marker.coreHash,
|
|
2098
|
+
...result.value.marker.profileHash ? { profileHash: result.value.marker.profileHash } : {}
|
|
2099
|
+
}
|
|
2100
|
+
} : {},
|
|
2101
|
+
summary: result.value.summary,
|
|
2102
|
+
timings: { total: Date.now() - startTime }
|
|
2103
|
+
};
|
|
2104
|
+
return ok4(dbInitResult);
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
if (CliStructuredError.is(error)) {
|
|
2107
|
+
return notOk4(error);
|
|
2108
|
+
}
|
|
2109
|
+
return notOk4(
|
|
2110
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2111
|
+
why: `Unexpected error during db init: ${error instanceof Error ? error.message : String(error)}`
|
|
2112
|
+
})
|
|
2113
|
+
);
|
|
2114
|
+
} finally {
|
|
2115
|
+
await client.close();
|
|
1277
2116
|
}
|
|
1278
2117
|
}
|
|
1279
|
-
|
|
1280
|
-
// src/commands/db-init.ts
|
|
1281
2118
|
function createDbInitCommand() {
|
|
1282
2119
|
const command = new Command2("init");
|
|
1283
2120
|
setCommandDescriptions(
|
|
@@ -1293,275 +2130,18 @@ function createDbInitCommand() {
|
|
|
1293
2130
|
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--plan", "Preview planned operations without applying", false).option("--json [format]", "Output as JSON (object)", false).option("-q, --quiet", "Quiet mode: errors only").option("-v, --verbose", "Verbose output: debug info, timings").option("-vv, --trace", "Trace output: deep internals, stack traces").option("--timestamps", "Add timestamps to output").option("--color", "Force color output").option("--no-color", "Disable color output").action(async (options) => {
|
|
1294
2131
|
const flags = parseGlobalFlags(options);
|
|
1295
2132
|
const startTime = Date.now();
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
2133
|
+
if (flags.json === "ndjson") {
|
|
2134
|
+
const result2 = notOk4(
|
|
2135
|
+
errorJsonFormatNotSupported({
|
|
1299
2136
|
command: "db init",
|
|
1300
2137
|
format: "ndjson",
|
|
1301
2138
|
supportedFormats: ["object"]
|
|
1302
|
-
})
|
|
1303
|
-
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
if (flags.json !== "object" && !flags.quiet) {
|
|
1309
|
-
const details = [
|
|
1310
|
-
{ label: "config", value: configPath },
|
|
1311
|
-
{ label: "contract", value: contractPath }
|
|
1312
|
-
];
|
|
1313
|
-
if (options.db) {
|
|
1314
|
-
details.push({ label: "database", value: options.db });
|
|
1315
|
-
}
|
|
1316
|
-
if (options.plan) {
|
|
1317
|
-
details.push({ label: "mode", value: "plan (dry run)" });
|
|
1318
|
-
}
|
|
1319
|
-
const header = formatStyledHeader({
|
|
1320
|
-
command: "db init",
|
|
1321
|
-
description: "Bootstrap a database to match the current contract",
|
|
1322
|
-
url: "https://pris.ly/db-init",
|
|
1323
|
-
details,
|
|
1324
|
-
flags
|
|
1325
|
-
});
|
|
1326
|
-
console.log(header);
|
|
1327
|
-
}
|
|
1328
|
-
let contractJsonContent;
|
|
1329
|
-
try {
|
|
1330
|
-
contractJsonContent = await readFile(contractPathAbsolute, "utf-8");
|
|
1331
|
-
} catch (error) {
|
|
1332
|
-
if (error instanceof Error && error.code === "ENOENT") {
|
|
1333
|
-
throw errorFileNotFound(contractPathAbsolute, {
|
|
1334
|
-
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
1335
|
-
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
throw errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
1339
|
-
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
let contractJson;
|
|
1343
|
-
try {
|
|
1344
|
-
contractJson = JSON.parse(contractJsonContent);
|
|
1345
|
-
} catch (error) {
|
|
1346
|
-
throw errorContractValidationFailed(
|
|
1347
|
-
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
1348
|
-
{ where: { path: contractPathAbsolute } }
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
const dbUrl = options.db ?? config.db?.url;
|
|
1352
|
-
if (!dbUrl) {
|
|
1353
|
-
throw errorDatabaseUrlRequired({
|
|
1354
|
-
why: `Database URL is required for db init (set db.url in ${configPath}, or pass --db <url>)`
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
if (!config.driver) {
|
|
1358
|
-
throw errorDriverRequired({ why: "Config.driver is required for db init" });
|
|
1359
|
-
}
|
|
1360
|
-
const driverDescriptor = config.driver;
|
|
1361
|
-
if (!config.target.migrations) {
|
|
1362
|
-
throw errorTargetMigrationNotSupported({
|
|
1363
|
-
why: `Target "${config.target.id}" does not support migrations`
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
const migrations = config.target.migrations;
|
|
1367
|
-
let driver;
|
|
1368
|
-
try {
|
|
1369
|
-
driver = await withSpinner(() => driverDescriptor.create(dbUrl), {
|
|
1370
|
-
message: "Connecting to database...",
|
|
1371
|
-
flags
|
|
1372
|
-
});
|
|
1373
|
-
} catch (error) {
|
|
1374
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1375
|
-
const code = error.code;
|
|
1376
|
-
const redacted = redactDatabaseUrl(dbUrl);
|
|
1377
|
-
throw errorRuntime("Database connection failed", {
|
|
1378
|
-
why: message,
|
|
1379
|
-
fix: "Verify the database URL, ensure the database is reachable, and confirm credentials/permissions",
|
|
1380
|
-
meta: {
|
|
1381
|
-
...typeof code !== "undefined" ? { code } : {},
|
|
1382
|
-
...redacted
|
|
1383
|
-
}
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
try {
|
|
1387
|
-
const stack = createControlPlaneStack2({
|
|
1388
|
-
target: config.target,
|
|
1389
|
-
adapter: config.adapter,
|
|
1390
|
-
driver: driverDescriptor,
|
|
1391
|
-
extensionPacks: config.extensionPacks
|
|
1392
|
-
});
|
|
1393
|
-
const familyInstance = config.family.create(stack);
|
|
1394
|
-
const rawComponents = [config.target, config.adapter, ...config.extensionPacks ?? []];
|
|
1395
|
-
const frameworkComponents = assertFrameworkComponentsCompatible(
|
|
1396
|
-
config.family.familyId,
|
|
1397
|
-
config.target.targetId,
|
|
1398
|
-
rawComponents
|
|
1399
|
-
);
|
|
1400
|
-
const contractIR = familyInstance.validateContractIR(contractJson);
|
|
1401
|
-
assertContractRequirementsSatisfied({ contract: contractIR, stack });
|
|
1402
|
-
const planner = migrations.createPlanner(familyInstance);
|
|
1403
|
-
const runner = migrations.createRunner(familyInstance);
|
|
1404
|
-
const schemaIR = await withSpinner(() => familyInstance.introspect({ driver }), {
|
|
1405
|
-
message: "Introspecting database schema...",
|
|
1406
|
-
flags
|
|
1407
|
-
});
|
|
1408
|
-
const policy = { allowedOperationClasses: ["additive"] };
|
|
1409
|
-
const plannerResult = await withSpinner(
|
|
1410
|
-
async () => planner.plan({
|
|
1411
|
-
contract: contractIR,
|
|
1412
|
-
schema: schemaIR,
|
|
1413
|
-
policy,
|
|
1414
|
-
frameworkComponents
|
|
1415
|
-
}),
|
|
1416
|
-
{
|
|
1417
|
-
message: "Planning migration...",
|
|
1418
|
-
flags
|
|
1419
|
-
}
|
|
1420
|
-
);
|
|
1421
|
-
if (plannerResult.kind === "failure") {
|
|
1422
|
-
throw errorMigrationPlanningFailed({ conflicts: plannerResult.conflicts });
|
|
1423
|
-
}
|
|
1424
|
-
const migrationPlan = plannerResult.plan;
|
|
1425
|
-
const existingMarker = await familyInstance.readMarker({ driver });
|
|
1426
|
-
if (existingMarker) {
|
|
1427
|
-
const markerMatchesDestination = existingMarker.coreHash === migrationPlan.destination.coreHash && (!migrationPlan.destination.profileHash || existingMarker.profileHash === migrationPlan.destination.profileHash);
|
|
1428
|
-
if (markerMatchesDestination) {
|
|
1429
|
-
const dbInitResult2 = {
|
|
1430
|
-
ok: true,
|
|
1431
|
-
mode: options.plan ? "plan" : "apply",
|
|
1432
|
-
plan: {
|
|
1433
|
-
targetId: migrationPlan.targetId,
|
|
1434
|
-
destination: migrationPlan.destination,
|
|
1435
|
-
operations: []
|
|
1436
|
-
},
|
|
1437
|
-
...options.plan ? {} : {
|
|
1438
|
-
execution: { operationsPlanned: 0, operationsExecuted: 0 },
|
|
1439
|
-
marker: {
|
|
1440
|
-
coreHash: existingMarker.coreHash,
|
|
1441
|
-
profileHash: existingMarker.profileHash
|
|
1442
|
-
}
|
|
1443
|
-
},
|
|
1444
|
-
summary: "Database already at target contract state",
|
|
1445
|
-
timings: { total: Date.now() - startTime }
|
|
1446
|
-
};
|
|
1447
|
-
return dbInitResult2;
|
|
1448
|
-
}
|
|
1449
|
-
const coreHashMismatch = existingMarker.coreHash !== migrationPlan.destination.coreHash;
|
|
1450
|
-
const profileHashMismatch = migrationPlan.destination.profileHash && existingMarker.profileHash !== migrationPlan.destination.profileHash;
|
|
1451
|
-
const mismatchParts = [];
|
|
1452
|
-
if (coreHashMismatch) {
|
|
1453
|
-
mismatchParts.push(
|
|
1454
|
-
`coreHash (marker: ${existingMarker.coreHash}, destination: ${migrationPlan.destination.coreHash})`
|
|
1455
|
-
);
|
|
1456
|
-
}
|
|
1457
|
-
if (profileHashMismatch) {
|
|
1458
|
-
mismatchParts.push(
|
|
1459
|
-
`profileHash (marker: ${existingMarker.profileHash}, destination: ${migrationPlan.destination.profileHash})`
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
throw errorRuntime(
|
|
1463
|
-
`Existing contract marker does not match plan destination. Mismatch in ${mismatchParts.join(" and ")}.`,
|
|
1464
|
-
{
|
|
1465
|
-
why: "Database has an existing contract marker that does not match the target contract",
|
|
1466
|
-
fix: "If bootstrapping, drop/reset the database then re-run `prisma-next db init`; otherwise reconcile schema/marker using your migration workflow",
|
|
1467
|
-
meta: {
|
|
1468
|
-
code: "MARKER_ORIGIN_MISMATCH",
|
|
1469
|
-
markerCoreHash: existingMarker.coreHash,
|
|
1470
|
-
destinationCoreHash: migrationPlan.destination.coreHash,
|
|
1471
|
-
...existingMarker.profileHash ? { markerProfileHash: existingMarker.profileHash } : {},
|
|
1472
|
-
...migrationPlan.destination.profileHash ? { destinationProfileHash: migrationPlan.destination.profileHash } : {}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1477
|
-
if (options.plan) {
|
|
1478
|
-
const dbInitResult2 = {
|
|
1479
|
-
ok: true,
|
|
1480
|
-
mode: "plan",
|
|
1481
|
-
plan: {
|
|
1482
|
-
targetId: migrationPlan.targetId,
|
|
1483
|
-
destination: migrationPlan.destination,
|
|
1484
|
-
operations: migrationPlan.operations.map((op) => ({
|
|
1485
|
-
id: op.id,
|
|
1486
|
-
label: op.label,
|
|
1487
|
-
operationClass: op.operationClass
|
|
1488
|
-
}))
|
|
1489
|
-
},
|
|
1490
|
-
summary: `Planned ${migrationPlan.operations.length} operation(s)`,
|
|
1491
|
-
timings: { total: Date.now() - startTime }
|
|
1492
|
-
};
|
|
1493
|
-
return dbInitResult2;
|
|
1494
|
-
}
|
|
1495
|
-
if (!flags.quiet && flags.json !== "object") {
|
|
1496
|
-
console.log("Applying migration plan and verifying schema...");
|
|
1497
|
-
}
|
|
1498
|
-
const callbacks = {
|
|
1499
|
-
onOperationStart: (op) => {
|
|
1500
|
-
if (!flags.quiet && flags.json !== "object") {
|
|
1501
|
-
console.log(` \u2192 ${op.label}...`);
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
onOperationComplete: (_op) => {
|
|
1505
|
-
}
|
|
1506
|
-
};
|
|
1507
|
-
const runnerResult = await runner.execute({
|
|
1508
|
-
plan: migrationPlan,
|
|
1509
|
-
driver,
|
|
1510
|
-
destinationContract: contractIR,
|
|
1511
|
-
policy,
|
|
1512
|
-
callbacks,
|
|
1513
|
-
// db init plans and applies back-to-back from a fresh introspection, so per-operation
|
|
1514
|
-
// pre/postchecks and the idempotency probe are usually redundant overhead. We still
|
|
1515
|
-
// enforce marker/origin compatibility and a full schema verification after apply.
|
|
1516
|
-
executionChecks: {
|
|
1517
|
-
prechecks: false,
|
|
1518
|
-
postchecks: false,
|
|
1519
|
-
idempotencyChecks: false
|
|
1520
|
-
},
|
|
1521
|
-
frameworkComponents
|
|
1522
|
-
});
|
|
1523
|
-
if (!runnerResult.ok) {
|
|
1524
|
-
const meta = {
|
|
1525
|
-
code: runnerResult.failure.code,
|
|
1526
|
-
...runnerResult.failure.meta ?? {}
|
|
1527
|
-
};
|
|
1528
|
-
const sqlState = typeof meta["sqlState"] === "string" ? meta["sqlState"] : void 0;
|
|
1529
|
-
const fix = sqlState === "42501" ? "Grant the database user sufficient privileges (insufficient_privilege), or run db init as a more privileged role" : runnerResult.failure.code === "SCHEMA_VERIFY_FAILED" ? "Fix the schema mismatch (db init is additive-only), or drop/reset the database and re-run `prisma-next db init`" : void 0;
|
|
1530
|
-
throw errorRuntime(runnerResult.failure.summary, {
|
|
1531
|
-
why: runnerResult.failure.why ?? `Migration runner failed: ${runnerResult.failure.code}`,
|
|
1532
|
-
...fix ? { fix } : {},
|
|
1533
|
-
meta
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
const execution = runnerResult.value;
|
|
1537
|
-
const dbInitResult = {
|
|
1538
|
-
ok: true,
|
|
1539
|
-
mode: "apply",
|
|
1540
|
-
plan: {
|
|
1541
|
-
targetId: migrationPlan.targetId,
|
|
1542
|
-
destination: migrationPlan.destination,
|
|
1543
|
-
operations: migrationPlan.operations.map((op) => ({
|
|
1544
|
-
id: op.id,
|
|
1545
|
-
label: op.label,
|
|
1546
|
-
operationClass: op.operationClass
|
|
1547
|
-
}))
|
|
1548
|
-
},
|
|
1549
|
-
execution: {
|
|
1550
|
-
operationsPlanned: execution.operationsPlanned,
|
|
1551
|
-
operationsExecuted: execution.operationsExecuted
|
|
1552
|
-
},
|
|
1553
|
-
marker: migrationPlan.destination.profileHash ? {
|
|
1554
|
-
coreHash: migrationPlan.destination.coreHash,
|
|
1555
|
-
profileHash: migrationPlan.destination.profileHash
|
|
1556
|
-
} : { coreHash: migrationPlan.destination.coreHash },
|
|
1557
|
-
summary: `Applied ${execution.operationsExecuted} operation(s), marker written`,
|
|
1558
|
-
timings: { total: Date.now() - startTime }
|
|
1559
|
-
};
|
|
1560
|
-
return dbInitResult;
|
|
1561
|
-
} finally {
|
|
1562
|
-
await driver.close();
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
2139
|
+
})
|
|
2140
|
+
);
|
|
2141
|
+
const exitCode2 = handleResult(result2, flags);
|
|
2142
|
+
process.exit(exitCode2);
|
|
2143
|
+
}
|
|
2144
|
+
const result = await executeDbInitCommand(options, flags, startTime);
|
|
1565
2145
|
const exitCode = handleResult(result, flags, (dbInitResult) => {
|
|
1566
2146
|
if (flags.json === "object") {
|
|
1567
2147
|
console.log(formatDbInitJson(dbInitResult));
|
|
@@ -1578,16 +2158,92 @@ function createDbInitCommand() {
|
|
|
1578
2158
|
}
|
|
1579
2159
|
|
|
1580
2160
|
// src/commands/db-introspect.ts
|
|
1581
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1582
2161
|
import { relative as relative4, resolve as resolve4 } from "path";
|
|
1583
|
-
import {
|
|
1584
|
-
errorDatabaseUrlRequired as errorDatabaseUrlRequired2,
|
|
1585
|
-
errorDriverRequired as errorDriverRequired2,
|
|
1586
|
-
errorRuntime as errorRuntime2,
|
|
1587
|
-
errorUnexpected as errorUnexpected3
|
|
1588
|
-
} from "@prisma-next/core-control-plane/errors";
|
|
1589
|
-
import { createControlPlaneStack as createControlPlaneStack3 } from "@prisma-next/core-control-plane/types";
|
|
2162
|
+
import { notOk as notOk5, ok as ok5 } from "@prisma-next/utils/result";
|
|
1590
2163
|
import { Command as Command3 } from "commander";
|
|
2164
|
+
async function executeDbIntrospectCommand(options, flags, startTime) {
|
|
2165
|
+
const config = await loadConfig(options.config);
|
|
2166
|
+
const configPath = options.config ? relative4(process.cwd(), resolve4(options.config)) : "prisma-next.config.ts";
|
|
2167
|
+
if (flags.json !== "object" && !flags.quiet) {
|
|
2168
|
+
const details = [
|
|
2169
|
+
{ label: "config", value: configPath }
|
|
2170
|
+
];
|
|
2171
|
+
if (options.db) {
|
|
2172
|
+
const maskedUrl = options.db.replace(/:([^:@]+)@/, ":****@");
|
|
2173
|
+
details.push({ label: "database", value: maskedUrl });
|
|
2174
|
+
} else if (config.db?.connection && typeof config.db.connection === "string") {
|
|
2175
|
+
const maskedUrl = config.db.connection.replace(/:([^:@]+)@/, ":****@");
|
|
2176
|
+
details.push({ label: "database", value: maskedUrl });
|
|
2177
|
+
}
|
|
2178
|
+
const header = formatStyledHeader({
|
|
2179
|
+
command: "db introspect",
|
|
2180
|
+
description: "Inspect the database schema",
|
|
2181
|
+
url: "https://pris.ly/db-introspect",
|
|
2182
|
+
details,
|
|
2183
|
+
flags
|
|
2184
|
+
});
|
|
2185
|
+
console.log(header);
|
|
2186
|
+
}
|
|
2187
|
+
const dbConnection = options.db ?? config.db?.connection;
|
|
2188
|
+
if (!dbConnection) {
|
|
2189
|
+
return notOk5(
|
|
2190
|
+
errorDatabaseConnectionRequired({
|
|
2191
|
+
why: `Database connection is required for db introspect (set db.connection in ${configPath}, or pass --db <url>)`
|
|
2192
|
+
})
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
if (!config.driver) {
|
|
2196
|
+
return notOk5(errorDriverRequired({ why: "Config.driver is required for db introspect" }));
|
|
2197
|
+
}
|
|
2198
|
+
const client = createControlClient({
|
|
2199
|
+
family: config.family,
|
|
2200
|
+
target: config.target,
|
|
2201
|
+
adapter: config.adapter,
|
|
2202
|
+
driver: config.driver,
|
|
2203
|
+
extensionPacks: config.extensionPacks ?? []
|
|
2204
|
+
});
|
|
2205
|
+
const onProgress = createProgressAdapter({ flags });
|
|
2206
|
+
try {
|
|
2207
|
+
const schemaIR = await client.introspect({
|
|
2208
|
+
connection: dbConnection,
|
|
2209
|
+
onProgress
|
|
2210
|
+
});
|
|
2211
|
+
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
2212
|
+
console.log("");
|
|
2213
|
+
}
|
|
2214
|
+
const schemaView = client.toSchemaView(schemaIR);
|
|
2215
|
+
const totalTime = Date.now() - startTime;
|
|
2216
|
+
const connectionForMeta = typeof dbConnection === "string" ? dbConnection.replace(/:([^:@]+)@/, ":****@") : void 0;
|
|
2217
|
+
const introspectResult = {
|
|
2218
|
+
ok: true,
|
|
2219
|
+
summary: "Schema introspected successfully",
|
|
2220
|
+
target: {
|
|
2221
|
+
familyId: config.family.familyId,
|
|
2222
|
+
id: config.target.targetId
|
|
2223
|
+
},
|
|
2224
|
+
schema: schemaIR,
|
|
2225
|
+
meta: {
|
|
2226
|
+
...configPath ? { configPath } : {},
|
|
2227
|
+
...connectionForMeta ? { dbUrl: connectionForMeta } : {}
|
|
2228
|
+
},
|
|
2229
|
+
timings: {
|
|
2230
|
+
total: totalTime
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
return ok5({ introspectResult, schemaView });
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
if (error instanceof CliStructuredError) {
|
|
2236
|
+
return notOk5(error);
|
|
2237
|
+
}
|
|
2238
|
+
return notOk5(
|
|
2239
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2240
|
+
why: `Unexpected error during db introspect: ${error instanceof Error ? error.message : String(error)}`
|
|
2241
|
+
})
|
|
2242
|
+
);
|
|
2243
|
+
} finally {
|
|
2244
|
+
await client.close();
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
1591
2247
|
function createDbIntrospectCommand() {
|
|
1592
2248
|
const command = new Command3("introspect");
|
|
1593
2249
|
setCommandDescriptions(
|
|
@@ -1600,132 +2256,21 @@ function createDbIntrospectCommand() {
|
|
|
1600
2256
|
const flags = parseGlobalFlags({});
|
|
1601
2257
|
return formatCommandHelp({ command: cmd, flags });
|
|
1602
2258
|
}
|
|
1603
|
-
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object
|
|
2259
|
+
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object)", false).option("-q, --quiet", "Quiet mode: errors only").option("-v, --verbose", "Verbose output: debug info, timings").option("-vv, --trace", "Trace output: deep internals, stack traces").option("--timestamps", "Add timestamps to output").option("--color", "Force color output").option("--no-color", "Disable color output").action(async (options) => {
|
|
1604
2260
|
const flags = parseGlobalFlags(options);
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
let contractIR;
|
|
1610
|
-
if (config.contract?.output) {
|
|
1611
|
-
const contractPath = resolve4(config.contract.output);
|
|
1612
|
-
try {
|
|
1613
|
-
const contractJsonContent = await readFile2(contractPath, "utf-8");
|
|
1614
|
-
contractIR = JSON.parse(contractJsonContent);
|
|
1615
|
-
} catch (error) {
|
|
1616
|
-
if (error instanceof Error && error.code !== "ENOENT") {
|
|
1617
|
-
throw errorUnexpected3(error.message, {
|
|
1618
|
-
why: `Failed to read contract file: ${error.message}`
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
if (flags.json !== "object" && !flags.quiet) {
|
|
1624
|
-
const details = [
|
|
1625
|
-
{ label: "config", value: configPath }
|
|
1626
|
-
];
|
|
1627
|
-
if (options.db) {
|
|
1628
|
-
const maskedUrl = options.db.replace(/:([^:@]+)@/, ":****@");
|
|
1629
|
-
details.push({ label: "database", value: maskedUrl });
|
|
1630
|
-
} else if (config.db?.url) {
|
|
1631
|
-
const maskedUrl = config.db.url.replace(/:([^:@]+)@/, ":****@");
|
|
1632
|
-
details.push({ label: "database", value: maskedUrl });
|
|
1633
|
-
}
|
|
1634
|
-
const header = formatStyledHeader({
|
|
2261
|
+
const startTime = Date.now();
|
|
2262
|
+
if (flags.json === "ndjson") {
|
|
2263
|
+
const result2 = notOk5(
|
|
2264
|
+
errorJsonFormatNotSupported({
|
|
1635
2265
|
command: "db introspect",
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
if (!dbUrl) {
|
|
1645
|
-
throw errorDatabaseUrlRequired2();
|
|
1646
|
-
}
|
|
1647
|
-
if (!config.driver) {
|
|
1648
|
-
throw errorDriverRequired2();
|
|
1649
|
-
}
|
|
1650
|
-
const driverDescriptor = config.driver;
|
|
1651
|
-
const driver = await withSpinner(() => driverDescriptor.create(dbUrl), {
|
|
1652
|
-
message: "Connecting to database...",
|
|
1653
|
-
flags
|
|
1654
|
-
});
|
|
1655
|
-
try {
|
|
1656
|
-
const stack = createControlPlaneStack3({
|
|
1657
|
-
target: config.target,
|
|
1658
|
-
adapter: config.adapter,
|
|
1659
|
-
driver: driverDescriptor,
|
|
1660
|
-
extensionPacks: config.extensionPacks
|
|
1661
|
-
});
|
|
1662
|
-
const familyInstance = config.family.create(stack);
|
|
1663
|
-
if (contractIR) {
|
|
1664
|
-
const validatedContract = familyInstance.validateContractIR(contractIR);
|
|
1665
|
-
assertContractRequirementsSatisfied({ contract: validatedContract, stack });
|
|
1666
|
-
contractIR = validatedContract;
|
|
1667
|
-
}
|
|
1668
|
-
let schemaIR;
|
|
1669
|
-
try {
|
|
1670
|
-
schemaIR = await withSpinner(
|
|
1671
|
-
() => familyInstance.introspect({
|
|
1672
|
-
driver,
|
|
1673
|
-
contractIR
|
|
1674
|
-
}),
|
|
1675
|
-
{
|
|
1676
|
-
message: "Introspecting database schema...",
|
|
1677
|
-
flags
|
|
1678
|
-
}
|
|
1679
|
-
);
|
|
1680
|
-
} catch (error) {
|
|
1681
|
-
throw errorRuntime2(error instanceof Error ? error.message : String(error), {
|
|
1682
|
-
why: `Failed to introspect database: ${error instanceof Error ? error.message : String(error)}`
|
|
1683
|
-
});
|
|
1684
|
-
}
|
|
1685
|
-
let schemaView;
|
|
1686
|
-
if (familyInstance.toSchemaView) {
|
|
1687
|
-
try {
|
|
1688
|
-
schemaView = familyInstance.toSchemaView(schemaIR);
|
|
1689
|
-
} catch (error) {
|
|
1690
|
-
if (flags.verbose) {
|
|
1691
|
-
console.error(
|
|
1692
|
-
`Warning: Failed to project schema to view: ${error instanceof Error ? error.message : String(error)}`
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
const totalTime = Date.now() - startTime;
|
|
1698
|
-
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
1699
|
-
console.log("");
|
|
1700
|
-
}
|
|
1701
|
-
const introspectResult = {
|
|
1702
|
-
ok: true,
|
|
1703
|
-
summary: "Schema introspected successfully",
|
|
1704
|
-
target: {
|
|
1705
|
-
familyId: config.family.familyId,
|
|
1706
|
-
id: config.target.targetId
|
|
1707
|
-
},
|
|
1708
|
-
schema: schemaIR,
|
|
1709
|
-
...configPath || options.db || config.db?.url ? {
|
|
1710
|
-
meta: {
|
|
1711
|
-
...configPath ? { configPath } : {},
|
|
1712
|
-
...options.db || config.db?.url ? {
|
|
1713
|
-
dbUrl: (options.db ?? config.db?.url ?? "").replace(
|
|
1714
|
-
/:([^:@]+)@/,
|
|
1715
|
-
":****@"
|
|
1716
|
-
)
|
|
1717
|
-
} : {}
|
|
1718
|
-
}
|
|
1719
|
-
} : {},
|
|
1720
|
-
timings: {
|
|
1721
|
-
total: totalTime
|
|
1722
|
-
}
|
|
1723
|
-
};
|
|
1724
|
-
return { introspectResult, schemaView };
|
|
1725
|
-
} finally {
|
|
1726
|
-
await driver.close();
|
|
1727
|
-
}
|
|
1728
|
-
});
|
|
2266
|
+
format: "ndjson",
|
|
2267
|
+
supportedFormats: ["object"]
|
|
2268
|
+
})
|
|
2269
|
+
);
|
|
2270
|
+
const exitCode2 = handleResult(result2, flags);
|
|
2271
|
+
process.exit(exitCode2);
|
|
2272
|
+
}
|
|
2273
|
+
const result = await executeDbIntrospectCommand(options, flags, startTime);
|
|
1729
2274
|
const exitCode = handleResult(result, flags, (value) => {
|
|
1730
2275
|
const { introspectResult, schemaView } = value;
|
|
1731
2276
|
if (flags.json === "object") {
|
|
@@ -1743,17 +2288,104 @@ function createDbIntrospectCommand() {
|
|
|
1743
2288
|
}
|
|
1744
2289
|
|
|
1745
2290
|
// src/commands/db-schema-verify.ts
|
|
1746
|
-
import { readFile as
|
|
2291
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1747
2292
|
import { relative as relative5, resolve as resolve5 } from "path";
|
|
1748
|
-
import {
|
|
1749
|
-
errorDatabaseUrlRequired as errorDatabaseUrlRequired3,
|
|
1750
|
-
errorDriverRequired as errorDriverRequired3,
|
|
1751
|
-
errorFileNotFound as errorFileNotFound2,
|
|
1752
|
-
errorRuntime as errorRuntime3,
|
|
1753
|
-
errorUnexpected as errorUnexpected4
|
|
1754
|
-
} from "@prisma-next/core-control-plane/errors";
|
|
1755
|
-
import { createControlPlaneStack as createControlPlaneStack4 } from "@prisma-next/core-control-plane/types";
|
|
2293
|
+
import { notOk as notOk6, ok as ok6 } from "@prisma-next/utils/result";
|
|
1756
2294
|
import { Command as Command4 } from "commander";
|
|
2295
|
+
async function executeDbSchemaVerifyCommand(options, flags) {
|
|
2296
|
+
const config = await loadConfig(options.config);
|
|
2297
|
+
const configPath = options.config ? relative5(process.cwd(), resolve5(options.config)) : "prisma-next.config.ts";
|
|
2298
|
+
const contractPathAbsolute = config.contract?.output ? resolve5(config.contract.output) : resolve5("src/prisma/contract.json");
|
|
2299
|
+
const contractPath = relative5(process.cwd(), contractPathAbsolute);
|
|
2300
|
+
if (flags.json !== "object" && !flags.quiet) {
|
|
2301
|
+
const details = [
|
|
2302
|
+
{ label: "config", value: configPath },
|
|
2303
|
+
{ label: "contract", value: contractPath }
|
|
2304
|
+
];
|
|
2305
|
+
if (options.db) {
|
|
2306
|
+
details.push({ label: "database", value: options.db });
|
|
2307
|
+
}
|
|
2308
|
+
const header = formatStyledHeader({
|
|
2309
|
+
command: "db schema-verify",
|
|
2310
|
+
description: "Check whether the database schema satisfies your contract",
|
|
2311
|
+
url: "https://pris.ly/db-schema-verify",
|
|
2312
|
+
details,
|
|
2313
|
+
flags
|
|
2314
|
+
});
|
|
2315
|
+
console.log(header);
|
|
2316
|
+
}
|
|
2317
|
+
let contractJsonContent;
|
|
2318
|
+
try {
|
|
2319
|
+
contractJsonContent = await readFile2(contractPathAbsolute, "utf-8");
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
2322
|
+
return notOk6(
|
|
2323
|
+
errorFileNotFound(contractPathAbsolute, {
|
|
2324
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
2325
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
2326
|
+
})
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
return notOk6(
|
|
2330
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2331
|
+
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
2332
|
+
})
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
let contractJson;
|
|
2336
|
+
try {
|
|
2337
|
+
contractJson = JSON.parse(contractJsonContent);
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
return notOk6(
|
|
2340
|
+
errorContractValidationFailed(
|
|
2341
|
+
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2342
|
+
{ where: { path: contractPathAbsolute } }
|
|
2343
|
+
)
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
const dbConnection = options.db ?? config.db?.connection;
|
|
2347
|
+
if (!dbConnection) {
|
|
2348
|
+
return notOk6(
|
|
2349
|
+
errorDatabaseConnectionRequired({
|
|
2350
|
+
why: `Database connection is required for db schema-verify (set db.connection in ${configPath}, or pass --db <url>)`
|
|
2351
|
+
})
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
if (!config.driver) {
|
|
2355
|
+
return notOk6(errorDriverRequired({ why: "Config.driver is required for db schema-verify" }));
|
|
2356
|
+
}
|
|
2357
|
+
const client = createControlClient({
|
|
2358
|
+
family: config.family,
|
|
2359
|
+
target: config.target,
|
|
2360
|
+
adapter: config.adapter,
|
|
2361
|
+
driver: config.driver,
|
|
2362
|
+
extensionPacks: config.extensionPacks ?? []
|
|
2363
|
+
});
|
|
2364
|
+
const onProgress = createProgressAdapter({ flags });
|
|
2365
|
+
try {
|
|
2366
|
+
const schemaVerifyResult = await client.schemaVerify({
|
|
2367
|
+
contractIR: contractJson,
|
|
2368
|
+
strict: options.strict ?? false,
|
|
2369
|
+
connection: dbConnection,
|
|
2370
|
+
onProgress
|
|
2371
|
+
});
|
|
2372
|
+
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
2373
|
+
console.log("");
|
|
2374
|
+
}
|
|
2375
|
+
return ok6(schemaVerifyResult);
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
if (error instanceof CliStructuredError) {
|
|
2378
|
+
return notOk6(error);
|
|
2379
|
+
}
|
|
2380
|
+
return notOk6(
|
|
2381
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2382
|
+
why: `Unexpected error during db schema-verify: ${error instanceof Error ? error.message : String(error)}`
|
|
2383
|
+
})
|
|
2384
|
+
);
|
|
2385
|
+
} finally {
|
|
2386
|
+
await client.close();
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
1757
2389
|
function createDbSchemaVerifyCommand() {
|
|
1758
2390
|
const command = new Command4("schema-verify");
|
|
1759
2391
|
setCommandDescriptions(
|
|
@@ -1766,101 +2398,20 @@ function createDbSchemaVerifyCommand() {
|
|
|
1766
2398
|
const flags = parseGlobalFlags({});
|
|
1767
2399
|
return formatCommandHelp({ command: cmd, flags });
|
|
1768
2400
|
}
|
|
1769
|
-
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object
|
|
2401
|
+
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object)", false).option("--strict", "Strict mode: extra schema elements cause failures", false).option("-q, --quiet", "Quiet mode: errors only").option("-v, --verbose", "Verbose output: debug info, timings").option("-vv, --trace", "Trace output: deep internals, stack traces").option("--timestamps", "Add timestamps to output").option("--color", "Force color output").option("--no-color", "Disable color output").action(async (options) => {
|
|
1770
2402
|
const flags = parseGlobalFlags(options);
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
const contractPathAbsolute = config.contract?.output ? resolve5(config.contract.output) : resolve5("src/prisma/contract.json");
|
|
1775
|
-
const contractPath = relative5(process.cwd(), contractPathAbsolute);
|
|
1776
|
-
if (flags.json !== "object" && !flags.quiet) {
|
|
1777
|
-
const details = [
|
|
1778
|
-
{ label: "config", value: configPath },
|
|
1779
|
-
{ label: "contract", value: contractPath }
|
|
1780
|
-
];
|
|
1781
|
-
if (options.db) {
|
|
1782
|
-
details.push({ label: "database", value: options.db });
|
|
1783
|
-
}
|
|
1784
|
-
const header = formatStyledHeader({
|
|
2403
|
+
if (flags.json === "ndjson") {
|
|
2404
|
+
const result2 = notOk6(
|
|
2405
|
+
errorJsonFormatNotSupported({
|
|
1785
2406
|
command: "db schema-verify",
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
try {
|
|
1795
|
-
contractJsonContent = await readFile3(contractPathAbsolute, "utf-8");
|
|
1796
|
-
} catch (error) {
|
|
1797
|
-
if (error instanceof Error && error.code === "ENOENT") {
|
|
1798
|
-
throw errorFileNotFound2(contractPathAbsolute, {
|
|
1799
|
-
why: `Contract file not found at ${contractPathAbsolute}`
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
throw errorUnexpected4(error instanceof Error ? error.message : String(error), {
|
|
1803
|
-
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
const contractJson = JSON.parse(contractJsonContent);
|
|
1807
|
-
if (!config.driver) {
|
|
1808
|
-
throw errorDriverRequired3();
|
|
1809
|
-
}
|
|
1810
|
-
const driverDescriptor = config.driver;
|
|
1811
|
-
const stack = createControlPlaneStack4({
|
|
1812
|
-
target: config.target,
|
|
1813
|
-
adapter: config.adapter,
|
|
1814
|
-
driver: driverDescriptor,
|
|
1815
|
-
extensionPacks: config.extensionPacks
|
|
1816
|
-
});
|
|
1817
|
-
const familyInstance = config.family.create(stack);
|
|
1818
|
-
const contractIR = familyInstance.validateContractIR(contractJson);
|
|
1819
|
-
assertContractRequirementsSatisfied({ contract: contractIR, stack });
|
|
1820
|
-
const dbUrl = options.db ?? config.db?.url;
|
|
1821
|
-
if (!dbUrl) {
|
|
1822
|
-
throw errorDatabaseUrlRequired3();
|
|
1823
|
-
}
|
|
1824
|
-
const driver = await withSpinner(() => driverDescriptor.create(dbUrl), {
|
|
1825
|
-
message: "Connecting to database...",
|
|
1826
|
-
flags
|
|
1827
|
-
});
|
|
1828
|
-
try {
|
|
1829
|
-
const rawComponents = [config.target, config.adapter, ...config.extensionPacks ?? []];
|
|
1830
|
-
const frameworkComponents = assertFrameworkComponentsCompatible(
|
|
1831
|
-
config.family.familyId,
|
|
1832
|
-
config.target.targetId,
|
|
1833
|
-
rawComponents
|
|
1834
|
-
);
|
|
1835
|
-
let schemaVerifyResult;
|
|
1836
|
-
try {
|
|
1837
|
-
schemaVerifyResult = await withSpinner(
|
|
1838
|
-
() => familyInstance.schemaVerify({
|
|
1839
|
-
driver,
|
|
1840
|
-
contractIR,
|
|
1841
|
-
strict: options.strict ?? false,
|
|
1842
|
-
contractPath: contractPathAbsolute,
|
|
1843
|
-
configPath,
|
|
1844
|
-
frameworkComponents
|
|
1845
|
-
}),
|
|
1846
|
-
{
|
|
1847
|
-
message: "Verifying database schema...",
|
|
1848
|
-
flags
|
|
1849
|
-
}
|
|
1850
|
-
);
|
|
1851
|
-
} catch (error) {
|
|
1852
|
-
throw errorRuntime3(error instanceof Error ? error.message : String(error), {
|
|
1853
|
-
why: `Failed to verify database schema: ${error instanceof Error ? error.message : String(error)}`
|
|
1854
|
-
});
|
|
1855
|
-
}
|
|
1856
|
-
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
1857
|
-
console.log("");
|
|
1858
|
-
}
|
|
1859
|
-
return schemaVerifyResult;
|
|
1860
|
-
} finally {
|
|
1861
|
-
await driver.close();
|
|
1862
|
-
}
|
|
1863
|
-
});
|
|
2407
|
+
format: "ndjson",
|
|
2408
|
+
supportedFormats: ["object"]
|
|
2409
|
+
})
|
|
2410
|
+
);
|
|
2411
|
+
const exitCode2 = handleResult(result2, flags);
|
|
2412
|
+
process.exit(exitCode2);
|
|
2413
|
+
}
|
|
2414
|
+
const result = await executeDbSchemaVerifyCommand(options, flags);
|
|
1864
2415
|
const exitCode = handleResult(result, flags, (schemaVerifyResult) => {
|
|
1865
2416
|
if (flags.json === "object") {
|
|
1866
2417
|
console.log(formatSchemaVerifyJson(schemaVerifyResult));
|
|
@@ -1881,17 +2432,116 @@ function createDbSchemaVerifyCommand() {
|
|
|
1881
2432
|
}
|
|
1882
2433
|
|
|
1883
2434
|
// src/commands/db-sign.ts
|
|
1884
|
-
import { readFile as
|
|
2435
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1885
2436
|
import { relative as relative6, resolve as resolve6 } from "path";
|
|
1886
|
-
import {
|
|
1887
|
-
errorDatabaseUrlRequired as errorDatabaseUrlRequired4,
|
|
1888
|
-
errorDriverRequired as errorDriverRequired4,
|
|
1889
|
-
errorFileNotFound as errorFileNotFound3,
|
|
1890
|
-
errorRuntime as errorRuntime4,
|
|
1891
|
-
errorUnexpected as errorUnexpected5
|
|
1892
|
-
} from "@prisma-next/core-control-plane/errors";
|
|
1893
|
-
import { createControlPlaneStack as createControlPlaneStack5 } from "@prisma-next/core-control-plane/types";
|
|
2437
|
+
import { notOk as notOk7, ok as ok7 } from "@prisma-next/utils/result";
|
|
1894
2438
|
import { Command as Command5 } from "commander";
|
|
2439
|
+
async function executeDbSignCommand(options, flags) {
|
|
2440
|
+
const config = await loadConfig(options.config);
|
|
2441
|
+
const configPath = options.config ? relative6(process.cwd(), resolve6(options.config)) : "prisma-next.config.ts";
|
|
2442
|
+
const contractPathAbsolute = config.contract?.output ? resolve6(config.contract.output) : resolve6("src/prisma/contract.json");
|
|
2443
|
+
const contractPath = relative6(process.cwd(), contractPathAbsolute);
|
|
2444
|
+
if (flags.json !== "object" && !flags.quiet) {
|
|
2445
|
+
const details = [
|
|
2446
|
+
{ label: "config", value: configPath },
|
|
2447
|
+
{ label: "contract", value: contractPath }
|
|
2448
|
+
];
|
|
2449
|
+
if (options.db) {
|
|
2450
|
+
details.push({ label: "database", value: options.db });
|
|
2451
|
+
}
|
|
2452
|
+
const header = formatStyledHeader({
|
|
2453
|
+
command: "db sign",
|
|
2454
|
+
description: "Sign the database with your contract so you can safely run queries",
|
|
2455
|
+
url: "https://pris.ly/db-sign",
|
|
2456
|
+
details,
|
|
2457
|
+
flags
|
|
2458
|
+
});
|
|
2459
|
+
console.log(header);
|
|
2460
|
+
}
|
|
2461
|
+
let contractJsonContent;
|
|
2462
|
+
try {
|
|
2463
|
+
contractJsonContent = await readFile3(contractPathAbsolute, "utf-8");
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
2466
|
+
return notOk7(
|
|
2467
|
+
errorFileNotFound(contractPathAbsolute, {
|
|
2468
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
2469
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
2470
|
+
})
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
return notOk7(
|
|
2474
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2475
|
+
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
2476
|
+
})
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
let contractJson;
|
|
2480
|
+
try {
|
|
2481
|
+
contractJson = JSON.parse(contractJsonContent);
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
return notOk7(
|
|
2484
|
+
errorContractValidationFailed(
|
|
2485
|
+
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2486
|
+
{ where: { path: contractPathAbsolute } }
|
|
2487
|
+
)
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
const dbConnection = options.db ?? config.db?.connection;
|
|
2491
|
+
if (!dbConnection) {
|
|
2492
|
+
return notOk7(
|
|
2493
|
+
errorDatabaseConnectionRequired({
|
|
2494
|
+
why: `Database connection is required for db sign (set db.connection in ${configPath}, or pass --db <url>)`
|
|
2495
|
+
})
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
if (!config.driver) {
|
|
2499
|
+
return notOk7(errorDriverRequired({ why: "Config.driver is required for db sign" }));
|
|
2500
|
+
}
|
|
2501
|
+
const client = createControlClient({
|
|
2502
|
+
family: config.family,
|
|
2503
|
+
target: config.target,
|
|
2504
|
+
adapter: config.adapter,
|
|
2505
|
+
driver: config.driver,
|
|
2506
|
+
extensionPacks: config.extensionPacks ?? []
|
|
2507
|
+
});
|
|
2508
|
+
const onProgress = createProgressAdapter({ flags });
|
|
2509
|
+
try {
|
|
2510
|
+
const schemaVerifyResult = await client.schemaVerify({
|
|
2511
|
+
contractIR: contractJson,
|
|
2512
|
+
strict: false,
|
|
2513
|
+
connection: dbConnection,
|
|
2514
|
+
onProgress
|
|
2515
|
+
});
|
|
2516
|
+
if (!schemaVerifyResult.ok) {
|
|
2517
|
+
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
2518
|
+
console.log("");
|
|
2519
|
+
}
|
|
2520
|
+
return notOk7(schemaVerifyResult);
|
|
2521
|
+
}
|
|
2522
|
+
const signResult = await client.sign({
|
|
2523
|
+
contractIR: contractJson,
|
|
2524
|
+
contractPath,
|
|
2525
|
+
configPath,
|
|
2526
|
+
onProgress
|
|
2527
|
+
});
|
|
2528
|
+
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
2529
|
+
console.log("");
|
|
2530
|
+
}
|
|
2531
|
+
return ok7(signResult);
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
if (error instanceof CliStructuredError) {
|
|
2534
|
+
return notOk7(error);
|
|
2535
|
+
}
|
|
2536
|
+
return notOk7(
|
|
2537
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2538
|
+
why: `Unexpected error during db sign: ${error instanceof Error ? error.message : String(error)}`
|
|
2539
|
+
})
|
|
2540
|
+
);
|
|
2541
|
+
} finally {
|
|
2542
|
+
await client.close();
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
1895
2545
|
function createDbSignCommand() {
|
|
1896
2546
|
const command = new Command5("sign");
|
|
1897
2547
|
setCommandDescriptions(
|
|
@@ -1904,168 +2554,170 @@ function createDbSignCommand() {
|
|
|
1904
2554
|
const flags = parseGlobalFlags({});
|
|
1905
2555
|
return formatCommandHelp({ command: cmd, flags });
|
|
1906
2556
|
}
|
|
1907
|
-
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object
|
|
2557
|
+
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object)", false).option("-q, --quiet", "Quiet mode: errors only").option("-v, --verbose", "Verbose output: debug info, timings").option("-vv, --trace", "Trace output: deep internals, stack traces").option("--timestamps", "Add timestamps to output").option("--color", "Force color output").option("--no-color", "Disable color output").action(async (options) => {
|
|
1908
2558
|
const flags = parseGlobalFlags(options);
|
|
1909
|
-
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
const contractPathAbsolute = config.contract?.output ? resolve6(config.contract.output) : resolve6("src/prisma/contract.json");
|
|
1913
|
-
const contractPath = relative6(process.cwd(), contractPathAbsolute);
|
|
1914
|
-
if (flags.json !== "object" && !flags.quiet) {
|
|
1915
|
-
const details = [
|
|
1916
|
-
{ label: "config", value: configPath },
|
|
1917
|
-
{ label: "contract", value: contractPath }
|
|
1918
|
-
];
|
|
1919
|
-
if (options.db) {
|
|
1920
|
-
details.push({ label: "database", value: options.db });
|
|
1921
|
-
}
|
|
1922
|
-
const header = formatStyledHeader({
|
|
2559
|
+
if (flags.json === "ndjson") {
|
|
2560
|
+
const result2 = notOk7(
|
|
2561
|
+
errorJsonFormatNotSupported({
|
|
1923
2562
|
command: "db sign",
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
flags
|
|
1928
|
-
});
|
|
1929
|
-
console.log(header);
|
|
1930
|
-
}
|
|
1931
|
-
let contractJsonContent;
|
|
1932
|
-
try {
|
|
1933
|
-
contractJsonContent = await readFile4(contractPathAbsolute, "utf-8");
|
|
1934
|
-
} catch (error) {
|
|
1935
|
-
if (error instanceof Error && error.code === "ENOENT") {
|
|
1936
|
-
throw errorFileNotFound3(contractPathAbsolute, {
|
|
1937
|
-
why: `Contract file not found at ${contractPathAbsolute}`
|
|
1938
|
-
});
|
|
1939
|
-
}
|
|
1940
|
-
throw errorUnexpected5(error instanceof Error ? error.message : String(error), {
|
|
1941
|
-
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
1942
|
-
});
|
|
1943
|
-
}
|
|
1944
|
-
const contractJson = JSON.parse(contractJsonContent);
|
|
1945
|
-
const dbUrl = options.db ?? config.db?.url;
|
|
1946
|
-
if (!dbUrl) {
|
|
1947
|
-
throw errorDatabaseUrlRequired4();
|
|
1948
|
-
}
|
|
1949
|
-
if (!config.driver) {
|
|
1950
|
-
throw errorDriverRequired4();
|
|
1951
|
-
}
|
|
1952
|
-
const driverDescriptor = config.driver;
|
|
1953
|
-
const stack = createControlPlaneStack5({
|
|
1954
|
-
target: config.target,
|
|
1955
|
-
adapter: config.adapter,
|
|
1956
|
-
driver: driverDescriptor,
|
|
1957
|
-
extensionPacks: config.extensionPacks
|
|
1958
|
-
});
|
|
1959
|
-
const familyInstance = config.family.create(stack);
|
|
1960
|
-
const contractIR = familyInstance.validateContractIR(contractJson);
|
|
1961
|
-
assertContractRequirementsSatisfied({ contract: contractIR, stack });
|
|
1962
|
-
const rawComponents = [config.target, config.adapter, ...config.extensionPacks ?? []];
|
|
1963
|
-
const frameworkComponents = assertFrameworkComponentsCompatible(
|
|
1964
|
-
config.family.familyId,
|
|
1965
|
-
config.target.targetId,
|
|
1966
|
-
rawComponents
|
|
2563
|
+
format: "ndjson",
|
|
2564
|
+
supportedFormats: ["object"]
|
|
2565
|
+
})
|
|
1967
2566
|
);
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
frameworkComponents
|
|
1980
|
-
}),
|
|
1981
|
-
{
|
|
1982
|
-
message: "Verifying database satisfies contract",
|
|
1983
|
-
flags
|
|
1984
|
-
}
|
|
1985
|
-
);
|
|
1986
|
-
} catch (error) {
|
|
1987
|
-
throw errorRuntime4(error instanceof Error ? error.message : String(error), {
|
|
1988
|
-
why: `Failed to verify database schema: ${error instanceof Error ? error.message : String(error)}`
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
1991
|
-
if (!schemaVerifyResult.ok) {
|
|
1992
|
-
return { schemaVerifyResult, signResult: void 0 };
|
|
1993
|
-
}
|
|
1994
|
-
let signResult;
|
|
1995
|
-
try {
|
|
1996
|
-
signResult = await withSpinner(
|
|
1997
|
-
() => familyInstance.sign({
|
|
1998
|
-
driver,
|
|
1999
|
-
contractIR,
|
|
2000
|
-
contractPath: contractPathAbsolute,
|
|
2001
|
-
configPath
|
|
2002
|
-
}),
|
|
2003
|
-
{
|
|
2004
|
-
message: "Signing database...",
|
|
2005
|
-
flags
|
|
2006
|
-
}
|
|
2007
|
-
);
|
|
2008
|
-
} catch (error) {
|
|
2009
|
-
throw errorRuntime4(error instanceof Error ? error.message : String(error), {
|
|
2010
|
-
why: `Failed to sign database: ${error instanceof Error ? error.message : String(error)}`
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
2014
|
-
console.log("");
|
|
2015
|
-
}
|
|
2016
|
-
return { schemaVerifyResult: void 0, signResult };
|
|
2017
|
-
} finally {
|
|
2018
|
-
await driver.close();
|
|
2019
|
-
}
|
|
2020
|
-
});
|
|
2021
|
-
const exitCode = handleResult(result, flags, (value) => {
|
|
2022
|
-
const { schemaVerifyResult, signResult } = value;
|
|
2023
|
-
if (schemaVerifyResult && !schemaVerifyResult.ok) {
|
|
2024
|
-
if (flags.json === "object") {
|
|
2025
|
-
console.log(formatSchemaVerifyJson(schemaVerifyResult));
|
|
2026
|
-
} else {
|
|
2027
|
-
const output = formatSchemaVerifyOutput(schemaVerifyResult, flags);
|
|
2028
|
-
if (output) {
|
|
2029
|
-
console.log(output);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
if (signResult) {
|
|
2035
|
-
if (flags.json === "object") {
|
|
2036
|
-
console.log(formatSignJson(signResult));
|
|
2037
|
-
} else {
|
|
2038
|
-
const output = formatSignOutput(signResult, flags);
|
|
2039
|
-
if (output) {
|
|
2040
|
-
console.log(output);
|
|
2041
|
-
}
|
|
2567
|
+
const exitCode = handleResult(result2, flags);
|
|
2568
|
+
process.exit(exitCode);
|
|
2569
|
+
}
|
|
2570
|
+
const result = await executeDbSignCommand(options, flags);
|
|
2571
|
+
if (result.ok) {
|
|
2572
|
+
if (flags.json === "object") {
|
|
2573
|
+
console.log(formatSignJson(result.value));
|
|
2574
|
+
} else {
|
|
2575
|
+
const output = formatSignOutput(result.value, flags);
|
|
2576
|
+
if (output) {
|
|
2577
|
+
console.log(output);
|
|
2042
2578
|
}
|
|
2043
2579
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2580
|
+
process.exit(0);
|
|
2581
|
+
}
|
|
2582
|
+
const failure = result.failure;
|
|
2583
|
+
if (failure instanceof CliStructuredError) {
|
|
2584
|
+
const exitCode = handleResult(result, flags);
|
|
2048
2585
|
process.exit(exitCode);
|
|
2049
2586
|
}
|
|
2587
|
+
if (flags.json === "object") {
|
|
2588
|
+
console.log(formatSchemaVerifyJson(failure));
|
|
2589
|
+
} else {
|
|
2590
|
+
const output = formatSchemaVerifyOutput(failure, flags);
|
|
2591
|
+
if (output) {
|
|
2592
|
+
console.log(output);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
process.exit(1);
|
|
2050
2596
|
});
|
|
2051
2597
|
return command;
|
|
2052
2598
|
}
|
|
2053
2599
|
|
|
2054
2600
|
// src/commands/db-verify.ts
|
|
2055
|
-
import { readFile as
|
|
2601
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2056
2602
|
import { relative as relative7, resolve as resolve7 } from "path";
|
|
2057
|
-
import {
|
|
2058
|
-
errorDatabaseUrlRequired as errorDatabaseUrlRequired5,
|
|
2059
|
-
errorDriverRequired as errorDriverRequired5,
|
|
2060
|
-
errorFileNotFound as errorFileNotFound4,
|
|
2061
|
-
errorHashMismatch as errorHashMismatch2,
|
|
2062
|
-
errorMarkerMissing as errorMarkerMissing2,
|
|
2063
|
-
errorRuntime as errorRuntime5,
|
|
2064
|
-
errorTargetMismatch as errorTargetMismatch2,
|
|
2065
|
-
errorUnexpected as errorUnexpected6
|
|
2066
|
-
} from "@prisma-next/core-control-plane/errors";
|
|
2067
|
-
import { createControlPlaneStack as createControlPlaneStack6 } from "@prisma-next/core-control-plane/types";
|
|
2603
|
+
import { notOk as notOk8, ok as ok8 } from "@prisma-next/utils/result";
|
|
2068
2604
|
import { Command as Command6 } from "commander";
|
|
2605
|
+
function mapVerifyFailure(verifyResult) {
|
|
2606
|
+
if (!verifyResult.ok && verifyResult.code) {
|
|
2607
|
+
if (verifyResult.code === "PN-RTM-3001") {
|
|
2608
|
+
return errorMarkerMissing();
|
|
2609
|
+
}
|
|
2610
|
+
if (verifyResult.code === "PN-RTM-3002") {
|
|
2611
|
+
return errorHashMismatch({
|
|
2612
|
+
expected: verifyResult.contract.coreHash,
|
|
2613
|
+
...verifyResult.marker?.coreHash ? { actual: verifyResult.marker.coreHash } : {}
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
if (verifyResult.code === "PN-RTM-3003") {
|
|
2617
|
+
return errorTargetMismatch(
|
|
2618
|
+
verifyResult.target.expected,
|
|
2619
|
+
verifyResult.target.actual ?? "unknown"
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return errorRuntime(verifyResult.summary);
|
|
2624
|
+
}
|
|
2625
|
+
async function executeDbVerifyCommand(options, flags) {
|
|
2626
|
+
const config = await loadConfig(options.config);
|
|
2627
|
+
const configPath = options.config ? relative7(process.cwd(), resolve7(options.config)) : "prisma-next.config.ts";
|
|
2628
|
+
const contractPathAbsolute = config.contract?.output ? resolve7(config.contract.output) : resolve7("src/prisma/contract.json");
|
|
2629
|
+
const contractPath = relative7(process.cwd(), contractPathAbsolute);
|
|
2630
|
+
if (flags.json !== "object" && !flags.quiet) {
|
|
2631
|
+
const details = [
|
|
2632
|
+
{ label: "config", value: configPath },
|
|
2633
|
+
{ label: "contract", value: contractPath }
|
|
2634
|
+
];
|
|
2635
|
+
if (options.db) {
|
|
2636
|
+
details.push({ label: "database", value: options.db });
|
|
2637
|
+
}
|
|
2638
|
+
const header = formatStyledHeader({
|
|
2639
|
+
command: "db verify",
|
|
2640
|
+
description: "Check whether the database has been signed with your contract",
|
|
2641
|
+
url: "https://pris.ly/db-verify",
|
|
2642
|
+
details,
|
|
2643
|
+
flags
|
|
2644
|
+
});
|
|
2645
|
+
console.log(header);
|
|
2646
|
+
}
|
|
2647
|
+
let contractJsonContent;
|
|
2648
|
+
try {
|
|
2649
|
+
contractJsonContent = await readFile4(contractPathAbsolute, "utf-8");
|
|
2650
|
+
} catch (error) {
|
|
2651
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
2652
|
+
return notOk8(
|
|
2653
|
+
errorFileNotFound(contractPathAbsolute, {
|
|
2654
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
2655
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
2656
|
+
})
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
return notOk8(
|
|
2660
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2661
|
+
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
2662
|
+
})
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
let contractJson;
|
|
2666
|
+
try {
|
|
2667
|
+
contractJson = JSON.parse(contractJsonContent);
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
return notOk8(
|
|
2670
|
+
errorContractValidationFailed(
|
|
2671
|
+
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2672
|
+
{ where: { path: contractPathAbsolute } }
|
|
2673
|
+
)
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
const dbConnection = options.db ?? config.db?.connection;
|
|
2677
|
+
if (!dbConnection) {
|
|
2678
|
+
return notOk8(
|
|
2679
|
+
errorDatabaseConnectionRequired({
|
|
2680
|
+
why: `Database connection is required for db verify (set db.connection in ${configPath}, or pass --db <url>)`
|
|
2681
|
+
})
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
if (!config.driver) {
|
|
2685
|
+
return notOk8(errorDriverRequired({ why: "Config.driver is required for db verify" }));
|
|
2686
|
+
}
|
|
2687
|
+
const client = createControlClient({
|
|
2688
|
+
family: config.family,
|
|
2689
|
+
target: config.target,
|
|
2690
|
+
adapter: config.adapter,
|
|
2691
|
+
driver: config.driver,
|
|
2692
|
+
extensionPacks: config.extensionPacks ?? []
|
|
2693
|
+
});
|
|
2694
|
+
const onProgress = createProgressAdapter({ flags });
|
|
2695
|
+
try {
|
|
2696
|
+
const verifyResult = await client.verify({
|
|
2697
|
+
contractIR: contractJson,
|
|
2698
|
+
connection: dbConnection,
|
|
2699
|
+
onProgress
|
|
2700
|
+
});
|
|
2701
|
+
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
2702
|
+
console.log("");
|
|
2703
|
+
}
|
|
2704
|
+
if (!verifyResult.ok) {
|
|
2705
|
+
return notOk8(mapVerifyFailure(verifyResult));
|
|
2706
|
+
}
|
|
2707
|
+
return ok8(verifyResult);
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
if (error instanceof CliStructuredError) {
|
|
2710
|
+
return notOk8(error);
|
|
2711
|
+
}
|
|
2712
|
+
return notOk8(
|
|
2713
|
+
errorUnexpected2(error instanceof Error ? error.message : String(error), {
|
|
2714
|
+
why: `Unexpected error during db verify: ${error instanceof Error ? error.message : String(error)}`
|
|
2715
|
+
})
|
|
2716
|
+
);
|
|
2717
|
+
} finally {
|
|
2718
|
+
await client.close();
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2069
2721
|
function createDbVerifyCommand() {
|
|
2070
2722
|
const command = new Command6("verify");
|
|
2071
2723
|
setCommandDescriptions(
|
|
@@ -2078,112 +2730,20 @@ function createDbVerifyCommand() {
|
|
|
2078
2730
|
const flags = parseGlobalFlags({});
|
|
2079
2731
|
return formatCommandHelp({ command: cmd, flags });
|
|
2080
2732
|
}
|
|
2081
|
-
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object
|
|
2733
|
+
}).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--json [format]", "Output as JSON (object)", false).option("-q, --quiet", "Quiet mode: errors only").option("-v, --verbose", "Verbose output: debug info, timings").option("-vv, --trace", "Trace output: deep internals, stack traces").option("--timestamps", "Add timestamps to output").option("--color", "Force color output").option("--no-color", "Disable color output").action(async (options) => {
|
|
2082
2734
|
const flags = parseGlobalFlags(options);
|
|
2083
|
-
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
const contractPathAbsolute = config.contract?.output ? resolve7(config.contract.output) : resolve7("src/prisma/contract.json");
|
|
2087
|
-
const contractPath = relative7(process.cwd(), contractPathAbsolute);
|
|
2088
|
-
if (flags.json !== "object" && !flags.quiet) {
|
|
2089
|
-
const details = [
|
|
2090
|
-
{ label: "config", value: configPath },
|
|
2091
|
-
{ label: "contract", value: contractPath }
|
|
2092
|
-
];
|
|
2093
|
-
if (options.db) {
|
|
2094
|
-
details.push({ label: "database", value: options.db });
|
|
2095
|
-
}
|
|
2096
|
-
const header = formatStyledHeader({
|
|
2735
|
+
if (flags.json === "ndjson") {
|
|
2736
|
+
const result2 = notOk8(
|
|
2737
|
+
errorJsonFormatNotSupported({
|
|
2097
2738
|
command: "db verify",
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
try {
|
|
2107
|
-
contractJsonContent = await readFile5(contractPathAbsolute, "utf-8");
|
|
2108
|
-
} catch (error) {
|
|
2109
|
-
if (error instanceof Error && error.code === "ENOENT") {
|
|
2110
|
-
throw errorFileNotFound4(contractPathAbsolute, {
|
|
2111
|
-
why: `Contract file not found at ${contractPathAbsolute}`
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
throw errorUnexpected6(error instanceof Error ? error.message : String(error), {
|
|
2115
|
-
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
const contractJson = JSON.parse(contractJsonContent);
|
|
2119
|
-
const dbUrl = options.db ?? config.db?.url;
|
|
2120
|
-
if (!dbUrl) {
|
|
2121
|
-
throw errorDatabaseUrlRequired5();
|
|
2122
|
-
}
|
|
2123
|
-
if (!config.driver) {
|
|
2124
|
-
throw errorDriverRequired5();
|
|
2125
|
-
}
|
|
2126
|
-
const driverDescriptor = config.driver;
|
|
2127
|
-
const driver = await withSpinner(() => driverDescriptor.create(dbUrl), {
|
|
2128
|
-
message: "Connecting to database...",
|
|
2129
|
-
flags
|
|
2130
|
-
});
|
|
2131
|
-
try {
|
|
2132
|
-
const stack = createControlPlaneStack6({
|
|
2133
|
-
target: config.target,
|
|
2134
|
-
adapter: config.adapter,
|
|
2135
|
-
driver: driverDescriptor,
|
|
2136
|
-
extensionPacks: config.extensionPacks
|
|
2137
|
-
});
|
|
2138
|
-
const familyInstance = config.family.create(stack);
|
|
2139
|
-
const contractIR = familyInstance.validateContractIR(contractJson);
|
|
2140
|
-
assertContractRequirementsSatisfied({ contract: contractIR, stack });
|
|
2141
|
-
let verifyResult;
|
|
2142
|
-
try {
|
|
2143
|
-
verifyResult = await withSpinner(
|
|
2144
|
-
() => familyInstance.verify({
|
|
2145
|
-
driver,
|
|
2146
|
-
contractIR,
|
|
2147
|
-
expectedTargetId: config.target.targetId,
|
|
2148
|
-
contractPath: contractPathAbsolute,
|
|
2149
|
-
configPath
|
|
2150
|
-
}),
|
|
2151
|
-
{
|
|
2152
|
-
message: "Verifying database schema...",
|
|
2153
|
-
flags
|
|
2154
|
-
}
|
|
2155
|
-
);
|
|
2156
|
-
} catch (error) {
|
|
2157
|
-
throw errorUnexpected6(error instanceof Error ? error.message : String(error), {
|
|
2158
|
-
why: `Failed to verify database: ${error instanceof Error ? error.message : String(error)}`
|
|
2159
|
-
});
|
|
2160
|
-
}
|
|
2161
|
-
if (!verifyResult.ok && verifyResult.code) {
|
|
2162
|
-
if (verifyResult.code === "PN-RTM-3001") {
|
|
2163
|
-
throw errorMarkerMissing2();
|
|
2164
|
-
}
|
|
2165
|
-
if (verifyResult.code === "PN-RTM-3002") {
|
|
2166
|
-
throw errorHashMismatch2({
|
|
2167
|
-
expected: verifyResult.contract.coreHash,
|
|
2168
|
-
...verifyResult.marker?.coreHash ? { actual: verifyResult.marker.coreHash } : {}
|
|
2169
|
-
});
|
|
2170
|
-
}
|
|
2171
|
-
if (verifyResult.code === "PN-RTM-3003") {
|
|
2172
|
-
throw errorTargetMismatch2(
|
|
2173
|
-
verifyResult.target.expected,
|
|
2174
|
-
verifyResult.target.actual ?? "unknown"
|
|
2175
|
-
);
|
|
2176
|
-
}
|
|
2177
|
-
throw errorRuntime5(verifyResult.summary);
|
|
2178
|
-
}
|
|
2179
|
-
if (!flags.quiet && flags.json !== "object" && process.stdout.isTTY) {
|
|
2180
|
-
console.log("");
|
|
2181
|
-
}
|
|
2182
|
-
return verifyResult;
|
|
2183
|
-
} finally {
|
|
2184
|
-
await driver.close();
|
|
2185
|
-
}
|
|
2186
|
-
});
|
|
2739
|
+
format: "ndjson",
|
|
2740
|
+
supportedFormats: ["object"]
|
|
2741
|
+
})
|
|
2742
|
+
);
|
|
2743
|
+
const exitCode2 = handleResult(result2, flags);
|
|
2744
|
+
process.exit(exitCode2);
|
|
2745
|
+
}
|
|
2746
|
+
const result = await executeDbVerifyCommand(options, flags);
|
|
2187
2747
|
const exitCode = handleResult(result, flags, (verifyResult) => {
|
|
2188
2748
|
if (flags.json === "object") {
|
|
2189
2749
|
console.log(formatVerifyJson(verifyResult));
|