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

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 (88) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +145 -6
  3. package/dist/cli-errors-JlPTsazx.mjs +3 -0
  4. package/dist/cli.mjs +29 -2
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{client-B7f4PZZ1.mjs → client-PimzSD1f.mjs} +155 -82
  7. package/dist/client-PimzSD1f.mjs.map +1 -0
  8. package/dist/commands/contract-emit.mjs +5 -3
  9. package/dist/commands/contract-emit.mjs.map +1 -1
  10. package/dist/commands/db-init.mjs +5 -4
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-introspect.mjs +5 -3
  13. package/dist/commands/db-introspect.mjs.map +1 -1
  14. package/dist/commands/db-schema-verify.d.mts.map +1 -1
  15. package/dist/commands/db-schema-verify.mjs +6 -4
  16. package/dist/commands/db-schema-verify.mjs.map +1 -1
  17. package/dist/commands/db-sign.d.mts.map +1 -1
  18. package/dist/commands/db-sign.mjs +6 -4
  19. package/dist/commands/db-sign.mjs.map +1 -1
  20. package/dist/commands/db-update.mjs +5 -4
  21. package/dist/commands/db-update.mjs.map +1 -1
  22. package/dist/commands/db-verify.d.mts.map +1 -1
  23. package/dist/commands/db-verify.mjs +6 -4
  24. package/dist/commands/db-verify.mjs.map +1 -1
  25. package/dist/commands/migration-apply.d.mts +23 -0
  26. package/dist/commands/migration-apply.d.mts.map +1 -0
  27. package/dist/commands/migration-apply.mjs +249 -0
  28. package/dist/commands/migration-apply.mjs.map +1 -0
  29. package/dist/commands/migration-plan.d.mts +25 -0
  30. package/dist/commands/migration-plan.d.mts.map +1 -0
  31. package/dist/commands/migration-plan.mjs +266 -0
  32. package/dist/commands/migration-plan.mjs.map +1 -0
  33. package/dist/commands/migration-show.d.mts +28 -0
  34. package/dist/commands/migration-show.d.mts.map +1 -0
  35. package/dist/commands/migration-show.mjs +138 -0
  36. package/dist/commands/migration-show.mjs.map +1 -0
  37. package/dist/commands/migration-status.d.mts +35 -0
  38. package/dist/commands/migration-status.d.mts.map +1 -0
  39. package/dist/commands/migration-status.mjs +259 -0
  40. package/dist/commands/migration-status.mjs.map +1 -0
  41. package/dist/commands/migration-verify.d.mts +16 -0
  42. package/dist/commands/migration-verify.d.mts.map +1 -0
  43. package/dist/commands/migration-verify.mjs +86 -0
  44. package/dist/commands/migration-verify.mjs.map +1 -0
  45. package/dist/{config-loader-DqKf1qSa.mjs → config-loader-PPf4CtDj.mjs} +4 -3
  46. package/dist/config-loader-PPf4CtDj.mjs.map +1 -0
  47. package/dist/config-loader.d.mts +1 -1
  48. package/dist/config-loader.d.mts.map +1 -1
  49. package/dist/config-loader.mjs +1 -1
  50. package/dist/exports/config-types.d.mts +1 -1
  51. package/dist/exports/config-types.mjs +1 -1
  52. package/dist/exports/control-api.d.mts +100 -2
  53. package/dist/exports/control-api.d.mts.map +1 -1
  54. package/dist/exports/control-api.mjs +3 -2
  55. package/dist/exports/control-api.mjs.map +1 -1
  56. package/dist/exports/index.mjs +1 -1
  57. package/dist/extract-sql-ddl-BmlKvk4o.mjs +26 -0
  58. package/dist/extract-sql-ddl-BmlKvk4o.mjs.map +1 -0
  59. package/dist/framework-components-CjV_jD8f.mjs +59 -0
  60. package/dist/framework-components-CjV_jD8f.mjs.map +1 -0
  61. package/dist/{migration-command-scaffold-BELw_do2.mjs → migration-command-scaffold-DfY_F3ev.mjs} +7 -5
  62. package/dist/migration-command-scaffold-DfY_F3ev.mjs.map +1 -0
  63. package/dist/progress-adapter-DENrzF6I.mjs +49 -0
  64. package/dist/progress-adapter-DENrzF6I.mjs.map +1 -0
  65. package/dist/{result-handler-BhmrXIvT.mjs → result-handler-iA9JtUC7.mjs} +158 -51
  66. package/dist/result-handler-iA9JtUC7.mjs.map +1 -0
  67. package/package.json +34 -12
  68. package/src/cli.ts +38 -0
  69. package/src/commands/db-schema-verify.ts +6 -4
  70. package/src/commands/db-sign.ts +6 -4
  71. package/src/commands/db-verify.ts +6 -4
  72. package/src/commands/migration-apply.ts +431 -0
  73. package/src/commands/migration-plan.ts +446 -0
  74. package/src/commands/migration-show.ts +255 -0
  75. package/src/commands/migration-status.ts +436 -0
  76. package/src/commands/migration-verify.ts +151 -0
  77. package/src/config-loader.ts +13 -3
  78. package/src/control-api/client.ts +31 -0
  79. package/src/control-api/operations/migration-apply.ts +195 -0
  80. package/src/control-api/types.ts +113 -1
  81. package/src/exports/config-types.ts +3 -3
  82. package/src/utils/command-helpers.ts +11 -0
  83. package/src/utils/migration-command-scaffold.ts +7 -6
  84. package/src/utils/output.ts +305 -3
  85. package/dist/client-B7f4PZZ1.mjs.map +0 -1
  86. package/dist/config-loader-DqKf1qSa.mjs.map +0 -1
  87. package/dist/migration-command-scaffold-BELw_do2.mjs.map +0 -1
  88. package/dist/result-handler-BhmrXIvT.mjs.map +0 -1
@@ -0,0 +1,195 @@
1
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
2
+ import type { ContractIR } from '@prisma-next/contract/ir';
3
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';
4
+ import type {
5
+ ControlDriverInstance,
6
+ ControlFamilyInstance,
7
+ MigrationPlanOperation,
8
+ MigrationRunnerResult,
9
+ TargetMigrationsCapability,
10
+ } from '@prisma-next/core-control-plane/types';
11
+ import { notOk, ok } from '@prisma-next/utils/result';
12
+ import type {
13
+ MigrationApplyAppliedEntry,
14
+ MigrationApplyResult,
15
+ MigrationApplyStep,
16
+ OnControlProgress,
17
+ } from '../types';
18
+
19
+ export interface ExecuteMigrationApplyOptions<TFamilyId extends string, TTargetId extends string> {
20
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
21
+ readonly familyInstance: ControlFamilyInstance<TFamilyId>;
22
+ readonly originHash: string;
23
+ readonly destinationHash: string;
24
+ readonly pendingMigrations: readonly MigrationApplyStep[];
25
+ readonly migrations: TargetMigrationsCapability<
26
+ TFamilyId,
27
+ TTargetId,
28
+ ControlFamilyInstance<TFamilyId>
29
+ >;
30
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
31
+ readonly targetId: string;
32
+ readonly onProgress?: OnControlProgress;
33
+ }
34
+
35
+ export async function executeMigrationApply<TFamilyId extends string, TTargetId extends string>(
36
+ options: ExecuteMigrationApplyOptions<TFamilyId, TTargetId>,
37
+ ): Promise<MigrationApplyResult> {
38
+ const {
39
+ driver,
40
+ familyInstance,
41
+ originHash,
42
+ destinationHash,
43
+ pendingMigrations,
44
+ migrations,
45
+ frameworkComponents,
46
+ targetId,
47
+ onProgress,
48
+ } = options;
49
+
50
+ if (pendingMigrations.length === 0) {
51
+ if (originHash !== destinationHash) {
52
+ return notOk({
53
+ code: 'MIGRATION_PATH_NOT_FOUND' as const,
54
+ summary: 'No migrations provided for requested origin and destination',
55
+ why: `Requested ${originHash} -> ${destinationHash} but pendingMigrations is empty`,
56
+ meta: { originHash, destinationHash },
57
+ });
58
+ }
59
+ return ok({
60
+ migrationsApplied: 0,
61
+ markerHash: originHash,
62
+ applied: [],
63
+ summary: 'Already up to date',
64
+ });
65
+ }
66
+
67
+ const firstMigration = pendingMigrations[0]!;
68
+ const lastMigration = pendingMigrations[pendingMigrations.length - 1]!;
69
+ if (firstMigration.from !== originHash || lastMigration.to !== destinationHash) {
70
+ return notOk({
71
+ code: 'MIGRATION_PATH_NOT_FOUND' as const,
72
+ summary: 'Migration apply path does not match requested origin and destination',
73
+ why: `Path resolved as ${firstMigration.from} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
74
+ meta: {
75
+ originHash,
76
+ destinationHash,
77
+ pathOrigin: firstMigration.from,
78
+ pathDestination: lastMigration.to,
79
+ },
80
+ });
81
+ }
82
+
83
+ for (let i = 1; i < pendingMigrations.length; i++) {
84
+ const previous = pendingMigrations[i - 1]!;
85
+ const current = pendingMigrations[i]!;
86
+ if (previous.to !== current.from) {
87
+ return notOk({
88
+ code: 'MIGRATION_PATH_NOT_FOUND' as const,
89
+ summary: 'Migration apply path contains a discontinuity between adjacent migrations',
90
+ why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${current.from}`,
91
+ meta: {
92
+ originHash,
93
+ destinationHash,
94
+ previousDirName: previous.dirName,
95
+ previousTo: previous.to,
96
+ currentDirName: current.dirName,
97
+ currentFrom: current.from,
98
+ discontinuityIndex: i,
99
+ },
100
+ });
101
+ }
102
+ }
103
+
104
+ const runner = migrations.createRunner(familyInstance);
105
+ const applied: MigrationApplyAppliedEntry[] = [];
106
+
107
+ for (const migration of pendingMigrations) {
108
+ const migrationSpanId = `migration:${migration.dirName}`;
109
+ onProgress?.({
110
+ action: 'migrationApply',
111
+ kind: 'spanStart',
112
+ spanId: migrationSpanId,
113
+ label: `Applying ${migration.dirName}`,
114
+ });
115
+
116
+ const operations = migration.operations as readonly MigrationPlanOperation[];
117
+
118
+ // Allow all operation classes. The policy gate belongs at plan time, not
119
+ // apply time — the planner already decided what to emit. Restricting here
120
+ // would be a tautology (the allowed set would just mirror what's in ops).
121
+ const policy = {
122
+ allowedOperationClasses: ['additive', 'widening', 'destructive'] as const,
123
+ };
124
+
125
+ // EMPTY_CONTRACT_HASH means "no prior state" — the runner expects origin: null
126
+ // for a fresh database (no marker present).
127
+ const plan = {
128
+ targetId,
129
+ origin: migration.from === EMPTY_CONTRACT_HASH ? null : { storageHash: migration.from },
130
+ destination: { storageHash: migration.to },
131
+ operations,
132
+ };
133
+
134
+ const destinationContract = familyInstance.validateContractIR(
135
+ migration.toContract as ContractIR,
136
+ );
137
+
138
+ const runnerResult: MigrationRunnerResult = await runner.execute({
139
+ plan,
140
+ driver,
141
+ destinationContract,
142
+ policy,
143
+ executionChecks: {
144
+ prechecks: true,
145
+ postchecks: true,
146
+ idempotencyChecks: true,
147
+ },
148
+ frameworkComponents,
149
+ });
150
+
151
+ if (!runnerResult.ok) {
152
+ onProgress?.({
153
+ action: 'migrationApply',
154
+ kind: 'spanEnd',
155
+ spanId: migrationSpanId,
156
+ outcome: 'error',
157
+ });
158
+ return notOk({
159
+ code: 'RUNNER_FAILED' as const,
160
+ summary: runnerResult.failure.summary,
161
+ why: runnerResult.failure.why,
162
+ meta: {
163
+ migration: migration.dirName,
164
+ from: migration.from,
165
+ to: migration.to,
166
+ ...(runnerResult.failure.meta ?? {}),
167
+ },
168
+ });
169
+ }
170
+
171
+ onProgress?.({
172
+ action: 'migrationApply',
173
+ kind: 'spanEnd',
174
+ spanId: migrationSpanId,
175
+ outcome: 'ok',
176
+ });
177
+
178
+ applied.push({
179
+ dirName: migration.dirName,
180
+ from: migration.from,
181
+ to: migration.to,
182
+ operationsExecuted: runnerResult.value.operationsExecuted,
183
+ });
184
+ }
185
+
186
+ const finalHash = pendingMigrations[pendingMigrations.length - 1]!.to;
187
+ const totalOps = applied.reduce((sum, a) => sum + a.operationsExecuted, 0);
188
+
189
+ return ok({
190
+ migrationsApplied: applied.length,
191
+ markerHash: finalHash,
192
+ applied,
193
+ summary: `Applied ${applied.length} migration(s) (${totalOps} operation(s)), marker at ${finalHash}`,
194
+ });
195
+ }
@@ -1,7 +1,8 @@
1
1
  import type {
2
2
  ContractSourceDiagnostics,
3
3
  ContractSourceProvider,
4
- } from '@prisma-next/core-control-plane/config-types';
4
+ } from '@prisma-next/config/config-types';
5
+ import type { ContractMarkerRecord } from '@prisma-next/contract/types';
5
6
  import type { CoreSchemaView } from '@prisma-next/core-control-plane/schema-view';
6
7
  import type {
7
8
  ControlAdapterDescriptor,
@@ -61,6 +62,7 @@ export interface ControlClientOptions {
61
62
  export type ControlActionName =
62
63
  | 'dbInit'
63
64
  | 'dbUpdate'
65
+ | 'migrationApply'
64
66
  | 'verify'
65
67
  | 'schemaVerify'
66
68
  | 'sign'
@@ -419,6 +421,95 @@ export interface EmitFailure {
419
421
  */
420
422
  export type EmitResult = Result<EmitSuccess, EmitFailure>;
421
423
 
424
+ // ============================================================================
425
+ // Migration Apply Types
426
+ // ============================================================================
427
+
428
+ /**
429
+ * A pre-planned migration step ready for execution.
430
+ * Contains the manifest metadata and the serialized operations from ops.json.
431
+ */
432
+ export interface MigrationApplyStep {
433
+ readonly dirName: string;
434
+ readonly from: string;
435
+ readonly to: string;
436
+ readonly toContract: unknown;
437
+ readonly operations: ReadonlyArray<{
438
+ readonly id: string;
439
+ readonly label: string;
440
+ readonly operationClass: string;
441
+ readonly [key: string]: unknown;
442
+ }>;
443
+ }
444
+
445
+ /**
446
+ * Options for the migrationApply operation.
447
+ */
448
+ export interface MigrationApplyOptions {
449
+ /**
450
+ * Hash of the database state this apply path starts from.
451
+ * This is resolved by the caller (typically the CLI orchestration layer).
452
+ */
453
+ readonly originHash: string;
454
+ /**
455
+ * Hash of the target contract this apply path must reach.
456
+ * This is resolved by the caller (typically the CLI orchestration layer).
457
+ */
458
+ readonly destinationHash: string;
459
+ /**
460
+ * Ordered list of migrations to execute from originHash to destinationHash.
461
+ * The execution layer does not choose defaults; it only executes this explicit path.
462
+ */
463
+ readonly pendingMigrations: readonly MigrationApplyStep[];
464
+ /**
465
+ * Database connection. If provided, migrationApply will connect before executing.
466
+ * If omitted, the client must already be connected.
467
+ */
468
+ readonly connection?: unknown;
469
+ /** Optional progress callback for observing operation progress */
470
+ readonly onProgress?: OnControlProgress;
471
+ }
472
+
473
+ /**
474
+ * Record of a successfully applied migration.
475
+ */
476
+ export interface MigrationApplyAppliedEntry {
477
+ readonly dirName: string;
478
+ readonly from: string;
479
+ readonly to: string;
480
+ readonly operationsExecuted: number;
481
+ }
482
+
483
+ /**
484
+ * Successful migrationApply result.
485
+ */
486
+ export interface MigrationApplySuccess {
487
+ readonly migrationsApplied: number;
488
+ readonly markerHash: string;
489
+ readonly applied: readonly MigrationApplyAppliedEntry[];
490
+ readonly summary: string;
491
+ }
492
+
493
+ /**
494
+ * Failure codes for migrationApply operation.
495
+ */
496
+ export type MigrationApplyFailureCode = 'RUNNER_FAILED' | 'MIGRATION_PATH_NOT_FOUND';
497
+
498
+ /**
499
+ * Failure details for migrationApply operation.
500
+ */
501
+ export interface MigrationApplyFailure {
502
+ readonly code: MigrationApplyFailureCode;
503
+ readonly summary: string;
504
+ readonly why: string | undefined;
505
+ readonly meta: Record<string, unknown> | undefined;
506
+ }
507
+
508
+ /**
509
+ * Result type for migrationApply operation.
510
+ */
511
+ export type MigrationApplyResult = Result<MigrationApplySuccess, MigrationApplyFailure>;
512
+
422
513
  // ============================================================================
423
514
  // Standalone Contract Emit Types
424
515
  // ============================================================================
@@ -547,6 +638,27 @@ export interface ControlClient {
547
638
  */
548
639
  dbUpdate(options: DbUpdateOptions): Promise<DbUpdateResult>;
549
640
 
641
+ /**
642
+ * Reads the contract marker from the database.
643
+ * Returns null if no marker exists (fresh database).
644
+ *
645
+ * @throws If not connected or infrastructure failure
646
+ */
647
+ readMarker(): Promise<ContractMarkerRecord | null>;
648
+
649
+ /**
650
+ * Applies pre-planned on-disk migrations to the database.
651
+ * Each migration runs in its own transaction with full execution checks.
652
+ * Resume-safe: re-running after failure picks up from the last applied migration.
653
+ *
654
+ * @param options.originHash - Explicit source hash for the apply path
655
+ * @param options.destinationHash - Explicit destination hash for the apply path
656
+ * @param options.pendingMigrations - Ordered migrations to execute
657
+ * @returns Result pattern: Ok with applied details, NotOk with failure details
658
+ * @throws If not connected, target doesn't support migrations, or infrastructure failure
659
+ */
660
+ migrationApply(options: MigrationApplyOptions): Promise<MigrationApplyResult>;
661
+
550
662
  /**
551
663
  * Introspects the database schema.
552
664
  *
@@ -1,6 +1,6 @@
1
- // Re-export core-control-plane config types for convenience
1
+ // Re-export config package types for convenience
2
2
  export type {
3
3
  ContractConfig,
4
4
  PrismaNextConfig,
5
- } from '@prisma-next/core-control-plane/config-types';
6
- export { defineConfig } from '@prisma-next/core-control-plane/config-types';
5
+ } from '@prisma-next/config/config-types';
6
+ export { defineConfig } from '@prisma-next/config/config-types';
@@ -1,4 +1,5 @@
1
1
  import type { Command } from 'commander';
2
+ import { resolve } from 'pathe';
2
3
 
3
4
  const longDescriptions = new WeakMap<Command, string>();
4
5
 
@@ -46,6 +47,16 @@ export interface MigrationCommandOptions {
46
47
  readonly 'no-color'?: boolean;
47
48
  }
48
49
 
50
+ /**
51
+ * Resolves the absolute path to contract.json from the config.
52
+ * Centralises the fallback logic shared by every command that reads the contract.
53
+ */
54
+ export function resolveContractPath(config: { contract?: { output?: string } }): string {
55
+ return config.contract?.output
56
+ ? resolve(config.contract.output)
57
+ : resolve('src/prisma/contract.json');
58
+ }
59
+
49
60
  /**
50
61
  * Masks credentials in a database connection URL.
51
62
  * Handles standard URLs (username + password + query params) and libpq-style key=value strings.
@@ -14,7 +14,7 @@ import {
14
14
  errorTargetMigrationNotSupported,
15
15
  errorUnexpected,
16
16
  } from './cli-errors';
17
- import { maskConnectionUrl } from './command-helpers';
17
+ import { maskConnectionUrl, resolveContractPath } from './command-helpers';
18
18
  import type { GlobalFlags } from './global-flags';
19
19
  import { formatStyledHeader } from './output';
20
20
  import { createProgressAdapter } from './progress-adapter';
@@ -61,9 +61,7 @@ export async function prepareMigrationContext(
61
61
  const configPath = options.config
62
62
  ? relative(process.cwd(), resolve(options.config))
63
63
  : 'prisma-next.config.ts';
64
- const contractPathAbsolute = config.contract?.output
65
- ? resolve(config.contract.output)
66
- : resolve('src/prisma/contract.json');
64
+ const contractPathAbsolute = resolveContractPath(config);
67
65
  const contractPath = relative(process.cwd(), contractPathAbsolute);
68
66
 
69
67
  // Output header
@@ -138,8 +136,11 @@ export async function prepareMigrationContext(
138
136
  );
139
137
  }
140
138
 
141
- // Check target supports migrations
142
- if (!config.target.migrations) {
139
+ // Check target supports migrations via optional descriptor capability
140
+ const targetWithMigrations = config.target as typeof config.target & {
141
+ readonly migrations?: unknown;
142
+ };
143
+ if (!targetWithMigrations.migrations) {
143
144
  return notOk(
144
145
  errorTargetMigrationNotSupported({
145
146
  why: `Target "${config.target.id}" does not support migrations`,