@prisma-next/cli 0.5.0-dev.67 → 0.5.0-dev.69
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.
- package/dist/cli.mjs +8 -8
- package/dist/cli.mjs.map +1 -1
- package/dist/client-0ZX24FXF.mjs +1398 -0
- package/dist/client-0ZX24FXF.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +7 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +2 -2
- package/dist/commands/db-sign.mjs +2 -2
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +7 -5
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -320
- package/dist/commands/migration-apply.mjs +11 -11
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.mjs +5 -5
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -344
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +8 -7
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/{contract-emit-B0nGrDtk.mjs → contract-emit-DkMqO7f2.mjs} +2 -2
- package/dist/{contract-emit-B0nGrDtk.mjs.map → contract-emit-DkMqO7f2.mjs.map} +1 -1
- package/dist/{contract-infer-BjMJaOOa.mjs → contract-infer-BDKAE0B0.mjs} +3 -3
- package/dist/{contract-infer-BjMJaOOa.mjs.map → contract-infer-BDKAE0B0.mjs.map} +1 -1
- package/dist/db-verify-B4TdDKOI.mjs +403 -0
- package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +201 -3
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +17 -17
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{init-C3qdc0Sh.mjs → init-Deo7U8_U.mjs} +2 -2
- package/dist/{init-C3qdc0Sh.mjs.map → init-Deo7U8_U.mjs.map} +1 -1
- package/dist/{inspect-live-schema-33rxnu0K.mjs → inspect-live-schema-BAgQMYpD.mjs} +3 -3
- package/dist/{inspect-live-schema-33rxnu0K.mjs.map → inspect-live-schema-BAgQMYpD.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-DQo4R0XT.mjs → migration-command-scaffold-B8J702Uh.mjs} +3 -3
- package/dist/{migration-command-scaffold-DQo4R0XT.mjs.map → migration-command-scaffold-B8J702Uh.mjs.map} +1 -1
- package/dist/migration-plan-BcKNnTM7.mjs +530 -0
- package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
- package/dist/{migration-status-C_2FSkbf.mjs → migration-status-CjwB2of-.mjs} +6 -6
- package/dist/{migration-status-C_2FSkbf.mjs.map → migration-status-CjwB2of-.mjs.map} +1 -1
- package/dist/{output-BTgnZ5c_.mjs → output-DnjfCC_u.mjs} +1 -1
- package/dist/{output-BTgnZ5c_.mjs.map → output-DnjfCC_u.mjs.map} +1 -1
- package/dist/{result-handler-C0QeiqKO.mjs → result-handler-DWb1rFS-.mjs} +18 -3
- package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
- package/package.json +15 -15
- package/src/commands/db-init.ts +13 -3
- package/src/commands/db-update.ts +7 -3
- package/src/commands/db-verify.ts +47 -15
- package/src/commands/init/index.ts +1 -1
- package/src/commands/init/init.ts +2 -2
- package/src/commands/migration-apply.ts +9 -9
- package/src/commands/migration-new.ts +4 -4
- package/src/commands/migration-plan.ts +66 -9
- package/src/commands/migration-show.ts +7 -5
- package/src/commands/migration-status.ts +3 -3
- package/src/control-api/client.ts +42 -0
- package/src/control-api/operations/db-apply-aggregate.ts +446 -0
- package/src/control-api/operations/db-init.ts +51 -258
- package/src/control-api/operations/db-update.ts +66 -188
- package/src/control-api/operations/db-verify.ts +342 -0
- package/src/control-api/types.ts +56 -0
- package/src/exports/control-api.ts +13 -2
- package/src/load-ts-contract.ts +28 -26
- package/src/utils/combine-schema-results.ts +84 -0
- package/src/utils/command-helpers.ts +24 -2
- package/src/utils/contract-space-aggregate-loader.ts +236 -0
- package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
- package/src/utils/contract-space-migrate-pass.ts +156 -0
- package/dist/client-CW1hcUtM.mjs +0 -1025
- package/dist/client-CW1hcUtM.mjs.map +0 -1
- package/dist/commands/db-verify.mjs.map +0 -1
- package/dist/commands/migration-plan.mjs.map +0 -1
- package/dist/result-handler-C0QeiqKO.mjs.map +0 -1
- /package/dist/{cli-errors-BWn943z2.d.mts → cli-errors-QH8kf-C2.d.mts} +0 -0
|
@@ -2,20 +2,30 @@ import type { Contract } from '@prisma-next/contract/types';
|
|
|
2
2
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
3
|
import type {
|
|
4
4
|
ControlDriverInstance,
|
|
5
|
+
ControlExtensionDescriptor,
|
|
5
6
|
ControlFamilyInstance,
|
|
6
|
-
MigrationPlan,
|
|
7
|
-
MigrationPlannerResult,
|
|
8
|
-
MigrationRunnerResult,
|
|
9
7
|
TargetMigrationsCapability,
|
|
10
8
|
} from '@prisma-next/framework-components/control';
|
|
11
|
-
import { APP_SPACE_ID, hasOperationPreview } from '@prisma-next/framework-components/control';
|
|
12
9
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import { createOperationCallbacks, stripOperations } from './migration-helpers';
|
|
10
|
+
import type { DbInitResult, OnControlProgress } from '../types';
|
|
11
|
+
import { executeAggregateApply } from './db-apply-aggregate';
|
|
16
12
|
|
|
17
13
|
/**
|
|
18
|
-
* Options for executing
|
|
14
|
+
* Options for executing the `db init` operation.
|
|
15
|
+
*
|
|
16
|
+
* `db init` runs the loader → planner → runner pipeline:
|
|
17
|
+
*
|
|
18
|
+
* 1. {@link executeAggregateApply} loads a `ContractSpaceAggregate` via
|
|
19
|
+
* {@link import('@prisma-next/migration-tools/aggregate').loadContractSpaceAggregate}
|
|
20
|
+
* from the supplied descriptor set + on-disk on-disk artefacts.
|
|
21
|
+
* 2. The aggregate planner runs with `callerPolicy.ignoreGraphFor`
|
|
22
|
+
* locked to the app member — synth strategy for the app, graph-walk
|
|
23
|
+
* for every extension.
|
|
24
|
+
* 3. The runner's `executeAcrossSpaces` applies the per-space plans
|
|
25
|
+
* inside one outer transaction.
|
|
26
|
+
*
|
|
27
|
+
* `extensionPacks` mirrors `Config.extensionPacks` (descriptor list).
|
|
28
|
+
* The loader (sub-spec § Loader) is the sole descriptor-import boundary.
|
|
19
29
|
*/
|
|
20
30
|
export interface ExecuteDbInitOptions<TFamilyId extends string, TTargetId extends string> {
|
|
21
31
|
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
@@ -28,266 +38,49 @@ export interface ExecuteDbInitOptions<TFamilyId extends string, TTargetId extend
|
|
|
28
38
|
ControlFamilyInstance<TFamilyId, unknown>
|
|
29
39
|
>;
|
|
30
40
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
|
|
41
|
+
/**
|
|
42
|
+
* On-disk migrations directory the aggregate loader reads on-disk
|
|
43
|
+
* artefacts from. Required.
|
|
44
|
+
*/
|
|
45
|
+
readonly migrationsDir: string;
|
|
46
|
+
/**
|
|
47
|
+
* Resolved adapter target id. Threaded through to the loader for
|
|
48
|
+
* target-consistency checks across descriptors and the app contract.
|
|
49
|
+
*/
|
|
50
|
+
readonly targetId: TTargetId;
|
|
51
|
+
/**
|
|
52
|
+
* Declared extension descriptors. Defaults to an empty list, which
|
|
53
|
+
* routes through the same loader → planner → runner pipeline with no
|
|
54
|
+
* extension members in the aggregate.
|
|
55
|
+
*/
|
|
56
|
+
readonly extensionPacks?: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
|
|
31
57
|
/** Optional progress callback for observing operation progress */
|
|
32
58
|
readonly onProgress?: OnControlProgress;
|
|
33
59
|
}
|
|
34
60
|
|
|
35
61
|
/**
|
|
36
|
-
*
|
|
62
|
+
* Execute `db init` against the configured contract.
|
|
37
63
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* @param options - The options for executing dbInit
|
|
43
|
-
* @returns Result with DbInitSuccess on success, DbInitFailure on failure
|
|
64
|
+
* Routes through the loader → planner → runner pipeline (sub-spec
|
|
65
|
+
* "Commit-by-commit § Commit 4"). Always additive-only; destructive
|
|
66
|
+
* changes belong to `db update`.
|
|
44
67
|
*/
|
|
45
68
|
export async function executeDbInit<TFamilyId extends string, TTargetId extends string>(
|
|
46
69
|
options: ExecuteDbInitOptions<TFamilyId, TTargetId>,
|
|
47
70
|
): Promise<DbInitResult> {
|
|
48
|
-
const
|
|
49
|
-
options
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
84
|
-
schema: schemaIR,
|
|
85
|
-
policy,
|
|
86
|
-
// `db init` reconciles against the live introspected schema; there is no
|
|
87
|
-
// prior contract to derive a "from" identity from. The required
|
|
88
|
-
// `fromContract: null` makes that structural fact visible at the call
|
|
89
|
-
// site (vs. silently letting the planner default to a baseline plan).
|
|
90
|
-
fromContract: null,
|
|
91
|
-
frameworkComponents,
|
|
92
|
-
spaceId: APP_SPACE_ID,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (plannerResult.kind === 'failure') {
|
|
96
|
-
onProgress?.({
|
|
97
|
-
action: 'dbInit',
|
|
98
|
-
kind: 'spanEnd',
|
|
99
|
-
spanId: planSpanId,
|
|
100
|
-
outcome: 'error',
|
|
101
|
-
});
|
|
102
|
-
return notOk({
|
|
103
|
-
code: 'PLANNING_FAILED' as const,
|
|
104
|
-
summary: 'Migration planning failed due to conflicts',
|
|
105
|
-
conflicts: plannerResult.conflicts,
|
|
106
|
-
why: undefined,
|
|
107
|
-
meta: undefined,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const migrationPlan: MigrationPlan = plannerResult.plan;
|
|
112
|
-
onProgress?.({
|
|
113
|
-
action: 'dbInit',
|
|
114
|
-
kind: 'spanEnd',
|
|
115
|
-
spanId: planSpanId,
|
|
116
|
-
outcome: 'ok',
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Check for existing marker - handle idempotency and mismatch errors
|
|
120
|
-
const checkMarkerSpanId = 'checkMarker';
|
|
121
|
-
onProgress?.({
|
|
122
|
-
action: 'dbInit',
|
|
123
|
-
kind: 'spanStart',
|
|
124
|
-
spanId: checkMarkerSpanId,
|
|
125
|
-
label: 'Checking database signature',
|
|
126
|
-
});
|
|
127
|
-
const existingMarker = await familyInstance.readMarker({ driver, space: APP_SPACE_ID });
|
|
128
|
-
if (existingMarker) {
|
|
129
|
-
const markerMatchesDestination =
|
|
130
|
-
existingMarker.storageHash === migrationPlan.destination.storageHash &&
|
|
131
|
-
(!migrationPlan.destination.profileHash ||
|
|
132
|
-
existingMarker.profileHash === migrationPlan.destination.profileHash);
|
|
133
|
-
|
|
134
|
-
if (markerMatchesDestination) {
|
|
135
|
-
// Already at destination - return success with no operations
|
|
136
|
-
onProgress?.({
|
|
137
|
-
action: 'dbInit',
|
|
138
|
-
kind: 'spanEnd',
|
|
139
|
-
spanId: checkMarkerSpanId,
|
|
140
|
-
outcome: 'skipped',
|
|
141
|
-
});
|
|
142
|
-
const result: DbInitSuccess = {
|
|
143
|
-
mode,
|
|
144
|
-
plan: { operations: [] },
|
|
145
|
-
destination: {
|
|
146
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
147
|
-
...ifDefined('profileHash', migrationPlan.destination.profileHash),
|
|
148
|
-
},
|
|
149
|
-
...ifDefined(
|
|
150
|
-
'execution',
|
|
151
|
-
mode === 'apply' ? { operationsPlanned: 0, operationsExecuted: 0 } : undefined,
|
|
152
|
-
),
|
|
153
|
-
...ifDefined(
|
|
154
|
-
'marker',
|
|
155
|
-
mode === 'apply'
|
|
156
|
-
? {
|
|
157
|
-
storageHash: existingMarker.storageHash,
|
|
158
|
-
profileHash: existingMarker.profileHash,
|
|
159
|
-
}
|
|
160
|
-
: undefined,
|
|
161
|
-
),
|
|
162
|
-
summary: 'Database already at target contract state',
|
|
163
|
-
};
|
|
164
|
-
return ok(result);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Marker exists but doesn't match destination - fail
|
|
168
|
-
onProgress?.({
|
|
169
|
-
action: 'dbInit',
|
|
170
|
-
kind: 'spanEnd',
|
|
171
|
-
spanId: checkMarkerSpanId,
|
|
172
|
-
outcome: 'error',
|
|
173
|
-
});
|
|
174
|
-
return notOk({
|
|
175
|
-
code: 'MARKER_ORIGIN_MISMATCH' as const,
|
|
176
|
-
summary: 'Existing contract marker does not match plan destination',
|
|
177
|
-
marker: {
|
|
178
|
-
storageHash: existingMarker.storageHash,
|
|
179
|
-
profileHash: existingMarker.profileHash,
|
|
180
|
-
},
|
|
181
|
-
destination: {
|
|
182
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
183
|
-
profileHash: migrationPlan.destination.profileHash,
|
|
184
|
-
},
|
|
185
|
-
why: undefined,
|
|
186
|
-
conflicts: undefined,
|
|
187
|
-
meta: undefined,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
onProgress?.({
|
|
192
|
-
action: 'dbInit',
|
|
193
|
-
kind: 'spanEnd',
|
|
194
|
-
spanId: checkMarkerSpanId,
|
|
195
|
-
outcome: 'ok',
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Plan mode - don't execute
|
|
199
|
-
if (mode === 'plan') {
|
|
200
|
-
const preview = hasOperationPreview(familyInstance)
|
|
201
|
-
? familyInstance.toOperationPreview(migrationPlan.operations)
|
|
202
|
-
: undefined;
|
|
203
|
-
const result: DbInitSuccess = {
|
|
204
|
-
mode: 'plan',
|
|
205
|
-
plan: {
|
|
206
|
-
operations: stripOperations(migrationPlan.operations),
|
|
207
|
-
...ifDefined('preview', preview),
|
|
208
|
-
},
|
|
209
|
-
destination: {
|
|
210
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
211
|
-
...ifDefined('profileHash', migrationPlan.destination.profileHash),
|
|
212
|
-
},
|
|
213
|
-
summary: `Planned ${migrationPlan.operations.length} operation(s)`,
|
|
214
|
-
};
|
|
215
|
-
return ok(result);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Apply mode - execute runner
|
|
219
|
-
const applySpanId = 'apply';
|
|
220
|
-
onProgress?.({
|
|
71
|
+
const result = await executeAggregateApply<TFamilyId, TTargetId>({
|
|
72
|
+
driver: options.driver,
|
|
73
|
+
familyInstance: options.familyInstance,
|
|
74
|
+
contract: options.contract,
|
|
75
|
+
mode: options.mode,
|
|
76
|
+
migrations: options.migrations,
|
|
77
|
+
frameworkComponents: options.frameworkComponents,
|
|
78
|
+
migrationsDir: options.migrationsDir,
|
|
79
|
+
targetId: options.targetId,
|
|
80
|
+
extensionPacks: options.extensionPacks ?? [],
|
|
81
|
+
policy: { allowedOperationClasses: ['additive'] },
|
|
221
82
|
action: 'dbInit',
|
|
222
|
-
|
|
223
|
-
spanId: applySpanId,
|
|
224
|
-
label: 'Applying migration plan',
|
|
83
|
+
...ifDefined('onProgress', options.onProgress),
|
|
225
84
|
});
|
|
226
|
-
|
|
227
|
-
const callbacks = createOperationCallbacks(onProgress, 'dbInit', applySpanId);
|
|
228
|
-
|
|
229
|
-
const runnerResult: MigrationRunnerResult = await runner.execute({
|
|
230
|
-
plan: migrationPlan,
|
|
231
|
-
driver,
|
|
232
|
-
destinationContract: contract,
|
|
233
|
-
policy,
|
|
234
|
-
...ifDefined('callbacks', callbacks),
|
|
235
|
-
// db init plans and applies back-to-back from a fresh introspection, so per-operation
|
|
236
|
-
// pre/postchecks and the idempotency probe are usually redundant overhead. We still
|
|
237
|
-
// enforce marker/origin compatibility and a full schema verification after apply.
|
|
238
|
-
executionChecks: {
|
|
239
|
-
prechecks: false,
|
|
240
|
-
postchecks: false,
|
|
241
|
-
idempotencyChecks: false,
|
|
242
|
-
},
|
|
243
|
-
frameworkComponents,
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
if (!runnerResult.ok) {
|
|
247
|
-
onProgress?.({
|
|
248
|
-
action: 'dbInit',
|
|
249
|
-
kind: 'spanEnd',
|
|
250
|
-
spanId: applySpanId,
|
|
251
|
-
outcome: 'error',
|
|
252
|
-
});
|
|
253
|
-
return notOk({
|
|
254
|
-
code: 'RUNNER_FAILED' as const,
|
|
255
|
-
summary: runnerResult.failure.summary,
|
|
256
|
-
why: runnerResult.failure.why,
|
|
257
|
-
meta: runnerResult.failure.meta,
|
|
258
|
-
conflicts: undefined,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const execution = runnerResult.value;
|
|
263
|
-
|
|
264
|
-
onProgress?.({
|
|
265
|
-
action: 'dbInit',
|
|
266
|
-
kind: 'spanEnd',
|
|
267
|
-
spanId: applySpanId,
|
|
268
|
-
outcome: 'ok',
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const result: DbInitSuccess = {
|
|
272
|
-
mode: 'apply',
|
|
273
|
-
plan: {
|
|
274
|
-
operations: stripOperations(migrationPlan.operations),
|
|
275
|
-
},
|
|
276
|
-
destination: {
|
|
277
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
278
|
-
...ifDefined('profileHash', migrationPlan.destination.profileHash),
|
|
279
|
-
},
|
|
280
|
-
execution: {
|
|
281
|
-
operationsPlanned: execution.operationsPlanned,
|
|
282
|
-
operationsExecuted: execution.operationsExecuted,
|
|
283
|
-
},
|
|
284
|
-
marker: migrationPlan.destination.profileHash
|
|
285
|
-
? {
|
|
286
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
287
|
-
profileHash: migrationPlan.destination.profileHash,
|
|
288
|
-
}
|
|
289
|
-
: { storageHash: migrationPlan.destination.storageHash },
|
|
290
|
-
summary: `Applied ${execution.operationsExecuted} operation(s), database signed`,
|
|
291
|
-
};
|
|
292
|
-
return ok(result);
|
|
85
|
+
return result as DbInitResult;
|
|
293
86
|
}
|
|
@@ -2,25 +2,27 @@ import type { Contract } from '@prisma-next/contract/types';
|
|
|
2
2
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
3
|
import type {
|
|
4
4
|
ControlDriverInstance,
|
|
5
|
+
ControlExtensionDescriptor,
|
|
5
6
|
ControlFamilyInstance,
|
|
6
|
-
MigrationPlannerResult,
|
|
7
|
-
MigrationRunnerResult,
|
|
8
7
|
TargetMigrationsCapability,
|
|
9
8
|
} from '@prisma-next/framework-components/control';
|
|
10
|
-
import { APP_SPACE_ID, hasOperationPreview } from '@prisma-next/framework-components/control';
|
|
11
9
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
12
|
-
import { notOk
|
|
13
|
-
import type { DbUpdateResult,
|
|
14
|
-
import {
|
|
10
|
+
import { notOk } from '@prisma-next/utils/result';
|
|
11
|
+
import type { DbUpdateResult, OnControlProgress } from '../types';
|
|
12
|
+
import { executeAggregateApply } from './db-apply-aggregate';
|
|
15
13
|
|
|
16
|
-
// F12: db update allows additive, widening, and destructive operations.
|
|
17
14
|
const DB_UPDATE_POLICY = {
|
|
18
15
|
allowedOperationClasses: ['additive', 'widening', 'destructive'] as const,
|
|
19
16
|
} as const;
|
|
20
17
|
|
|
21
18
|
/**
|
|
22
|
-
* Options for the
|
|
23
|
-
*
|
|
19
|
+
* Options for the `db update` operation.
|
|
20
|
+
*
|
|
21
|
+
* Same loader → planner → runner pipeline as `db init`, but with the
|
|
22
|
+
* widened operation policy (additive + widening + destructive). The
|
|
23
|
+
* destructive-change confirmation gate runs at this layer: when
|
|
24
|
+
* `mode === 'apply'` and `acceptDataLoss` is `false`, the operation
|
|
25
|
+
* pre-plans, surfaces destructive ops to the caller, and aborts.
|
|
24
26
|
*/
|
|
25
27
|
export interface ExecuteDbUpdateOptions<TFamilyId extends string, TTargetId extends string> {
|
|
26
28
|
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
@@ -34,195 +36,71 @@ export interface ExecuteDbUpdateOptions<TFamilyId extends string, TTargetId exte
|
|
|
34
36
|
>;
|
|
35
37
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
|
|
36
38
|
readonly acceptDataLoss?: boolean;
|
|
37
|
-
|
|
39
|
+
readonly migrationsDir: string;
|
|
40
|
+
readonly targetId: TTargetId;
|
|
41
|
+
readonly extensionPacks?: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
|
|
38
42
|
readonly onProgress?: OnControlProgress;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
|
-
*
|
|
46
|
+
* Execute `db update` against the configured contract.
|
|
43
47
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
+
* Routes through the loader → planner → runner pipeline. Destructive
|
|
49
|
+
* operations require either `acceptDataLoss: true` or a prior
|
|
50
|
+
* `mode: 'plan'` invocation that surfaces the destructive ops; the
|
|
51
|
+
* confirmation gate is implemented here so the lower-level applier
|
|
52
|
+
* remains policy-agnostic.
|
|
48
53
|
*/
|
|
49
54
|
export async function executeDbUpdate<TFamilyId extends string, TTargetId extends string>(
|
|
50
55
|
options: ExecuteDbUpdateOptions<TFamilyId, TTargetId>,
|
|
51
56
|
): Promise<DbUpdateResult> {
|
|
52
|
-
const
|
|
53
|
-
options
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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,
|
|
84
|
-
schema: schemaIR,
|
|
85
|
-
policy,
|
|
86
|
-
// `db update` reconciles against the live introspected schema; there is
|
|
87
|
-
// no prior contract to derive a "from" identity from. The required
|
|
88
|
-
// `fromContract: null` makes that structural fact visible at the call
|
|
89
|
-
// site (vs. silently letting the planner default to a baseline plan).
|
|
90
|
-
fromContract: null,
|
|
91
|
-
frameworkComponents,
|
|
92
|
-
spaceId: APP_SPACE_ID,
|
|
93
|
-
});
|
|
94
|
-
if (plannerResult.kind === 'failure') {
|
|
95
|
-
onProgress?.({
|
|
96
|
-
action: 'dbUpdate',
|
|
97
|
-
kind: 'spanEnd',
|
|
98
|
-
spanId: planSpanId,
|
|
99
|
-
outcome: 'error',
|
|
100
|
-
});
|
|
101
|
-
return notOk({
|
|
102
|
-
code: 'PLANNING_FAILED',
|
|
103
|
-
summary: 'Migration planning failed due to conflicts',
|
|
104
|
-
conflicts: plannerResult.conflicts,
|
|
105
|
-
why: undefined,
|
|
106
|
-
meta: undefined,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
onProgress?.({
|
|
110
|
-
action: 'dbUpdate',
|
|
111
|
-
kind: 'spanEnd',
|
|
112
|
-
spanId: planSpanId,
|
|
113
|
-
outcome: 'ok',
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const migrationPlan = plannerResult.plan;
|
|
117
|
-
|
|
118
|
-
if (mode === 'plan') {
|
|
119
|
-
const preview = hasOperationPreview(familyInstance)
|
|
120
|
-
? familyInstance.toOperationPreview(migrationPlan.operations)
|
|
121
|
-
: undefined;
|
|
122
|
-
const result: DbUpdateSuccess = {
|
|
123
|
-
mode: 'plan',
|
|
124
|
-
plan: {
|
|
125
|
-
operations: stripOperations(migrationPlan.operations),
|
|
126
|
-
...ifDefined('preview', preview),
|
|
127
|
-
},
|
|
128
|
-
destination: {
|
|
129
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
130
|
-
...ifDefined('profileHash', migrationPlan.destination.profileHash),
|
|
131
|
-
},
|
|
132
|
-
summary: `Planned ${migrationPlan.operations.length} operation(s)`,
|
|
133
|
-
};
|
|
134
|
-
return ok(result);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// When applying, require explicit acceptance for destructive operations
|
|
138
|
-
if (!options.acceptDataLoss) {
|
|
139
|
-
const destructiveOps = migrationPlan.operations
|
|
140
|
-
.filter((op) => op.operationClass === 'destructive')
|
|
141
|
-
.map((op) => ({ id: op.id, label: op.label }));
|
|
142
|
-
if (destructiveOps.length > 0) {
|
|
143
|
-
return notOk({
|
|
144
|
-
code: 'DESTRUCTIVE_CHANGES',
|
|
145
|
-
summary: `Planned ${destructiveOps.length} destructive operation(s) that require confirmation`,
|
|
146
|
-
why: 'Destructive operations require confirmation — re-run with -y to accept',
|
|
147
|
-
conflicts: undefined,
|
|
148
|
-
meta: { destructiveOperations: destructiveOps },
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const applySpanId = 'apply';
|
|
154
|
-
onProgress?.({
|
|
155
|
-
action: 'dbUpdate',
|
|
156
|
-
kind: 'spanStart',
|
|
157
|
-
spanId: applySpanId,
|
|
158
|
-
label: 'Applying migration plan',
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const callbacks = createOperationCallbacks(onProgress, 'dbUpdate', applySpanId);
|
|
162
|
-
|
|
163
|
-
const runnerResult: MigrationRunnerResult = await runner.execute({
|
|
164
|
-
plan: migrationPlan,
|
|
165
|
-
driver,
|
|
166
|
-
destinationContract: contract,
|
|
167
|
-
policy,
|
|
168
|
-
...(callbacks ? { callbacks } : {}),
|
|
169
|
-
// db update plans and applies from a single introspection pass, so per-operation pre/postchecks
|
|
170
|
-
// and idempotency probes are intentionally disabled to avoid redundant roundtrips.
|
|
171
|
-
executionChecks: {
|
|
172
|
-
prechecks: false,
|
|
173
|
-
postchecks: false,
|
|
174
|
-
idempotencyChecks: false,
|
|
175
|
-
},
|
|
176
|
-
frameworkComponents,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (!runnerResult.ok) {
|
|
180
|
-
onProgress?.({
|
|
181
|
-
action: 'dbUpdate',
|
|
182
|
-
kind: 'spanEnd',
|
|
183
|
-
spanId: applySpanId,
|
|
184
|
-
outcome: 'error',
|
|
185
|
-
});
|
|
186
|
-
return notOk({
|
|
187
|
-
code: 'RUNNER_FAILED',
|
|
188
|
-
summary: runnerResult.failure.summary,
|
|
189
|
-
why: runnerResult.failure.why,
|
|
190
|
-
meta: runnerResult.failure.meta,
|
|
191
|
-
conflicts: undefined,
|
|
192
|
-
});
|
|
57
|
+
const sharedInputs = {
|
|
58
|
+
driver: options.driver,
|
|
59
|
+
familyInstance: options.familyInstance,
|
|
60
|
+
contract: options.contract,
|
|
61
|
+
migrations: options.migrations,
|
|
62
|
+
frameworkComponents: options.frameworkComponents,
|
|
63
|
+
migrationsDir: options.migrationsDir,
|
|
64
|
+
targetId: options.targetId,
|
|
65
|
+
extensionPacks: options.extensionPacks ?? [],
|
|
66
|
+
policy: DB_UPDATE_POLICY,
|
|
67
|
+
action: 'dbUpdate' as const,
|
|
68
|
+
...ifDefined('onProgress', options.onProgress),
|
|
69
|
+
};
|
|
70
|
+
if (options.mode === 'apply' && !options.acceptDataLoss) {
|
|
71
|
+
const gate = await guardDestructiveChanges<TFamilyId, TTargetId>(sharedInputs);
|
|
72
|
+
if (gate !== null) return gate;
|
|
193
73
|
}
|
|
74
|
+
return (await executeAggregateApply<TFamilyId, TTargetId>({
|
|
75
|
+
...sharedInputs,
|
|
76
|
+
mode: options.mode,
|
|
77
|
+
})) as DbUpdateResult;
|
|
78
|
+
}
|
|
194
79
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Pre-plan once when running `db update apply` without `acceptDataLoss`.
|
|
82
|
+
* Surfaces destructive operations across every space; if any are
|
|
83
|
+
* planned, returns a `DESTRUCTIVE_CHANGES` failure that the CLI shows
|
|
84
|
+
* as a confirmation prompt. Returns `null` when the apply is safe to
|
|
85
|
+
* run.
|
|
86
|
+
*/
|
|
87
|
+
async function guardDestructiveChanges<TFamilyId extends string, TTargetId extends string>(
|
|
88
|
+
sharedInputs: Omit<Parameters<typeof executeAggregateApply<TFamilyId, TTargetId>>[0], 'mode'>,
|
|
89
|
+
): Promise<DbUpdateResult | null> {
|
|
90
|
+
const planResult = (await executeAggregateApply<TFamilyId, TTargetId>({
|
|
91
|
+
...sharedInputs,
|
|
92
|
+
mode: 'plan',
|
|
93
|
+
})) as DbUpdateResult;
|
|
94
|
+
if (!planResult.ok) return planResult;
|
|
95
|
+
const destructiveOps = planResult.value.plan.operations
|
|
96
|
+
.filter((op) => op.operationClass === 'destructive')
|
|
97
|
+
.map((op) => ({ id: op.id, label: op.label }));
|
|
98
|
+
if (destructiveOps.length === 0) return null;
|
|
99
|
+
return notOk({
|
|
100
|
+
code: 'DESTRUCTIVE_CHANGES',
|
|
101
|
+
summary: `Planned ${destructiveOps.length} destructive operation(s) that require confirmation`,
|
|
102
|
+
why: 'Destructive operations require confirmation — re-run with -y to accept',
|
|
103
|
+
conflicts: undefined,
|
|
104
|
+
meta: { destructiveOperations: destructiveOps },
|
|
201
105
|
});
|
|
202
|
-
|
|
203
|
-
const result: DbUpdateSuccess = {
|
|
204
|
-
mode: 'apply',
|
|
205
|
-
plan: {
|
|
206
|
-
operations: stripOperations(migrationPlan.operations),
|
|
207
|
-
},
|
|
208
|
-
destination: {
|
|
209
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
210
|
-
...ifDefined('profileHash', migrationPlan.destination.profileHash),
|
|
211
|
-
},
|
|
212
|
-
execution: {
|
|
213
|
-
operationsPlanned: execution.operationsPlanned,
|
|
214
|
-
operationsExecuted: execution.operationsExecuted,
|
|
215
|
-
},
|
|
216
|
-
marker: migrationPlan.destination.profileHash
|
|
217
|
-
? {
|
|
218
|
-
storageHash: migrationPlan.destination.storageHash,
|
|
219
|
-
profileHash: migrationPlan.destination.profileHash,
|
|
220
|
-
}
|
|
221
|
-
: { storageHash: migrationPlan.destination.storageHash },
|
|
222
|
-
summary:
|
|
223
|
-
execution.operationsExecuted === 0
|
|
224
|
-
? 'Database already matches contract, signature updated'
|
|
225
|
-
: `Applied ${execution.operationsExecuted} operation(s), signature updated`,
|
|
226
|
-
};
|
|
227
|
-
return ok(result);
|
|
228
106
|
}
|