@prisma-next/cli 0.3.0-dev.53 → 0.3.0-dev.55

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.
Files changed (62) hide show
  1. package/README.md +24 -0
  2. package/dist/cli.mjs +5 -3
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-BSZKpZTF.mjs → client-B7f4PZZ1.mjs} +367 -170
  5. package/dist/client-B7f4PZZ1.mjs.map +1 -0
  6. package/dist/commands/contract-emit.d.mts.map +1 -1
  7. package/dist/commands/contract-emit.mjs +7 -6
  8. package/dist/commands/contract-emit.mjs.map +1 -1
  9. package/dist/commands/db-init.d.mts.map +1 -1
  10. package/dist/commands/db-init.mjs +28 -76
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-introspect.d.mts.map +1 -1
  13. package/dist/commands/db-introspect.mjs +12 -17
  14. package/dist/commands/db-introspect.mjs.map +1 -1
  15. package/dist/commands/db-schema-verify.d.mts.map +1 -1
  16. package/dist/commands/db-schema-verify.mjs +5 -4
  17. package/dist/commands/db-schema-verify.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +6 -5
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts +7 -0
  22. package/dist/commands/db-update.d.mts.map +1 -0
  23. package/dist/commands/db-update.mjs +120 -0
  24. package/dist/commands/db-update.mjs.map +1 -0
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +5 -4
  27. package/dist/commands/db-verify.mjs.map +1 -1
  28. package/dist/{config-loader-BJ8HsEdA.mjs → config-loader-DqKf1qSa.mjs} +1 -1
  29. package/dist/{config-loader-BJ8HsEdA.mjs.map → config-loader-DqKf1qSa.mjs.map} +1 -1
  30. package/dist/config-loader.mjs +1 -1
  31. package/dist/exports/control-api.d.mts +96 -6
  32. package/dist/exports/control-api.d.mts.map +1 -1
  33. package/dist/exports/control-api.mjs +2 -2
  34. package/dist/exports/index.mjs +1 -3
  35. package/dist/exports/index.mjs.map +1 -1
  36. package/dist/migration-command-scaffold-BELw_do2.mjs +95 -0
  37. package/dist/migration-command-scaffold-BELw_do2.mjs.map +1 -0
  38. package/dist/{result-handler-BZPY7HX4.mjs → result-handler-BhmrXIvT.mjs} +63 -13
  39. package/dist/result-handler-BhmrXIvT.mjs.map +1 -0
  40. package/package.json +14 -10
  41. package/src/cli.ts +5 -0
  42. package/src/commands/contract-emit.ts +22 -6
  43. package/src/commands/db-init.ts +89 -197
  44. package/src/commands/db-introspect.ts +4 -8
  45. package/src/commands/db-schema-verify.ts +11 -2
  46. package/src/commands/db-sign.ts +13 -4
  47. package/src/commands/db-update.ts +220 -0
  48. package/src/commands/db-verify.ts +11 -2
  49. package/src/control-api/client.ts +109 -145
  50. package/src/control-api/errors.ts +9 -0
  51. package/src/control-api/operations/db-init.ts +39 -34
  52. package/src/control-api/operations/db-update.ts +221 -0
  53. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  54. package/src/control-api/operations/migration-helpers.ts +49 -0
  55. package/src/control-api/types.ts +104 -4
  56. package/src/exports/control-api.ts +5 -0
  57. package/src/utils/cli-errors.ts +2 -0
  58. package/src/utils/command-helpers.ts +81 -3
  59. package/src/utils/migration-command-scaffold.ts +189 -0
  60. package/src/utils/output.ts +43 -13
  61. package/dist/client-BSZKpZTF.mjs.map +0 -1
  62. package/dist/result-handler-BZPY7HX4.mjs.map +0 -1
@@ -0,0 +1,220 @@
1
+ import { ifDefined } from '@prisma-next/utils/defined';
2
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
3
+ import { Command } from 'commander';
4
+ import { ContractValidationError } from '../control-api/errors';
5
+ import type { DbUpdateFailure } from '../control-api/types';
6
+ import {
7
+ CliStructuredError,
8
+ errorContractValidationFailed,
9
+ errorDestructiveChanges,
10
+ errorJsonFormatNotSupported,
11
+ errorMigrationPlanningFailed,
12
+ errorRunnerFailed,
13
+ errorUnexpected,
14
+ } from '../utils/cli-errors';
15
+ import type { MigrationCommandOptions } from '../utils/command-helpers';
16
+ import { sanitizeErrorMessage, setCommandDescriptions } from '../utils/command-helpers';
17
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
18
+ import {
19
+ addMigrationCommandOptions,
20
+ prepareMigrationContext,
21
+ } from '../utils/migration-command-scaffold';
22
+ import {
23
+ formatCommandHelp,
24
+ formatMigrationApplyOutput,
25
+ formatMigrationJson,
26
+ formatMigrationPlanOutput,
27
+ type MigrationCommandResult,
28
+ } from '../utils/output';
29
+ import { handleResult } from '../utils/result-handler';
30
+
31
+ type DbUpdateOptions = MigrationCommandOptions & {
32
+ readonly acceptDataLoss?: boolean;
33
+ };
34
+
35
+ /**
36
+ * Maps a DbUpdateFailure to a CliStructuredError for consistent error handling.
37
+ */
38
+ function mapDbUpdateFailure(failure: DbUpdateFailure): CliStructuredError {
39
+ if (failure.code === 'PLANNING_FAILED') {
40
+ return errorMigrationPlanningFailed({ conflicts: failure.conflicts ?? [] });
41
+ }
42
+
43
+ if (failure.code === 'RUNNER_FAILED') {
44
+ return errorRunnerFailed(failure.summary, {
45
+ why: failure.why ?? 'Migration runner failed',
46
+ fix: 'Inspect the reported conflict, reconcile schema drift if needed, then re-run `prisma-next db update`',
47
+ ...ifDefined('meta', failure.meta),
48
+ });
49
+ }
50
+
51
+ if (failure.code === 'DESTRUCTIVE_CHANGES') {
52
+ return errorDestructiveChanges(failure.summary, {
53
+ ...ifDefined('why', failure.why),
54
+ fix: 'Use `prisma-next db update --plan` to preview, then re-run with `--accept-data-loss` to apply destructive changes',
55
+ ...ifDefined('meta', failure.meta),
56
+ });
57
+ }
58
+
59
+ const exhaustive: never = failure.code;
60
+ throw new Error(`Unhandled DbUpdateFailure code: ${exhaustive}`);
61
+ }
62
+
63
+ /**
64
+ * Executes the db update command and returns a structured Result.
65
+ */
66
+ async function executeDbUpdateCommand(
67
+ options: DbUpdateOptions,
68
+ flags: GlobalFlags,
69
+ startTime: number,
70
+ ): Promise<Result<MigrationCommandResult, CliStructuredError>> {
71
+ // Prepare shared migration context (config, contract, connection, client)
72
+ const ctxResult = await prepareMigrationContext(options, flags, {
73
+ commandName: 'db update',
74
+ description: 'Update your database schema to match your contract',
75
+ url: 'https://pris.ly/db-update',
76
+ });
77
+ if (!ctxResult.ok) {
78
+ return ctxResult;
79
+ }
80
+ const { client, contractJson, dbConnection, onProgress, contractPathAbsolute } = ctxResult.value;
81
+
82
+ try {
83
+ // Call dbUpdate with connection and progress callback
84
+ const result = await client.dbUpdate({
85
+ contractIR: contractJson,
86
+ mode: options.plan ? 'plan' : 'apply',
87
+ connection: dbConnection,
88
+ ...(options.acceptDataLoss ? { acceptDataLoss: true } : {}),
89
+ onProgress,
90
+ });
91
+
92
+ // Handle failures by mapping to CLI structured error
93
+ if (!result.ok) {
94
+ return notOk(mapDbUpdateFailure(result.failure));
95
+ }
96
+
97
+ // Convert success result to CLI output format
98
+ const dbUpdateResult: MigrationCommandResult = {
99
+ ok: true,
100
+ mode: result.value.mode,
101
+ plan: {
102
+ targetId: ctxResult.value.config.target.targetId,
103
+ destination: {
104
+ storageHash: result.value.destination.storageHash,
105
+ ...ifDefined('profileHash', result.value.destination.profileHash),
106
+ },
107
+ operations: result.value.plan.operations.map((op) => ({
108
+ id: op.id,
109
+ label: op.label,
110
+ operationClass: op.operationClass,
111
+ })),
112
+ ...ifDefined('sql', result.value.plan.sql),
113
+ },
114
+ ...ifDefined(
115
+ 'execution',
116
+ result.value.execution
117
+ ? {
118
+ operationsPlanned: result.value.execution.operationsPlanned,
119
+ operationsExecuted: result.value.execution.operationsExecuted,
120
+ }
121
+ : undefined,
122
+ ),
123
+ ...ifDefined(
124
+ 'marker',
125
+ result.value.marker
126
+ ? {
127
+ storageHash: result.value.marker.storageHash,
128
+ ...ifDefined('profileHash', result.value.marker.profileHash),
129
+ }
130
+ : undefined,
131
+ ),
132
+ summary: result.value.summary,
133
+ timings: { total: Date.now() - startTime },
134
+ };
135
+
136
+ return ok(dbUpdateResult);
137
+ } catch (error) {
138
+ if (CliStructuredError.is(error)) {
139
+ return notOk(error);
140
+ }
141
+
142
+ if (error instanceof ContractValidationError) {
143
+ return notOk(
144
+ errorContractValidationFailed(`Contract validation failed: ${error.message}`, {
145
+ where: { path: contractPathAbsolute },
146
+ }),
147
+ );
148
+ }
149
+
150
+ const rawMessage = error instanceof Error ? error.message : String(error);
151
+ const safeMessage = sanitizeErrorMessage(
152
+ rawMessage,
153
+ typeof dbConnection === 'string' ? dbConnection : undefined,
154
+ );
155
+ return notOk(
156
+ errorUnexpected(safeMessage, {
157
+ why: `Unexpected error during db update: ${safeMessage}`,
158
+ }),
159
+ );
160
+ } finally {
161
+ await client.close();
162
+ }
163
+ }
164
+
165
+ export function createDbUpdateCommand(): Command {
166
+ const command = new Command('update');
167
+ setCommandDescriptions(
168
+ command,
169
+ 'Update your database schema to match your contract',
170
+ 'Compares your database schema to the emitted contract and applies the necessary\n' +
171
+ 'changes. Works on any database, whether or not it has been initialized with `db init`.\n' +
172
+ 'Use --plan to preview operations before applying.',
173
+ );
174
+ addMigrationCommandOptions(command);
175
+ command.option(
176
+ '--accept-data-loss',
177
+ 'Confirm destructive operations (required when plan includes drops or type changes)',
178
+ false,
179
+ );
180
+ command.configureHelp({
181
+ formatHelp: (cmd) => {
182
+ const flags = parseGlobalFlags({});
183
+ return formatCommandHelp({ command: cmd, flags });
184
+ },
185
+ });
186
+ command.action(async (options: DbUpdateOptions) => {
187
+ const flags = parseGlobalFlags(options);
188
+ const startTime = Date.now();
189
+
190
+ if (flags.json === 'ndjson') {
191
+ const result = notOk(
192
+ errorJsonFormatNotSupported({
193
+ command: 'db update',
194
+ format: 'ndjson',
195
+ supportedFormats: ['object'],
196
+ }),
197
+ );
198
+ const exitCode = handleResult(result, flags);
199
+ process.exit(exitCode);
200
+ }
201
+
202
+ const result = await executeDbUpdateCommand(options, flags, startTime);
203
+ const exitCode = handleResult(result, flags, (dbUpdateResult) => {
204
+ if (flags.json === 'object') {
205
+ console.log(formatMigrationJson(dbUpdateResult));
206
+ } else {
207
+ const output =
208
+ dbUpdateResult.mode === 'plan'
209
+ ? formatMigrationPlanOutput(dbUpdateResult, flags)
210
+ : formatMigrationApplyOutput(dbUpdateResult, flags);
211
+ if (output) {
212
+ console.log(output);
213
+ }
214
+ }
215
+ });
216
+ process.exit(exitCode);
217
+ });
218
+
219
+ return command;
220
+ }
@@ -6,6 +6,7 @@ import { notOk, ok, type Result } from '@prisma-next/utils/result';
6
6
  import { Command } from 'commander';
7
7
  import { loadConfig } from '../config-loader';
8
8
  import { createControlClient } from '../control-api/client';
9
+ import { ContractValidationError } from '../control-api/errors';
9
10
  import {
10
11
  CliStructuredError,
11
12
  errorContractValidationFailed,
@@ -19,7 +20,7 @@ import {
19
20
  errorTargetMismatch,
20
21
  errorUnexpected,
21
22
  } from '../utils/cli-errors';
22
- import { setCommandDescriptions } from '../utils/command-helpers';
23
+ import { maskConnectionUrl, setCommandDescriptions } from '../utils/command-helpers';
23
24
  import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
24
25
  import {
25
26
  formatCommandHelp,
@@ -94,7 +95,7 @@ async function executeDbVerifyCommand(
94
95
  { label: 'contract', value: contractPath },
95
96
  ];
96
97
  if (options.db) {
97
- details.push({ label: 'database', value: options.db });
98
+ details.push({ label: 'database', value: maskConnectionUrl(options.db) });
98
99
  }
99
100
  const header = formatStyledHeader({
100
101
  command: 'db verify',
@@ -189,6 +190,14 @@ async function executeDbVerifyCommand(
189
190
  return notOk(error);
190
191
  }
191
192
 
193
+ if (error instanceof ContractValidationError) {
194
+ return notOk(
195
+ errorContractValidationFailed(`Contract validation failed: ${error.message}`, {
196
+ where: { path: contractPathAbsolute },
197
+ }),
198
+ );
199
+ }
200
+
192
201
  // Wrap unexpected errors
193
202
  return notOk(
194
203
  errorUnexpected(error instanceof Error ? error.message : String(error), {
@@ -1,4 +1,5 @@
1
1
  import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
2
+ import type { ContractIR } from '@prisma-next/contract/ir';
2
3
  import type { CoreSchemaView } from '@prisma-next/core-control-plane/schema-view';
3
4
  import { createControlPlaneStack } from '@prisma-next/core-control-plane/stack';
4
5
  import type {
@@ -12,15 +13,21 @@ import type {
12
13
  import { ifDefined } from '@prisma-next/utils/defined';
13
14
  import { notOk, ok } from '@prisma-next/utils/result';
14
15
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
16
+ import { ContractValidationError } from './errors';
15
17
  import { executeDbInit } from './operations/db-init';
18
+ import { executeDbUpdate } from './operations/db-update';
16
19
  import type {
20
+ ControlActionName,
17
21
  ControlClient,
18
22
  ControlClientOptions,
19
23
  DbInitOptions,
20
24
  DbInitResult,
25
+ DbUpdateOptions,
26
+ DbUpdateResult,
21
27
  EmitOptions,
22
28
  EmitResult,
23
29
  IntrospectOptions,
30
+ OnControlProgress,
24
31
  SchemaVerifyOptions,
25
32
  SignOptions,
26
33
  VerifyOptions,
@@ -152,47 +159,47 @@ class ControlClientImpl implements ControlClient {
152
159
  };
153
160
  }
154
161
 
155
- async verify(options: VerifyOptions): Promise<VerifyDatabaseResult> {
156
- const { onProgress } = options;
157
-
158
- // Connect with progress span if connection provided
159
- if (options.connection !== undefined) {
160
- onProgress?.({
161
- action: 'verify',
162
- kind: 'spanStart',
163
- spanId: 'connect',
164
- label: 'Connecting to database...',
165
- });
166
- try {
167
- await this.connect(options.connection);
168
- onProgress?.({
169
- action: 'verify',
170
- kind: 'spanEnd',
171
- spanId: 'connect',
172
- outcome: 'ok',
173
- });
174
- } catch (error) {
175
- onProgress?.({
176
- action: 'verify',
177
- kind: 'spanEnd',
178
- spanId: 'connect',
179
- outcome: 'error',
180
- });
181
- throw error;
182
- }
162
+ private async connectWithProgress(
163
+ connection: unknown | undefined,
164
+ action: ControlActionName,
165
+ onProgress?: OnControlProgress,
166
+ ): Promise<void> {
167
+ if (connection === undefined) return;
168
+ onProgress?.({
169
+ action,
170
+ kind: 'spanStart',
171
+ spanId: 'connect',
172
+ label: 'Connecting to database...',
173
+ });
174
+ try {
175
+ await this.connect(connection);
176
+ onProgress?.({ action, kind: 'spanEnd', spanId: 'connect', outcome: 'ok' });
177
+ } catch (error) {
178
+ onProgress?.({ action, kind: 'spanEnd', spanId: 'connect', outcome: 'error' });
179
+ throw error;
183
180
  }
181
+ }
184
182
 
183
+ async verify(options: VerifyOptions): Promise<VerifyDatabaseResult> {
184
+ const { onProgress } = options;
185
+ await this.connectWithProgress(options.connection, 'verify', onProgress);
185
186
  const { driver, familyInstance } = await this.ensureConnected();
186
187
 
187
188
  // Validate contract using family instance
188
- const contractIR = familyInstance.validateContractIR(options.contractIR);
189
+ let contractIR: ContractIR;
190
+ try {
191
+ contractIR = familyInstance.validateContractIR(options.contractIR);
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ throw new ContractValidationError(message, error);
195
+ }
189
196
 
190
197
  // Emit verify span
191
198
  onProgress?.({
192
199
  action: 'verify',
193
200
  kind: 'spanStart',
194
201
  spanId: 'verify',
195
- label: 'Verifying contract marker...',
202
+ label: 'Verifying database signature...',
196
203
  });
197
204
 
198
205
  try {
@@ -228,38 +235,17 @@ class ControlClientImpl implements ControlClient {
228
235
 
229
236
  async schemaVerify(options: SchemaVerifyOptions): Promise<VerifyDatabaseSchemaResult> {
230
237
  const { onProgress } = options;
231
-
232
- // Connect with progress span if connection provided
233
- if (options.connection !== undefined) {
234
- onProgress?.({
235
- action: 'schemaVerify',
236
- kind: 'spanStart',
237
- spanId: 'connect',
238
- label: 'Connecting to database...',
239
- });
240
- try {
241
- await this.connect(options.connection);
242
- onProgress?.({
243
- action: 'schemaVerify',
244
- kind: 'spanEnd',
245
- spanId: 'connect',
246
- outcome: 'ok',
247
- });
248
- } catch (error) {
249
- onProgress?.({
250
- action: 'schemaVerify',
251
- kind: 'spanEnd',
252
- spanId: 'connect',
253
- outcome: 'error',
254
- });
255
- throw error;
256
- }
257
- }
258
-
238
+ await this.connectWithProgress(options.connection, 'schemaVerify', onProgress);
259
239
  const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
260
240
 
261
241
  // Validate contract using family instance
262
- const contractIR = familyInstance.validateContractIR(options.contractIR);
242
+ let contractIR: ContractIR;
243
+ try {
244
+ contractIR = familyInstance.validateContractIR(options.contractIR);
245
+ } catch (error) {
246
+ const message = error instanceof Error ? error.message : String(error);
247
+ throw new ContractValidationError(message, error);
248
+ }
263
249
 
264
250
  // Emit schemaVerify span
265
251
  onProgress?.({
@@ -300,38 +286,17 @@ class ControlClientImpl implements ControlClient {
300
286
 
301
287
  async sign(options: SignOptions): Promise<SignDatabaseResult> {
302
288
  const { onProgress } = options;
303
-
304
- // Connect with progress span if connection provided
305
- if (options.connection !== undefined) {
306
- onProgress?.({
307
- action: 'sign',
308
- kind: 'spanStart',
309
- spanId: 'connect',
310
- label: 'Connecting to database...',
311
- });
312
- try {
313
- await this.connect(options.connection);
314
- onProgress?.({
315
- action: 'sign',
316
- kind: 'spanEnd',
317
- spanId: 'connect',
318
- outcome: 'ok',
319
- });
320
- } catch (error) {
321
- onProgress?.({
322
- action: 'sign',
323
- kind: 'spanEnd',
324
- spanId: 'connect',
325
- outcome: 'error',
326
- });
327
- throw error;
328
- }
329
- }
330
-
289
+ await this.connectWithProgress(options.connection, 'sign', onProgress);
331
290
  const { driver, familyInstance } = await this.ensureConnected();
332
291
 
333
292
  // Validate contract using family instance
334
- const contractIR = familyInstance.validateContractIR(options.contractIR);
293
+ let contractIR: ContractIR;
294
+ try {
295
+ contractIR = familyInstance.validateContractIR(options.contractIR);
296
+ } catch (error) {
297
+ const message = error instanceof Error ? error.message : String(error);
298
+ throw new ContractValidationError(message, error);
299
+ }
335
300
 
336
301
  // Emit sign span
337
302
  onProgress?.({
@@ -371,34 +336,7 @@ class ControlClientImpl implements ControlClient {
371
336
 
372
337
  async dbInit(options: DbInitOptions): Promise<DbInitResult> {
373
338
  const { onProgress } = options;
374
-
375
- // Connect with progress span if connection provided
376
- if (options.connection !== undefined) {
377
- onProgress?.({
378
- action: 'dbInit',
379
- kind: 'spanStart',
380
- spanId: 'connect',
381
- label: 'Connecting to database...',
382
- });
383
- try {
384
- await this.connect(options.connection);
385
- onProgress?.({
386
- action: 'dbInit',
387
- kind: 'spanEnd',
388
- spanId: 'connect',
389
- outcome: 'ok',
390
- });
391
- } catch (error) {
392
- onProgress?.({
393
- action: 'dbInit',
394
- kind: 'spanEnd',
395
- spanId: 'connect',
396
- outcome: 'error',
397
- });
398
- throw error;
399
- }
400
- }
401
-
339
+ await this.connectWithProgress(options.connection, 'dbInit', onProgress);
402
340
  const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
403
341
 
404
342
  // Check target supports migrations
@@ -407,7 +345,13 @@ class ControlClientImpl implements ControlClient {
407
345
  }
408
346
 
409
347
  // Validate contract using family instance
410
- const contractIR = familyInstance.validateContractIR(options.contractIR);
348
+ let contractIR: ContractIR;
349
+ try {
350
+ contractIR = familyInstance.validateContractIR(options.contractIR);
351
+ } catch (error) {
352
+ const message = error instanceof Error ? error.message : String(error);
353
+ throw new ContractValidationError(message, error);
354
+ }
411
355
 
412
356
  // Delegate to extracted dbInit operation
413
357
  return executeDbInit({
@@ -417,40 +361,42 @@ class ControlClientImpl implements ControlClient {
417
361
  mode: options.mode,
418
362
  migrations: this.options.target.migrations,
419
363
  frameworkComponents,
420
- ...(onProgress ? { onProgress } : {}),
364
+ ...ifDefined('onProgress', onProgress),
421
365
  });
422
366
  }
423
367
 
424
- async introspect(options?: IntrospectOptions): Promise<unknown> {
425
- const onProgress = options?.onProgress;
368
+ async dbUpdate(options: DbUpdateOptions): Promise<DbUpdateResult> {
369
+ const { onProgress } = options;
370
+ await this.connectWithProgress(options.connection, 'dbUpdate', onProgress);
371
+ const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
426
372
 
427
- // Connect with progress span if connection provided
428
- if (options?.connection !== undefined) {
429
- onProgress?.({
430
- action: 'introspect',
431
- kind: 'spanStart',
432
- spanId: 'connect',
433
- label: 'Connecting to database...',
434
- });
435
- try {
436
- await this.connect(options.connection);
437
- onProgress?.({
438
- action: 'introspect',
439
- kind: 'spanEnd',
440
- spanId: 'connect',
441
- outcome: 'ok',
442
- });
443
- } catch (error) {
444
- onProgress?.({
445
- action: 'introspect',
446
- kind: 'spanEnd',
447
- spanId: 'connect',
448
- outcome: 'error',
449
- });
450
- throw error;
451
- }
373
+ if (!this.options.target.migrations) {
374
+ throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
452
375
  }
453
376
 
377
+ let contractIR: ContractIR;
378
+ try {
379
+ contractIR = familyInstance.validateContractIR(options.contractIR);
380
+ } catch (error) {
381
+ const message = error instanceof Error ? error.message : String(error);
382
+ throw new ContractValidationError(message, error);
383
+ }
384
+
385
+ return executeDbUpdate({
386
+ driver,
387
+ familyInstance,
388
+ contractIR,
389
+ mode: options.mode,
390
+ migrations: this.options.target.migrations,
391
+ frameworkComponents,
392
+ ...ifDefined('acceptDataLoss', options.acceptDataLoss),
393
+ ...ifDefined('onProgress', onProgress),
394
+ });
395
+ }
396
+
397
+ async introspect(options?: IntrospectOptions): Promise<unknown> {
398
+ const onProgress = options?.onProgress;
399
+ await this.connectWithProgress(options?.connection, 'introspect', onProgress);
454
400
  const { driver, familyInstance } = await this.ensureConnected();
455
401
 
456
402
  // TODO: Pass schema option to familyInstance.introspect when schema filtering is implemented
@@ -575,6 +521,24 @@ class ControlClientImpl implements ControlClient {
575
521
  });
576
522
 
577
523
  try {
524
+ try {
525
+ this.familyInstance.validateContractIR(contractRaw);
526
+ } catch (error) {
527
+ onProgress?.({
528
+ action: 'emit',
529
+ kind: 'spanEnd',
530
+ spanId: 'emit',
531
+ outcome: 'error',
532
+ });
533
+ const message = error instanceof Error ? error.message : String(error);
534
+ return notOk({
535
+ code: 'CONTRACT_VALIDATION_FAILED',
536
+ summary: 'Contract validation failed',
537
+ why: message,
538
+ meta: undefined,
539
+ });
540
+ }
541
+
578
542
  const emitResult = await this.familyInstance.emitContract({ contractIR: contractRaw });
579
543
 
580
544
  onProgress?.({
@@ -0,0 +1,9 @@
1
+ export class ContractValidationError extends Error {
2
+ override readonly cause?: unknown;
3
+
4
+ constructor(message: string, cause?: unknown) {
5
+ super(message);
6
+ this.name = 'ContractValidationError';
7
+ this.cause = cause;
8
+ }
9
+ }