@prisma-next/cli 0.5.0 → 0.5.1
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/README.md +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-qVH-rEgd.mjs → client-BCnP7cHo.mjs} +9 -119
- package/dist/client-BCnP7cHo.mjs.map +1 -0
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +3 -3
- package/dist/commands/db-schema.mjs +1 -1
- package/dist/commands/db-sign.mjs +1 -1
- package/dist/commands/db-update.mjs +3 -3
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migration-apply.d.mts +1 -1
- package/dist/commands/migration-apply.mjs +2 -2
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +55 -7
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +153 -46
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/{contract-infer-BK9YFGEG.mjs → contract-infer-ByxhPjpW.mjs} +2 -2
- package/dist/{contract-infer-BK9YFGEG.mjs.map → contract-infer-ByxhPjpW.mjs.map} +1 -1
- package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs +160 -0
- package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs.map +1 -0
- package/dist/{db-verify-C0y1PCO2.mjs → db-verify-Czm5T-J4.mjs} +2 -2
- package/dist/{db-verify-C0y1PCO2.mjs.map → db-verify-Czm5T-J4.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.mjs +1 -1
- package/dist/{inspect-live-schema-CWYxGKlb.mjs → inspect-live-schema-DxdBd4Er.mjs} +2 -2
- package/dist/{inspect-live-schema-CWYxGKlb.mjs.map → inspect-live-schema-DxdBd4Er.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-B5dORFEv.mjs → migration-command-scaffold-BdV8JYXV.mjs} +2 -2
- package/dist/{migration-command-scaffold-B5dORFEv.mjs.map → migration-command-scaffold-BdV8JYXV.mjs.map} +1 -1
- package/dist/{migration-plan-C6lVaHsO.mjs → migration-plan-mRu5K81L.mjs} +89 -149
- package/dist/migration-plan-mRu5K81L.mjs.map +1 -0
- package/dist/{migration-status-CZ-D5k7k.mjs → migration-status-By9G5p2H.mjs} +6 -8
- package/dist/{migration-status-CZ-D5k7k.mjs.map → migration-status-By9G5p2H.mjs.map} +1 -1
- package/dist/{migrations-D_UJnpuW.mjs → migrations-CTsyBXCA.mjs} +42 -29
- package/dist/migrations-CTsyBXCA.mjs.map +1 -0
- package/dist/{types-D7x-IFLO.d.mts → types-LItU7E4l.d.mts} +7 -9
- package/dist/{types-D7x-IFLO.d.mts.map → types-LItU7E4l.d.mts.map} +1 -1
- package/package.json +14 -14
- package/src/commands/migration-plan.ts +45 -47
- package/src/commands/migration-show.ts +245 -60
- package/src/commands/migration-status.ts +17 -9
- package/src/control-api/operations/db-apply-aggregate.ts +12 -10
- package/src/control-api/operations/migration-apply.ts +7 -1
- package/src/control-api/types.ts +6 -8
- package/src/utils/contract-space-aggregate-loader.ts +7 -34
- package/src/utils/contract-space-seed-phase.ts +201 -0
- package/src/utils/extension-pack-inputs.ts +47 -55
- package/src/utils/formatters/migrations.ts +80 -38
- package/dist/client-qVH-rEgd.mjs.map +0 -1
- package/dist/extension-pack-inputs-C7xgE-vv.mjs +0 -74
- package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +0 -1
- package/dist/migration-plan-C6lVaHsO.mjs.map +0 -1
- package/dist/migrations-D_UJnpuW.mjs.map +0 -1
- package/src/utils/contract-space-extension-migrations-pass.ts +0 -120
- package/src/utils/contract-space-migrate-pass.ts +0 -156
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
+
import {
|
|
4
|
+
createControlStack,
|
|
5
|
+
type MigrationPlanOperation,
|
|
6
|
+
type OperationPreview,
|
|
4
7
|
} from '@prisma-next/framework-components/control';
|
|
5
8
|
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
6
9
|
import { readMigrationPackage, readMigrationsDir } from '@prisma-next/migration-tools/io';
|
|
@@ -9,23 +12,29 @@ import {
|
|
|
9
12
|
reconstructGraph,
|
|
10
13
|
} from '@prisma-next/migration-tools/migration-graph';
|
|
11
14
|
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
12
|
-
import {
|
|
15
|
+
import { spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
|
|
16
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
13
17
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
14
18
|
import { Command } from 'commander';
|
|
15
|
-
import { relative, resolve } from 'pathe';
|
|
19
|
+
import { isAbsolute, relative, resolve } from 'pathe';
|
|
16
20
|
import { loadConfig } from '../config-loader';
|
|
17
21
|
import { createControlClient } from '../control-api/client';
|
|
18
22
|
import {
|
|
19
23
|
type CliStructuredError,
|
|
24
|
+
errorContractValidationFailed,
|
|
25
|
+
errorFileNotFound,
|
|
20
26
|
errorRuntime,
|
|
21
27
|
errorUnexpected,
|
|
22
28
|
mapMigrationToolsError,
|
|
23
29
|
} from '../utils/cli-errors';
|
|
24
30
|
import {
|
|
25
31
|
addGlobalOptions,
|
|
32
|
+
resolveContractPath,
|
|
33
|
+
resolveMigrationPaths,
|
|
26
34
|
setCommandDescriptions,
|
|
27
35
|
setCommandExamples,
|
|
28
36
|
} from '../utils/command-helpers';
|
|
37
|
+
import { buildContractSpaceAggregate } from '../utils/contract-space-aggregate-loader';
|
|
29
38
|
import { formatMigrationShowOutput } from '../utils/formatters/migrations';
|
|
30
39
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
31
40
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
@@ -37,8 +46,12 @@ interface MigrationShowOptions extends CommonCommandOptions {
|
|
|
37
46
|
readonly config?: string;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Details of one space's latest (or targeted) migration package.
|
|
51
|
+
*/
|
|
52
|
+
export interface MigrationShowSpacePresent {
|
|
53
|
+
readonly kind: 'present';
|
|
54
|
+
readonly spaceId: string;
|
|
42
55
|
readonly dirName: string;
|
|
43
56
|
readonly dirPath: string;
|
|
44
57
|
readonly from: string | null;
|
|
@@ -51,19 +64,78 @@ export interface MigrationShowResult {
|
|
|
51
64
|
readonly operationClass: string;
|
|
52
65
|
}[];
|
|
53
66
|
/**
|
|
54
|
-
* Family-agnostic textual preview of the migration's operations.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* `OperationPreviewCapable` capability.
|
|
67
|
+
* Family-agnostic textual preview of the migration's operations. Always
|
|
68
|
+
* defined; statements is empty for a no-op migration or a family that does
|
|
69
|
+
* not implement the `OperationPreviewCapable` capability.
|
|
58
70
|
*/
|
|
59
71
|
readonly preview: OperationPreview;
|
|
60
72
|
readonly summary: string;
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Placeholder for a loaded contract space that has no on-disk migration
|
|
77
|
+
* package — the extension descriptor declared the space but no migrations
|
|
78
|
+
* directory has been materialised for it yet. Surfaces the space in the
|
|
79
|
+
* response so JSON consumers see every loaded extension instead of having
|
|
80
|
+
* silently-skipped entries.
|
|
81
|
+
*/
|
|
82
|
+
export interface MigrationShowSpaceMissing {
|
|
83
|
+
readonly kind: 'missing';
|
|
84
|
+
readonly spaceId: string;
|
|
85
|
+
readonly summary: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type MigrationShowSpaceResult = MigrationShowSpacePresent | MigrationShowSpaceMissing;
|
|
89
|
+
|
|
90
|
+
export interface MigrationShowResult {
|
|
91
|
+
readonly ok: true;
|
|
92
|
+
/**
|
|
93
|
+
* Per-space results, ordered: app first, then extensions alphabetically
|
|
94
|
+
* (matching the aggregate's canonical ordering).
|
|
95
|
+
*/
|
|
96
|
+
readonly spaces: readonly MigrationShowSpaceResult[];
|
|
97
|
+
}
|
|
98
|
+
|
|
63
99
|
function looksLikePath(target: string): boolean {
|
|
64
100
|
return target.includes('/') || target.includes('\\');
|
|
65
101
|
}
|
|
66
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Validate that a path-like `migration show` target resolves inside the app
|
|
105
|
+
* migrations directory. The returned result is always emitted under
|
|
106
|
+
* `aggregate.app.spaceId`, so accepting an extension-space (or otherwise
|
|
107
|
+
* external) path here would silently mislabel the result. Returns the
|
|
108
|
+
* resolved absolute path on success.
|
|
109
|
+
*
|
|
110
|
+
* `pathe.relative` can return an absolute path when the target cannot be
|
|
111
|
+
* expressed relative to the base (e.g. on Windows when `target` is on a
|
|
112
|
+
* different drive than `appMigrationsDir`). That case does not start with
|
|
113
|
+
* `..`, so the absolute-check below is required to reject cross-drive
|
|
114
|
+
* targets rather than mislabeling them as app-space.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveAppTargetPath(
|
|
117
|
+
target: string,
|
|
118
|
+
appMigrationsDir: string,
|
|
119
|
+
appMigrationsRelative: string,
|
|
120
|
+
): Result<string, CliStructuredError> {
|
|
121
|
+
const targetPath = resolve(target);
|
|
122
|
+
const relativeToApp = relative(appMigrationsDir, targetPath);
|
|
123
|
+
const isOutsideAppDir =
|
|
124
|
+
relativeToApp === '' ||
|
|
125
|
+
relativeToApp === '.' ||
|
|
126
|
+
relativeToApp.startsWith('..') ||
|
|
127
|
+
isAbsolute(relativeToApp);
|
|
128
|
+
if (isOutsideAppDir) {
|
|
129
|
+
return notOk(
|
|
130
|
+
errorRuntime('Target must point to an app-space migration', {
|
|
131
|
+
why: `Expected a path under ${appMigrationsRelative}, got ${target}`,
|
|
132
|
+
fix: 'Pass an app-space migration directory or use a hash prefix.',
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return ok(targetPath);
|
|
137
|
+
}
|
|
138
|
+
|
|
67
139
|
export function resolveByHashPrefix(
|
|
68
140
|
packages: readonly OnDiskMigrationPackage[],
|
|
69
141
|
prefix: string,
|
|
@@ -93,6 +165,71 @@ export function resolveByHashPrefix(
|
|
|
93
165
|
);
|
|
94
166
|
}
|
|
95
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the latest migration from a space directory.
|
|
170
|
+
*
|
|
171
|
+
* Returns `ok(null)` only when the directory is empty or absent (ENOENT is
|
|
172
|
+
* absorbed by `readMigrationsDir`). If `readMigrationsDir` returned packages
|
|
173
|
+
* but `findLatestMigration` cannot pick a leaf, the on-disk history is
|
|
174
|
+
* corrupt — return a runtime error rather than collapsing it to a `missing`
|
|
175
|
+
* placeholder, which would hide the corruption from the caller.
|
|
176
|
+
*/
|
|
177
|
+
export async function resolveLatestFromDir(
|
|
178
|
+
spaceDir: string,
|
|
179
|
+
): Promise<Result<OnDiskMigrationPackage | null, CliStructuredError>> {
|
|
180
|
+
try {
|
|
181
|
+
const allPackages = await readMigrationsDir(spaceDir);
|
|
182
|
+
if (allPackages.length === 0) return ok(null);
|
|
183
|
+
const graph = reconstructGraph(allPackages);
|
|
184
|
+
const latestMigration = findLatestMigration(graph);
|
|
185
|
+
if (!latestMigration) {
|
|
186
|
+
return notOk(
|
|
187
|
+
errorRuntime('Could not resolve latest migration', {
|
|
188
|
+
why: `No latest migration found in ${relative(process.cwd(), spaceDir)}`,
|
|
189
|
+
fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const leafPkg = allPackages.find(
|
|
194
|
+
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
195
|
+
);
|
|
196
|
+
return ok(leafPkg ?? null);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
199
|
+
return notOk(
|
|
200
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
201
|
+
why: `Failed to read migrations: ${error instanceof Error ? error.message : String(error)}`,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function pkgToSpaceResult(
|
|
208
|
+
spaceId: string,
|
|
209
|
+
pkg: OnDiskMigrationPackage,
|
|
210
|
+
client: ReturnType<typeof createControlClient>,
|
|
211
|
+
): MigrationShowSpacePresent {
|
|
212
|
+
const ops = pkg.ops as readonly MigrationPlanOperation[];
|
|
213
|
+
const preview: OperationPreview = client.toOperationPreview(ops) ?? { statements: [] };
|
|
214
|
+
return {
|
|
215
|
+
kind: 'present',
|
|
216
|
+
spaceId,
|
|
217
|
+
dirName: pkg.dirName,
|
|
218
|
+
dirPath: relative(process.cwd(), pkg.dirPath),
|
|
219
|
+
from: pkg.metadata.from,
|
|
220
|
+
to: pkg.metadata.to,
|
|
221
|
+
migrationHash: pkg.metadata.migrationHash,
|
|
222
|
+
createdAt: pkg.metadata.createdAt,
|
|
223
|
+
operations: ops.map((op) => ({
|
|
224
|
+
id: op.id,
|
|
225
|
+
label: op.label,
|
|
226
|
+
operationClass: op.operationClass,
|
|
227
|
+
})),
|
|
228
|
+
preview,
|
|
229
|
+
summary: `${ops.length} operation(s)`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
96
233
|
async function executeMigrationShowCommand(
|
|
97
234
|
target: string | undefined,
|
|
98
235
|
options: MigrationShowOptions,
|
|
@@ -100,20 +237,16 @@ async function executeMigrationShowCommand(
|
|
|
100
237
|
ui: TerminalUI,
|
|
101
238
|
): Promise<Result<MigrationShowResult, CliStructuredError>> {
|
|
102
239
|
const config = await loadConfig(options.config);
|
|
103
|
-
const configPath =
|
|
104
|
-
|
|
105
|
-
: 'prisma-next.config.ts';
|
|
240
|
+
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
|
|
241
|
+
resolveMigrationPaths(options.config, config);
|
|
106
242
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
config.migrations?.dir ?? 'migrations',
|
|
110
|
-
);
|
|
111
|
-
const appMigrationsDir = spaceMigrationDirectory(migrationsDirRoot, APP_SPACE_ID);
|
|
112
|
-
const appMigrationsRelative = relative(process.cwd(), appMigrationsDir);
|
|
243
|
+
const contractPathAbsolute = resolveContractPath(config);
|
|
244
|
+
const contractPath = relative(process.cwd(), contractPathAbsolute);
|
|
113
245
|
|
|
114
246
|
if (!flags.json && !flags.quiet) {
|
|
115
247
|
const details: Array<{ label: string; value: string }> = [
|
|
116
248
|
{ label: 'config', value: configPath },
|
|
249
|
+
{ label: 'contract', value: contractPath },
|
|
117
250
|
{ label: 'migrations', value: appMigrationsRelative },
|
|
118
251
|
];
|
|
119
252
|
if (target) {
|
|
@@ -128,11 +261,73 @@ async function executeMigrationShowCommand(
|
|
|
128
261
|
ui.stderr(header);
|
|
129
262
|
}
|
|
130
263
|
|
|
131
|
-
|
|
264
|
+
// Load the app contract so the aggregate loader can validate it.
|
|
265
|
+
let contractJsonContent: string;
|
|
266
|
+
try {
|
|
267
|
+
contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
270
|
+
return notOk(
|
|
271
|
+
errorFileNotFound(contractPathAbsolute, {
|
|
272
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
273
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}`,
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return notOk(
|
|
278
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
279
|
+
why: 'Failed to read contract file',
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
132
283
|
|
|
284
|
+
let appContract: Contract;
|
|
133
285
|
try {
|
|
286
|
+
appContract = JSON.parse(contractJsonContent) as Contract;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return notOk(
|
|
289
|
+
errorContractValidationFailed(
|
|
290
|
+
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
291
|
+
{ where: { path: contractPathAbsolute } },
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Build the aggregate against current disk state to enumerate all spaces.
|
|
297
|
+
const stack = createControlStack(config);
|
|
298
|
+
const familyInstance = config.family.create(stack);
|
|
299
|
+
const aggregateResult = await buildContractSpaceAggregate({
|
|
300
|
+
targetId: config.target.targetId,
|
|
301
|
+
migrationsDir,
|
|
302
|
+
appContract,
|
|
303
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
304
|
+
validateContract: (json: unknown) => familyInstance.validateContract(json),
|
|
305
|
+
});
|
|
306
|
+
if (!aggregateResult.ok) {
|
|
307
|
+
return notOk(aggregateResult.failure);
|
|
308
|
+
}
|
|
309
|
+
const aggregate = aggregateResult.value;
|
|
310
|
+
|
|
311
|
+
// `migration show` is an offline command; the control client is constructed
|
|
312
|
+
// purely to dispatch the family-specific `toOperationPreview` capability and
|
|
313
|
+
// is not connected to a database.
|
|
314
|
+
const client = createControlClient({
|
|
315
|
+
family: config.family,
|
|
316
|
+
target: config.target,
|
|
317
|
+
adapter: config.adapter,
|
|
318
|
+
...ifDefined('driver', config.driver),
|
|
319
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const spaces: MigrationShowSpaceResult[] = [];
|
|
323
|
+
|
|
324
|
+
// App space: honour the `target` argument (path or hash prefix) when provided.
|
|
325
|
+
try {
|
|
326
|
+
let appPkg: OnDiskMigrationPackage;
|
|
134
327
|
if (target && looksLikePath(target)) {
|
|
135
|
-
|
|
328
|
+
const resolved = resolveAppTargetPath(target, appMigrationsDir, appMigrationsRelative);
|
|
329
|
+
if (!resolved.ok) return resolved;
|
|
330
|
+
appPkg = await readMigrationPackage(resolved.value);
|
|
136
331
|
} else {
|
|
137
332
|
const allPackages = await readMigrationsDir(appMigrationsDir);
|
|
138
333
|
if (allPackages.length === 0) {
|
|
@@ -143,11 +338,10 @@ async function executeMigrationShowCommand(
|
|
|
143
338
|
}),
|
|
144
339
|
);
|
|
145
340
|
}
|
|
146
|
-
|
|
147
341
|
if (target) {
|
|
148
342
|
const resolved = resolveByHashPrefix(allPackages, target);
|
|
149
343
|
if (!resolved.ok) return resolved;
|
|
150
|
-
|
|
344
|
+
appPkg = resolved.value;
|
|
151
345
|
} else {
|
|
152
346
|
const graph = reconstructGraph(allPackages);
|
|
153
347
|
const latestMigration = findLatestMigration(graph);
|
|
@@ -170,51 +364,42 @@ async function executeMigrationShowCommand(
|
|
|
170
364
|
}),
|
|
171
365
|
);
|
|
172
366
|
}
|
|
173
|
-
|
|
367
|
+
appPkg = leafPkg;
|
|
174
368
|
}
|
|
175
369
|
}
|
|
370
|
+
spaces.push(pkgToSpaceResult(aggregate.app.spaceId, appPkg, client));
|
|
176
371
|
} catch (error) {
|
|
177
372
|
if (MigrationToolsError.is(error)) {
|
|
178
373
|
return notOk(mapMigrationToolsError(error));
|
|
179
374
|
}
|
|
180
375
|
return notOk(
|
|
181
376
|
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
182
|
-
why: `Failed to read migration: ${error instanceof Error ? error.message : String(error)}`,
|
|
377
|
+
why: `Failed to read app-space migration: ${error instanceof Error ? error.message : String(error)}`,
|
|
183
378
|
}),
|
|
184
379
|
);
|
|
185
380
|
}
|
|
186
381
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
382
|
+
// Extension spaces: always emit one entry per loaded extension so the
|
|
383
|
+
// response enumerates every space the aggregate knows about. Spaces
|
|
384
|
+
// with no on-disk migration package yet (e.g. an extension was declared
|
|
385
|
+
// but never `migrate`d) become `kind: 'missing'` placeholders instead
|
|
386
|
+
// of being silently skipped.
|
|
387
|
+
for (const ext of aggregate.extensions) {
|
|
388
|
+
const extSpaceDir = spaceMigrationDirectory(migrationsDir, ext.spaceId);
|
|
389
|
+
const extPkgResult = await resolveLatestFromDir(extSpaceDir);
|
|
390
|
+
if (!extPkgResult.ok) return extPkgResult;
|
|
391
|
+
if (extPkgResult.value !== null) {
|
|
392
|
+
spaces.push(pkgToSpaceResult(ext.spaceId, extPkgResult.value, client));
|
|
393
|
+
} else {
|
|
394
|
+
spaces.push({
|
|
395
|
+
kind: 'missing',
|
|
396
|
+
spaceId: ext.spaceId,
|
|
397
|
+
summary: 'No on-disk migration package for this space',
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
200
401
|
|
|
201
|
-
|
|
202
|
-
ok: true,
|
|
203
|
-
dirName: pkg.dirName,
|
|
204
|
-
dirPath: relative(process.cwd(), pkg.dirPath),
|
|
205
|
-
from: pkg.metadata.from,
|
|
206
|
-
to: pkg.metadata.to,
|
|
207
|
-
migrationHash: pkg.metadata.migrationHash,
|
|
208
|
-
createdAt: pkg.metadata.createdAt,
|
|
209
|
-
operations: ops.map((op) => ({
|
|
210
|
-
id: op.id,
|
|
211
|
-
label: op.label,
|
|
212
|
-
operationClass: op.operationClass,
|
|
213
|
-
})),
|
|
214
|
-
preview,
|
|
215
|
-
summary: `${ops.length} operation(s)`,
|
|
216
|
-
};
|
|
217
|
-
return ok(result);
|
|
402
|
+
return ok({ ok: true, spaces });
|
|
218
403
|
}
|
|
219
404
|
|
|
220
405
|
export function createMigrationShowCommand(): Command {
|
|
@@ -222,16 +407,16 @@ export function createMigrationShowCommand(): Command {
|
|
|
222
407
|
setCommandDescriptions(
|
|
223
408
|
command,
|
|
224
409
|
'Display migration package contents',
|
|
225
|
-
'Shows the operations, statement preview, and metadata for
|
|
226
|
-
'Accepts a directory path
|
|
227
|
-
'latest
|
|
410
|
+
'Shows the operations, statement preview, and metadata for every loaded contract\n' +
|
|
411
|
+
'space (app + extensions). Accepts a directory path or hash prefix to target a\n' +
|
|
412
|
+
'specific app-space migration; defaults to the latest per space.',
|
|
228
413
|
);
|
|
229
414
|
setCommandExamples(command, [
|
|
230
415
|
'prisma-next migration show',
|
|
231
416
|
'prisma-next migration show sha256:a1b2c3',
|
|
232
417
|
]);
|
|
233
418
|
addGlobalOptions(command)
|
|
234
|
-
.argument('[target]', '
|
|
419
|
+
.argument('[target]', 'App-space migration path or migrationHash prefix (defaults to latest)')
|
|
235
420
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
236
421
|
.action(async (target: string | undefined, options: MigrationShowOptions) => {
|
|
237
422
|
const flags = parseGlobalFlags(options);
|
|
@@ -513,7 +513,10 @@ export async function loadAggregateStatusSpaces(args: {
|
|
|
513
513
|
let pendingCount = 0;
|
|
514
514
|
let status: MigrationStatusSpaceEntry['status'];
|
|
515
515
|
if (walked.kind === 'ok') {
|
|
516
|
-
|
|
516
|
+
// Count pending *migrations* (graph edges), not operations: a
|
|
517
|
+
// single authored migration that lowers to N ops or zero ops
|
|
518
|
+
// both count as exactly one pending unit of work for the user.
|
|
519
|
+
pendingCount = walked.result.migrationEdges?.length ?? 0;
|
|
517
520
|
if (liveMarker === null) {
|
|
518
521
|
status = pendingCount === 0 ? 'no-marker' : 'pending';
|
|
519
522
|
} else {
|
|
@@ -723,15 +726,20 @@ async function executeMigrationStatusCommand(
|
|
|
723
726
|
// surface per-space marker state. `readAllMarkers` mirrors what
|
|
724
727
|
// `db init` / `db update` already use to drive the multi-space
|
|
725
728
|
// planner; here it powers the aggregate status output.
|
|
726
|
-
|
|
729
|
+
//
|
|
730
|
+
// Probe for the method first so we only swallow the
|
|
731
|
+
// unsupported-method case: older family instances may not
|
|
732
|
+
// implement `readAllMarkers` (per-space enumeration then falls
|
|
733
|
+
// back to "marker unknown"). Real query / runtime errors from
|
|
734
|
+
// an instance that *does* expose the method must propagate up
|
|
735
|
+
// — otherwise transient DB failures would silently degrade
|
|
736
|
+
// status to "markers unknown".
|
|
737
|
+
if (typeof client.readAllMarkers === 'function') {
|
|
727
738
|
allMarkers = await client.readAllMarkers();
|
|
728
|
-
}
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
// `allMarkers` as `null` signals "unknown" to the aggregate
|
|
733
|
-
// loader (an empty `Map` would instead mean "every space has
|
|
734
|
-
// no marker", which is a different condition).
|
|
739
|
+
} else {
|
|
740
|
+
// Leaving `allMarkers` as `null` signals "unknown" to the
|
|
741
|
+
// aggregate loader (an empty `Map` would instead mean "every
|
|
742
|
+
// space has no marker", which is a different condition).
|
|
735
743
|
allMarkers = null;
|
|
736
744
|
}
|
|
737
745
|
} catch {
|
|
@@ -127,16 +127,18 @@ export async function executeAggregateApply<TFamilyId extends string, TTargetId
|
|
|
127
127
|
// 2. Read live DB state (markers + schema).
|
|
128
128
|
const markerRows = await familyInstance.readAllMarkers({ driver });
|
|
129
129
|
|
|
130
|
-
// 2a. Orphan-marker pre-flight: refuse to apply when a marker row
|
|
131
|
-
// exists for a space that is not declared in the aggregate.
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
// 2a. Orphan-marker pre-flight: refuse to *apply* when a marker row
|
|
131
|
+
// exists for a space that is not declared in the aggregate. Plan mode
|
|
132
|
+
// (`db init/update --dry-run`) must still be able to introspect the
|
|
133
|
+
// aggregate plan in this state — a retired extension whose marker
|
|
134
|
+
// happens to linger should not block the user from inspecting what a
|
|
135
|
+
// run would do. Apply mode tells the user to clean up the orphan
|
|
136
|
+
// before silently advancing the app's marker.
|
|
137
|
+
if (mode === 'apply') {
|
|
138
|
+
const orphanMarkerError = detectOrphanMarkers(aggregate, markerRows);
|
|
139
|
+
if (orphanMarkerError !== null) {
|
|
140
|
+
throw orphanMarkerError;
|
|
141
|
+
}
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
onProgress?.({
|
|
@@ -216,7 +216,13 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
|
|
|
216
216
|
// (the error rendering pipeline maps it to meta.code +
|
|
217
217
|
// meta.required + meta.missing + meta.structuralPath that the
|
|
218
218
|
// cli-journeys invariant suite asserts on).
|
|
219
|
-
|
|
219
|
+
// Greenfield runs (no marker yet) use the canonical empty-hash
|
|
220
|
+
// sentinel so the structural path stays attached to the
|
|
221
|
+
// `MIGRATION.NO_INVARIANT_PATH` error envelope. Using an empty
|
|
222
|
+
// string here would leave the structural lookup with a hash that
|
|
223
|
+
// is never a graph node, producing an empty `structuralPath` and
|
|
224
|
+
// a less actionable diagnostic.
|
|
225
|
+
const fromHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
|
|
220
226
|
const structural = findPathWithDecision(targetMember.migrations.graph, fromHash, targetHash, {
|
|
221
227
|
required: new Set<string>(),
|
|
222
228
|
});
|
package/src/control-api/types.ts
CHANGED
|
@@ -316,10 +316,10 @@ export interface EmitOptions {
|
|
|
316
316
|
* then app — together with the operations attributed to each space and,
|
|
317
317
|
* when the run was applied, the resulting per-space marker hash.
|
|
318
318
|
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
319
|
+
* Every space involved in a run is observable in the success summary,
|
|
320
|
+
* including its post-apply marker — the per-space marker is visible
|
|
321
|
+
* to the user instead of being collapsed into a single ambiguous
|
|
322
|
+
* top-level hash.
|
|
323
323
|
*/
|
|
324
324
|
export interface AggregatePerSpaceExecutionEntry {
|
|
325
325
|
readonly spaceId: string;
|
|
@@ -536,8 +536,6 @@ export type EmitResult = Result<EmitSuccess, EmitFailure>;
|
|
|
536
536
|
* through the shared `applyAggregate` primitive. The CLI command
|
|
537
537
|
* just resolves the descriptor surface (config, refs, contract
|
|
538
538
|
* envelope, app-space migration packages) and hands the inputs in.
|
|
539
|
-
*
|
|
540
|
-
* Sub-spec § `migration apply` semantics + § Required changes 1.
|
|
541
539
|
*/
|
|
542
540
|
export interface MigrationApplyOptions {
|
|
543
541
|
/** Already-validated app contract (the canonical "where we are heading" hash). */
|
|
@@ -628,8 +626,8 @@ export interface MigrationApplyAppliedEntry {
|
|
|
628
626
|
* Successful migrationApply result. Carries both the legacy
|
|
629
627
|
* single-space fields (`markerHash` is the **app member's** post-apply
|
|
630
628
|
* marker, surfaced for back-compat with single-space callers) and the
|
|
631
|
-
* per-space breakdown (`perSpace` — markers / operations
|
|
632
|
-
* order
|
|
629
|
+
* per-space breakdown (`perSpace` — markers / operations in canonical
|
|
630
|
+
* schedule order).
|
|
633
631
|
*/
|
|
634
632
|
/**
|
|
635
633
|
* Path-decision summary for the **app member** post-apply. Surfaced
|
|
@@ -10,14 +10,14 @@ import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggrega
|
|
|
10
10
|
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
11
11
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
12
12
|
import { CliStructuredError } from './cli-errors';
|
|
13
|
-
import {
|
|
13
|
+
import { toDeclaredExtensionsFromRaw } from './extension-pack-inputs';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Render a {@link LoadAggregateError} into a CLI structured-error
|
|
17
17
|
* envelope. Preserves error codes `5001` (layout) and `5002` (marker /
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* disjointness / etc.) so existing integration tests and downstream
|
|
19
|
+
* tooling continue to assert on the same `meta.violations[]` shape
|
|
20
|
+
* they did under the old precheck/marker-check helpers.
|
|
21
21
|
*/
|
|
22
22
|
export function mapLoadAggregateError(error: LoadAggregateError): CliStructuredError {
|
|
23
23
|
if (error.kind === 'layoutViolation') {
|
|
@@ -39,24 +39,6 @@ export function mapLoadAggregateError(error: LoadAggregateError): CliStructuredE
|
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
if (error.kind === 'driftViolation') {
|
|
43
|
-
return new CliStructuredError('5002', `Contract-space drift detected for "${error.spaceId}"`, {
|
|
44
|
-
domain: 'MIG',
|
|
45
|
-
why: `The on-disk contract for space "${error.spaceId}" (hash ${error.priorHeadHash}) does not match the live extension descriptor (hash ${error.liveHash}).`,
|
|
46
|
-
fix: 'Run `prisma-next migrate` to refresh the on-disk artefacts to match the live descriptor.',
|
|
47
|
-
docsUrl: 'https://pris.ly/contract-spaces',
|
|
48
|
-
meta: {
|
|
49
|
-
violations: [
|
|
50
|
-
{
|
|
51
|
-
kind: 'drift',
|
|
52
|
-
spaceId: error.spaceId,
|
|
53
|
-
priorHeadHash: error.priorHeadHash,
|
|
54
|
-
liveHash: error.liveHash,
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
42
|
if (error.kind === 'disjointnessViolation') {
|
|
61
43
|
return new CliStructuredError(
|
|
62
44
|
'5002',
|
|
@@ -174,25 +156,16 @@ export async function buildContractSpaceAggregate<
|
|
|
174
156
|
>(
|
|
175
157
|
inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
|
|
176
158
|
): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
|
|
177
|
-
const
|
|
178
|
-
|
|
159
|
+
const declaredExtensions = toDeclaredExtensionsFromRaw(
|
|
160
|
+
inputs.extensionPacks as ReadonlyArray<unknown>,
|
|
179
161
|
);
|
|
180
162
|
|
|
181
163
|
const loadInput: LoadAggregateInput = {
|
|
182
164
|
targetId: inputs.targetId,
|
|
183
165
|
migrationsDir: inputs.migrationsDir,
|
|
184
166
|
appContract: inputs.appContract,
|
|
185
|
-
declaredExtensions
|
|
167
|
+
declaredExtensions,
|
|
186
168
|
validateContract: inputs.validateContract,
|
|
187
|
-
hashContract: (contractJson: unknown) => {
|
|
188
|
-
const precomputed = hashByContractJson.get(contractJson);
|
|
189
|
-
if (precomputed === undefined) {
|
|
190
|
-
throw new Error(
|
|
191
|
-
'CLI aggregate loader: encountered an extension contract without a pre-computed descriptor hash. This is a wiring bug.',
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
return precomputed;
|
|
195
|
-
},
|
|
196
169
|
appMigrationPackages: inputs.appMigrationPackages ?? [],
|
|
197
170
|
};
|
|
198
171
|
|