@prisma-next/cli 0.4.0-dev.9 → 0.5.0-dev.1

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 (116) hide show
  1. package/README.md +26 -18
  2. package/dist/cli-errors-C0JhVj0c.d.mts +4 -0
  3. package/dist/cli-errors-DHq6GQGu.mjs +5 -0
  4. package/dist/cli.mjs +7 -18
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{client-CJxHfhze.mjs → client-TG7rbCWT.mjs} +7 -6
  7. package/dist/{client-CJxHfhze.mjs.map → client-TG7rbCWT.mjs.map} +1 -1
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +2 -7
  10. package/dist/commands/contract-infer.mjs +2 -8
  11. package/dist/commands/db-init.mjs +6 -7
  12. package/dist/commands/db-init.mjs.map +1 -1
  13. package/dist/commands/db-schema.mjs +4 -7
  14. package/dist/commands/db-schema.mjs.map +1 -1
  15. package/dist/commands/db-sign.mjs +5 -6
  16. package/dist/commands/db-sign.mjs.map +1 -1
  17. package/dist/commands/db-update.mjs +6 -7
  18. package/dist/commands/db-update.mjs.map +1 -1
  19. package/dist/commands/db-verify.mjs +6 -7
  20. package/dist/commands/db-verify.mjs.map +1 -1
  21. package/dist/commands/migration-apply.d.mts +1 -1
  22. package/dist/commands/migration-apply.d.mts.map +1 -1
  23. package/dist/commands/migration-apply.mjs +33 -25
  24. package/dist/commands/migration-apply.mjs.map +1 -1
  25. package/dist/commands/migration-new.d.mts.map +1 -1
  26. package/dist/commands/migration-new.mjs +47 -22
  27. package/dist/commands/migration-new.mjs.map +1 -1
  28. package/dist/commands/migration-plan.d.mts +6 -1
  29. package/dist/commands/migration-plan.d.mts.map +1 -1
  30. package/dist/commands/migration-plan.mjs +92 -69
  31. package/dist/commands/migration-plan.mjs.map +1 -1
  32. package/dist/commands/migration-ref.d.mts +1 -1
  33. package/dist/commands/migration-ref.mjs +4 -4
  34. package/dist/commands/migration-show.d.mts +2 -2
  35. package/dist/commands/migration-show.d.mts.map +1 -1
  36. package/dist/commands/migration-show.mjs +9 -14
  37. package/dist/commands/migration-show.mjs.map +1 -1
  38. package/dist/commands/migration-status.d.mts +4 -5
  39. package/dist/commands/migration-status.d.mts.map +1 -1
  40. package/dist/commands/migration-status.mjs +2 -7
  41. package/dist/config-loader-_W4T21X1.mjs +90 -0
  42. package/dist/config-loader-_W4T21X1.mjs.map +1 -0
  43. package/dist/config-loader.d.mts.map +1 -1
  44. package/dist/config-loader.mjs +1 -1
  45. package/dist/{contract-emit-gpJNLGs7.mjs → contract-emit-CNYyzJwF.mjs} +18 -14
  46. package/dist/contract-emit-CNYyzJwF.mjs.map +1 -0
  47. package/dist/{contract-emit-CKig_Lra.mjs → contract-emit-CQfj7xJn.mjs} +24 -20
  48. package/dist/contract-emit-CQfj7xJn.mjs.map +1 -0
  49. package/dist/contract-emit-fhNwwhkQ.mjs +4 -0
  50. package/dist/{contract-infer-BDJgg7Xb.mjs → contract-infer-BP3DrGgz.mjs} +3 -3
  51. package/dist/{contract-infer-BDJgg7Xb.mjs.map → contract-infer-BP3DrGgz.mjs.map} +1 -1
  52. package/dist/exports/control-api.d.mts +2 -2
  53. package/dist/exports/control-api.d.mts.map +1 -1
  54. package/dist/exports/control-api.mjs +3 -5
  55. package/dist/exports/index.mjs +2 -7
  56. package/dist/exports/index.mjs.map +1 -1
  57. package/dist/{framework-components-Bsr1GaIj.mjs → framework-components-DfZKQBQ2.mjs} +2 -2
  58. package/dist/{framework-components-Bsr1GaIj.mjs.map → framework-components-DfZKQBQ2.mjs.map} +1 -1
  59. package/dist/{init-DZWvhEP0.mjs → init-CQfo_4Ro.mjs} +2 -2
  60. package/dist/{init-DZWvhEP0.mjs.map → init-CQfo_4Ro.mjs.map} +1 -1
  61. package/dist/{inspect-live-schema-ChqrALmw.mjs → inspect-live-schema-DWzf4Q_m.mjs} +5 -5
  62. package/dist/{inspect-live-schema-ChqrALmw.mjs.map → inspect-live-schema-DWzf4Q_m.mjs.map} +1 -1
  63. package/dist/migration-cli.d.mts +50 -0
  64. package/dist/migration-cli.d.mts.map +1 -0
  65. package/dist/migration-cli.mjs +184 -0
  66. package/dist/migration-cli.mjs.map +1 -0
  67. package/dist/{migration-command-scaffold-B0oH_hyB.mjs → migration-command-scaffold-CLMD302g.mjs} +6 -6
  68. package/dist/{migration-command-scaffold-B0oH_hyB.mjs.map → migration-command-scaffold-CLMD302g.mjs.map} +1 -1
  69. package/dist/{migration-status-CPamfEPj.mjs → migration-status-B0HLF7So.mjs} +18 -34
  70. package/dist/migration-status-B0HLF7So.mjs.map +1 -0
  71. package/dist/{migrations-BIsjFjSV.mjs → migrations-B0dOQlk0.mjs} +4 -15
  72. package/dist/migrations-B0dOQlk0.mjs.map +1 -0
  73. package/dist/{result-handler-AFK4hxyX.mjs → result-handler-CIyu0Pdt.mjs} +22 -11
  74. package/dist/result-handler-CIyu0Pdt.mjs.map +1 -0
  75. package/dist/{validate-contract-deps-DBH6iTAD.mjs → validate-contract-deps-esa-VQ0h.mjs} +1 -1
  76. package/dist/{validate-contract-deps-DBH6iTAD.mjs.map → validate-contract-deps-esa-VQ0h.mjs.map} +1 -1
  77. package/dist/{verify-C56CuQc7.mjs → verify-BxiVp50b.mjs} +2 -2
  78. package/dist/{verify-C56CuQc7.mjs.map → verify-BxiVp50b.mjs.map} +1 -1
  79. package/package.json +19 -19
  80. package/src/cli.ts +1 -5
  81. package/src/commands/contract-emit.ts +9 -10
  82. package/src/commands/migration-apply.ts +34 -23
  83. package/src/commands/migration-new.ts +39 -17
  84. package/src/commands/migration-plan.ts +119 -104
  85. package/src/commands/migration-show.ts +6 -16
  86. package/src/commands/migration-status.ts +14 -34
  87. package/src/config-loader.ts +35 -29
  88. package/src/config-path-validation.ts +75 -0
  89. package/src/control-api/client.ts +2 -1
  90. package/src/control-api/operations/contract-emit.ts +24 -23
  91. package/src/control-api/types.ts +1 -1
  92. package/src/migration-cli.ts +254 -0
  93. package/src/utils/cli-errors.ts +1 -0
  94. package/src/utils/command-helpers.ts +15 -19
  95. package/src/utils/formatters/graph-migration-mapper.ts +5 -14
  96. package/src/utils/formatters/help.ts +0 -1
  97. package/src/utils/formatters/migrations.ts +2 -29
  98. package/dist/cli-errors-BUuJr6py.mjs +0 -5
  99. package/dist/cli-errors-Dic2eADK.d.mts +0 -4
  100. package/dist/commands/migration-emit.d.mts +0 -38
  101. package/dist/commands/migration-emit.d.mts.map +0 -1
  102. package/dist/commands/migration-emit.mjs +0 -81
  103. package/dist/commands/migration-emit.mjs.map +0 -1
  104. package/dist/config-loader-C4VXKl8f.mjs +0 -43
  105. package/dist/config-loader-C4VXKl8f.mjs.map +0 -1
  106. package/dist/contract-emit-CKig_Lra.mjs.map +0 -1
  107. package/dist/contract-emit-CU-SYNe4.mjs +0 -6
  108. package/dist/contract-emit-gpJNLGs7.mjs.map +0 -1
  109. package/dist/migration-emit-Du4DBMqz.mjs +0 -125
  110. package/dist/migration-emit-Du4DBMqz.mjs.map +0 -1
  111. package/dist/migration-status-CPamfEPj.mjs.map +0 -1
  112. package/dist/migrations-BIsjFjSV.mjs.map +0 -1
  113. package/dist/result-handler-AFK4hxyX.mjs.map +0 -1
  114. package/src/commands/migration-emit.ts +0 -134
  115. package/src/lib/migration-emit.ts +0 -125
  116. package/src/lib/migration-strategy.ts +0 -49
@@ -1,10 +1,15 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
- import { createControlStack } from '@prisma-next/framework-components/control';
3
+ import { getEmittedArtifactPaths } from '@prisma-next/emitter';
4
+ import {
5
+ createControlStack,
6
+ type MigrationPlanOperation,
7
+ } from '@prisma-next/framework-components/control';
8
+ import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
4
9
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
5
10
  import { findLatestMigration } from '@prisma-next/migration-tools/dag';
6
11
  import {
7
- copyContractToMigrationDir,
12
+ copyFilesWithRename,
8
13
  formatMigrationDirName,
9
14
  writeMigrationPackage,
10
15
  } from '@prisma-next/migration-tools/io';
@@ -15,8 +20,6 @@ import { Command } from 'commander';
15
20
  import { join, relative } from 'pathe';
16
21
  import { loadConfig } from '../config-loader';
17
22
  import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
18
- import { emitMigration } from '../lib/migration-emit';
19
- import { migrationStrategy } from '../lib/migration-strategy';
20
23
  import {
21
24
  type CliErrorConflict,
22
25
  CliStructuredError,
@@ -54,7 +57,6 @@ export interface MigrationPlanResult {
54
57
  readonly noOp: boolean;
55
58
  readonly from: string;
56
59
  readonly to: string;
57
- readonly migrationId?: string;
58
60
  readonly dir?: string;
59
61
  readonly operations: readonly {
60
62
  readonly id: string;
@@ -63,6 +65,12 @@ export interface MigrationPlanResult {
63
65
  }[];
64
66
  readonly sql?: readonly string[];
65
67
  readonly summary: string;
68
+ /**
69
+ * When true, `migration.ts` was written but contains unfilled
70
+ * `placeholder(...)` calls. The user must edit the file and then run
71
+ * `node migration.ts` to self-emit `ops.json` / `migration.json`.
72
+ */
73
+ readonly pendingPlaceholders?: boolean;
66
74
  readonly timings: {
67
75
  readonly total: number;
68
76
  };
@@ -166,20 +174,10 @@ async function executeMigrationPlanCommand(
166
174
  // Read existing migrations and determine "from" contract
167
175
  let fromContract: Contract | null = null;
168
176
  let fromHash: string = EMPTY_CONTRACT_HASH;
177
+ let fromContractSourceDir: string | null = null;
169
178
 
170
179
  try {
171
- const { attested: bundles, drafts, graph } = await loadAllBundles(migrationsDir);
172
-
173
- // Check if a draft migration already targets this contract
174
- const existingDraft = drafts.find((d) => d.manifest.to === toStorageHash);
175
- if (existingDraft) {
176
- return notOk(
177
- errorRuntime('A draft migration to this contract already exists', {
178
- why: `Draft migration at "${existingDraft.dirName}" already targets ${toStorageHash}`,
179
- fix: `Run 'prisma-next migration emit --dir ${migrationsRelative}/${existingDraft.dirName}' to attest it, or delete it and re-plan.`,
180
- }),
181
- );
182
- }
180
+ const { bundles, graph } = await loadAllBundles(migrationsDir);
183
181
 
184
182
  if (options.from) {
185
183
  const resolved = resolveBundleByPrefix(bundles, options.from);
@@ -199,6 +197,7 @@ async function executeMigrationPlanCommand(
199
197
  }
200
198
  fromHash = resolved.value.manifest.to;
201
199
  fromContract = resolved.value.manifest.toContract;
200
+ fromContractSourceDir = resolved.value.dirPath;
202
201
  } else {
203
202
  const latestMigration = findLatestMigration(graph);
204
203
  if (latestMigration) {
@@ -206,6 +205,7 @@ async function executeMigrationPlanCommand(
206
205
  const leafPkg = bundles.find((p) => p.manifest.migrationId === latestMigration.migrationId);
207
206
  if (leafPkg) {
208
207
  fromContract = leafPkg.manifest.toContract;
208
+ fromContractSourceDir = leafPkg.dirPath;
209
209
  }
210
210
  }
211
211
  }
@@ -245,18 +245,15 @@ async function executeMigrationPlanCommand(
245
245
  [config.target, config.adapter, ...(config.extensionPacks ?? [])],
246
246
  );
247
247
 
248
- const strategy = migrationStrategy(migrations, config.target.targetId);
249
-
250
248
  // Build manifest and write migration package
251
249
  const timestamp = new Date();
252
250
  const slug = options.name ?? 'migration';
253
251
  const dirName = formatMigrationDirName(timestamp, slug);
254
252
  const packageDir = join(migrationsDir, dirName);
255
253
 
256
- const manifest: MigrationManifest = {
254
+ const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
257
255
  from: fromHash,
258
256
  to: toStorageHash,
259
- migrationId: null,
260
257
  kind: 'regular',
261
258
  fromContract,
262
259
  toContract: toContractJson,
@@ -264,41 +261,42 @@ async function executeMigrationPlanCommand(
264
261
  used: [],
265
262
  applied: [],
266
263
  plannerVersion: '2.0.0',
267
- planningStrategy: strategy === 'descriptor' ? 'descriptors' : 'class-based',
268
264
  },
269
265
  labels: [],
270
266
  createdAt: timestamp.toISOString(),
271
267
  };
272
268
 
273
- const scaffoldContext = {
274
- packageDir,
275
- contractJsonPath: contractPathAbsolute,
276
- fromHash,
277
- toHash: toStorageHash,
278
- };
279
-
280
269
  try {
281
- let migrationTsContent: string;
270
+ const stack = createControlStack(config);
271
+ const familyInstance = config.family.create(stack);
272
+ const planner = migrations.createPlanner(familyInstance);
273
+ const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
274
+ const plannerResult = planner.plan({
275
+ contract: toContractJson,
276
+ schema: fromSchema,
277
+ policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
278
+ fromHash,
279
+ fromContract,
280
+ frameworkComponents,
281
+ });
282
+ if (plannerResult.kind === 'failure') {
283
+ return notOk(
284
+ errorMigrationPlanningFailed({
285
+ conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
286
+ }),
287
+ );
288
+ }
282
289
 
283
- if (strategy === 'descriptor') {
284
- if (!migrations.planWithDescriptors || !migrations.renderDescriptorTypeScript) {
285
- throw errorTargetMigrationNotSupported({
286
- why: `Target "${config.target.targetId}" advertises descriptor flow but is missing required hooks`,
287
- });
288
- }
289
- const descriptorResult = migrations.planWithDescriptors({
290
- fromContract,
291
- toContract: toContractJson,
292
- frameworkComponents,
293
- });
294
- if (!descriptorResult.ok) {
295
- return notOk(
296
- errorMigrationPlanningFailed({
297
- conflicts: descriptorResult.conflicts as readonly CliErrorConflict[],
298
- }),
299
- );
300
- }
301
- if (descriptorResult.descriptors.length === 0) {
290
+ // Accessing .operations triggers toOp() on each call. If any call
291
+ // is a DataTransformCall with an unfilled placeholder stub, toOp()
292
+ // throws PN-MIG-2001. We catch that here so the migration can still
293
+ // be scaffolded with `ops: []`; the user fills the placeholder, then
294
+ // re-runs `node migration.ts` to attest with the real ops.
295
+ let plannedOps: readonly MigrationPlanOperation[] = [];
296
+ let hasPlaceholders = false;
297
+ try {
298
+ plannedOps = plannerResult.plan.operations;
299
+ if (plannedOps.length === 0) {
302
300
  return notOk(
303
301
  errorMigrationPlanningFailed({
304
302
  conflicts: [
@@ -312,75 +310,74 @@ async function executeMigrationPlanCommand(
312
310
  }),
313
311
  );
314
312
  }
315
- migrationTsContent = migrations.renderDescriptorTypeScript(
316
- descriptorResult.descriptors,
317
- scaffoldContext,
318
- );
319
- } else {
320
- const stack = createControlStack(config);
321
- const familyInstance = config.family.create(stack);
322
- const planner = migrations.createPlanner(familyInstance);
323
- const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
324
- const plannerResult = planner.plan({
325
- contract: toContractJson,
326
- schema: fromSchema,
327
- policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
328
- fromHash,
329
- frameworkComponents,
330
- });
331
- if (plannerResult.kind === 'failure') {
332
- return notOk(
333
- errorMigrationPlanningFailed({
334
- conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
335
- }),
336
- );
337
- }
338
- if (plannerResult.plan.operations.length === 0) {
339
- return notOk(
340
- errorMigrationPlanningFailed({
341
- conflicts: [
342
- {
343
- kind: 'unsupportedChange',
344
- summary:
345
- 'Contract changed but planner produced no operations. ' +
346
- 'This indicates unsupported or ignored changes.',
347
- },
348
- ],
349
- }),
350
- );
313
+ } catch (e) {
314
+ if (CliStructuredError.is(e) && e.domain === 'MIG' && e.code === '2001') {
315
+ hasPlaceholders = true;
316
+ } else {
317
+ throw e;
351
318
  }
352
- migrationTsContent = plannerResult.plan.renderTypeScript();
353
319
  }
354
320
 
355
- await writeMigrationPackage(packageDir, manifest, []);
356
- await copyContractToMigrationDir(packageDir, contractPathAbsolute);
321
+ const migrationTsContent = plannerResult.plan.renderTypeScript();
322
+
323
+ // Always-attest: compute migrationId over (manifest, ops). When
324
+ // placeholders blocked lowering, ops is `[]` and the id hashes over
325
+ // the empty list — re-emitting after the user fills the placeholder
326
+ // produces a different id (over the real ops). This is intentional;
327
+ // there is no on-disk "draft" state.
328
+ const opsForWrite = hasPlaceholders ? [] : plannedOps;
329
+ const manifest: MigrationManifest = {
330
+ ...baseManifest,
331
+ migrationId: computeMigrationId(baseManifest, opsForWrite),
332
+ };
333
+
334
+ await writeMigrationPackage(packageDir, manifest, opsForWrite);
335
+ const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
336
+ await copyFilesWithRename(packageDir, [
337
+ { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
338
+ { sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
339
+ ]);
340
+ if (fromContractSourceDir !== null) {
341
+ const sourceArtifacts = getEmittedArtifactPaths(
342
+ join(fromContractSourceDir, 'end-contract.json'),
343
+ );
344
+ await copyFilesWithRename(packageDir, [
345
+ { sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
346
+ { sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
347
+ ]);
348
+ }
357
349
  await writeMigrationTs(packageDir, migrationTsContent);
358
350
 
359
- // Always run emit inline. If migration.ts contains unfilled
360
- // placeholders (e.g. user must hand-author a dataTransform body),
361
- // emitMigration throws errorUnfilledPlaceholder (PN-MIG-2001) and
362
- // we propagate that structured error to the user.
363
- const { operations, migrationId } = await emitMigration(packageDir, {
364
- targetId: config.target.targetId,
365
- migrations,
366
- frameworkComponents,
367
- });
351
+ if (hasPlaceholders) {
352
+ const result: MigrationPlanResult = {
353
+ ok: true,
354
+ noOp: false,
355
+ from: fromHash,
356
+ to: toStorageHash,
357
+ dir: relative(process.cwd(), packageDir),
358
+ operations: [],
359
+ pendingPlaceholders: true,
360
+ summary:
361
+ 'Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
362
+ timings: { total: Date.now() - startTime },
363
+ };
364
+ return ok(result);
365
+ }
368
366
 
369
- const sql = extractSqlDdl(operations);
367
+ const sql = extractSqlDdl(plannedOps);
370
368
  const result: MigrationPlanResult = {
371
369
  ok: true,
372
370
  noOp: false,
373
371
  from: fromHash,
374
372
  to: toStorageHash,
375
- migrationId,
376
373
  dir: relative(process.cwd(), packageDir),
377
- operations: operations.map((op) => ({
374
+ operations: plannedOps.map((op) => ({
378
375
  id: op.id,
379
376
  label: op.label,
380
377
  operationClass: op.operationClass,
381
378
  })),
382
379
  sql,
383
- summary: `Planned ${operations.length} operation(s)`,
380
+ summary: `Planned ${plannedOps.length} operation(s)`,
384
381
  timings: { total: Date.now() - startTime },
385
382
  };
386
383
  return ok(result);
@@ -442,6 +439,22 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
442
439
  return lines.join('\n');
443
440
  }
444
441
 
442
+ if (result.pendingPlaceholders) {
443
+ lines.push(`${yellow_('⚠')} ${result.summary}`);
444
+ lines.push('');
445
+ lines.push(dim_(`from: ${result.from}`));
446
+ lines.push(dim_(`to: ${result.to}`));
447
+ if (result.dir) {
448
+ lines.push(dim_(`dir: ${result.dir}`));
449
+ }
450
+ lines.push('');
451
+ lines.push(
452
+ 'Open migration.ts and replace each `placeholder(...)` call with your actual query.',
453
+ );
454
+ lines.push(`Then run: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)}`);
455
+ return lines.join('\n');
456
+ }
457
+
445
458
  lines.push(`${green_('✔')} ${result.summary}`);
446
459
  lines.push('');
447
460
 
@@ -470,13 +483,15 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
470
483
 
471
484
  lines.push(dim_(`from: ${result.from}`));
472
485
  lines.push(dim_(`to: ${result.to}`));
473
- if (result.migrationId) {
474
- lines.push(dim_(`migrationId: ${result.migrationId}`));
475
- }
476
486
  if (result.dir) {
477
487
  lines.push(dim_(`dir: ${result.dir}`));
478
488
  }
479
489
 
490
+ lines.push('');
491
+ lines.push(
492
+ `Next: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)} to emit ops.json and attest migrationId before running ${green_('prisma-next migration apply')}.`,
493
+ );
494
+
480
495
  if (result.sql && result.sql.length > 0) {
481
496
  lines.push('');
482
497
  lines.push(dim_('DDL preview'));
@@ -2,7 +2,7 @@ import type { MigrationPlanOperation } from '@prisma-next/framework-components/c
2
2
  import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
3
3
  import { readMigrationPackage, readMigrationsDir } from '@prisma-next/migration-tools/io';
4
4
  import type { MigrationBundle } from '@prisma-next/migration-tools/types';
5
- import { isAttested, MigrationToolsError } from '@prisma-next/migration-tools/types';
5
+ import { MigrationToolsError } from '@prisma-next/migration-tools/types';
6
6
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
7
7
  import { Command } from 'commander';
8
8
  import { relative, resolve } from 'pathe';
@@ -31,7 +31,7 @@ export interface MigrationShowResult {
31
31
  readonly dirPath: string;
32
32
  readonly from: string;
33
33
  readonly to: string;
34
- readonly migrationId: string | null;
34
+ readonly migrationId: string;
35
35
  readonly kind: string;
36
36
  readonly createdAt: string;
37
37
  readonly operations: readonly {
@@ -52,8 +52,7 @@ export function resolveByHashPrefix(
52
52
  prefix: string,
53
53
  ): Result<MigrationBundle, CliStructuredError> {
54
54
  const normalizedPrefix = prefix.startsWith('sha256:') ? prefix : `sha256:${prefix}`;
55
- const attested = packages.filter((p) => typeof p.manifest.migrationId === 'string');
56
- const matches = attested.filter((p) => p.manifest.migrationId!.startsWith(normalizedPrefix));
55
+ const matches = packages.filter((p) => p.manifest.migrationId.startsWith(normalizedPrefix));
57
56
 
58
57
  if (matches.length === 1) {
59
58
  return ok(matches[0]!);
@@ -62,7 +61,7 @@ export function resolveByHashPrefix(
62
61
  if (matches.length === 0) {
63
62
  return notOk(
64
63
  errorRuntime('No migration found matching prefix', {
65
- why: `No attested migration has a migrationId starting with "${normalizedPrefix}"`,
64
+ why: `No migration has a migrationId starting with "${normalizedPrefix}"`,
66
65
  fix: 'Run `prisma-next migration show` (no argument) to see the latest migration, or check the migrations directory for available packages.',
67
66
  }),
68
67
  );
@@ -132,16 +131,7 @@ async function executeMigrationShowCommand(
132
131
  if (!resolved.ok) return resolved;
133
132
  pkg = resolved.value;
134
133
  } else {
135
- const attested = allPackages.filter(isAttested);
136
- if (attested.length === 0) {
137
- return notOk(
138
- errorRuntime('No attested migrations found', {
139
- why: `All migrations in ${migrationsRelative} are drafts (migrationId: null)`,
140
- fix: 'Run `prisma-next migration emit --dir <path>` to attest a draft migration.',
141
- }),
142
- );
143
- }
144
- const graph = reconstructGraph(attested);
134
+ const graph = reconstructGraph(allPackages);
145
135
  const latestMigration = findLatestMigration(graph);
146
136
  if (!latestMigration) {
147
137
  return notOk(
@@ -151,7 +141,7 @@ async function executeMigrationShowCommand(
151
141
  }),
152
142
  );
153
143
  }
154
- const leafPkg = attested.find(
144
+ const leafPkg = allPackages.find(
155
145
  (p) => p.manifest.migrationId === latestMigration.migrationId,
156
146
  );
157
147
  if (!leafPkg) {
@@ -8,8 +8,7 @@ import {
8
8
  import type { Refs } from '@prisma-next/migration-tools/refs';
9
9
  import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
10
10
  import type {
11
- AttestedMigrationBundle,
12
- DraftMigrationBundle,
11
+ MigrationBundle,
13
12
  MigrationChainEntry,
14
13
  MigrationGraph,
15
14
  } from '@prisma-next/migration-tools/types';
@@ -62,7 +61,7 @@ export interface MigrationStatusEntry {
62
61
  readonly dirName: string;
63
62
  readonly from: string;
64
63
  readonly to: string;
65
- readonly migrationId: string | null;
64
+ readonly migrationId: string;
66
65
  readonly operationCount: number;
67
66
  readonly operationSummary: string;
68
67
  readonly hasDestructive: boolean;
@@ -87,7 +86,7 @@ export interface MigrationStatusResult {
87
86
  readonly refName?: string;
88
87
  readonly selectedPath: readonly {
89
88
  readonly dirName: string;
90
- readonly migrationId: string | null;
89
+ readonly migrationId: string;
91
90
  readonly from: string;
92
91
  readonly to: string;
93
92
  }[];
@@ -95,8 +94,7 @@ export interface MigrationStatusResult {
95
94
  readonly summary: string;
96
95
  readonly diagnostics: readonly StatusDiagnostic[];
97
96
  readonly graph?: MigrationGraph;
98
- readonly bundles?: readonly AttestedMigrationBundle[];
99
- readonly drafts?: readonly DraftMigrationBundle[];
97
+ readonly bundles?: readonly MigrationBundle[];
100
98
  readonly edgeStatuses?: readonly EdgeStatus[];
101
99
  readonly activeRefHash?: string;
102
100
  readonly activeRefName?: string;
@@ -227,7 +225,7 @@ export function deriveEdgeStatuses(
227
225
  */
228
226
  function buildMigrationEntries(
229
227
  chain: readonly MigrationChainEntry[],
230
- packages: readonly AttestedMigrationBundle[],
228
+ packages: readonly MigrationBundle[],
231
229
  mode: 'online' | 'offline',
232
230
  markerHash: string | undefined,
233
231
  edgeStatuses?: readonly EdgeStatus[],
@@ -436,11 +434,10 @@ async function executeMigrationStatusCommand(
436
434
  });
437
435
  }
438
436
 
439
- let attested: readonly AttestedMigrationBundle[];
440
- let drafts: readonly DraftMigrationBundle[];
437
+ let bundles: readonly MigrationBundle[];
441
438
  let graph: MigrationGraph;
442
439
  try {
443
- ({ attested, drafts, graph } = await loadAllBundles(migrationsDir));
440
+ ({ bundles, graph } = await loadAllBundles(migrationsDir));
444
441
  } catch (error) {
445
442
  if (MigrationToolsError.is(error)) {
446
443
  return notOk(
@@ -454,18 +451,7 @@ async function executeMigrationStatusCommand(
454
451
  );
455
452
  }
456
453
 
457
- if (drafts.length > 0) {
458
- diagnostics.push({
459
- code: 'MIGRATION.DRAFTS',
460
- severity: 'warn',
461
- message: `${drafts.length} draft migration(s) found: ${drafts.map((d) => d.dirName).join(', ')}`,
462
- hints: [
463
- "Run 'prisma-next migration emit --dir <path>' to attest draft migrations before applying",
464
- ],
465
- });
466
- }
467
-
468
- if (attested.length === 0) {
454
+ if (bundles.length === 0) {
469
455
  if (contractHash !== EMPTY_CONTRACT_HASH) {
470
456
  diagnostics.push({
471
457
  code: 'CONTRACT.AHEAD',
@@ -576,7 +562,7 @@ async function executeMigrationStatusCommand(
576
562
  migrations: [],
577
563
  targetHash: EMPTY_CONTRACT_HASH,
578
564
  contractHash,
579
- summary: `${attested.length} migration(s) on disk`,
565
+ summary: `${bundles.length} migration(s) on disk`,
580
566
  diagnostics,
581
567
  markerHash,
582
568
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
@@ -616,12 +602,12 @@ async function executeMigrationStatusCommand(
616
602
  migrations: [],
617
603
  targetHash: EMPTY_CONTRACT_HASH,
618
604
  contractHash,
619
- summary: `${attested.length} migration(s) on disk`,
605
+ summary: `${bundles.length} migration(s) on disk`,
620
606
  diagnostics,
621
607
  ...ifDefined('markerHash', markerHash),
622
608
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
623
609
  graph,
624
- bundles: attested,
610
+ bundles,
625
611
  diverged: true,
626
612
  });
627
613
  }
@@ -638,7 +624,7 @@ async function executeMigrationStatusCommand(
638
624
  }
639
625
 
640
626
  const edgeStatuses = deriveEdgeStatuses(graph, targetHash, contractHash, markerHash, mode);
641
- const entries = buildMigrationEntries(chain, attested, mode, markerHash, edgeStatuses);
627
+ const entries = buildMigrationEntries(chain, bundles, mode, markerHash, edgeStatuses);
642
628
 
643
629
  const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
644
630
  const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
@@ -646,7 +632,7 @@ async function executeMigrationStatusCommand(
646
632
  let summary: string;
647
633
  if (mode === 'online') {
648
634
  if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
649
- summary = `${attested.length} migration(s) on disk`;
635
+ summary = `${bundles.length} migration(s) on disk`;
650
636
  } else if (activeRefHash && markerHash !== undefined) {
651
637
  summary = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
652
638
  } else if (pendingCount === 0) {
@@ -706,8 +692,7 @@ async function executeMigrationStatusCommand(
706
692
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
707
693
  ...ifDefined('pathDecision', pathDecision),
708
694
  graph,
709
- bundles: attested,
710
- ...(drafts.length > 0 ? { drafts } : {}),
695
+ bundles,
711
696
  edgeStatuses,
712
697
  ...ifDefined('activeRefHash', activeRefHash),
713
698
  ...ifDefined('activeRefName', activeRefName),
@@ -768,11 +753,6 @@ export function createMigrationStatusCommand(): Command {
768
753
  activeRefHash: statusResult.activeRefHash,
769
754
  activeRefName: statusResult.activeRefName,
770
755
  edgeStatuses: statusResult.edgeStatuses,
771
- draftEdges: statusResult.drafts?.map((d) => ({
772
- from: d.manifest.from,
773
- to: d.manifest.to,
774
- dirName: d.dirName,
775
- })),
776
756
  });
777
757
 
778
758
  const graphToRender =
@@ -1,4 +1,3 @@
1
- import { dirname, resolve } from 'node:path';
2
1
  import type { PrismaNextConfig } from '@prisma-next/config/config-types';
3
2
  import { ConfigValidationError, validateConfig } from '@prisma-next/config/config-validation';
4
3
  import {
@@ -6,7 +5,41 @@ import {
6
5
  errorConfigValidation,
7
6
  errorUnexpected,
8
7
  } from '@prisma-next/errors/control';
8
+ import { ifDefined } from '@prisma-next/utils/defined';
9
9
  import { loadConfig as loadConfigC12 } from 'c12';
10
+ import { dirname, resolve } from 'pathe';
11
+ import { finalizeConfig } from './config-path-validation';
12
+
13
+ async function loadValidatedConfig(configPath?: string): Promise<PrismaNextConfig> {
14
+ const cwd = process.cwd();
15
+ const resolvedConfigPath = configPath ? resolve(cwd, configPath) : undefined;
16
+ const configCwd = resolvedConfigPath ? dirname(resolvedConfigPath) : cwd;
17
+
18
+ const result = await loadConfigC12<PrismaNextConfig>({
19
+ name: 'prisma-next',
20
+ ...ifDefined('configFile', resolvedConfigPath),
21
+ cwd: configCwd,
22
+ });
23
+
24
+ // When a specific config file was requested, verify it was actually loaded
25
+ // (c12 falls back to searching by name if the specified file doesn't exist)
26
+ if (resolvedConfigPath && result.configFile !== resolvedConfigPath) {
27
+ throw errorConfigFileNotFound(resolvedConfigPath);
28
+ }
29
+
30
+ // Check if config is missing or empty (c12 may return empty object when file doesn't exist)
31
+ if (!result.config || Object.keys(result.config).length === 0) {
32
+ // Use c12's configFile if available, otherwise use explicit configPath, otherwise omit path
33
+ const displayPath = result.configFile || resolvedConfigPath || configPath;
34
+ throw errorConfigFileNotFound(displayPath);
35
+ }
36
+
37
+ // Validate config structure
38
+ validateConfig(result.config);
39
+
40
+ const loadedConfigDir = result.configFile ? dirname(result.configFile) : configCwd;
41
+ return finalizeConfig(result.config, loadedConfigDir);
42
+ }
10
43
 
11
44
  /**
12
45
  * Loads the Prisma Next config from a TypeScript file.
@@ -19,34 +52,7 @@ import { loadConfig as loadConfigC12 } from 'c12';
19
52
  */
20
53
  export async function loadConfig(configPath?: string): Promise<PrismaNextConfig> {
21
54
  try {
22
- const cwd = process.cwd();
23
- // Resolve config path to absolute path and set cwd to config directory when path is provided
24
- const resolvedConfigPath = configPath ? resolve(cwd, configPath) : undefined;
25
- const configCwd = resolvedConfigPath ? dirname(resolvedConfigPath) : cwd;
26
-
27
- const result = await loadConfigC12<PrismaNextConfig>({
28
- name: 'prisma-next',
29
- ...(resolvedConfigPath ? { configFile: resolvedConfigPath } : {}),
30
- cwd: configCwd,
31
- });
32
-
33
- // When a specific config file was requested, verify it was actually loaded
34
- // (c12 falls back to searching by name if the specified file doesn't exist)
35
- if (resolvedConfigPath && result.configFile !== resolvedConfigPath) {
36
- throw errorConfigFileNotFound(resolvedConfigPath);
37
- }
38
-
39
- // Check if config is missing or empty (c12 may return empty object when file doesn't exist)
40
- if (!result.config || Object.keys(result.config).length === 0) {
41
- // Use c12's configFile if available, otherwise use explicit configPath, otherwise omit path
42
- const displayPath = result.configFile || resolvedConfigPath || configPath;
43
- throw errorConfigFileNotFound(displayPath);
44
- }
45
-
46
- // Validate config structure
47
- validateConfig(result.config);
48
-
49
- return result.config;
55
+ return await loadValidatedConfig(configPath);
50
56
  } catch (error) {
51
57
  if (error instanceof ConfigValidationError) {
52
58
  throw errorConfigValidation(error.field, {