@prisma-next/cli 0.3.0-dev.6 → 0.3.0-dev.64

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 (180) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +314 -80
  3. package/dist/cli-errors-JlPTsazx.mjs +3 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.js +1 -2376
  6. package/dist/cli.mjs +203 -0
  7. package/dist/cli.mjs.map +1 -0
  8. package/dist/client-PimzSD1f.mjs +981 -0
  9. package/dist/client-PimzSD1f.mjs.map +1 -0
  10. package/dist/commands/contract-emit.d.mts +7 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -0
  12. package/dist/commands/contract-emit.mjs +151 -0
  13. package/dist/commands/contract-emit.mjs.map +1 -0
  14. package/dist/commands/db-init.d.mts +7 -0
  15. package/dist/commands/db-init.d.mts.map +1 -0
  16. package/dist/commands/db-init.mjs +134 -0
  17. package/dist/commands/db-init.mjs.map +1 -0
  18. package/dist/commands/db-introspect.d.mts +7 -0
  19. package/dist/commands/db-introspect.d.mts.map +1 -0
  20. package/dist/commands/db-introspect.mjs +118 -0
  21. package/dist/commands/db-introspect.mjs.map +1 -0
  22. package/dist/commands/db-schema-verify.d.mts +7 -0
  23. package/dist/commands/db-schema-verify.d.mts.map +1 -0
  24. package/dist/commands/db-schema-verify.mjs +120 -0
  25. package/dist/commands/db-schema-verify.mjs.map +1 -0
  26. package/dist/commands/db-sign.d.mts +7 -0
  27. package/dist/commands/db-sign.d.mts.map +1 -0
  28. package/dist/commands/db-sign.mjs +142 -0
  29. package/dist/commands/db-sign.mjs.map +1 -0
  30. package/dist/commands/db-update.d.mts +7 -0
  31. package/dist/commands/db-update.d.mts.map +1 -0
  32. package/dist/commands/db-update.mjs +123 -0
  33. package/dist/commands/db-update.mjs.map +1 -0
  34. package/dist/commands/db-verify.d.mts +7 -0
  35. package/dist/commands/db-verify.d.mts.map +1 -0
  36. package/dist/commands/db-verify.mjs +133 -0
  37. package/dist/commands/db-verify.mjs.map +1 -0
  38. package/dist/commands/migration-apply.d.mts +23 -0
  39. package/dist/commands/migration-apply.d.mts.map +1 -0
  40. package/dist/commands/migration-apply.mjs +250 -0
  41. package/dist/commands/migration-apply.mjs.map +1 -0
  42. package/dist/commands/migration-plan.d.mts +25 -0
  43. package/dist/commands/migration-plan.d.mts.map +1 -0
  44. package/dist/commands/migration-plan.mjs +266 -0
  45. package/dist/commands/migration-plan.mjs.map +1 -0
  46. package/dist/commands/migration-show.d.mts +28 -0
  47. package/dist/commands/migration-show.d.mts.map +1 -0
  48. package/dist/commands/migration-show.mjs +138 -0
  49. package/dist/commands/migration-show.mjs.map +1 -0
  50. package/dist/commands/migration-status.d.mts +35 -0
  51. package/dist/commands/migration-status.d.mts.map +1 -0
  52. package/dist/commands/migration-status.mjs +260 -0
  53. package/dist/commands/migration-status.mjs.map +1 -0
  54. package/dist/commands/migration-verify.d.mts +16 -0
  55. package/dist/commands/migration-verify.d.mts.map +1 -0
  56. package/dist/commands/migration-verify.mjs +86 -0
  57. package/dist/commands/migration-verify.mjs.map +1 -0
  58. package/dist/config-loader-PPf4CtDj.mjs +43 -0
  59. package/dist/config-loader-PPf4CtDj.mjs.map +1 -0
  60. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  61. package/dist/config-loader.d.mts.map +1 -0
  62. package/dist/config-loader.mjs +3 -0
  63. package/dist/exports/config-types.d.mts +2 -0
  64. package/dist/exports/config-types.mjs +3 -0
  65. package/dist/exports/control-api.d.mts +621 -0
  66. package/dist/exports/control-api.d.mts.map +1 -0
  67. package/dist/exports/control-api.mjs +98 -0
  68. package/dist/exports/control-api.mjs.map +1 -0
  69. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +10 -5
  70. package/dist/exports/index.d.mts.map +1 -0
  71. package/dist/exports/index.mjs +135 -0
  72. package/dist/exports/index.mjs.map +1 -0
  73. package/dist/extract-sql-ddl-BmlKvk4o.mjs +26 -0
  74. package/dist/extract-sql-ddl-BmlKvk4o.mjs.map +1 -0
  75. package/dist/framework-components-CjV_jD8f.mjs +59 -0
  76. package/dist/framework-components-CjV_jD8f.mjs.map +1 -0
  77. package/dist/migration-command-scaffold-DfY_F3ev.mjs +97 -0
  78. package/dist/migration-command-scaffold-DfY_F3ev.mjs.map +1 -0
  79. package/dist/progress-adapter-DENrzF6I.mjs +49 -0
  80. package/dist/progress-adapter-DENrzF6I.mjs.map +1 -0
  81. package/dist/result-handler-iA9JtUC7.mjs +1186 -0
  82. package/dist/result-handler-iA9JtUC7.mjs.map +1 -0
  83. package/package.json +75 -38
  84. package/src/cli.ts +43 -0
  85. package/src/commands/contract-emit.ts +221 -111
  86. package/src/commands/db-init.ts +217 -426
  87. package/src/commands/db-introspect.ts +148 -185
  88. package/src/commands/db-schema-verify.ts +162 -149
  89. package/src/commands/db-sign.ts +215 -202
  90. package/src/commands/db-update.ts +220 -0
  91. package/src/commands/db-verify.ts +193 -156
  92. package/src/commands/migration-apply.ts +431 -0
  93. package/src/commands/migration-plan.ts +446 -0
  94. package/src/commands/migration-show.ts +255 -0
  95. package/src/commands/migration-status.ts +436 -0
  96. package/src/commands/migration-verify.ts +151 -0
  97. package/src/config-loader.ts +13 -3
  98. package/src/control-api/client.ts +605 -0
  99. package/src/control-api/errors.ts +9 -0
  100. package/src/control-api/operations/contract-emit.ts +161 -0
  101. package/src/control-api/operations/db-init.ts +286 -0
  102. package/src/control-api/operations/db-update.ts +221 -0
  103. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  104. package/src/control-api/operations/migration-apply.ts +195 -0
  105. package/src/control-api/operations/migration-helpers.ts +49 -0
  106. package/src/control-api/types.ts +687 -0
  107. package/src/exports/config-types.ts +3 -3
  108. package/src/exports/control-api.ts +53 -0
  109. package/src/load-ts-contract.ts +16 -11
  110. package/src/utils/cli-errors.ts +3 -1
  111. package/src/utils/command-helpers.ts +92 -3
  112. package/src/utils/framework-components.ts +11 -30
  113. package/src/utils/migration-command-scaffold.ts +190 -0
  114. package/src/utils/output.ts +363 -25
  115. package/src/utils/progress-adapter.ts +86 -0
  116. package/dist/chunk-464LNZCE.js +0 -134
  117. package/dist/chunk-464LNZCE.js.map +0 -1
  118. package/dist/chunk-BZMBKEEQ.js +0 -997
  119. package/dist/chunk-BZMBKEEQ.js.map +0 -1
  120. package/dist/chunk-HWYQOCAJ.js +0 -47
  121. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  122. package/dist/chunk-ZKYEJROM.js +0 -94
  123. package/dist/chunk-ZKYEJROM.js.map +0 -1
  124. package/dist/cli.d.ts +0 -2
  125. package/dist/cli.d.ts.map +0 -1
  126. package/dist/cli.js.map +0 -1
  127. package/dist/commands/contract-emit.d.ts +0 -3
  128. package/dist/commands/contract-emit.d.ts.map +0 -1
  129. package/dist/commands/contract-emit.js +0 -9
  130. package/dist/commands/contract-emit.js.map +0 -1
  131. package/dist/commands/db-init.d.ts +0 -3
  132. package/dist/commands/db-init.d.ts.map +0 -1
  133. package/dist/commands/db-init.js +0 -341
  134. package/dist/commands/db-init.js.map +0 -1
  135. package/dist/commands/db-introspect.d.ts +0 -3
  136. package/dist/commands/db-introspect.d.ts.map +0 -1
  137. package/dist/commands/db-introspect.js +0 -190
  138. package/dist/commands/db-introspect.js.map +0 -1
  139. package/dist/commands/db-schema-verify.d.ts +0 -3
  140. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  141. package/dist/commands/db-schema-verify.js +0 -164
  142. package/dist/commands/db-schema-verify.js.map +0 -1
  143. package/dist/commands/db-sign.d.ts +0 -3
  144. package/dist/commands/db-sign.d.ts.map +0 -1
  145. package/dist/commands/db-sign.js +0 -199
  146. package/dist/commands/db-sign.js.map +0 -1
  147. package/dist/commands/db-verify.d.ts +0 -3
  148. package/dist/commands/db-verify.d.ts.map +0 -1
  149. package/dist/commands/db-verify.js +0 -173
  150. package/dist/commands/db-verify.js.map +0 -1
  151. package/dist/config-loader.d.ts.map +0 -1
  152. package/dist/config-loader.js +0 -7
  153. package/dist/config-loader.js.map +0 -1
  154. package/dist/exports/config-types.d.ts +0 -3
  155. package/dist/exports/config-types.d.ts.map +0 -1
  156. package/dist/exports/config-types.js +0 -6
  157. package/dist/exports/config-types.js.map +0 -1
  158. package/dist/exports/index.d.ts +0 -4
  159. package/dist/exports/index.d.ts.map +0 -1
  160. package/dist/exports/index.js +0 -175
  161. package/dist/exports/index.js.map +0 -1
  162. package/dist/load-ts-contract.d.ts.map +0 -1
  163. package/dist/utils/action.d.ts +0 -16
  164. package/dist/utils/action.d.ts.map +0 -1
  165. package/dist/utils/cli-errors.d.ts +0 -7
  166. package/dist/utils/cli-errors.d.ts.map +0 -1
  167. package/dist/utils/command-helpers.d.ts +0 -12
  168. package/dist/utils/command-helpers.d.ts.map +0 -1
  169. package/dist/utils/framework-components.d.ts +0 -81
  170. package/dist/utils/framework-components.d.ts.map +0 -1
  171. package/dist/utils/global-flags.d.ts +0 -25
  172. package/dist/utils/global-flags.d.ts.map +0 -1
  173. package/dist/utils/output.d.ts +0 -142
  174. package/dist/utils/output.d.ts.map +0 -1
  175. package/dist/utils/result-handler.d.ts +0 -15
  176. package/dist/utils/result-handler.d.ts.map +0 -1
  177. package/dist/utils/spinner.d.ts +0 -29
  178. package/dist/utils/spinner.d.ts.map +0 -1
  179. package/src/utils/action.ts +0 -43
  180. package/src/utils/spinner.ts +0 -67
@@ -0,0 +1,161 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { createControlPlaneStack } from '@prisma-next/core-control-plane/stack';
3
+ import { abortable } from '@prisma-next/utils/abortable';
4
+ import { ifDefined } from '@prisma-next/utils/defined';
5
+ import { dirname, isAbsolute, join, resolve } from 'pathe';
6
+ import { loadConfig } from '../../config-loader';
7
+ import { errorContractConfigMissing, errorRuntime } from '../../utils/cli-errors';
8
+ import type { ContractEmitOptions, ContractEmitResult } from '../types';
9
+
10
+ interface ProviderFailureLike {
11
+ readonly summary: string;
12
+ readonly diagnostics: readonly unknown[];
13
+ readonly meta?: unknown;
14
+ }
15
+
16
+ function isRecord(value: unknown): value is Record<string, unknown> {
17
+ return typeof value === 'object' && value !== null;
18
+ }
19
+
20
+ function isAbortError(error: unknown): boolean {
21
+ return isRecord(error) && typeof error['name'] === 'string' && error['name'] === 'AbortError';
22
+ }
23
+
24
+ function isProviderFailureLike(value: unknown): value is ProviderFailureLike {
25
+ return (
26
+ isRecord(value) && typeof value['summary'] === 'string' && Array.isArray(value['diagnostics'])
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Executes the contract emit operation.
32
+ *
33
+ * This is an offline operation that:
34
+ * 1. Loads the Prisma Next config from the specified path
35
+ * 2. Resolves the contract source from config
36
+ * 3. Creates a control plane stack and family instance
37
+ * 4. Emits contract artifacts (JSON and DTS)
38
+ * 5. Writes files to the paths specified in config
39
+ *
40
+ * Supports AbortSignal for cancellation, enabling "last change wins" behavior.
41
+ *
42
+ * @param options - Options including configPath and optional signal
43
+ * @returns File paths and hashes of emitted artifacts
44
+ * @throws If config loading fails, contract is invalid, or file I/O fails
45
+ * @throws signal.reason if cancelled via AbortSignal (typically DOMException with name 'AbortError')
46
+ */
47
+ export async function executeContractEmit(
48
+ options: ContractEmitOptions,
49
+ ): Promise<ContractEmitResult> {
50
+ const { configPath, signal = new AbortController().signal } = options;
51
+ const unlessAborted = abortable(signal);
52
+
53
+ // Load config using the existing config loader
54
+ const config = await unlessAborted(loadConfig(configPath));
55
+
56
+ // Validate contract config is present
57
+ if (!config.contract) {
58
+ throw errorContractConfigMissing({
59
+ why: 'Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ... }',
60
+ });
61
+ }
62
+
63
+ const contractConfig = config.contract;
64
+
65
+ // Validate output path is present and ends with .json
66
+ if (!contractConfig.output) {
67
+ throw errorContractConfigMissing({
68
+ why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
69
+ });
70
+ }
71
+ if (!contractConfig.output.endsWith('.json')) {
72
+ throw errorContractConfigMissing({
73
+ why: 'Contract config output path must end with .json (e.g., "src/prisma/contract.json")',
74
+ });
75
+ }
76
+
77
+ // Validate source exists and is callable
78
+ if (typeof contractConfig.source !== 'function') {
79
+ throw errorContractConfigMissing({
80
+ why: 'Contract config must include a valid source provider function',
81
+ });
82
+ }
83
+
84
+ // Normalize configPath and resolve artifact paths relative to config file directory
85
+ const normalizedConfigPath = resolve(configPath);
86
+ const configDir = dirname(normalizedConfigPath);
87
+ const outputJsonPath = isAbsolute(contractConfig.output)
88
+ ? contractConfig.output
89
+ : join(configDir, contractConfig.output);
90
+ // Colocate .d.ts with .json (contract.json → contract.d.ts)
91
+ const outputDtsPath = `${outputJsonPath.slice(0, -5)}.d.ts`;
92
+
93
+ let providerResult: Awaited<ReturnType<typeof contractConfig.source>>;
94
+ try {
95
+ providerResult = await unlessAborted(contractConfig.source());
96
+ } catch (error) {
97
+ if (signal.aborted || isAbortError(error)) {
98
+ throw error;
99
+ }
100
+ throw errorRuntime('Failed to resolve contract source', {
101
+ why: error instanceof Error ? error.message : String(error),
102
+ fix: 'Ensure contract.source resolves to ok(contractIR) or returns structured diagnostics.',
103
+ });
104
+ }
105
+
106
+ if (!isRecord(providerResult) || typeof providerResult.ok !== 'boolean') {
107
+ throw errorRuntime('Failed to resolve contract source', {
108
+ why: 'Contract source provider returned malformed result shape.',
109
+ fix: 'Ensure contract.source resolves to ok(contractIR) or notOk({ summary, diagnostics }).',
110
+ });
111
+ }
112
+
113
+ if (providerResult.ok && !('value' in providerResult)) {
114
+ throw errorRuntime('Failed to resolve contract source', {
115
+ why: 'Contract source provider returned malformed success result: missing value.',
116
+ fix: 'Ensure contract.source success payload is ok(contractIR).',
117
+ });
118
+ }
119
+
120
+ if (!providerResult.ok && !isProviderFailureLike(providerResult.failure)) {
121
+ throw errorRuntime('Failed to resolve contract source', {
122
+ why: 'Contract source provider returned malformed failure result: expected summary and diagnostics.',
123
+ fix: 'Ensure contract.source failure payload is notOk({ summary, diagnostics, meta? }).',
124
+ });
125
+ }
126
+
127
+ if (!providerResult.ok) {
128
+ throw errorRuntime('Failed to resolve contract source', {
129
+ why: providerResult.failure.summary,
130
+ fix: 'Fix contract source diagnostics and return ok(contractIR).',
131
+ meta: {
132
+ diagnostics: providerResult.failure.diagnostics,
133
+ ...ifDefined('providerMeta', providerResult.failure.meta),
134
+ },
135
+ });
136
+ }
137
+
138
+ // Create control plane stack from config
139
+ const stack = createControlPlaneStack(config);
140
+ const familyInstance = config.family.create(stack);
141
+
142
+ // Emit contract via family instance
143
+ const emitResult = await unlessAborted(
144
+ familyInstance.emitContract({ contractIR: providerResult.value }),
145
+ );
146
+
147
+ // Create directory if needed and write files (both colocated)
148
+ await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
149
+ await unlessAborted(writeFile(outputJsonPath, emitResult.contractJson, 'utf-8'));
150
+ await unlessAborted(writeFile(outputDtsPath, emitResult.contractDts, 'utf-8'));
151
+
152
+ return {
153
+ storageHash: emitResult.storageHash,
154
+ ...ifDefined('executionHash', emitResult.executionHash),
155
+ profileHash: emitResult.profileHash,
156
+ files: {
157
+ json: outputJsonPath,
158
+ dts: outputDtsPath,
159
+ },
160
+ };
161
+ }
@@ -0,0 +1,286 @@
1
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
2
+ import type { ContractIR } from '@prisma-next/contract/ir';
3
+ import type {
4
+ ControlDriverInstance,
5
+ ControlFamilyInstance,
6
+ MigrationPlan,
7
+ MigrationPlannerResult,
8
+ MigrationRunnerResult,
9
+ TargetMigrationsCapability,
10
+ } from '@prisma-next/core-control-plane/types';
11
+ import { ifDefined } from '@prisma-next/utils/defined';
12
+ import { notOk, ok } from '@prisma-next/utils/result';
13
+ import type { DbInitResult, DbInitSuccess, OnControlProgress } from '../types';
14
+ import { extractSqlDdl } from './extract-sql-ddl';
15
+ import { createOperationCallbacks, stripOperations } from './migration-helpers';
16
+
17
+ /**
18
+ * Options for executing dbInit operation.
19
+ */
20
+ export interface ExecuteDbInitOptions<TFamilyId extends string, TTargetId extends string> {
21
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
22
+ readonly familyInstance: ControlFamilyInstance<TFamilyId>;
23
+ readonly contractIR: ContractIR;
24
+ readonly mode: 'plan' | 'apply';
25
+ readonly migrations: TargetMigrationsCapability<
26
+ TFamilyId,
27
+ TTargetId,
28
+ ControlFamilyInstance<TFamilyId>
29
+ >;
30
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
31
+ /** Optional progress callback for observing operation progress */
32
+ readonly onProgress?: OnControlProgress;
33
+ }
34
+
35
+ /**
36
+ * Executes the dbInit operation.
37
+ *
38
+ * This is the core logic extracted from the CLI command, without any file I/O,
39
+ * process.exit(), or console output. It uses the Result pattern to return
40
+ * success or failure details.
41
+ *
42
+ * @param options - The options for executing dbInit
43
+ * @returns Result with DbInitSuccess on success, DbInitFailure on failure
44
+ */
45
+ export async function executeDbInit<TFamilyId extends string, TTargetId extends string>(
46
+ options: ExecuteDbInitOptions<TFamilyId, TTargetId>,
47
+ ): Promise<DbInitResult> {
48
+ const { driver, familyInstance, contractIR, mode, migrations, frameworkComponents, onProgress } =
49
+ options;
50
+
51
+ // Create planner and runner from target migrations capability
52
+ const planner = migrations.createPlanner(familyInstance);
53
+ const runner = migrations.createRunner(familyInstance);
54
+
55
+ // Introspect live schema
56
+ const introspectSpanId = 'introspect';
57
+ onProgress?.({
58
+ action: 'dbInit',
59
+ kind: 'spanStart',
60
+ spanId: introspectSpanId,
61
+ label: 'Introspecting database schema',
62
+ });
63
+ const schemaIR = await familyInstance.introspect({ driver });
64
+ onProgress?.({
65
+ action: 'dbInit',
66
+ kind: 'spanEnd',
67
+ spanId: introspectSpanId,
68
+ outcome: 'ok',
69
+ });
70
+
71
+ // Policy for init mode (additive only)
72
+ const policy = { allowedOperationClasses: ['additive'] as const };
73
+
74
+ // Plan migration
75
+ const planSpanId = 'plan';
76
+ onProgress?.({
77
+ action: 'dbInit',
78
+ kind: 'spanStart',
79
+ spanId: planSpanId,
80
+ label: 'Planning migration',
81
+ });
82
+ const plannerResult: MigrationPlannerResult = await planner.plan({
83
+ contract: contractIR,
84
+ schema: schemaIR,
85
+ policy,
86
+ frameworkComponents,
87
+ });
88
+
89
+ if (plannerResult.kind === 'failure') {
90
+ onProgress?.({
91
+ action: 'dbInit',
92
+ kind: 'spanEnd',
93
+ spanId: planSpanId,
94
+ outcome: 'error',
95
+ });
96
+ return notOk({
97
+ code: 'PLANNING_FAILED' as const,
98
+ summary: 'Migration planning failed due to conflicts',
99
+ conflicts: plannerResult.conflicts,
100
+ why: undefined,
101
+ meta: undefined,
102
+ });
103
+ }
104
+
105
+ const migrationPlan: MigrationPlan = plannerResult.plan;
106
+ onProgress?.({
107
+ action: 'dbInit',
108
+ kind: 'spanEnd',
109
+ spanId: planSpanId,
110
+ outcome: 'ok',
111
+ });
112
+
113
+ // Check for existing marker - handle idempotency and mismatch errors
114
+ const checkMarkerSpanId = 'checkMarker';
115
+ onProgress?.({
116
+ action: 'dbInit',
117
+ kind: 'spanStart',
118
+ spanId: checkMarkerSpanId,
119
+ label: 'Checking database signature',
120
+ });
121
+ const existingMarker = await familyInstance.readMarker({ driver });
122
+ if (existingMarker) {
123
+ const markerMatchesDestination =
124
+ existingMarker.storageHash === migrationPlan.destination.storageHash &&
125
+ (!migrationPlan.destination.profileHash ||
126
+ existingMarker.profileHash === migrationPlan.destination.profileHash);
127
+
128
+ if (markerMatchesDestination) {
129
+ // Already at destination - return success with no operations
130
+ onProgress?.({
131
+ action: 'dbInit',
132
+ kind: 'spanEnd',
133
+ spanId: checkMarkerSpanId,
134
+ outcome: 'skipped',
135
+ });
136
+ const result: DbInitSuccess = {
137
+ mode,
138
+ plan: { operations: [] },
139
+ destination: {
140
+ storageHash: migrationPlan.destination.storageHash,
141
+ ...ifDefined('profileHash', migrationPlan.destination.profileHash),
142
+ },
143
+ ...ifDefined(
144
+ 'execution',
145
+ mode === 'apply' ? { operationsPlanned: 0, operationsExecuted: 0 } : undefined,
146
+ ),
147
+ ...ifDefined(
148
+ 'marker',
149
+ mode === 'apply'
150
+ ? {
151
+ storageHash: existingMarker.storageHash,
152
+ profileHash: existingMarker.profileHash,
153
+ }
154
+ : undefined,
155
+ ),
156
+ summary: 'Database already at target contract state',
157
+ };
158
+ return ok(result);
159
+ }
160
+
161
+ // Marker exists but doesn't match destination - fail
162
+ onProgress?.({
163
+ action: 'dbInit',
164
+ kind: 'spanEnd',
165
+ spanId: checkMarkerSpanId,
166
+ outcome: 'error',
167
+ });
168
+ return notOk({
169
+ code: 'MARKER_ORIGIN_MISMATCH' as const,
170
+ summary: 'Existing contract marker does not match plan destination',
171
+ marker: {
172
+ storageHash: existingMarker.storageHash,
173
+ profileHash: existingMarker.profileHash,
174
+ },
175
+ destination: {
176
+ storageHash: migrationPlan.destination.storageHash,
177
+ profileHash: migrationPlan.destination.profileHash,
178
+ },
179
+ why: undefined,
180
+ conflicts: undefined,
181
+ meta: undefined,
182
+ });
183
+ }
184
+
185
+ onProgress?.({
186
+ action: 'dbInit',
187
+ kind: 'spanEnd',
188
+ spanId: checkMarkerSpanId,
189
+ outcome: 'ok',
190
+ });
191
+
192
+ // Plan mode - don't execute
193
+ if (mode === 'plan') {
194
+ const planSql =
195
+ familyInstance.familyId === 'sql' ? extractSqlDdl(migrationPlan.operations) : undefined;
196
+ const result: DbInitSuccess = {
197
+ mode: 'plan',
198
+ plan: {
199
+ operations: stripOperations(migrationPlan.operations),
200
+ ...ifDefined('sql', planSql),
201
+ },
202
+ destination: {
203
+ storageHash: migrationPlan.destination.storageHash,
204
+ ...ifDefined('profileHash', migrationPlan.destination.profileHash),
205
+ },
206
+ summary: `Planned ${migrationPlan.operations.length} operation(s)`,
207
+ };
208
+ return ok(result);
209
+ }
210
+
211
+ // Apply mode - execute runner
212
+ const applySpanId = 'apply';
213
+ onProgress?.({
214
+ action: 'dbInit',
215
+ kind: 'spanStart',
216
+ spanId: applySpanId,
217
+ label: 'Applying migration plan',
218
+ });
219
+
220
+ const callbacks = createOperationCallbacks(onProgress, 'dbInit', applySpanId);
221
+
222
+ const runnerResult: MigrationRunnerResult = await runner.execute({
223
+ plan: migrationPlan,
224
+ driver,
225
+ destinationContract: contractIR,
226
+ policy,
227
+ ...ifDefined('callbacks', callbacks),
228
+ // db init plans and applies back-to-back from a fresh introspection, so per-operation
229
+ // pre/postchecks and the idempotency probe are usually redundant overhead. We still
230
+ // enforce marker/origin compatibility and a full schema verification after apply.
231
+ executionChecks: {
232
+ prechecks: false,
233
+ postchecks: false,
234
+ idempotencyChecks: false,
235
+ },
236
+ frameworkComponents,
237
+ });
238
+
239
+ if (!runnerResult.ok) {
240
+ onProgress?.({
241
+ action: 'dbInit',
242
+ kind: 'spanEnd',
243
+ spanId: applySpanId,
244
+ outcome: 'error',
245
+ });
246
+ return notOk({
247
+ code: 'RUNNER_FAILED' as const,
248
+ summary: runnerResult.failure.summary,
249
+ why: runnerResult.failure.why,
250
+ meta: runnerResult.failure.meta,
251
+ conflicts: undefined,
252
+ });
253
+ }
254
+
255
+ const execution = runnerResult.value;
256
+
257
+ onProgress?.({
258
+ action: 'dbInit',
259
+ kind: 'spanEnd',
260
+ spanId: applySpanId,
261
+ outcome: 'ok',
262
+ });
263
+
264
+ const result: DbInitSuccess = {
265
+ mode: 'apply',
266
+ plan: {
267
+ operations: stripOperations(migrationPlan.operations),
268
+ },
269
+ destination: {
270
+ storageHash: migrationPlan.destination.storageHash,
271
+ ...ifDefined('profileHash', migrationPlan.destination.profileHash),
272
+ },
273
+ execution: {
274
+ operationsPlanned: execution.operationsPlanned,
275
+ operationsExecuted: execution.operationsExecuted,
276
+ },
277
+ marker: migrationPlan.destination.profileHash
278
+ ? {
279
+ storageHash: migrationPlan.destination.storageHash,
280
+ profileHash: migrationPlan.destination.profileHash,
281
+ }
282
+ : { storageHash: migrationPlan.destination.storageHash },
283
+ summary: `Applied ${execution.operationsExecuted} operation(s), database signed`,
284
+ };
285
+ return ok(result);
286
+ }
@@ -0,0 +1,221 @@
1
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
2
+ import type { ContractIR } from '@prisma-next/contract/ir';
3
+ import type {
4
+ ControlDriverInstance,
5
+ ControlFamilyInstance,
6
+ MigrationPlannerResult,
7
+ MigrationRunnerResult,
8
+ TargetMigrationsCapability,
9
+ } from '@prisma-next/core-control-plane/types';
10
+ import { ifDefined } from '@prisma-next/utils/defined';
11
+ import { notOk, ok } from '@prisma-next/utils/result';
12
+ import type { DbUpdateResult, DbUpdateSuccess, OnControlProgress } from '../types';
13
+ import { extractSqlDdl } from './extract-sql-ddl';
14
+ import { createOperationCallbacks, stripOperations } from './migration-helpers';
15
+
16
+ // F12: db update allows additive, widening, and destructive operations.
17
+ const DB_UPDATE_POLICY = {
18
+ allowedOperationClasses: ['additive', 'widening', 'destructive'] as const,
19
+ } as const;
20
+
21
+ /**
22
+ * Options for the executeDbUpdate operation.
23
+ * Config-agnostic: receives pre-resolved driver, family, contract, and migrations capability.
24
+ */
25
+ export interface ExecuteDbUpdateOptions<TFamilyId extends string, TTargetId extends string> {
26
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
27
+ readonly familyInstance: ControlFamilyInstance<TFamilyId>;
28
+ readonly contractIR: ContractIR;
29
+ readonly mode: 'plan' | 'apply';
30
+ readonly migrations: TargetMigrationsCapability<
31
+ TFamilyId,
32
+ TTargetId,
33
+ ControlFamilyInstance<TFamilyId>
34
+ >;
35
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
36
+ readonly acceptDataLoss?: boolean;
37
+ /** Optional progress callback for observing operation progress. */
38
+ readonly onProgress?: OnControlProgress;
39
+ }
40
+
41
+ /**
42
+ * Executes the db update operation: introspect → plan → (optionally) apply → marker.
43
+ *
44
+ * db update is a pure reconciliation command: it introspects the live schema, plans the diff
45
+ * to the destination contract, and applies operations. The marker is bookkeeping only — written
46
+ * after apply so that `verify` and `db init` can reference it, but never read or validated
47
+ * by db update itself. The runner creates the marker table if it does not exist.
48
+ */
49
+ export async function executeDbUpdate<TFamilyId extends string, TTargetId extends string>(
50
+ options: ExecuteDbUpdateOptions<TFamilyId, TTargetId>,
51
+ ): Promise<DbUpdateResult> {
52
+ const { driver, familyInstance, contractIR, mode, migrations, frameworkComponents, onProgress } =
53
+ options;
54
+
55
+ const planner = migrations.createPlanner(familyInstance);
56
+ const runner = migrations.createRunner(familyInstance);
57
+
58
+ const introspectSpanId = 'introspect';
59
+ onProgress?.({
60
+ action: 'dbUpdate',
61
+ kind: 'spanStart',
62
+ spanId: introspectSpanId,
63
+ label: 'Introspecting database schema',
64
+ });
65
+ const schemaIR = await familyInstance.introspect({ driver });
66
+ onProgress?.({
67
+ action: 'dbUpdate',
68
+ kind: 'spanEnd',
69
+ spanId: introspectSpanId,
70
+ outcome: 'ok',
71
+ });
72
+
73
+ const policy = DB_UPDATE_POLICY;
74
+
75
+ const planSpanId = 'plan';
76
+ onProgress?.({
77
+ action: 'dbUpdate',
78
+ kind: 'spanStart',
79
+ spanId: planSpanId,
80
+ label: 'Planning migration',
81
+ });
82
+ const plannerResult: MigrationPlannerResult = await planner.plan({
83
+ contract: contractIR,
84
+ schema: schemaIR,
85
+ policy,
86
+ frameworkComponents,
87
+ });
88
+ if (plannerResult.kind === 'failure') {
89
+ onProgress?.({
90
+ action: 'dbUpdate',
91
+ kind: 'spanEnd',
92
+ spanId: planSpanId,
93
+ outcome: 'error',
94
+ });
95
+ return notOk({
96
+ code: 'PLANNING_FAILED',
97
+ summary: 'Migration planning failed due to conflicts',
98
+ conflicts: plannerResult.conflicts,
99
+ why: undefined,
100
+ meta: undefined,
101
+ });
102
+ }
103
+ onProgress?.({
104
+ action: 'dbUpdate',
105
+ kind: 'spanEnd',
106
+ spanId: planSpanId,
107
+ outcome: 'ok',
108
+ });
109
+
110
+ const migrationPlan = plannerResult.plan;
111
+
112
+ if (mode === 'plan') {
113
+ const planSql =
114
+ familyInstance.familyId === 'sql' ? extractSqlDdl(migrationPlan.operations) : undefined;
115
+ const result: DbUpdateSuccess = {
116
+ mode: 'plan',
117
+ plan: {
118
+ operations: stripOperations(migrationPlan.operations),
119
+ ...(planSql !== undefined ? { sql: planSql } : {}),
120
+ },
121
+ destination: {
122
+ storageHash: migrationPlan.destination.storageHash,
123
+ ...ifDefined('profileHash', migrationPlan.destination.profileHash),
124
+ },
125
+ summary: `Planned ${migrationPlan.operations.length} operation(s)`,
126
+ };
127
+ return ok(result);
128
+ }
129
+
130
+ // When applying, require explicit acceptance for destructive operations
131
+ if (!options.acceptDataLoss) {
132
+ const destructiveOps = migrationPlan.operations
133
+ .filter((op) => op.operationClass === 'destructive')
134
+ .map((op) => ({ id: op.id, label: op.label }));
135
+ if (destructiveOps.length > 0) {
136
+ return notOk({
137
+ code: 'DESTRUCTIVE_CHANGES',
138
+ summary: `Planned ${destructiveOps.length} destructive operation(s) that require confirmation`,
139
+ why: 'Use --plan to preview destructive operations, then re-run with --accept-data-loss to apply',
140
+ conflicts: undefined,
141
+ meta: { destructiveOperations: destructiveOps },
142
+ });
143
+ }
144
+ }
145
+
146
+ const applySpanId = 'apply';
147
+ onProgress?.({
148
+ action: 'dbUpdate',
149
+ kind: 'spanStart',
150
+ spanId: applySpanId,
151
+ label: 'Applying migration plan',
152
+ });
153
+
154
+ const callbacks = createOperationCallbacks(onProgress, 'dbUpdate', applySpanId);
155
+
156
+ const runnerResult: MigrationRunnerResult = await runner.execute({
157
+ plan: migrationPlan,
158
+ driver,
159
+ destinationContract: contractIR,
160
+ policy,
161
+ ...(callbacks ? { callbacks } : {}),
162
+ // db update plans and applies from a single introspection pass, so per-operation pre/postchecks
163
+ // and idempotency probes are intentionally disabled to avoid redundant roundtrips.
164
+ executionChecks: {
165
+ prechecks: false,
166
+ postchecks: false,
167
+ idempotencyChecks: false,
168
+ },
169
+ frameworkComponents,
170
+ });
171
+
172
+ if (!runnerResult.ok) {
173
+ onProgress?.({
174
+ action: 'dbUpdate',
175
+ kind: 'spanEnd',
176
+ spanId: applySpanId,
177
+ outcome: 'error',
178
+ });
179
+ return notOk({
180
+ code: 'RUNNER_FAILED',
181
+ summary: runnerResult.failure.summary,
182
+ why: runnerResult.failure.why,
183
+ meta: runnerResult.failure.meta,
184
+ conflicts: undefined,
185
+ });
186
+ }
187
+
188
+ const execution = runnerResult.value;
189
+ onProgress?.({
190
+ action: 'dbUpdate',
191
+ kind: 'spanEnd',
192
+ spanId: applySpanId,
193
+ outcome: 'ok',
194
+ });
195
+
196
+ const result: DbUpdateSuccess = {
197
+ mode: 'apply',
198
+ plan: {
199
+ operations: stripOperations(migrationPlan.operations),
200
+ },
201
+ destination: {
202
+ storageHash: migrationPlan.destination.storageHash,
203
+ ...ifDefined('profileHash', migrationPlan.destination.profileHash),
204
+ },
205
+ execution: {
206
+ operationsPlanned: execution.operationsPlanned,
207
+ operationsExecuted: execution.operationsExecuted,
208
+ },
209
+ marker: migrationPlan.destination.profileHash
210
+ ? {
211
+ storageHash: migrationPlan.destination.storageHash,
212
+ profileHash: migrationPlan.destination.profileHash,
213
+ }
214
+ : { storageHash: migrationPlan.destination.storageHash },
215
+ summary:
216
+ execution.operationsExecuted === 0
217
+ ? 'Database already matches contract, signature updated'
218
+ : `Applied ${execution.operationsExecuted} operation(s), signature updated`,
219
+ };
220
+ return ok(result);
221
+ }