@prisma-next/cli 0.5.0-dev.7 → 0.5.0-dev.71

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 (171) hide show
  1. package/README.md +56 -21
  2. package/dist/cli-errors-D3_sMh2K.mjs +33 -0
  3. package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
  4. package/dist/cli-errors-QH8kf-C2.d.mts +3 -0
  5. package/dist/cli.mjs +16 -78
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client-0ZX24FXF.mjs +1398 -0
  8. package/dist/client-0ZX24FXF.mjs.map +1 -0
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +2 -4
  11. package/dist/commands/contract-infer.d.mts.map +1 -1
  12. package/dist/commands/contract-infer.mjs +2 -4
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +14 -13
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +5 -7
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +8 -9
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +13 -13
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -321
  27. package/dist/commands/migration-apply.d.mts +5 -2
  28. package/dist/commands/migration-apply.d.mts.map +1 -1
  29. package/dist/commands/migration-apply.mjs +64 -66
  30. package/dist/commands/migration-apply.mjs.map +1 -1
  31. package/dist/commands/migration-new.d.mts +0 -1
  32. package/dist/commands/migration-new.d.mts.map +1 -1
  33. package/dist/commands/migration-new.mjs +33 -40
  34. package/dist/commands/migration-new.mjs.map +1 -1
  35. package/dist/commands/migration-plan.d.mts +14 -5
  36. package/dist/commands/migration-plan.d.mts.map +1 -1
  37. package/dist/commands/migration-plan.mjs +1 -347
  38. package/dist/commands/migration-ref.d.mts +1 -1
  39. package/dist/commands/migration-ref.d.mts.map +1 -1
  40. package/dist/commands/migration-ref.mjs +7 -12
  41. package/dist/commands/migration-ref.mjs.map +1 -1
  42. package/dist/commands/migration-show.d.mts +13 -7
  43. package/dist/commands/migration-show.d.mts.map +1 -1
  44. package/dist/commands/migration-show.mjs +34 -36
  45. package/dist/commands/migration-show.mjs.map +1 -1
  46. package/dist/commands/migration-status.d.mts +23 -5
  47. package/dist/commands/migration-status.d.mts.map +1 -1
  48. package/dist/commands/migration-status.mjs +2 -4
  49. package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
  50. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  51. package/dist/config-loader.d.mts +0 -1
  52. package/dist/config-loader.d.mts.map +1 -1
  53. package/dist/config-loader.mjs +2 -3
  54. package/dist/contract-emit-B3ChISB_.mjs +338 -0
  55. package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
  56. package/dist/contract-emit-DkMqO7f2.mjs +148 -0
  57. package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
  58. package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-CF6ogEJ_.mjs} +4 -6
  59. package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
  60. package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BDKAE0B0.mjs} +12 -22
  61. package/dist/contract-infer-BDKAE0B0.mjs.map +1 -0
  62. package/dist/db-verify-B4TdDKOI.mjs +403 -0
  63. package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
  64. package/dist/exports/config-types.mjs +1 -2
  65. package/dist/exports/control-api.d.mts +287 -29
  66. package/dist/exports/control-api.d.mts.map +1 -1
  67. package/dist/exports/control-api.mjs +4 -6
  68. package/dist/exports/index.d.mts.map +1 -1
  69. package/dist/exports/index.mjs +28 -30
  70. package/dist/exports/index.mjs.map +1 -1
  71. package/dist/exports/init-output.d.mts +2 -4
  72. package/dist/exports/init-output.d.mts.map +1 -1
  73. package/dist/exports/init-output.mjs +2 -3
  74. package/dist/{framework-components-Cr--XBKy.mjs → framework-components-gwAHl7ml.mjs} +3 -4
  75. package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
  76. package/dist/{init-C5220SY9.mjs → init-Deo7U8_U.mjs} +26 -35
  77. package/dist/init-Deo7U8_U.mjs.map +1 -0
  78. package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-BAgQMYpD.mjs} +10 -11
  79. package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
  80. package/dist/migration-cli.d.mts +41 -12
  81. package/dist/migration-cli.d.mts.map +1 -1
  82. package/dist/migration-cli.mjs +309 -86
  83. package/dist/migration-cli.mjs.map +1 -1
  84. package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B8J702Uh.mjs} +8 -9
  85. package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
  86. package/dist/migration-plan-BcKNnTM7.mjs +530 -0
  87. package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
  88. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CjwB2of-.mjs} +117 -64
  89. package/dist/migration-status-CjwB2of-.mjs.map +1 -0
  90. package/dist/{migrations-Bo5WtTla.mjs → migrations-CIK94AJf.mjs} +43 -23
  91. package/dist/migrations-CIK94AJf.mjs.map +1 -0
  92. package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
  93. package/dist/output-DnjfCC_u.mjs.map +1 -0
  94. package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-xASh41wr.mjs} +2 -2
  95. package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  96. package/dist/{result-handler-Ba3zWQsI.mjs → result-handler-DWb1rFS-.mjs} +52 -27
  97. package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
  98. package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
  99. package/dist/{terminal-ui-C3ZLwQxK.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
  100. package/dist/{verify-Bkycc-Tf.mjs → verify-BEIa9638.mjs} +3 -4
  101. package/dist/verify-BEIa9638.mjs.map +1 -0
  102. package/package.json +28 -26
  103. package/src/cli.ts +32 -6
  104. package/src/commands/contract-emit.ts +67 -163
  105. package/src/commands/contract-infer.ts +7 -20
  106. package/src/commands/db-init.ts +14 -3
  107. package/src/commands/db-update.ts +8 -4
  108. package/src/commands/db-verify.ts +47 -15
  109. package/src/commands/init/index.ts +1 -1
  110. package/src/commands/init/init.ts +2 -2
  111. package/src/commands/init/templates/code-templates.ts +12 -4
  112. package/src/commands/inspect-live-schema.ts +10 -5
  113. package/src/commands/migration-apply.ts +92 -71
  114. package/src/commands/migration-new.ts +42 -45
  115. package/src/commands/migration-plan.ts +147 -64
  116. package/src/commands/migration-ref.ts +8 -7
  117. package/src/commands/migration-show.ts +60 -41
  118. package/src/commands/migration-status.ts +196 -60
  119. package/src/config-path-validation.ts +0 -1
  120. package/src/control-api/client.ts +69 -1
  121. package/src/control-api/contract-enrichment.ts +6 -4
  122. package/src/control-api/operations/contract-emit.ts +198 -115
  123. package/src/control-api/operations/db-apply-aggregate.ts +446 -0
  124. package/src/control-api/operations/db-init.ts +51 -253
  125. package/src/control-api/operations/db-update.ts +66 -183
  126. package/src/control-api/operations/db-verify.ts +342 -0
  127. package/src/control-api/operations/migration-apply.ts +37 -9
  128. package/src/control-api/types.ts +125 -7
  129. package/src/exports/control-api.ts +15 -3
  130. package/src/load-ts-contract.ts +28 -26
  131. package/src/migration-cli.ts +445 -122
  132. package/src/utils/cli-errors.ts +49 -2
  133. package/src/utils/combine-schema-results.ts +84 -0
  134. package/src/utils/command-helpers.ts +69 -25
  135. package/src/utils/contract-space-aggregate-loader.ts +236 -0
  136. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  137. package/src/utils/contract-space-migrate-pass.ts +156 -0
  138. package/src/utils/emit-queue.ts +26 -0
  139. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  140. package/src/utils/formatters/migrations.ts +62 -26
  141. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  142. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  143. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  144. package/dist/client-CrsnY58k.mjs +0 -997
  145. package/dist/client-CrsnY58k.mjs.map +0 -1
  146. package/dist/commands/db-verify.mjs.map +0 -1
  147. package/dist/commands/migration-plan.mjs.map +0 -1
  148. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  149. package/dist/contract-emit--feXyNd7.mjs +0 -4
  150. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  151. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  152. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  153. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  154. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  155. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  156. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  157. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  158. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  159. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  160. package/dist/init-C5220SY9.mjs.map +0 -1
  161. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  162. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  163. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  164. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  165. package/dist/output-BpcQrnnq.mjs.map +0 -1
  166. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  167. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  168. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  169. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  170. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  171. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -108,12 +108,15 @@ export const contract = defineContract(
108
108
  }
109
109
 
110
110
  function starterSchemaPslPostgres(): string {
111
- return `model User {
111
+ return `// use prisma-next
112
+
113
+ model User {
112
114
  id Int @id @default(autoincrement())
113
115
  email String @unique
114
116
  name String?
115
117
  posts Post[]
116
118
  createdAt DateTime @default(now())
119
+ updatedAt temporal.updatedAt()
117
120
  }
118
121
 
119
122
  model Post {
@@ -123,12 +126,15 @@ model Post {
123
126
  author User @relation(fields: [authorId], references: [id])
124
127
  authorId Int
125
128
  createdAt DateTime @default(now())
129
+ updatedAt temporal.updatedAt()
126
130
  }
127
131
  `;
128
132
  }
129
133
 
130
134
  function starterSchemaPslMongo(): string {
131
- return `model User {
135
+ return `// use prisma-next
136
+
137
+ model User {
132
138
  id ObjectId @id @map("_id")
133
139
  email String @unique
134
140
  name String?
@@ -161,7 +167,8 @@ export const contract = defineContract(
161
167
  id: field.id.uuidv7(),
162
168
  email: field.text().unique(),
163
169
  name: field.text().optional(),
164
- createdAt: field.createdAt(),
170
+ createdAt: field.temporal.createdAt(),
171
+ updatedAt: field.temporal.updatedAt(),
165
172
  },
166
173
  relations: {
167
174
  posts: rel.hasMany('Post', { by: 'authorId' }),
@@ -174,7 +181,8 @@ export const contract = defineContract(
174
181
  title: field.text(),
175
182
  content: field.text().optional(),
176
183
  authorId: field.uuid(),
177
- createdAt: field.createdAt(),
184
+ createdAt: field.temporal.createdAt(),
185
+ updatedAt: field.temporal.updatedAt(),
178
186
  },
179
187
  relations: {
180
188
  author: rel.belongsTo('User', { from: 'authorId', to: 'id' }),
@@ -1,5 +1,5 @@
1
1
  import type { CoreSchemaView } from '@prisma-next/framework-components/control';
2
- import { validatePrintableSqlSchemaIR } from '@prisma-next/psl-printer';
2
+ import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
3
3
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
4
4
  import { relative, resolve } from 'pathe';
5
5
  import { loadConfig } from '../config-loader';
@@ -33,6 +33,12 @@ export interface InspectLiveSchemaResult {
33
33
  readonly config: LoadedCliConfig;
34
34
  readonly schema: unknown;
35
35
  readonly schemaView: CoreSchemaView | undefined;
36
+ /**
37
+ * PSL AST inferred from the introspected schema, when the configured family
38
+ * implements `PslContractInferCapable`. `undefined` for families that do not
39
+ * support inference (e.g. Mongo today).
40
+ */
41
+ readonly pslContractAst: PslDocumentAst | undefined;
36
42
  readonly target: {
37
43
  readonly familyId: string;
38
44
  readonly id: string;
@@ -122,14 +128,12 @@ export async function inspectLiveSchema(
122
128
  const onProgress = createProgressAdapter({ ui, flags });
123
129
 
124
130
  try {
125
- const schemaIR = await client.introspect({
131
+ const schema = await client.introspect({
126
132
  connection: dbConnection,
127
133
  onProgress,
128
134
  });
129
- // TODO(TML-2251): Remove SQL-specific branching — SQL should use the same family-agnostic path as Mongo.
130
- const schema =
131
- config.family.familyId === 'sql' ? validatePrintableSqlSchemaIR(schemaIR) : schemaIR;
132
135
  const schemaView = client.toSchemaView(schema);
136
+ const pslContractAst = client.inferPslContract(schema);
133
137
 
134
138
  const dbUrl = typeof dbConnection === 'string' ? maskConnectionUrl(dbConnection) : undefined;
135
139
 
@@ -137,6 +141,7 @@ export async function inspectLiveSchema(
137
141
  config,
138
142
  schema,
139
143
  schemaView,
144
+ pslContractAst,
140
145
  target: {
141
146
  familyId: config.family.familyId,
142
147
  id: config.target.targetId,
@@ -1,9 +1,14 @@
1
- import { verifyMigrationBundle } from '@prisma-next/migration-tools/attestation';
2
1
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
- import { findPathWithDecision } from '@prisma-next/migration-tools/dag';
2
+ import {
3
+ errorNoInvariantPath,
4
+ errorUnknownInvariant,
5
+ MigrationToolsError,
6
+ } from '@prisma-next/migration-tools/errors';
7
+ import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
8
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
9
+ import type { RefEntry } from '@prisma-next/migration-tools/refs';
4
10
  import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
5
- import type { MigrationBundle } from '@prisma-next/migration-tools/types';
6
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
11
+ import { ifDefined } from '@prisma-next/utils/defined';
7
12
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
8
13
  import { Command } from 'commander';
9
14
 
@@ -18,11 +23,12 @@ import {
18
23
  errorRuntime,
19
24
  errorTargetMigrationNotSupported,
20
25
  errorUnexpected,
26
+ mapMigrationToolsError,
21
27
  } from '../utils/cli-errors';
22
28
  import {
23
29
  addGlobalOptions,
24
- loadAllBundles,
25
- type MigrationBundleSet,
30
+ collectDeclaredInvariants,
31
+ loadMigrationPackages,
26
32
  maskConnectionUrl,
27
33
  readContractEnvelope,
28
34
  resolveMigrationPaths,
@@ -30,6 +36,7 @@ import {
30
36
  setCommandExamples,
31
37
  targetSupportsMigrations,
32
38
  toPathDecisionResult,
39
+ toStructuralEdge,
33
40
  } from '../utils/command-helpers';
34
41
  import { formatMigrationApplyCommandOutput } from '../utils/formatters/migrations';
35
42
  import { formatStyledHeader } from '../utils/formatters/styled';
@@ -51,7 +58,7 @@ export interface MigrationApplyResult {
51
58
  readonly markerHash: string;
52
59
  readonly applied: readonly {
53
60
  readonly dirName: string;
54
- readonly from: string;
61
+ readonly from: string | null;
55
62
  readonly to: string;
56
63
  readonly operationsExecuted: number;
57
64
  }[];
@@ -62,11 +69,14 @@ export interface MigrationApplyResult {
62
69
  readonly alternativeCount: number;
63
70
  readonly tieBreakReasons: readonly string[];
64
71
  readonly refName?: string;
72
+ readonly requiredInvariants: readonly string[];
73
+ readonly satisfiedInvariants: readonly string[];
65
74
  readonly selectedPath: readonly {
66
75
  readonly dirName: string;
67
- readonly migrationId: string;
76
+ readonly migrationHash: string;
68
77
  readonly from: string;
69
78
  readonly to: string;
79
+ readonly invariants: readonly string[];
70
80
  }[];
71
81
  };
72
82
  readonly timings: {
@@ -74,19 +84,6 @@ export interface MigrationApplyResult {
74
84
  };
75
85
  }
76
86
 
77
- function mapMigrationToolsError(error: unknown): CliStructuredErrorType {
78
- if (MigrationToolsError.is(error)) {
79
- return errorRuntime(error.message, {
80
- why: error.why,
81
- fix: error.fix,
82
- meta: { code: error.code, ...(error.details ?? {}) },
83
- });
84
- }
85
- return errorUnexpected(error instanceof Error ? error.message : String(error), {
86
- why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
87
- });
88
- }
89
-
90
87
  function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
91
88
  return errorRuntime(failure.summary, {
92
89
  why: failure.why ?? 'Migration runner failed',
@@ -95,13 +92,14 @@ function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType
95
92
  });
96
93
  }
97
94
 
98
- function packageToStep(pkg: MigrationBundle): MigrationApplyStep {
95
+ function packageToStep(pkg: OnDiskMigrationPackage): MigrationApplyStep {
99
96
  return {
100
97
  dirName: pkg.dirName,
101
- from: pkg.manifest.from,
102
- to: pkg.manifest.to,
103
- toContract: pkg.manifest.toContract,
98
+ from: pkg.metadata.from,
99
+ to: pkg.metadata.to,
100
+ toContract: pkg.metadata.toContract,
104
101
  operations: pkg.ops,
102
+ providedInvariants: pkg.metadata.providedInvariants,
105
103
  };
106
104
  }
107
105
 
@@ -112,7 +110,7 @@ async function executeMigrationApplyCommand(
112
110
  startTime: number,
113
111
  ): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
114
112
  const config = await loadConfig(options.config);
115
- const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
113
+ const { configPath, appMigrationsDir, appMigrationsRelative, refsDir } = resolveMigrationPaths(
116
114
  options.config,
117
115
  config,
118
116
  );
@@ -143,14 +141,14 @@ async function executeMigrationApplyCommand(
143
141
  );
144
142
  }
145
143
 
146
- let destinationHash: string;
147
- let refName: string | undefined;
144
+ let refEntry: RefEntry | undefined;
145
+ let envelopeHash: string | undefined;
146
+ const refName = options.ref;
148
147
 
149
- if (options.ref) {
150
- refName = options.ref;
148
+ if (refName) {
151
149
  try {
152
150
  const refs = await readRefs(refsDir);
153
- destinationHash = resolveRef(refs, refName).hash;
151
+ refEntry = resolveRef(refs, refName);
154
152
  } catch (error) {
155
153
  if (MigrationToolsError.is(error)) {
156
154
  return notOk(mapMigrationToolsError(error));
@@ -160,7 +158,7 @@ async function executeMigrationApplyCommand(
160
158
  } else {
161
159
  try {
162
160
  const envelope = await readContractEnvelope(config);
163
- destinationHash = envelope.storageHash;
161
+ envelopeHash = envelope.storageHash;
164
162
  } catch (error) {
165
163
  return notOk(
166
164
  errorRuntime('Current contract is unavailable', {
@@ -170,11 +168,12 @@ async function executeMigrationApplyCommand(
170
168
  );
171
169
  }
172
170
  }
171
+ const destinationHash = refEntry?.hash ?? envelopeHash!;
173
172
 
174
173
  if (!flags.json && !flags.quiet) {
175
174
  const details: Array<{ label: string; value: string }> = [
176
175
  { label: 'config', value: configPath },
177
- { label: 'migrations', value: migrationsRelative },
176
+ { label: 'migrations', value: appMigrationsRelative },
178
177
  ];
179
178
  if (typeof dbConnection === 'string') {
180
179
  details.push({
@@ -196,9 +195,9 @@ async function executeMigrationApplyCommand(
196
195
  }
197
196
 
198
197
  // Read migrations and build migration chain model (offline — no DB needed)
199
- let migrations: MigrationBundleSet;
198
+ let migrations: Awaited<ReturnType<typeof loadMigrationPackages>>;
200
199
  try {
201
- migrations = await loadAllBundles(migrationsDir);
200
+ migrations = await loadMigrationPackages(appMigrationsDir);
202
201
  } catch (error) {
203
202
  if (MigrationToolsError.is(error)) {
204
203
  return notOk(mapMigrationToolsError(error));
@@ -206,27 +205,6 @@ async function executeMigrationApplyCommand(
206
205
  throw error;
207
206
  }
208
207
 
209
- // Defense in depth: re-hash every bundle and confirm the recorded
210
- // `migrationId` matches the on-disk `(manifest, ops)`. Catches FS
211
- // corruption, partial writes, and post-emit hand edits before we
212
- // start touching the database.
213
- for (const bundle of migrations.bundles) {
214
- const verified = verifyMigrationBundle(bundle);
215
- if (!verified.ok) {
216
- return notOk(
217
- errorRuntime(`Migration package is corrupt: ${bundle.dirName}`, {
218
- why: `Stored migrationId "${verified.storedMigrationId}" does not match the recomputed hash "${verified.computedMigrationId}" for ${migrationsRelative}/${bundle.dirName}. The migration.json or ops.json has been edited or partially written since emit.`,
219
- fix: `Re-emit the package by running \`node "${migrationsRelative}/${bundle.dirName}/migration.ts"\`, or restore the directory from version control.`,
220
- meta: {
221
- dirName: bundle.dirName,
222
- storedMigrationId: verified.storedMigrationId,
223
- computedMigrationId: verified.computedMigrationId,
224
- },
225
- }),
226
- );
227
- }
228
- }
229
-
230
208
  const client = createControlClient({
231
209
  family: config.family,
232
210
  target: config.target,
@@ -239,14 +217,40 @@ async function executeMigrationApplyCommand(
239
217
  await client.connect(dbConnection);
240
218
  const marker = await client.readMarker();
241
219
 
220
+ // Pre-check unknown invariants against `(declared by graph) ∪
221
+ // (already on the marker)`. The union catches the edge case where the
222
+ // ref carries an invariant whose declaring migration was retired (e.g.
223
+ // history rewritten) but whose id is recorded on the marker —
224
+ // surfacing that as MIGRATION.UNKNOWN_INVARIANT would be misleading
225
+ // because the database has already satisfied the requirement, so the
226
+ // marker-subtraction below empties `effectiveRequired` and apply
227
+ // short-circuits to "Already up to date".
228
+ if (refEntry && refEntry.invariants.length > 0) {
229
+ const declared = collectDeclaredInvariants(migrations.graph);
230
+ const known = new Set<string>(declared);
231
+ for (const id of marker?.invariants ?? []) known.add(id);
232
+ const unknown = refEntry.invariants.filter((id) => !known.has(id));
233
+ if (unknown.length > 0) {
234
+ return notOk(
235
+ mapMigrationToolsError(
236
+ errorUnknownInvariant({
237
+ ...ifDefined('refName', refName),
238
+ unknown,
239
+ declared: [...declared].sort(),
240
+ }),
241
+ ),
242
+ );
243
+ }
244
+ }
245
+
242
246
  // --- No migrations on disk ---
243
247
  if (migrations.bundles.length === 0) {
244
248
  if (marker?.storageHash) {
245
249
  return notOk(
246
250
  errorRuntime('Database has state but no migrations exist', {
247
- why: `The database marker hash "${marker.storageHash}" exists but no migrations were found in ${migrationsRelative}`,
251
+ why: `The database marker hash "${marker.storageHash}" exists but no migrations were found in ${appMigrationsRelative}`,
248
252
  fix: 'Ensure the migrations directory is correct. If the database was managed with `db init` or `db update`, run `prisma-next db sign` to update the marker.',
249
- meta: { markerHash: marker.storageHash, migrationsDir: migrationsRelative },
253
+ meta: { markerHash: marker.storageHash, migrationsDir: appMigrationsRelative },
250
254
  }),
251
255
  );
252
256
  }
@@ -254,9 +258,9 @@ async function executeMigrationApplyCommand(
254
258
  if (destinationHash !== EMPTY_CONTRACT_HASH) {
255
259
  return notOk(
256
260
  errorRuntime('Current contract has no planned migrations', {
257
- why: `No migrations were found in ${migrationsRelative}, but current contract hash is "${destinationHash}"`,
261
+ why: `No migrations were found in ${appMigrationsRelative}, but current contract hash is "${destinationHash}"`,
258
262
  fix: 'Run `prisma-next migration plan` to create a migration for the current contract.',
259
- meta: { destinationHash, migrationsDir: migrationsRelative },
263
+ meta: { destinationHash, migrationsDir: appMigrationsRelative },
260
264
  }),
261
265
  );
262
266
  }
@@ -291,7 +295,7 @@ async function executeMigrationApplyCommand(
291
295
  if (markerHash !== undefined && !migrations.graph.nodes.has(markerHash)) {
292
296
  return notOk(
293
297
  errorRuntime('Database marker does not match any known migration', {
294
- why: `The database marker hash "${markerHash}" is not found in the migration history at ${migrationsRelative}`,
298
+ why: `The database marker hash "${markerHash}" is not found in the migration history at ${appMigrationsRelative}`,
295
299
  fix: 'Ensure the migrations directory matches this database. If the database was managed with `db init` or `db update`, run `prisma-next db sign` to update the marker.',
296
300
  meta: { markerHash, knownNodes: [...migrations.graph.nodes] },
297
301
  }),
@@ -301,20 +305,38 @@ async function executeMigrationApplyCommand(
301
305
  if (!migrations.graph.nodes.has(destinationHash)) {
302
306
  return notOk(
303
307
  errorRuntime('Current contract has no planned migration path', {
304
- why: `Current contract hash "${destinationHash}" is not present in the migration history at ${migrationsRelative}`,
308
+ why: `Current contract hash "${destinationHash}" is not present in the migration history at ${appMigrationsRelative}`,
305
309
  fix: 'Run `prisma-next migration plan` to create a migration for the current contract, then re-run apply.',
306
310
  meta: { destinationHash, knownNodes: [...migrations.graph.nodes] },
307
311
  }),
308
312
  );
309
313
  }
310
314
 
311
- // --- Resolve path and apply ---
312
-
313
315
  // "No marker" means the database is fresh — start from the empty contract hash.
314
316
  const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
315
317
 
316
- const decision = findPathWithDecision(migrations.graph, originHash, destinationHash, refName);
317
- if (!decision) {
318
+ const appliedInvariants = new Set(marker?.invariants ?? []);
319
+ const effectiveRequired = new Set(
320
+ (refEntry?.invariants ?? []).filter((id) => !appliedInvariants.has(id)),
321
+ );
322
+
323
+ const outcome = findPathWithDecision(migrations.graph, originHash, destinationHash, {
324
+ ...ifDefined('refName', refName),
325
+ required: effectiveRequired,
326
+ });
327
+ if (outcome.kind === 'unsatisfiable') {
328
+ return notOk(
329
+ mapMigrationToolsError(
330
+ errorNoInvariantPath({
331
+ ...ifDefined('refName', refName),
332
+ required: [...effectiveRequired].sort(),
333
+ missing: outcome.missing,
334
+ structuralPath: outcome.structuralPath.map(toStructuralEdge),
335
+ }),
336
+ ),
337
+ );
338
+ }
339
+ if (outcome.kind === 'unreachable') {
318
340
  return notOk(
319
341
  errorRuntime('No migration path from current state to target', {
320
342
  why: `Cannot find a path from "${originHash}" to target "${destinationHash}"`,
@@ -324,10 +346,9 @@ async function executeMigrationApplyCommand(
324
346
  );
325
347
  }
326
348
 
327
- const pendingPath = decision.selectedPath;
328
- const pathDecision = toPathDecisionResult(decision);
349
+ const pathDecision = toPathDecisionResult(outcome.decision);
329
350
 
330
- if (pendingPath.length === 0) {
351
+ if (outcome.decision.selectedPath.length === 0) {
331
352
  return ok({
332
353
  ok: true,
333
354
  migrationsApplied: 0,
@@ -342,7 +363,7 @@ async function executeMigrationApplyCommand(
342
363
 
343
364
  const bundleByDir = new Map(migrations.bundles.map((b) => [b.dirName, b]));
344
365
  const pendingMigrations: MigrationApplyStep[] = [];
345
- for (const migration of pendingPath) {
366
+ for (const migration of outcome.decision.selectedPath) {
346
367
  const pkg = bundleByDir.get(migration.dirName);
347
368
  if (!pkg) {
348
369
  return notOk(
@@ -376,7 +397,7 @@ async function executeMigrationApplyCommand(
376
397
  return ok({
377
398
  ok: true,
378
399
  migrationsApplied: value.migrationsApplied,
379
- migrationsTotal: pendingPath.length,
400
+ migrationsTotal: outcome.decision.selectedPath.length,
380
401
  markerHash: value.markerHash,
381
402
  applied: value.applied,
382
403
  summary: value.summary,
@@ -11,32 +11,36 @@
11
11
  import { readFileSync } from 'node:fs';
12
12
  import type { Contract } from '@prisma-next/contract/types';
13
13
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
14
- import { createControlStack } from '@prisma-next/framework-components/control';
15
- import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
16
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
17
- import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
14
+ import { APP_SPACE_ID, createControlStack } from '@prisma-next/framework-components/control';
15
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
16
+ import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
18
17
  import {
19
18
  copyFilesWithRename,
20
19
  formatMigrationDirName,
21
20
  readMigrationsDir,
22
21
  writeMigrationPackage,
23
22
  } from '@prisma-next/migration-tools/io';
23
+ import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
24
+ import {
25
+ findLatestMigration,
26
+ reconstructGraph,
27
+ } from '@prisma-next/migration-tools/migration-graph';
24
28
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
25
- import type { MigrationManifest } from '@prisma-next/migration-tools/types';
26
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
27
29
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
28
30
  import { Command } from 'commander';
29
- import { join, relative, resolve } from 'pathe';
31
+ import { join, relative } from 'pathe';
30
32
  import { loadConfig } from '../config-loader';
31
33
  import {
32
34
  CliStructuredError,
33
35
  errorRuntime,
34
36
  errorTargetMigrationNotSupported,
35
37
  errorUnexpected,
38
+ mapMigrationToolsError,
36
39
  } from '../utils/cli-errors';
37
40
  import {
38
41
  addGlobalOptions,
39
42
  getTargetMigrations,
43
+ resolveContractPath,
40
44
  resolveMigrationPaths,
41
45
  setCommandDescriptions,
42
46
  setCommandExamples,
@@ -57,7 +61,7 @@ interface MigrationNewOptions extends CommonCommandOptions {
57
61
  interface MigrationNewResult {
58
62
  readonly ok: true;
59
63
  readonly dir: string;
60
- readonly from: string;
64
+ readonly from: string | null;
61
65
  readonly to: string;
62
66
  readonly summary: string;
63
67
  }
@@ -66,13 +70,9 @@ async function executeMigrationNewCommand(
66
70
  options: MigrationNewOptions,
67
71
  ): Promise<Result<MigrationNewResult, CliStructuredError>> {
68
72
  const config = await loadConfig(options.config);
69
- const { migrationsDir, migrationsRelative } = resolveMigrationPaths(options.config, config);
73
+ const { appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.config, config);
70
74
 
71
- const contractPath = config.contract?.output ?? 'contract.json';
72
- const contractPathAbsolute = resolve(
73
- options.config ? resolve(options.config, '..') : process.cwd(),
74
- contractPath,
75
- );
75
+ const contractPathAbsolute = resolveContractPath(config);
76
76
 
77
77
  let contractJsonContent: string;
78
78
  try {
@@ -116,37 +116,37 @@ async function executeMigrationNewCommand(
116
116
  }
117
117
 
118
118
  let fromContract: Contract | null = null;
119
- let fromHash: string = EMPTY_CONTRACT_HASH;
119
+ let fromHash: string | null = null;
120
120
  let fromContractSourceDir: string | null = null;
121
121
 
122
122
  try {
123
- const packages = await readMigrationsDir(migrationsDir);
123
+ const packages = await readMigrationsDir(appMigrationsDir);
124
124
 
125
125
  if (packages.length > 0) {
126
126
  const graph = reconstructGraph(packages);
127
127
 
128
128
  if (options.from) {
129
- const match = packages.find((p) => p.manifest.to.startsWith(options.from!));
129
+ const match = packages.find((p) => p.metadata.to.startsWith(options.from!));
130
130
  if (!match) {
131
131
  return notOk(
132
132
  errorRuntime('Starting contract not found', {
133
- why: `No migration with to hash matching "${options.from}" exists in ${migrationsRelative}`,
133
+ why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
134
134
  fix: 'Check that the --from hash matches a known migration target hash.',
135
135
  }),
136
136
  );
137
137
  }
138
- fromHash = match.manifest.to;
139
- fromContract = match.manifest.toContract;
138
+ fromHash = match.metadata.to;
139
+ fromContract = match.metadata.toContract;
140
140
  fromContractSourceDir = match.dirPath;
141
141
  } else {
142
142
  const latestMigration = findLatestMigration(graph);
143
143
  if (latestMigration) {
144
144
  fromHash = latestMigration.to;
145
145
  const leafPkg = packages.find(
146
- (p) => p.manifest.migrationId === latestMigration.migrationId,
146
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
147
147
  );
148
148
  if (leafPkg) {
149
- fromContract = leafPkg.manifest.toContract;
149
+ fromContract = leafPkg.metadata.toContract;
150
150
  fromContractSourceDir = leafPkg.dirPath;
151
151
  }
152
152
  }
@@ -154,22 +154,16 @@ async function executeMigrationNewCommand(
154
154
  }
155
155
  } catch (error) {
156
156
  if (MigrationToolsError.is(error)) {
157
- return notOk(
158
- errorRuntime(error.message, {
159
- why: error.why,
160
- fix: error.fix,
161
- meta: { code: error.code },
162
- }),
163
- );
157
+ return notOk(mapMigrationToolsError(error));
164
158
  }
165
159
  throw error;
166
160
  }
167
161
 
168
- if (fromHash === toStorageHash) {
162
+ if (fromHash === toStorageHash && !options.from) {
169
163
  return notOk(
170
164
  errorRuntime('No changes detected', {
171
165
  why: 'The from and to contract hashes are identical — there is nothing to migrate.',
172
- fix: 'Change the contract and run `prisma-next contract emit` before creating a new migration.',
166
+ fix: 'Change the contract and run `prisma-next contract emit` before creating a new migration. To author a data-only migration on the current contract hash, pass `--from <hash>` explicitly.',
173
167
  }),
174
168
  );
175
169
  }
@@ -177,16 +171,15 @@ async function executeMigrationNewCommand(
177
171
  const timestamp = new Date();
178
172
  const slug = options.name ?? 'migration';
179
173
  const dirName = formatMigrationDirName(timestamp, slug);
180
- const packageDir = join(migrationsDir, dirName);
174
+ const packageDir = join(appMigrationsDir, dirName);
181
175
 
182
176
  // `migration new` scaffolds an empty `migration.ts` for the user to
183
177
  // fill, so we attest over `ops: []`. Re-running self-emit after the
184
- // user adds operations will produce a different `migrationId` (over
178
+ // user adds operations will produce a different `migrationHash` (over
185
179
  // the real ops). This is intentional — there is no on-disk draft.
186
- const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
180
+ const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
187
181
  from: fromHash,
188
182
  to: toStorageHash,
189
- kind: 'regular',
190
183
  fromContract,
191
184
  toContract: toContractJson,
192
185
  hints: {
@@ -195,11 +188,12 @@ async function executeMigrationNewCommand(
195
188
  plannerVersion: '1.0.0',
196
189
  },
197
190
  labels: [],
191
+ providedInvariants: [],
198
192
  createdAt: timestamp.toISOString(),
199
193
  };
200
- const manifest: MigrationManifest = {
201
- ...baseManifest,
202
- migrationId: computeMigrationId(baseManifest, []),
194
+ const metadata: MigrationMetadata = {
195
+ ...baseMetadata,
196
+ migrationHash: computeMigrationHash(baseMetadata, []),
203
197
  };
204
198
 
205
199
  const migrations = getTargetMigrations(config.target);
@@ -218,7 +212,7 @@ async function executeMigrationNewCommand(
218
212
  ...(config.extensionPacks ?? []),
219
213
  ]);
220
214
 
221
- await writeMigrationPackage(packageDir, manifest, []);
215
+ await writeMigrationPackage(packageDir, metadata, []);
222
216
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
223
217
  await copyFilesWithRename(packageDir, [
224
218
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
@@ -237,12 +231,15 @@ async function executeMigrationNewCommand(
237
231
  const stack = createControlStack(config);
238
232
  const familyInstance = config.family.create(stack);
239
233
  const planner = migrations.createPlanner(familyInstance);
240
- const emptyPlan = planner.emptyMigration({
241
- packageDir,
242
- contractJsonPath: join(packageDir, 'end-contract.json'),
243
- fromHash,
244
- toHash: toStorageHash,
245
- });
234
+ const emptyPlan = planner.emptyMigration(
235
+ {
236
+ packageDir,
237
+ contractJsonPath: join(packageDir, 'end-contract.json'),
238
+ fromHash,
239
+ toHash: toStorageHash,
240
+ },
241
+ APP_SPACE_ID,
242
+ );
246
243
  await writeMigrationTs(packageDir, emptyPlan.renderTypeScript());
247
244
 
248
245
  return ok({