@prisma-next/cli 0.5.0-dev.73 → 0.5.0-dev.75
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/{client-0ZX24FXF.mjs → client-qVH-rEgd.mjs} +433 -236
- package/dist/client-qVH-rEgd.mjs.map +1 -0
- package/dist/{result-handler-DWb1rFS-.mjs → command-helpers-BeZHkxV8.mjs} +22 -24
- package/dist/command-helpers-BeZHkxV8.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 +5 -4
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +6 -5
- package/dist/commands/db-sign.mjs.map +1 -1
- 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.mjs +1 -1
- package/dist/commands/migration-apply.d.mts +29 -17
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +35 -129
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.mjs +4 -3
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +19 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +2 -2
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +3 -2
- package/dist/commands/migration-ref.mjs.map +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +5 -4
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +104 -1
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -2
- package/dist/{contract-emit-DkMqO7f2.mjs → contract-emit-9DBda5Ou.mjs} +7 -5
- package/dist/{contract-emit-DkMqO7f2.mjs.map → contract-emit-9DBda5Ou.mjs.map} +1 -1
- package/dist/{contract-emit-B3ChISB_.mjs → contract-emit-B77TsJqf.mjs} +4 -15
- package/dist/{contract-emit-B3ChISB_.mjs.map → contract-emit-B77TsJqf.mjs.map} +1 -1
- package/dist/{contract-enrichment-CF6ogEJ_.mjs → contract-enrichment-Dani0mMW.mjs} +1 -1
- package/dist/{contract-enrichment-CF6ogEJ_.mjs.map → contract-enrichment-Dani0mMW.mjs.map} +1 -1
- package/dist/{contract-infer-BDKAE0B0.mjs → contract-infer-BK9YFGEG.mjs} +5 -4
- package/dist/{contract-infer-BDKAE0B0.mjs.map → contract-infer-BK9YFGEG.mjs.map} +1 -1
- package/dist/{db-verify-B4TdDKOI.mjs → db-verify-C0y1PCO2.mjs} +7 -6
- package/dist/{db-verify-B4TdDKOI.mjs.map → db-verify-C0y1PCO2.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +3 -746
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -3
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/extension-pack-inputs-C7xgE-vv.mjs +74 -0
- package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +1 -0
- package/dist/{framework-components-gwAHl7ml.mjs → framework-components-ChqVUxR-.mjs} +1 -1
- package/dist/{framework-components-gwAHl7ml.mjs.map → framework-components-ChqVUxR-.mjs.map} +1 -1
- package/dist/global-flags-Icqpxk23.d.mts +12 -0
- package/dist/global-flags-Icqpxk23.d.mts.map +1 -0
- package/dist/helpers-eqdN8tH6.mjs +25 -0
- package/dist/helpers-eqdN8tH6.mjs.map +1 -0
- package/dist/{init-Deo7U8_U.mjs → init-CoDVPvQ4.mjs} +4 -4
- package/dist/{init-Deo7U8_U.mjs.map → init-CoDVPvQ4.mjs.map} +1 -1
- package/dist/{inspect-live-schema-BAgQMYpD.mjs → inspect-live-schema-CWYxGKlb.mjs} +4 -4
- package/dist/{inspect-live-schema-BAgQMYpD.mjs.map → inspect-live-schema-CWYxGKlb.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-B8J702Uh.mjs → migration-command-scaffold-B5dORFEv.mjs} +4 -4
- package/dist/{migration-command-scaffold-B8J702Uh.mjs.map → migration-command-scaffold-B5dORFEv.mjs.map} +1 -1
- package/dist/{migration-plan-BcKNnTM7.mjs → migration-plan-C6lVaHsO.mjs} +47 -23
- package/dist/migration-plan-C6lVaHsO.mjs.map +1 -0
- package/dist/{migration-status-CjwB2of-.mjs → migration-status-CZ-D5k7k.mjs} +161 -7
- package/dist/migration-status-CZ-D5k7k.mjs.map +1 -0
- package/dist/{migrations-CIK94AJf.mjs → migrations-D_UJnpuW.mjs} +67 -24
- package/dist/migrations-D_UJnpuW.mjs.map +1 -0
- package/dist/{output-DnjfCC_u.mjs → output-B16Kefzx.mjs} +1 -1
- package/dist/{output-DnjfCC_u.mjs.map → output-B16Kefzx.mjs.map} +1 -1
- package/dist/{progress-adapter-xASh41wr.mjs → progress-adapter-DFfvZcYL.mjs} +1 -1
- package/dist/{progress-adapter-xASh41wr.mjs.map → progress-adapter-DFfvZcYL.mjs.map} +1 -1
- package/dist/result-handler-rmPVKIP2.mjs +25 -0
- package/dist/result-handler-rmPVKIP2.mjs.map +1 -0
- package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
- package/dist/{terminal-ui-zaRDhJnP.mjs → terminal-ui-C_hFNbAn.mjs} +3 -23
- package/dist/terminal-ui-C_hFNbAn.mjs.map +1 -0
- package/dist/types-D7x-IFLO.d.mts +858 -0
- package/dist/types-D7x-IFLO.d.mts.map +1 -0
- package/dist/{verify-BEIa9638.mjs → verify-CiwNWM9N.mjs} +2 -2
- package/dist/{verify-BEIa9638.mjs.map → verify-CiwNWM9N.mjs.map} +1 -1
- package/package.json +14 -14
- package/src/commands/db-init.ts +1 -0
- package/src/commands/db-update.ts +1 -0
- package/src/commands/migration-apply.ts +94 -213
- package/src/commands/migration-plan.ts +89 -32
- package/src/commands/migration-status.ts +288 -5
- package/src/control-api/client.ts +16 -4
- package/src/control-api/operations/apply-aggregate.ts +290 -0
- package/src/control-api/operations/db-apply-aggregate.ts +42 -91
- package/src/control-api/operations/migration-apply.ts +420 -155
- package/src/control-api/types.ts +165 -32
- package/src/utils/contract-space-aggregate-loader.ts +24 -56
- package/src/utils/extension-pack-inputs.ts +170 -0
- package/src/utils/formatters/migrations.ts +135 -35
- package/dist/client-0ZX24FXF.mjs.map +0 -1
- package/dist/migration-plan-BcKNnTM7.mjs.map +0 -1
- package/dist/migration-status-CjwB2of-.mjs.map +0 -1
- package/dist/migrations-CIK94AJf.mjs.map +0 -1
- package/dist/result-handler-DWb1rFS-.mjs.map +0 -1
- package/dist/terminal-ui-zaRDhJnP.mjs.map +0 -1
- /package/dist/{cli-errors-QH8kf-C2.d.mts → cli-errors-B9OBbled.d.mts} +0 -0
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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';
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
+
import { errorUnknownInvariant, MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
9
4
|
import type { RefEntry } from '@prisma-next/migration-tools/refs';
|
|
10
5
|
import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
|
|
11
6
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
@@ -14,12 +9,18 @@ import { Command } from 'commander';
|
|
|
14
9
|
|
|
15
10
|
import { loadConfig } from '../config-loader';
|
|
16
11
|
import { createControlClient } from '../control-api/client';
|
|
17
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
AggregatePerSpaceExecutionEntry,
|
|
14
|
+
MigrationApplyFailure,
|
|
15
|
+
MigrationApplyPathDecision,
|
|
16
|
+
} from '../control-api/types';
|
|
18
17
|
import {
|
|
19
18
|
CliStructuredError,
|
|
20
19
|
type CliStructuredError as CliStructuredErrorType,
|
|
20
|
+
errorContractValidationFailed,
|
|
21
21
|
errorDatabaseConnectionRequired,
|
|
22
22
|
errorDriverRequired,
|
|
23
|
+
errorFileNotFound,
|
|
23
24
|
errorRuntime,
|
|
24
25
|
errorTargetMigrationNotSupported,
|
|
25
26
|
errorUnexpected,
|
|
@@ -30,13 +31,11 @@ import {
|
|
|
30
31
|
collectDeclaredInvariants,
|
|
31
32
|
loadMigrationPackages,
|
|
32
33
|
maskConnectionUrl,
|
|
33
|
-
|
|
34
|
+
resolveContractPath,
|
|
34
35
|
resolveMigrationPaths,
|
|
35
36
|
setCommandDescriptions,
|
|
36
37
|
setCommandExamples,
|
|
37
38
|
targetSupportsMigrations,
|
|
38
|
-
toPathDecisionResult,
|
|
39
|
-
toStructuralEdge,
|
|
40
39
|
} from '../utils/command-helpers';
|
|
41
40
|
import { formatMigrationApplyCommandOutput } from '../utils/formatters/migrations';
|
|
42
41
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
@@ -51,34 +50,45 @@ interface MigrationApplyCommandOptions extends CommonCommandOptions {
|
|
|
51
50
|
readonly ref?: string;
|
|
52
51
|
}
|
|
53
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Per-space breakdown of an apply run. The CLI command surfaces these
|
|
55
|
+
* for both the JSON shape (`appliedSpaces[]`) and the human-readable
|
|
56
|
+
* formatter (per-space block — same shape `db init` / `db update`
|
|
57
|
+
* use, M6 sub-spec § Output shape contract).
|
|
58
|
+
*/
|
|
54
59
|
export interface MigrationApplyResult {
|
|
55
60
|
readonly ok: boolean;
|
|
61
|
+
/** Number of contract spaces that had non-zero pending operations applied. */
|
|
56
62
|
readonly migrationsApplied: number;
|
|
63
|
+
/** Total contract spaces visible in the aggregate (pending + already-up-to-date). */
|
|
57
64
|
readonly migrationsTotal: number;
|
|
65
|
+
/**
|
|
66
|
+
* Marker hash for the **app member** post-apply. Surfaced for
|
|
67
|
+
* back-compat with single-space callers; per-space markers live on
|
|
68
|
+
* `perSpace[].marker.storageHash`.
|
|
69
|
+
*/
|
|
58
70
|
readonly markerHash: string;
|
|
59
71
|
readonly applied: readonly {
|
|
72
|
+
readonly spaceId: string;
|
|
60
73
|
readonly dirName: string;
|
|
61
|
-
readonly
|
|
74
|
+
readonly migrationHash: string;
|
|
75
|
+
readonly from: string;
|
|
62
76
|
readonly to: string;
|
|
63
77
|
readonly operationsExecuted: number;
|
|
64
78
|
}[];
|
|
65
79
|
readonly summary: string;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
readonly to: string;
|
|
79
|
-
readonly invariants: readonly string[];
|
|
80
|
-
}[];
|
|
81
|
-
};
|
|
80
|
+
/**
|
|
81
|
+
* Per-space breakdown in canonical schedule order (extensions
|
|
82
|
+
* alphabetically, then app). Always present for the aggregate-walking
|
|
83
|
+
* apply path.
|
|
84
|
+
*/
|
|
85
|
+
readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
|
|
86
|
+
/**
|
|
87
|
+
* Path-decision data for the app member. Surfaced for back-compat
|
|
88
|
+
* with single-space callers (cli-journeys invariant tests).
|
|
89
|
+
* Absent for no-op applies where the app had nothing to do.
|
|
90
|
+
*/
|
|
91
|
+
readonly pathDecision?: MigrationApplyPathDecision;
|
|
82
92
|
readonly timings: {
|
|
83
93
|
readonly total: number;
|
|
84
94
|
};
|
|
@@ -92,17 +102,6 @@ function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType
|
|
|
92
102
|
});
|
|
93
103
|
}
|
|
94
104
|
|
|
95
|
-
function packageToStep(pkg: OnDiskMigrationPackage): MigrationApplyStep {
|
|
96
|
-
return {
|
|
97
|
-
dirName: pkg.dirName,
|
|
98
|
-
from: pkg.metadata.from,
|
|
99
|
-
to: pkg.metadata.to,
|
|
100
|
-
toContract: pkg.metadata.toContract,
|
|
101
|
-
operations: pkg.ops,
|
|
102
|
-
providedInvariants: pkg.metadata.providedInvariants,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
105
|
async function executeMigrationApplyCommand(
|
|
107
106
|
options: MigrationApplyCommandOptions,
|
|
108
107
|
flags: GlobalFlags,
|
|
@@ -110,10 +109,8 @@ async function executeMigrationApplyCommand(
|
|
|
110
109
|
startTime: number,
|
|
111
110
|
): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
|
|
112
111
|
const config = await loadConfig(options.config);
|
|
113
|
-
const { configPath, appMigrationsDir, appMigrationsRelative, refsDir } =
|
|
114
|
-
options.config,
|
|
115
|
-
config,
|
|
116
|
-
);
|
|
112
|
+
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative, refsDir } =
|
|
113
|
+
resolveMigrationPaths(options.config, config);
|
|
117
114
|
|
|
118
115
|
const dbConnection = options.db ?? config.db?.connection;
|
|
119
116
|
if (!dbConnection) {
|
|
@@ -142,7 +139,6 @@ async function executeMigrationApplyCommand(
|
|
|
142
139
|
}
|
|
143
140
|
|
|
144
141
|
let refEntry: RefEntry | undefined;
|
|
145
|
-
let envelopeHash: string | undefined;
|
|
146
142
|
const refName = options.ref;
|
|
147
143
|
|
|
148
144
|
if (refName) {
|
|
@@ -155,20 +151,31 @@ async function executeMigrationApplyCommand(
|
|
|
155
151
|
}
|
|
156
152
|
throw error;
|
|
157
153
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Resolve and parse the contract envelope. The aggregate-walking
|
|
157
|
+
// operation needs the validated app contract to load the aggregate.
|
|
158
|
+
const contractPathAbsolute = resolveContractPath(config);
|
|
159
|
+
let contractRaw: Contract;
|
|
160
|
+
try {
|
|
161
|
+
const contractContent = await readFile(contractPathAbsolute, 'utf-8');
|
|
162
|
+
contractRaw = JSON.parse(contractContent) as Contract;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
163
165
|
return notOk(
|
|
164
|
-
|
|
165
|
-
why: `
|
|
166
|
+
errorFileNotFound(contractPathAbsolute, {
|
|
167
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
166
168
|
fix: 'Run `prisma-next contract emit` to generate a valid contract.json, then retry apply.',
|
|
167
169
|
}),
|
|
168
170
|
);
|
|
169
171
|
}
|
|
172
|
+
return notOk(
|
|
173
|
+
errorContractValidationFailed(
|
|
174
|
+
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
175
|
+
{ where: { path: contractPathAbsolute } },
|
|
176
|
+
),
|
|
177
|
+
);
|
|
170
178
|
}
|
|
171
|
-
const destinationHash = refEntry?.hash ?? envelopeHash!;
|
|
172
179
|
|
|
173
180
|
if (!flags.json && !flags.quiet) {
|
|
174
181
|
const details: Array<{ label: string; value: string }> = [
|
|
@@ -194,10 +201,11 @@ async function executeMigrationApplyCommand(
|
|
|
194
201
|
ui.stderr(header);
|
|
195
202
|
}
|
|
196
203
|
|
|
197
|
-
//
|
|
198
|
-
|
|
204
|
+
// Load app-space migration packages — the aggregate operation
|
|
205
|
+
// needs them to hydrate the app member's graph for graph-walk.
|
|
206
|
+
let appPackages: Awaited<ReturnType<typeof loadMigrationPackages>>;
|
|
199
207
|
try {
|
|
200
|
-
|
|
208
|
+
appPackages = await loadMigrationPackages(appMigrationsDir);
|
|
201
209
|
} catch (error) {
|
|
202
210
|
if (MigrationToolsError.is(error)) {
|
|
203
211
|
return notOk(mapMigrationToolsError(error));
|
|
@@ -215,20 +223,20 @@ async function executeMigrationApplyCommand(
|
|
|
215
223
|
|
|
216
224
|
try {
|
|
217
225
|
await client.connect(dbConnection);
|
|
218
|
-
const marker = await client.readMarker();
|
|
219
226
|
|
|
220
|
-
// Pre-check unknown invariants against `(declared by graph) ∪
|
|
221
|
-
// (already on the marker)`. The
|
|
222
|
-
// ref carries an invariant whose
|
|
223
|
-
// history rewritten) but whose
|
|
224
|
-
//
|
|
225
|
-
// because the database has already
|
|
226
|
-
//
|
|
227
|
-
// short-circuits to "Already up to date".
|
|
227
|
+
// Pre-check unknown invariants against `(declared by app graph) ∪
|
|
228
|
+
// (already on the app marker)`. The marker side of the union
|
|
229
|
+
// catches the case where the ref carries an invariant whose
|
|
230
|
+
// declaring migration was retired (history rewritten) but whose
|
|
231
|
+
// id is recorded on the marker — surfacing UNKNOWN_INVARIANT
|
|
232
|
+
// there would be misleading because the database has already
|
|
233
|
+
// satisfied the requirement.
|
|
228
234
|
if (refEntry && refEntry.invariants.length > 0) {
|
|
229
|
-
const
|
|
235
|
+
const allMarkers = await client.readAllMarkers();
|
|
236
|
+
const appMarker = allMarkers.get('app') ?? null;
|
|
237
|
+
const declared = collectDeclaredInvariants(appPackages.graph);
|
|
230
238
|
const known = new Set<string>(declared);
|
|
231
|
-
for (const id of
|
|
239
|
+
for (const id of appMarker?.invariants ?? []) known.add(id);
|
|
232
240
|
const unknown = refEntry.invariants.filter((id) => !known.has(id));
|
|
233
241
|
if (unknown.length > 0) {
|
|
234
242
|
return notOk(
|
|
@@ -243,149 +251,17 @@ async function executeMigrationApplyCommand(
|
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
// --- No migrations on disk ---
|
|
247
|
-
if (migrations.bundles.length === 0) {
|
|
248
|
-
if (marker?.storageHash) {
|
|
249
|
-
return notOk(
|
|
250
|
-
errorRuntime('Database has state but no migrations exist', {
|
|
251
|
-
why: `The database marker hash "${marker.storageHash}" exists but no migrations were found in ${appMigrationsRelative}`,
|
|
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.',
|
|
253
|
-
meta: { markerHash: marker.storageHash, migrationsDir: appMigrationsRelative },
|
|
254
|
-
}),
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
// Non-empty contract + no migrations = user needs to plan first.
|
|
258
|
-
if (destinationHash !== EMPTY_CONTRACT_HASH) {
|
|
259
|
-
return notOk(
|
|
260
|
-
errorRuntime('Current contract has no planned migrations', {
|
|
261
|
-
why: `No migrations were found in ${appMigrationsRelative}, but current contract hash is "${destinationHash}"`,
|
|
262
|
-
fix: 'Run `prisma-next migration plan` to create a migration for the current contract.',
|
|
263
|
-
meta: { destinationHash, migrationsDir: appMigrationsRelative },
|
|
264
|
-
}),
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
// Empty contract + no migrations = nothing to do.
|
|
268
|
-
return ok({
|
|
269
|
-
ok: true,
|
|
270
|
-
migrationsApplied: 0,
|
|
271
|
-
migrationsTotal: 0,
|
|
272
|
-
markerHash: EMPTY_CONTRACT_HASH,
|
|
273
|
-
applied: [],
|
|
274
|
-
summary: 'No migrations found',
|
|
275
|
-
timings: { total: Date.now() - startTime },
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// --- Validate marker state ---
|
|
280
|
-
|
|
281
|
-
// The empty sentinel should never appear in a real marker row — if it does,
|
|
282
|
-
// the marker was corrupted and replaying all migrations would be dangerous.
|
|
283
|
-
if (marker?.storageHash === EMPTY_CONTRACT_HASH) {
|
|
284
|
-
return notOk(
|
|
285
|
-
errorRuntime('Database marker contains the empty sentinel hash', {
|
|
286
|
-
why: `The marker row exists but contains the empty sentinel value "${EMPTY_CONTRACT_HASH}". This should never happen — the marker should contain the hash of the last applied contract.`,
|
|
287
|
-
fix: 'The marker is corrupted. Run `prisma-next db sign` to overwrite it with the correct contract hash, or drop and recreate the database.',
|
|
288
|
-
meta: { markerHash: EMPTY_CONTRACT_HASH },
|
|
289
|
-
}),
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const markerHash = marker?.storageHash;
|
|
294
|
-
|
|
295
|
-
if (markerHash !== undefined && !migrations.graph.nodes.has(markerHash)) {
|
|
296
|
-
return notOk(
|
|
297
|
-
errorRuntime('Database marker does not match any known migration', {
|
|
298
|
-
why: `The database marker hash "${markerHash}" is not found in the migration history at ${appMigrationsRelative}`,
|
|
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.',
|
|
300
|
-
meta: { markerHash, knownNodes: [...migrations.graph.nodes] },
|
|
301
|
-
}),
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (!migrations.graph.nodes.has(destinationHash)) {
|
|
306
|
-
return notOk(
|
|
307
|
-
errorRuntime('Current contract has no planned migration path', {
|
|
308
|
-
why: `Current contract hash "${destinationHash}" is not present in the migration history at ${appMigrationsRelative}`,
|
|
309
|
-
fix: 'Run `prisma-next migration plan` to create a migration for the current contract, then re-run apply.',
|
|
310
|
-
meta: { destinationHash, knownNodes: [...migrations.graph.nodes] },
|
|
311
|
-
}),
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// "No marker" means the database is fresh — start from the empty contract hash.
|
|
316
|
-
const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
317
|
-
|
|
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') {
|
|
340
|
-
return notOk(
|
|
341
|
-
errorRuntime('No migration path from current state to target', {
|
|
342
|
-
why: `Cannot find a path from "${originHash}" to target "${destinationHash}"`,
|
|
343
|
-
fix: 'Check the migration history for gaps or inconsistencies.',
|
|
344
|
-
meta: { markerHash: originHash, destinationHash },
|
|
345
|
-
}),
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const pathDecision = toPathDecisionResult(outcome.decision);
|
|
350
|
-
|
|
351
|
-
if (outcome.decision.selectedPath.length === 0) {
|
|
352
|
-
return ok({
|
|
353
|
-
ok: true,
|
|
354
|
-
migrationsApplied: 0,
|
|
355
|
-
migrationsTotal: 0,
|
|
356
|
-
markerHash: originHash,
|
|
357
|
-
applied: [],
|
|
358
|
-
summary: 'Already up to date',
|
|
359
|
-
pathDecision,
|
|
360
|
-
timings: { total: Date.now() - startTime },
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const bundleByDir = new Map(migrations.bundles.map((b) => [b.dirName, b]));
|
|
365
|
-
const pendingMigrations: MigrationApplyStep[] = [];
|
|
366
|
-
for (const migration of outcome.decision.selectedPath) {
|
|
367
|
-
const pkg = bundleByDir.get(migration.dirName);
|
|
368
|
-
if (!pkg) {
|
|
369
|
-
return notOk(
|
|
370
|
-
errorRuntime(`Migration package not found: ${migration.dirName}`, {
|
|
371
|
-
why: `The migration directory for path segment ${migration.from} → ${migration.to} was not found`,
|
|
372
|
-
fix: 'Ensure all migration directories are present and intact.',
|
|
373
|
-
}),
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
pendingMigrations.push(packageToStep(pkg));
|
|
377
|
-
}
|
|
378
|
-
|
|
379
254
|
if (!flags.quiet && !flags.json) {
|
|
380
|
-
|
|
381
|
-
ui.step(`Pending ${migration.dirName}`);
|
|
382
|
-
}
|
|
255
|
+
ui.step('Loading contract spaces…');
|
|
383
256
|
}
|
|
384
257
|
|
|
385
258
|
const applyResult = await client.migrationApply({
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
259
|
+
contract: contractRaw,
|
|
260
|
+
migrationsDir,
|
|
261
|
+
appMigrationPackages: appPackages.bundles,
|
|
262
|
+
...ifDefined('refHash', refEntry?.hash),
|
|
263
|
+
...(refEntry?.invariants ? { refInvariants: refEntry.invariants } : {}),
|
|
264
|
+
...(refEntry !== undefined ? ifDefined('refName', refName) : {}),
|
|
389
265
|
});
|
|
390
266
|
|
|
391
267
|
if (!applyResult.ok) {
|
|
@@ -397,17 +273,21 @@ async function executeMigrationApplyCommand(
|
|
|
397
273
|
return ok({
|
|
398
274
|
ok: true,
|
|
399
275
|
migrationsApplied: value.migrationsApplied,
|
|
400
|
-
migrationsTotal:
|
|
276
|
+
migrationsTotal: value.perSpace.length,
|
|
401
277
|
markerHash: value.markerHash,
|
|
402
278
|
applied: value.applied,
|
|
403
279
|
summary: value.summary,
|
|
404
|
-
|
|
280
|
+
perSpace: value.perSpace,
|
|
281
|
+
...ifDefined('pathDecision', value.pathDecision),
|
|
405
282
|
timings: { total: Date.now() - startTime },
|
|
406
283
|
});
|
|
407
284
|
} catch (error) {
|
|
408
285
|
if (CliStructuredError.is(error)) {
|
|
409
286
|
return notOk(error);
|
|
410
287
|
}
|
|
288
|
+
if (MigrationToolsError.is(error)) {
|
|
289
|
+
return notOk(mapMigrationToolsError(error));
|
|
290
|
+
}
|
|
411
291
|
return notOk(
|
|
412
292
|
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
413
293
|
why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -423,16 +303,17 @@ export function createMigrationApplyCommand(): Command {
|
|
|
423
303
|
setCommandDescriptions(
|
|
424
304
|
command,
|
|
425
305
|
'Apply planned migrations to the database',
|
|
426
|
-
'
|
|
427
|
-
'
|
|
428
|
-
'
|
|
429
|
-
|
|
306
|
+
'Walks every contract space (app + extensions) and applies pending\n' +
|
|
307
|
+
'on-disk migrations in canonical order (extensions alphabetically,\n' +
|
|
308
|
+
'then app). Graph-walks the on-disk migration graph for every space —\n' +
|
|
309
|
+
"no introspection, no synth. Each space's marker advances inside its\n" +
|
|
310
|
+
"transaction; per-space failure rolls back every space's writes.",
|
|
430
311
|
);
|
|
431
312
|
setCommandExamples(command, ['prisma-next migration apply --db $DATABASE_URL']);
|
|
432
313
|
addGlobalOptions(command)
|
|
433
314
|
.option('--db <url>', 'Database connection string')
|
|
434
315
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
435
|
-
.option('--ref <name>', '
|
|
316
|
+
.option('--ref <name>', 'App-space target ref name from migrations/app/refs/')
|
|
436
317
|
.action(async (options: MigrationApplyCommandOptions) => {
|
|
437
318
|
const flags = parseGlobalFlags(options);
|
|
438
319
|
const startTime = Date.now();
|
|
@@ -43,15 +43,16 @@ import {
|
|
|
43
43
|
setCommandDescriptions,
|
|
44
44
|
setCommandExamples,
|
|
45
45
|
} from '../utils/command-helpers';
|
|
46
|
-
import {
|
|
47
|
-
type ExtensionMigrationsExtensionInput,
|
|
48
|
-
runContractSpaceExtensionMigrationsPass,
|
|
49
|
-
} from '../utils/contract-space-extension-migrations-pass';
|
|
46
|
+
import { runContractSpaceExtensionMigrationsPass } from '../utils/contract-space-extension-migrations-pass';
|
|
50
47
|
import {
|
|
51
48
|
formatContractSpaceDriftWarning,
|
|
52
|
-
type MigrateExtensionInput,
|
|
53
49
|
runContractSpaceMigratePass,
|
|
54
50
|
} from '../utils/contract-space-migrate-pass';
|
|
51
|
+
import {
|
|
52
|
+
toExtensionInputs,
|
|
53
|
+
toExtensionMigrationsInputs,
|
|
54
|
+
toMigratePassInputs,
|
|
55
|
+
} from '../utils/extension-pack-inputs';
|
|
55
56
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
56
57
|
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
57
58
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
@@ -71,6 +72,19 @@ export interface MigrationPlanResult {
|
|
|
71
72
|
readonly from: string | null;
|
|
72
73
|
readonly to: string;
|
|
73
74
|
readonly dir?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Extension-space migration packages materialised onto disk during this
|
|
77
|
+
* `plan` run. Each entry names a `migrations/<spaceId>/<dirName>/`
|
|
78
|
+
* tree the framework wrote alongside the app-space migration directory.
|
|
79
|
+
* Empty when the project has no extension packs declaring a contract
|
|
80
|
+
* space, or when every extension-space package is already on disk.
|
|
81
|
+
*
|
|
82
|
+
* Surfacing these in the result (rather than only via `ui.step` log
|
|
83
|
+
* lines) makes the cross-space side effect explicit to JSON consumers
|
|
84
|
+
* and the success-summary renderer — the same multi-space side effect
|
|
85
|
+
* that `migration apply` will replay.
|
|
86
|
+
*/
|
|
87
|
+
readonly emittedExtensionDirs: readonly { readonly spaceId: string; readonly dirName: string }[];
|
|
74
88
|
readonly operations: readonly {
|
|
75
89
|
readonly id: string;
|
|
76
90
|
readonly label: string;
|
|
@@ -233,16 +247,12 @@ async function executeMigrationPlanCommand(
|
|
|
233
247
|
// structural app-space change) still re-pins extension artefacts on
|
|
234
248
|
// disk. Drift warnings are non-fatal — the on-disk artefacts are refreshed
|
|
235
249
|
// and the user is notified that the bump is being captured.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
.contractSpace;
|
|
240
|
-
return cs !== undefined ? { id: pack.id, contractSpace: cs } : { id: pack.id };
|
|
241
|
-
},
|
|
242
|
-
);
|
|
250
|
+
// Single descriptor-import boundary: every consumer of `extensionPacks`
|
|
251
|
+
// goes through `toExtensionInputs` + a per-consumer adapter. AC11.
|
|
252
|
+
const canonicalExtensionInputs = toExtensionInputs(config.extensionPacks ?? []);
|
|
243
253
|
const migratePass = await runContractSpaceMigratePass({
|
|
244
254
|
migrationsDir,
|
|
245
|
-
extensionPacks:
|
|
255
|
+
extensionPacks: toMigratePassInputs(canonicalExtensionInputs),
|
|
246
256
|
});
|
|
247
257
|
if (!flags.json && !flags.quiet) {
|
|
248
258
|
for (const drift of migratePass.drifts) {
|
|
@@ -257,19 +267,9 @@ async function executeMigrationPlanCommand(
|
|
|
257
267
|
// Idempotent (existing dirs are left untouched).
|
|
258
268
|
// Uses `planAllSpaces` for deterministic ordering + duplicate-spaceId
|
|
259
269
|
// detection.
|
|
260
|
-
const extensionMigrationsInputs: readonly ExtensionMigrationsExtensionInput[] = (
|
|
261
|
-
config.extensionPacks ?? []
|
|
262
|
-
).map((pack) => {
|
|
263
|
-
const cs = (
|
|
264
|
-
pack as {
|
|
265
|
-
readonly contractSpace?: ExtensionMigrationsExtensionInput['contractSpace'];
|
|
266
|
-
}
|
|
267
|
-
).contractSpace;
|
|
268
|
-
return cs !== undefined ? { id: pack.id, contractSpace: cs } : { id: pack.id };
|
|
269
|
-
});
|
|
270
270
|
const extensionMigrationsResult = await runContractSpaceExtensionMigrationsPass({
|
|
271
271
|
migrationsDir,
|
|
272
|
-
extensionPacks:
|
|
272
|
+
extensionPacks: toExtensionMigrationsInputs(canonicalExtensionInputs),
|
|
273
273
|
});
|
|
274
274
|
if (!flags.json && !flags.quiet) {
|
|
275
275
|
for (const entry of extensionMigrationsResult.emitted) {
|
|
@@ -285,6 +285,7 @@ async function executeMigrationPlanCommand(
|
|
|
285
285
|
from: fromHash,
|
|
286
286
|
to: toStorageHash,
|
|
287
287
|
operations: [],
|
|
288
|
+
emittedExtensionDirs: extensionMigrationsResult.emitted,
|
|
288
289
|
summary: 'No changes detected between contracts',
|
|
289
290
|
timings: { total: Date.now() - startTime },
|
|
290
291
|
};
|
|
@@ -420,6 +421,7 @@ async function executeMigrationPlanCommand(
|
|
|
420
421
|
to: toStorageHash,
|
|
421
422
|
dir: relative(process.cwd(), packageDir),
|
|
422
423
|
operations: [],
|
|
424
|
+
emittedExtensionDirs: extensionMigrationsResult.emitted,
|
|
423
425
|
pendingPlaceholders: true,
|
|
424
426
|
summary:
|
|
425
427
|
'Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
|
|
@@ -442,8 +444,9 @@ async function executeMigrationPlanCommand(
|
|
|
442
444
|
label: op.label,
|
|
443
445
|
operationClass: op.operationClass,
|
|
444
446
|
})),
|
|
447
|
+
emittedExtensionDirs: extensionMigrationsResult.emitted,
|
|
445
448
|
...(preview !== undefined ? { preview } : {}),
|
|
446
|
-
summary:
|
|
449
|
+
summary: buildPlanSummary(plannedOps.length, extensionMigrationsResult.emitted.length),
|
|
447
450
|
timings: { total: Date.now() - startTime },
|
|
448
451
|
};
|
|
449
452
|
return ok(result);
|
|
@@ -501,7 +504,29 @@ export function createMigrationPlanCommand(): Command {
|
|
|
501
504
|
return command;
|
|
502
505
|
}
|
|
503
506
|
|
|
504
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Compose the success-line summary so the cross-space side effect
|
|
509
|
+
* (extension-space migration packages materialised on disk during
|
|
510
|
+
* this `plan` run) is visible in the top line — not just in the
|
|
511
|
+
* step log above it.
|
|
512
|
+
*
|
|
513
|
+
* Example outputs:
|
|
514
|
+
* - `Planned 3 operation(s)` (app-space-only project)
|
|
515
|
+
* - `Planned 3 operation(s); materialised 1 extension-space migration` (one extension)
|
|
516
|
+
* - `Planned 3 operation(s); materialised 2 extension-space migrations` (two extensions)
|
|
517
|
+
*
|
|
518
|
+
* Locks AC3 at the summary-line level: a reader of the success line
|
|
519
|
+
* can tell that something happened beyond the app space.
|
|
520
|
+
*/
|
|
521
|
+
function buildPlanSummary(plannedOpsCount: number, emittedExtensionDirsCount: number): string {
|
|
522
|
+
const base = `Planned ${plannedOpsCount} operation(s)`;
|
|
523
|
+
if (emittedExtensionDirsCount === 0) return base;
|
|
524
|
+
const noun =
|
|
525
|
+
emittedExtensionDirsCount === 1 ? 'extension-space migration' : 'extension-space migrations';
|
|
526
|
+
return `${base}; materialised ${emittedExtensionDirsCount} ${noun}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
|
|
505
530
|
const lines: string[] = [];
|
|
506
531
|
const useColor = flags.color !== false;
|
|
507
532
|
|
|
@@ -509,10 +534,29 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
509
534
|
const yellow_ = useColor ? (s: string) => `\x1b[33m${s}\x1b[0m` : (s: string) => s;
|
|
510
535
|
const dim_ = useColor ? (s: string) => `\x1b[2m${s}\x1b[0m` : (s: string) => s;
|
|
511
536
|
|
|
537
|
+
// Renders the extension-space materialisation block + canonical apply-step
|
|
538
|
+
// hint shared by the no-op, placeholder, and full-plan branches. The app
|
|
539
|
+
// space short-circuits do not skip it: an extension-only bump emits new
|
|
540
|
+
// `migrations/<spaceId>/<dirName>/` directories on disk that the user
|
|
541
|
+
// still has to apply, so the success line must surface them.
|
|
542
|
+
function appendEmittedExtensions(): void {
|
|
543
|
+
if (result.emittedExtensionDirs.length === 0) return;
|
|
544
|
+
lines.push('');
|
|
545
|
+
lines.push(dim_('Emitted extension migrations:'));
|
|
546
|
+
for (const entry of result.emittedExtensionDirs) {
|
|
547
|
+
lines.push(dim_(` ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`));
|
|
548
|
+
}
|
|
549
|
+
lines.push('');
|
|
550
|
+
lines.push(
|
|
551
|
+
`Next: review the extension migrations above, then run ${green_('prisma-next migration apply')}.`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
512
555
|
if (result.noOp) {
|
|
513
556
|
lines.push(`${green_('✔')} No changes detected`);
|
|
514
557
|
lines.push(dim_(` from: ${result.from}`));
|
|
515
558
|
lines.push(dim_(` to: ${result.to}`));
|
|
559
|
+
appendEmittedExtensions();
|
|
516
560
|
return lines.join('\n');
|
|
517
561
|
}
|
|
518
562
|
|
|
@@ -529,6 +573,7 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
529
573
|
'Open migration.ts and replace each `placeholder(...)` call with your actual query.',
|
|
530
574
|
);
|
|
531
575
|
lines.push(`Then run: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)}`);
|
|
576
|
+
appendEmittedExtensions();
|
|
532
577
|
return lines.join('\n');
|
|
533
578
|
}
|
|
534
579
|
|
|
@@ -541,11 +586,11 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
541
586
|
const op = result.operations[i]!;
|
|
542
587
|
const isLast = i === result.operations.length - 1;
|
|
543
588
|
const treeChar = isLast ? '└' : '├';
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
lines.push(`${dim_(treeChar)}─ ${op.label}
|
|
589
|
+
// operationClass tag is intentionally NOT inlined per spec:
|
|
590
|
+
// a destructive footer warning still surfaces below this list.
|
|
591
|
+
const destructiveMarker =
|
|
592
|
+
op.operationClass === 'destructive' ? ` ${yellow_('(destructive)')}` : '';
|
|
593
|
+
lines.push(`${dim_(treeChar)}─ ${op.label}${destructiveMarker}`);
|
|
549
594
|
}
|
|
550
595
|
|
|
551
596
|
const hasDestructive = result.operations.some((op) => op.operationClass === 'destructive');
|
|
@@ -561,10 +606,22 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
561
606
|
lines.push(dim_(`from: ${result.from}`));
|
|
562
607
|
lines.push(dim_(`to: ${result.to}`));
|
|
563
608
|
if (result.dir) {
|
|
564
|
-
lines.push(dim_(`
|
|
609
|
+
lines.push(dim_(`App space → ${result.dir}`));
|
|
610
|
+
}
|
|
611
|
+
// Per-space block: surface the extension-space directories materialised
|
|
612
|
+
// alongside the app-space migration. Without this block the cross-space
|
|
613
|
+
// side effect is invisible in the success summary (e2e finding F1).
|
|
614
|
+
for (const entry of result.emittedExtensionDirs) {
|
|
615
|
+
lines.push(
|
|
616
|
+
dim_(`Extension space ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`),
|
|
617
|
+
);
|
|
565
618
|
}
|
|
566
619
|
|
|
567
620
|
lines.push('');
|
|
621
|
+
// The "Next:" hint always points at the canonical apply path
|
|
622
|
+
// (`prisma-next migration apply`) regardless of how many spaces
|
|
623
|
+
// were materialised — `db update` is a dev-time convenience, not
|
|
624
|
+
// the canonical replay step.
|
|
568
625
|
lines.push(
|
|
569
626
|
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
|
|
570
627
|
);
|