@prisma-next/cli 0.6.0-dev.1 → 0.6.0-dev.10
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 +5 -5
- 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/{init-DETSgw3h.mjs → init-BRKnARU6.mjs} +125 -25
- package/dist/init-BRKnARU6.mjs.map +1 -0
- 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/init/detect-package-manager.ts +28 -4
- package/src/commands/init/errors.ts +0 -16
- package/src/commands/init/exit-codes.ts +1 -1
- package/src/commands/init/hygiene-package-scripts.ts +77 -0
- package/src/commands/init/init.ts +81 -13
- 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/init-DETSgw3h.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
|
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
3
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import {
|
|
5
|
-
APP_SPACE_ID,
|
|
6
5
|
createControlStack,
|
|
7
6
|
hasOperationPreview,
|
|
8
7
|
type MigrationPlanOperation,
|
|
@@ -43,16 +42,9 @@ import {
|
|
|
43
42
|
setCommandDescriptions,
|
|
44
43
|
setCommandExamples,
|
|
45
44
|
} from '../utils/command-helpers';
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
runContractSpaceMigratePass,
|
|
50
|
-
} from '../utils/contract-space-migrate-pass';
|
|
51
|
-
import {
|
|
52
|
-
toExtensionInputs,
|
|
53
|
-
toExtensionMigrationsInputs,
|
|
54
|
-
toMigratePassInputs,
|
|
55
|
-
} from '../utils/extension-pack-inputs';
|
|
45
|
+
import { buildContractSpaceAggregate } from '../utils/contract-space-aggregate-loader';
|
|
46
|
+
import { runContractSpaceSeedPhase } from '../utils/contract-space-seed-phase';
|
|
47
|
+
import { toExtensionInputs } from '../utils/extension-pack-inputs';
|
|
56
48
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
57
49
|
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
58
50
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
@@ -241,41 +233,30 @@ async function executeMigrationPlanCommand(
|
|
|
241
233
|
);
|
|
242
234
|
}
|
|
243
235
|
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// and the user is notified that the bump is being captured.
|
|
250
|
-
// Single descriptor-import boundary: every consumer of `extensionPacks`
|
|
251
|
-
// goes through `toExtensionInputs` + a per-consumer adapter. AC11.
|
|
236
|
+
// Phase 1 — seed: unconditionally re-emit per-space pinned artefacts
|
|
237
|
+
// (contract.json / contract.d.ts / refs/head.json) and materialise any
|
|
238
|
+
// descriptor-shipped migration packages not yet on disk. Runs before
|
|
239
|
+
// the no-op check so that an extension bump alone (with no structural
|
|
240
|
+
// app-space change) still re-pins extension artefacts on disk.
|
|
252
241
|
const canonicalExtensionInputs = toExtensionInputs(config.extensionPacks ?? []);
|
|
253
|
-
const
|
|
242
|
+
const seedResult = await runContractSpaceSeedPhase({
|
|
254
243
|
migrationsDir,
|
|
255
|
-
extensionPacks:
|
|
244
|
+
extensionPacks: canonicalExtensionInputs,
|
|
256
245
|
});
|
|
257
246
|
if (!flags.json && !flags.quiet) {
|
|
258
|
-
for (const
|
|
259
|
-
if (
|
|
260
|
-
|
|
247
|
+
for (const record of seedResult.seeded) {
|
|
248
|
+
if (record.action === 'updated') {
|
|
249
|
+
const pkgSuffix =
|
|
250
|
+
record.newMigrationDirs.length > 0
|
|
251
|
+
? `; ${record.newMigrationDirs.length} new migration package(s) materialised`
|
|
252
|
+
: '';
|
|
253
|
+
ui.step(`Updated ${record.spaceId} to ${record.newHash}${pkgSuffix}`);
|
|
261
254
|
}
|
|
262
255
|
}
|
|
263
256
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// Idempotent (existing dirs are left untouched).
|
|
268
|
-
// Uses `planAllSpaces` for deterministic ordering + duplicate-spaceId
|
|
269
|
-
// detection.
|
|
270
|
-
const extensionMigrationsResult = await runContractSpaceExtensionMigrationsPass({
|
|
271
|
-
migrationsDir,
|
|
272
|
-
extensionPacks: toExtensionMigrationsInputs(canonicalExtensionInputs),
|
|
273
|
-
});
|
|
274
|
-
if (!flags.json && !flags.quiet) {
|
|
275
|
-
for (const entry of extensionMigrationsResult.emitted) {
|
|
276
|
-
ui.step(`Emitted ${entry.spaceId}/${entry.dirName}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
257
|
+
const emittedExtensionDirs = seedResult.seeded.flatMap((r) =>
|
|
258
|
+
r.newMigrationDirs.map((dirName) => ({ spaceId: r.spaceId, dirName })),
|
|
259
|
+
);
|
|
279
260
|
|
|
280
261
|
// Check for no-op (same hash means no changes)
|
|
281
262
|
if (fromHash === toStorageHash) {
|
|
@@ -285,7 +266,7 @@ async function executeMigrationPlanCommand(
|
|
|
285
266
|
from: fromHash,
|
|
286
267
|
to: toStorageHash,
|
|
287
268
|
operations: [],
|
|
288
|
-
emittedExtensionDirs
|
|
269
|
+
emittedExtensionDirs,
|
|
289
270
|
summary: 'No changes detected between contracts',
|
|
290
271
|
timings: { total: Date.now() - startTime },
|
|
291
272
|
};
|
|
@@ -301,6 +282,25 @@ async function executeMigrationPlanCommand(
|
|
|
301
282
|
}),
|
|
302
283
|
);
|
|
303
284
|
}
|
|
285
|
+
|
|
286
|
+
// Phase 2 — load: build the aggregate against the now-consistent disk
|
|
287
|
+
// state that phase 1 just seeded. The seed phase guarantees every
|
|
288
|
+
// declared extension has its head ref pinned, so the loader's
|
|
289
|
+
// declaredButUnmigrated precheck always passes here.
|
|
290
|
+
const stack = createControlStack(config);
|
|
291
|
+
const familyInstance = config.family.create(stack);
|
|
292
|
+
const aggregateResult = await buildContractSpaceAggregate({
|
|
293
|
+
targetId: config.target.targetId,
|
|
294
|
+
migrationsDir,
|
|
295
|
+
appContract: toContractJson,
|
|
296
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
297
|
+
validateContract: (json: unknown) => familyInstance.validateContract(json),
|
|
298
|
+
});
|
|
299
|
+
if (!aggregateResult.ok) {
|
|
300
|
+
return notOk(aggregateResult.failure);
|
|
301
|
+
}
|
|
302
|
+
const aggregate = aggregateResult.value;
|
|
303
|
+
|
|
304
304
|
const frameworkComponents = assertFrameworkComponentsCompatible(
|
|
305
305
|
config.family.familyId,
|
|
306
306
|
config.target.targetId,
|
|
@@ -328,17 +328,15 @@ async function executeMigrationPlanCommand(
|
|
|
328
328
|
};
|
|
329
329
|
|
|
330
330
|
try {
|
|
331
|
-
const stack = createControlStack(config);
|
|
332
|
-
const familyInstance = config.family.create(stack);
|
|
333
331
|
const planner = migrations.createPlanner(familyInstance);
|
|
334
332
|
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
335
333
|
const plannerResult = planner.plan({
|
|
336
|
-
contract:
|
|
334
|
+
contract: aggregate.app.contract,
|
|
337
335
|
schema: fromSchema,
|
|
338
336
|
policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
|
|
339
337
|
fromContract,
|
|
340
338
|
frameworkComponents,
|
|
341
|
-
spaceId:
|
|
339
|
+
spaceId: aggregate.app.spaceId,
|
|
342
340
|
});
|
|
343
341
|
if (plannerResult.kind === 'failure') {
|
|
344
342
|
return notOk(
|
|
@@ -421,7 +419,7 @@ async function executeMigrationPlanCommand(
|
|
|
421
419
|
to: toStorageHash,
|
|
422
420
|
dir: relative(process.cwd(), packageDir),
|
|
423
421
|
operations: [],
|
|
424
|
-
emittedExtensionDirs
|
|
422
|
+
emittedExtensionDirs,
|
|
425
423
|
pendingPlaceholders: true,
|
|
426
424
|
summary:
|
|
427
425
|
'Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
|
|
@@ -444,9 +442,9 @@ async function executeMigrationPlanCommand(
|
|
|
444
442
|
label: op.label,
|
|
445
443
|
operationClass: op.operationClass,
|
|
446
444
|
})),
|
|
447
|
-
emittedExtensionDirs
|
|
445
|
+
emittedExtensionDirs,
|
|
448
446
|
...(preview !== undefined ? { preview } : {}),
|
|
449
|
-
summary: buildPlanSummary(plannedOps.length,
|
|
447
|
+
summary: buildPlanSummary(plannedOps.length, emittedExtensionDirs.length),
|
|
450
448
|
timings: { total: Date.now() - startTime },
|
|
451
449
|
};
|
|
452
450
|
return ok(result);
|
|
@@ -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?.({
|