@prisma-next/cli 0.4.0-dev.9 → 0.4.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 (119) hide show
  1. package/README.md +31 -18
  2. package/dist/cli-errors-CznZA5-d.mjs +5 -0
  3. package/dist/cli.mjs +8 -19
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{client-CJxHfhze.mjs → client-DGKrciLM.mjs} +9 -8
  6. package/dist/{client-CJxHfhze.mjs.map → client-DGKrciLM.mjs.map} +1 -1
  7. package/dist/commands/contract-emit.d.mts.map +1 -1
  8. package/dist/commands/contract-emit.mjs +2 -7
  9. package/dist/commands/contract-infer.mjs +2 -8
  10. package/dist/commands/db-init.mjs +7 -8
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-schema.mjs +5 -8
  13. package/dist/commands/db-schema.mjs.map +1 -1
  14. package/dist/commands/db-sign.mjs +7 -8
  15. package/dist/commands/db-sign.mjs.map +1 -1
  16. package/dist/commands/db-update.mjs +7 -8
  17. package/dist/commands/db-update.mjs.map +1 -1
  18. package/dist/commands/db-verify.mjs +8 -9
  19. package/dist/commands/db-verify.mjs.map +1 -1
  20. package/dist/commands/migration-apply.d.mts +1 -1
  21. package/dist/commands/migration-apply.d.mts.map +1 -1
  22. package/dist/commands/migration-apply.mjs +34 -26
  23. package/dist/commands/migration-apply.mjs.map +1 -1
  24. package/dist/commands/migration-new.d.mts.map +1 -1
  25. package/dist/commands/migration-new.mjs +48 -23
  26. package/dist/commands/migration-new.mjs.map +1 -1
  27. package/dist/commands/migration-plan.d.mts +6 -1
  28. package/dist/commands/migration-plan.d.mts.map +1 -1
  29. package/dist/commands/migration-plan.mjs +94 -71
  30. package/dist/commands/migration-plan.mjs.map +1 -1
  31. package/dist/commands/migration-ref.d.mts +1 -1
  32. package/dist/commands/migration-ref.mjs +5 -5
  33. package/dist/commands/migration-show.d.mts +2 -2
  34. package/dist/commands/migration-show.d.mts.map +1 -1
  35. package/dist/commands/migration-show.mjs +11 -16
  36. package/dist/commands/migration-show.mjs.map +1 -1
  37. package/dist/commands/migration-status.d.mts +4 -5
  38. package/dist/commands/migration-status.d.mts.map +1 -1
  39. package/dist/commands/migration-status.mjs +2 -7
  40. package/dist/config-loader-_xQZsw0i.mjs +90 -0
  41. package/dist/config-loader-_xQZsw0i.mjs.map +1 -0
  42. package/dist/config-loader.d.mts.map +1 -1
  43. package/dist/config-loader.mjs +1 -1
  44. package/dist/contract-emit-304WZtZJ.mjs +4 -0
  45. package/dist/{contract-emit-CKig_Lra.mjs → contract-emit-DgeWdonT.mjs} +25 -21
  46. package/dist/contract-emit-DgeWdonT.mjs.map +1 -0
  47. package/dist/{contract-emit-gpJNLGs7.mjs → contract-emit-mU1_B_m9.mjs} +18 -14
  48. package/dist/contract-emit-mU1_B_m9.mjs.map +1 -0
  49. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-BV4KpbNW.mjs} +1 -1
  50. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-BV4KpbNW.mjs.map} +1 -1
  51. package/dist/{contract-infer-BDJgg7Xb.mjs → contract-infer-CUbiWGX0.mjs} +4 -4
  52. package/dist/{contract-infer-BDJgg7Xb.mjs.map → contract-infer-CUbiWGX0.mjs.map} +1 -1
  53. package/dist/exports/control-api.d.mts +2 -2
  54. package/dist/exports/control-api.d.mts.map +1 -1
  55. package/dist/exports/control-api.mjs +4 -6
  56. package/dist/exports/index.mjs +2 -7
  57. package/dist/exports/index.mjs.map +1 -1
  58. package/dist/{extract-operation-statements-DZUJNmL3.mjs → extract-operation-statements-DWWFz1PK.mjs} +2 -2
  59. package/dist/{extract-operation-statements-DZUJNmL3.mjs.map → extract-operation-statements-DWWFz1PK.mjs.map} +1 -1
  60. package/dist/{extract-sql-ddl-DDMX-9mz.mjs → extract-sql-ddl-7zn_AFS8.mjs} +1 -1
  61. package/dist/{extract-sql-ddl-DDMX-9mz.mjs.map → extract-sql-ddl-7zn_AFS8.mjs.map} +1 -1
  62. package/dist/{framework-components-Bsr1GaIj.mjs → framework-components-B__p--vT.mjs} +2 -2
  63. package/dist/{framework-components-Bsr1GaIj.mjs.map → framework-components-B__p--vT.mjs.map} +1 -1
  64. package/dist/{init-DZWvhEP0.mjs → init-DRquYpPa.mjs} +3 -3
  65. package/dist/{init-DZWvhEP0.mjs.map → init-DRquYpPa.mjs.map} +1 -1
  66. package/dist/{inspect-live-schema-ChqrALmw.mjs → inspect-live-schema-wIYBTdL3.mjs} +6 -6
  67. package/dist/{inspect-live-schema-ChqrALmw.mjs.map → inspect-live-schema-wIYBTdL3.mjs.map} +1 -1
  68. package/dist/{migration-command-scaffold-B0oH_hyB.mjs → migration-command-scaffold-BC73xQSo.mjs} +7 -7
  69. package/dist/{migration-command-scaffold-B0oH_hyB.mjs.map → migration-command-scaffold-BC73xQSo.mjs.map} +1 -1
  70. package/dist/{migration-status-CPamfEPj.mjs → migration-status-CXBbScH5.mjs} +19 -35
  71. package/dist/migration-status-CXBbScH5.mjs.map +1 -0
  72. package/dist/{migrations-BIsjFjSV.mjs → migrations-DYRAjiVh.mjs} +4 -15
  73. package/dist/migrations-DYRAjiVh.mjs.map +1 -0
  74. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-Bwouy73-.mjs} +1 -1
  75. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-Bwouy73-.mjs.map} +1 -1
  76. package/dist/{result-handler-AFK4hxyX.mjs → result-handler-CGohaH1o.mjs} +22 -11
  77. package/dist/result-handler-CGohaH1o.mjs.map +1 -0
  78. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-BuPXVRFY.mjs} +1 -1
  79. package/dist/{terminal-ui-C5k88MmW.mjs.map → terminal-ui-BuPXVRFY.mjs.map} +1 -1
  80. package/dist/{validate-contract-deps-DBH6iTAD.mjs → validate-contract-deps-DZqv9m7H.mjs} +1 -1
  81. package/dist/{validate-contract-deps-DBH6iTAD.mjs.map → validate-contract-deps-DZqv9m7H.mjs.map} +1 -1
  82. package/dist/{verify-C56CuQc7.mjs → verify-Cm2UFuZA.mjs} +2 -2
  83. package/dist/{verify-C56CuQc7.mjs.map → verify-Cm2UFuZA.mjs.map} +1 -1
  84. package/package.json +16 -20
  85. package/src/cli.ts +1 -5
  86. package/src/commands/contract-emit.ts +9 -10
  87. package/src/commands/migration-apply.ts +34 -23
  88. package/src/commands/migration-new.ts +39 -17
  89. package/src/commands/migration-plan.ts +119 -104
  90. package/src/commands/migration-show.ts +6 -16
  91. package/src/commands/migration-status.ts +14 -34
  92. package/src/config-loader.ts +35 -29
  93. package/src/config-path-validation.ts +75 -0
  94. package/src/control-api/client.ts +2 -1
  95. package/src/control-api/operations/contract-emit.ts +24 -23
  96. package/src/control-api/types.ts +1 -1
  97. package/src/utils/command-helpers.ts +15 -19
  98. package/src/utils/formatters/graph-migration-mapper.ts +5 -14
  99. package/src/utils/formatters/help.ts +0 -1
  100. package/src/utils/formatters/migrations.ts +2 -29
  101. package/dist/cli-errors-BUuJr6py.mjs +0 -5
  102. package/dist/commands/migration-emit.d.mts +0 -38
  103. package/dist/commands/migration-emit.d.mts.map +0 -1
  104. package/dist/commands/migration-emit.mjs +0 -81
  105. package/dist/commands/migration-emit.mjs.map +0 -1
  106. package/dist/config-loader-C4VXKl8f.mjs +0 -43
  107. package/dist/config-loader-C4VXKl8f.mjs.map +0 -1
  108. package/dist/contract-emit-CKig_Lra.mjs.map +0 -1
  109. package/dist/contract-emit-CU-SYNe4.mjs +0 -6
  110. package/dist/contract-emit-gpJNLGs7.mjs.map +0 -1
  111. package/dist/migration-emit-Du4DBMqz.mjs +0 -125
  112. package/dist/migration-emit-Du4DBMqz.mjs.map +0 -1
  113. package/dist/migration-status-CPamfEPj.mjs.map +0 -1
  114. package/dist/migrations-BIsjFjSV.mjs.map +0 -1
  115. package/dist/result-handler-AFK4hxyX.mjs.map +0 -1
  116. package/src/commands/migration-emit.ts +0 -134
  117. package/src/lib/migration-emit.ts +0 -125
  118. package/src/lib/migration-strategy.ts +0 -49
  119. /package/dist/{cli-errors-Dic2eADK.d.mts → cli-errors-z37sV3eR.d.mts} +0 -0
@@ -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, {
@@ -0,0 +1,75 @@
1
+ import {
2
+ type ContractSourceProvider,
3
+ normalizeContractConfig,
4
+ type PrismaNextConfig,
5
+ } from '@prisma-next/config/config-types';
6
+ import { ConfigValidationError } from '@prisma-next/config/config-validation';
7
+ import { getEmittedArtifactPaths } from '@prisma-next/emitter';
8
+ import { resolve } from 'pathe';
9
+
10
+ function throwValidation(field: string, why: string): never {
11
+ throw new ConfigValidationError(field, why);
12
+ }
13
+
14
+ function finalizeContractSource(
15
+ source: ContractSourceProvider,
16
+ configDir: string,
17
+ ): ContractSourceProvider {
18
+ const resolvedInputs = source.inputs?.map((input) => resolve(configDir, input));
19
+ if (resolvedInputs === undefined) {
20
+ return source;
21
+ }
22
+
23
+ return {
24
+ ...source,
25
+ inputs: resolvedInputs,
26
+ };
27
+ }
28
+
29
+ function validateNoOutputsAreInputs(
30
+ inputs: readonly string[] | undefined,
31
+ output: string | undefined,
32
+ ): void {
33
+ if (inputs === undefined || output === undefined) {
34
+ return;
35
+ }
36
+
37
+ let emittedArtifactPaths: ReturnType<typeof getEmittedArtifactPaths>;
38
+ try {
39
+ emittedArtifactPaths = getEmittedArtifactPaths(output);
40
+ } catch (error) {
41
+ throwValidation('contract.output', error instanceof Error ? error.message : String(error));
42
+ }
43
+
44
+ const emittedPaths = new Set([emittedArtifactPaths.jsonPath, emittedArtifactPaths.dtsPath]);
45
+
46
+ for (const input of inputs) {
47
+ if (emittedPaths.has(input)) {
48
+ throwValidation(
49
+ 'contract.source.inputs[]',
50
+ 'Config.contract.source.inputs must not include emitted artifact paths derived from contract.output',
51
+ );
52
+ }
53
+ }
54
+ }
55
+
56
+ export function finalizeConfig(config: PrismaNextConfig, configDir: string): PrismaNextConfig {
57
+ if (!config.contract) {
58
+ return config;
59
+ }
60
+
61
+ const contract = normalizeContractConfig(config.contract);
62
+ const source = finalizeContractSource(contract.source, configDir);
63
+ const output = resolve(configDir, contract.output);
64
+
65
+ validateNoOutputsAreInputs(source.inputs, output);
66
+
67
+ return {
68
+ ...config,
69
+ contract: {
70
+ ...contract,
71
+ source,
72
+ output,
73
+ },
74
+ };
75
+ }
@@ -496,8 +496,9 @@ class ControlClientImpl implements ControlClient {
496
496
  authoringContributions: stack.authoringContributions,
497
497
  codecLookup: stack.codecLookup,
498
498
  controlMutationDefaults: stack.controlMutationDefaults,
499
+ resolvedInputs: contractConfig.source.inputs ?? [],
499
500
  };
500
- const providerResult = await contractConfig.sourceProvider(sourceContext);
501
+ const providerResult = await contractConfig.source.load(sourceContext);
501
502
  if (!providerResult.ok) {
502
503
  onProgress?.({
503
504
  action: 'emit',
@@ -1,10 +1,10 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
- import { emit } from '@prisma-next/emitter';
3
+ import { emit, getEmittedArtifactPaths } from '@prisma-next/emitter';
4
4
  import { createControlStack } from '@prisma-next/framework-components/control';
5
5
  import { abortable } from '@prisma-next/utils/abortable';
6
6
  import { ifDefined } from '@prisma-next/utils/defined';
7
- import { dirname, isAbsolute, join, resolve } from 'pathe';
7
+ import { dirname } from 'pathe';
8
8
  import { loadConfig } from '../../config-loader';
9
9
  import { errorContractConfigMissing, errorRuntime } from '../../utils/cli-errors';
10
10
  import { assertFrameworkComponentsCompatible } from '../../utils/framework-components';
@@ -72,27 +72,23 @@ export async function executeContractEmit(
72
72
  why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
73
73
  });
74
74
  }
75
- if (!contractConfig.output.endsWith('.json')) {
75
+
76
+ // Validate source exists and is callable
77
+ if (typeof contractConfig.source?.load !== 'function') {
76
78
  throw errorContractConfigMissing({
77
- why: 'Contract config output path must end with .json (e.g., "src/prisma/contract.json")',
79
+ why: 'Contract config must include a valid source provider object',
78
80
  });
79
81
  }
80
82
 
81
- // Validate source exists and is callable
82
- if (typeof contractConfig.source !== 'function') {
83
+ let outputPaths: ReturnType<typeof getEmittedArtifactPaths>;
84
+ try {
85
+ outputPaths = getEmittedArtifactPaths(contractConfig.output);
86
+ } catch (error) {
83
87
  throw errorContractConfigMissing({
84
- why: 'Contract config must include a valid source provider function',
88
+ why: error instanceof Error ? error.message : String(error),
85
89
  });
86
90
  }
87
-
88
- // Normalize configPath and resolve artifact paths relative to config file directory
89
- const normalizedConfigPath = resolve(configPath);
90
- const configDir = dirname(normalizedConfigPath);
91
- const outputJsonPath = isAbsolute(contractConfig.output)
92
- ? contractConfig.output
93
- : join(configDir, contractConfig.output);
94
- // Colocate .d.ts with .json (contract.json → contract.d.ts)
95
- const outputDtsPath = `${outputJsonPath.slice(0, -5)}.d.ts`;
91
+ const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = outputPaths;
96
92
 
97
93
  const stack = createControlStack(config);
98
94
 
@@ -102,39 +98,40 @@ export async function executeContractEmit(
102
98
  authoringContributions: stack.authoringContributions,
103
99
  codecLookup: stack.codecLookup,
104
100
  controlMutationDefaults: stack.controlMutationDefaults,
101
+ resolvedInputs: contractConfig.source.inputs ?? [],
105
102
  };
106
103
 
107
- let providerResult: Awaited<ReturnType<typeof contractConfig.source>>;
104
+ let providerResult: Awaited<ReturnType<typeof contractConfig.source.load>>;
108
105
  try {
109
- providerResult = await unlessAborted(contractConfig.source(sourceContext));
106
+ providerResult = await unlessAborted(contractConfig.source.load(sourceContext));
110
107
  } catch (error) {
111
108
  if (signal.aborted || isAbortError(error)) {
112
109
  throw error;
113
110
  }
114
111
  throw errorRuntime('Failed to resolve contract source', {
115
112
  why: error instanceof Error ? error.message : String(error),
116
- fix: 'Ensure contract.source resolves to ok(Contract) or returns structured diagnostics.',
113
+ fix: 'Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.',
117
114
  });
118
115
  }
119
116
 
120
117
  if (!isRecord(providerResult) || typeof providerResult.ok !== 'boolean') {
121
118
  throw errorRuntime('Failed to resolve contract source', {
122
119
  why: 'Contract source provider returned malformed result shape.',
123
- fix: 'Ensure contract.source resolves to ok(Contract) or notOk({ summary, diagnostics }).',
120
+ fix: 'Ensure contract.source.load resolves to ok(Contract) or notOk({ summary, diagnostics }).',
124
121
  });
125
122
  }
126
123
 
127
124
  if (providerResult.ok && !('value' in providerResult)) {
128
125
  throw errorRuntime('Failed to resolve contract source', {
129
126
  why: 'Contract source provider returned malformed success result: missing value.',
130
- fix: 'Ensure contract.source success payload is ok(Contract).',
127
+ fix: 'Ensure contract.source.load success payload is ok(Contract).',
131
128
  });
132
129
  }
133
130
 
134
131
  if (!providerResult.ok && !isProviderFailureLike(providerResult.failure)) {
135
132
  throw errorRuntime('Failed to resolve contract source', {
136
133
  why: 'Contract source provider returned malformed failure result: expected summary and diagnostics.',
137
- fix: 'Ensure contract.source failure payload is notOk({ summary, diagnostics, meta? }).',
134
+ fix: 'Ensure contract.source.load failure payload is notOk({ summary, diagnostics, meta? }).',
138
135
  });
139
136
  }
140
137
 
@@ -160,7 +157,11 @@ export async function executeContractEmit(
160
157
  const enrichedIR = enrichContract(providerResult.value as Contract, frameworkComponents);
161
158
 
162
159
  familyInstance.validateContract(enrichedIR);
163
- const emitResult = await unlessAborted(emit(enrichedIR, stack, config.family.emission));
160
+ const emitResult = await unlessAborted(
161
+ emit(enrichedIR, stack, config.family.emission, {
162
+ outputJsonPath: outputJsonPath,
163
+ }),
164
+ );
164
165
 
165
166
  // Create directory if needed and write files (both colocated)
166
167
  await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
@@ -248,7 +248,7 @@ export interface EmitContractConfig {
248
248
  /**
249
249
  * Contract source provider.
250
250
  */
251
- readonly sourceProvider: ContractSourceProvider;
251
+ readonly source: ContractSourceProvider;
252
252
  /**
253
253
  * Output path for contract.json.
254
254
  * The .d.ts types file will be colocated (e.g., contract.json → contract.d.ts).
@@ -4,12 +4,7 @@ import { hasMigrations } from '@prisma-next/framework-components/control';
4
4
  import type { PathDecision } from '@prisma-next/migration-tools/dag';
5
5
  import { reconstructGraph } from '@prisma-next/migration-tools/dag';
6
6
  import { readMigrationsDir } from '@prisma-next/migration-tools/io';
7
- import type {
8
- AttestedMigrationBundle,
9
- DraftMigrationBundle,
10
- MigrationGraph,
11
- } from '@prisma-next/migration-tools/types';
12
- import { isAttested, isDraft } from '@prisma-next/migration-tools/types';
7
+ import type { MigrationBundle, MigrationGraph } from '@prisma-next/migration-tools/types';
13
8
  import { ifDefined } from '@prisma-next/utils/defined';
14
9
  import type { Command } from 'commander';
15
10
  import { relative, resolve } from 'pathe';
@@ -150,32 +145,33 @@ export function getTargetMigrations(target: ControlTargetDescriptor<string, stri
150
145
  }
151
146
 
152
147
  /**
153
- * Reads the migrations directory, filters to attested bundles, and builds
154
- * the migration graph. Throws on I/O or graph errors — callers handle
155
- * error mapping.
148
+ * Reads the migrations directory and builds the migration graph from all
149
+ * bundles. Throws on I/O or graph errors — callers handle error mapping.
150
+ *
151
+ * Every on-disk bundle is content-addressed (`migrationId` is always a
152
+ * string); there is no draft state to filter out.
156
153
  */
157
154
  export async function loadMigrationBundles(migrationsDir: string): Promise<{
158
- bundles: readonly AttestedMigrationBundle[];
155
+ bundles: readonly MigrationBundle[];
159
156
  graph: MigrationGraph;
160
157
  }> {
161
- const allBundles = await readMigrationsDir(migrationsDir);
162
- const bundles = allBundles.filter(isAttested);
158
+ const bundles = await readMigrationsDir(migrationsDir);
163
159
  const graph = reconstructGraph(bundles);
164
160
  return { bundles, graph };
165
161
  }
166
162
 
167
163
  export interface MigrationBundleSet {
168
- readonly attested: readonly AttestedMigrationBundle[];
169
- readonly drafts: readonly DraftMigrationBundle[];
164
+ readonly bundles: readonly MigrationBundle[];
170
165
  readonly graph: MigrationGraph;
171
166
  }
172
167
 
168
+ /**
169
+ * Alias of `loadMigrationBundles` retained for naming-clarity in commands
170
+ * that previously needed both attested and draft splits. With the
171
+ * collapse of the draft state, both helpers do the same thing.
172
+ */
173
173
  export async function loadAllBundles(migrationsDir: string): Promise<MigrationBundleSet> {
174
- const all = await readMigrationsDir(migrationsDir);
175
- const attested = all.filter(isAttested);
176
- const drafts = all.filter(isDraft);
177
- const graph = reconstructGraph(attested);
178
- return { attested, drafts, graph };
174
+ return loadMigrationBundles(migrationsDir);
179
175
  }
180
176
 
181
177
  /**
@@ -39,12 +39,6 @@ export interface EdgeStatus {
39
39
  readonly status: EdgeStatusKind;
40
40
  }
41
41
 
42
- export interface DraftEdge {
43
- readonly from: string;
44
- readonly to: string;
45
- readonly dirName: string;
46
- }
47
-
48
42
  export interface MigrationGraphInput {
49
43
  readonly graph: MigrationGraph;
50
44
  readonly mode: 'online' | 'offline';
@@ -58,8 +52,6 @@ export interface MigrationGraphInput {
58
52
  * icons (✓/⧗) are baked into edge labels. Undefined in offline mode.
59
53
  */
60
54
  readonly edgeStatuses?: readonly EdgeStatus[] | undefined;
61
- /** Draft migrations to render as dashed edges. */
62
- readonly draftEdges?: readonly DraftEdge[] | undefined;
63
55
  }
64
56
 
65
57
  export interface MigrationRenderInput {
@@ -204,7 +196,9 @@ export function migrationGraphToRenderInput(input: MigrationGraphInput): Migrati
204
196
  spineTargetHash = lastEdge?.to ?? EMPTY_CONTRACT_HASH;
205
197
  }
206
198
 
207
- // Contract not in attested graph — connect from spine target with a dashed edge
199
+ // Contract not in the migration graph — connect from spine target with a
200
+ // dashed edge so the user can see the gap (contract has changed but no
201
+ // migration has been planned yet).
208
202
  if (contractHash !== EMPTY_CONTRACT_HASH && !graph.nodes.has(contractHash)) {
209
203
  const contractMarkers: NodeMarker[] = [];
210
204
  if (mode === 'online' && markerHash === contractHash) {
@@ -216,13 +210,10 @@ export function migrationGraphToRenderInput(input: MigrationGraphInput): Migrati
216
210
  markers: contractMarkers,
217
211
  });
218
212
 
219
- const matchingDraft = input.draftEdges?.find((d) => d.to === contractHash);
220
- const fromHash = matchingDraft?.from ?? spineTargetHash;
221
- if (graph.nodes.has(fromHash) || fromHash === spineTargetHash) {
213
+ if (graph.nodes.has(spineTargetHash) || spineTargetHash === EMPTY_CONTRACT_HASH) {
222
214
  edgeList.push({
223
- from: toShortId(fromHash),
215
+ from: toShortId(spineTargetHash),
224
216
  to: shortHash(contractHash),
225
- ...ifDefined('label', matchingDraft ? `${matchingDraft.dirName} [draft]` : undefined),
226
217
  style: 'dashed',
227
218
  });
228
219
  }
@@ -137,7 +137,6 @@ function getCommandDocsUrl(commandPath: string): string | undefined {
137
137
  'migration apply': 'https://pris.ly/migration-apply',
138
138
  'migration show': 'https://pris.ly/migration-show',
139
139
  'migration status': 'https://pris.ly/migration-status',
140
- 'migration emit': 'https://pris.ly/migration-emit',
141
140
  };
142
141
  return docsMap[commandPath];
143
142
  }
@@ -136,10 +136,6 @@ export interface MigrationApplyCommandOutputResult {
136
136
  };
137
137
  }
138
138
 
139
- export interface MigrationEmitCommandOutputResult {
140
- readonly migrationId: string;
141
- }
142
-
143
139
  export function formatMigrationApplyCommandOutput(
144
140
  result: MigrationApplyCommandOutputResult,
145
141
  flags: GlobalFlags,
@@ -182,31 +178,12 @@ export function formatMigrationApplyCommandOutput(
182
178
  return lines.join('\n');
183
179
  }
184
180
 
185
- export function formatMigrationEmitCommandOutput(
186
- result: MigrationEmitCommandOutputResult,
187
- flags: GlobalFlags,
188
- ): string {
189
- if (flags.quiet) {
190
- return '';
191
- }
192
-
193
- const lines: string[] = [];
194
- const useColor = flags.color !== false;
195
- const formatGreen = createColorFormatter(useColor, green);
196
- const formatDimText = (text: string) => formatDim(useColor, text);
197
-
198
- lines.push(`${formatGreen('✔')} Emitted ops.json and attested migration`);
199
- lines.push(formatDimText(` migrationId: ${result.migrationId}`));
200
-
201
- return lines.join('\n');
202
- }
203
-
204
181
  interface MigrationShowResult {
205
182
  readonly dirName: string;
206
183
  readonly dirPath: string;
207
184
  readonly from: string;
208
185
  readonly to: string;
209
- readonly migrationId: string | null;
186
+ readonly migrationId: string;
210
187
  readonly kind: string;
211
188
  readonly createdAt: string;
212
189
  readonly operations: readonly {
@@ -234,11 +211,7 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
234
211
  lines.push(`${formatDimText(` kind: ${result.kind}`)}`);
235
212
  lines.push(`${formatDimText(` from: ${result.from}`)}`);
236
213
  lines.push(`${formatDimText(` to: ${result.to}`)}`);
237
- if (result.migrationId) {
238
- lines.push(`${formatDimText(` migrationId: ${result.migrationId}`)}`);
239
- } else {
240
- lines.push(`${formatYellow(' migrationId: (draft — not yet attested)')}`);
241
- }
214
+ lines.push(`${formatDimText(` migrationId: ${result.migrationId}`)}`);
242
215
  lines.push(`${formatDimText(` created: ${result.createdAt}`)}`);
243
216
 
244
217
  lines.push('');