@prisma-next/cli 0.5.0-dev.4 → 0.5.0-dev.41
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 +56 -21
- package/dist/agent-skill-mongo.md +63 -31
- package/dist/agent-skill-postgres.md +1 -1
- package/dist/cli-errors-By1iVE3z.mjs +34 -0
- package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
- package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
- package/dist/cli.mjs +123 -15
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-TG7rbCWT.mjs → client-1JqqkiC7.mjs} +45 -20
- package/dist/client-1JqqkiC7.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -2
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +2 -2
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +10 -9
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +5 -5
- package/dist/commands/db-sign.mjs +7 -7
- package/dist/commands/db-update.mjs +9 -9
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +9 -9
- package/dist/commands/migration-apply.d.mts +5 -2
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +55 -56
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +26 -32
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +14 -5
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +45 -48
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.d.mts.map +1 -1
- package/dist/commands/migration-ref.mjs +6 -10
- package/dist/commands/migration-ref.mjs.map +1 -1
- package/dist/commands/migration-show.d.mts +13 -7
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +27 -29
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +23 -5
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +3 -3
- package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
- package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
- package/dist/config-loader.mjs +1 -1
- package/dist/contract-emit-LjzCoicC.mjs +4 -0
- package/dist/contract-emit-RZBWzkop.mjs +329 -0
- package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
- package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
- package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
- package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
- package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
- package/dist/{contract-infer-BS4kIX9c.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
- package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +86 -21
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +5 -5
- package/dist/exports/index.mjs +3 -3
- package/dist/exports/init-output.d.mts +39 -0
- package/dist/exports/init-output.d.mts.map +1 -0
- package/dist/exports/init-output.mjs +3 -0
- package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
- package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
- package/dist/init-C7dE9KOJ.mjs +2062 -0
- package/dist/init-C7dE9KOJ.mjs.map +1 -0
- package/dist/{inspect-live-schema-BsoFVoS1.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
- package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
- package/dist/migration-cli.d.mts +41 -11
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +308 -84
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-DOXnheFa.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
- package/dist/{migration-command-scaffold-DOXnheFa.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
- package/dist/{migration-status-Ry3TnEya.mjs → migration-status-DoPrFIOQ.mjs} +114 -57
- package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
- package/dist/{migrations-fU0xoKjS.mjs → migrations-MEoKMiV5.mjs} +42 -21
- package/dist/migrations-MEoKMiV5.mjs.map +1 -0
- package/dist/output-BpcQrnnq.mjs +103 -0
- package/dist/output-BpcQrnnq.mjs.map +1 -0
- package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
- package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
- package/dist/quick-reference-mongo.md +34 -13
- package/dist/quick-reference-postgres.md +11 -9
- package/dist/{result-handler-BJwA7ufw.mjs → result-handler-Ch6hVnOo.mjs} +35 -93
- package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
- package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
- package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
- package/dist/{verify-bl__PkXk.mjs → verify-BT9tgCOH.mjs} +2 -2
- package/dist/{verify-bl__PkXk.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
- package/package.json +22 -16
- package/src/cli.ts +32 -6
- package/src/commands/contract-emit.ts +67 -163
- package/src/commands/contract-infer.ts +7 -20
- package/src/commands/db-init.ts +1 -0
- package/src/commands/db-update.ts +1 -1
- package/src/commands/init/detect-pnpm-catalog.ts +141 -0
- package/src/commands/init/errors.ts +254 -0
- package/src/commands/init/exit-codes.ts +62 -0
- package/src/commands/init/hygiene-gitattributes.ts +97 -0
- package/src/commands/init/hygiene-gitignore.ts +48 -0
- package/src/commands/init/hygiene-package-scripts.ts +91 -0
- package/src/commands/init/index.ts +112 -7
- package/src/commands/init/init.ts +766 -144
- package/src/commands/init/inputs.ts +421 -0
- package/src/commands/init/output.ts +147 -0
- package/src/commands/init/probe-db.ts +308 -0
- package/src/commands/init/reinit-cleanup.ts +83 -0
- package/src/commands/init/templates/agent-skill-mongo.md +63 -31
- package/src/commands/init/templates/agent-skill-postgres.md +1 -1
- package/src/commands/init/templates/agent-skill.ts +25 -3
- package/src/commands/init/templates/code-templates.ts +125 -32
- package/src/commands/init/templates/env.ts +80 -0
- package/src/commands/init/templates/quick-reference-mongo.md +34 -13
- package/src/commands/init/templates/quick-reference-postgres.md +11 -9
- package/src/commands/init/templates/quick-reference.ts +42 -3
- package/src/commands/init/templates/tsconfig.ts +167 -5
- package/src/commands/inspect-live-schema.ts +10 -5
- package/src/commands/migration-apply.ts +84 -63
- package/src/commands/migration-new.ts +28 -34
- package/src/commands/migration-plan.ts +80 -56
- package/src/commands/migration-ref.ts +8 -7
- package/src/commands/migration-show.ts +53 -36
- package/src/commands/migration-status.ts +194 -58
- package/src/config-path-validation.ts +0 -1
- package/src/control-api/client.ts +21 -0
- package/src/control-api/operations/contract-emit.ts +198 -115
- package/src/control-api/operations/db-init.ts +10 -6
- package/src/control-api/operations/db-update.ts +10 -6
- package/src/control-api/operations/migration-apply.ts +30 -9
- package/src/control-api/types.ts +69 -7
- package/src/exports/control-api.ts +2 -1
- package/src/exports/init-output.ts +10 -0
- package/src/migration-cli.ts +445 -122
- package/src/utils/cli-errors.ts +49 -2
- package/src/utils/command-helpers.ts +45 -23
- package/src/utils/emit-queue.ts +26 -0
- package/src/utils/formatters/graph-migration-mapper.ts +7 -3
- package/src/utils/formatters/migrations.ts +62 -26
- package/src/utils/publish-contract-artifact-pair.ts +134 -0
- package/dist/cli-errors-DHq6GQGu.mjs +0 -5
- package/dist/client-TG7rbCWT.mjs.map +0 -1
- package/dist/config-loader-_W4T21X1.mjs.map +0 -1
- package/dist/contract-emit-CQfj7xJn.mjs +0 -122
- package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
- package/dist/contract-emit-DpPjuFy-.mjs +0 -195
- package/dist/contract-emit-DpPjuFy-.mjs.map +0 -1
- package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
- package/dist/contract-infer-BS4kIX9c.mjs.map +0 -1
- package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
- package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
- package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
- package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
- package/dist/init-CQfo_4Ro.mjs +0 -430
- package/dist/init-CQfo_4Ro.mjs.map +0 -1
- package/dist/inspect-live-schema-BsoFVoS1.mjs.map +0 -1
- package/dist/migration-status-Ry3TnEya.mjs.map +0 -1
- package/dist/migrations-fU0xoKjS.mjs.map +0 -1
- package/dist/result-handler-BJwA7ufw.mjs.map +0 -1
- package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
- package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
- package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
- package/src/control-api/operations/extract-operation-statements.ts +0 -14
- package/src/control-api/operations/extract-sql-ddl.ts +0 -47
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
|
|
2
2
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
3
|
+
import {
|
|
4
|
+
errorNoInvariantPath,
|
|
5
|
+
errorUnknownInvariant,
|
|
6
|
+
MigrationToolsError,
|
|
7
|
+
} from '@prisma-next/migration-tools/errors';
|
|
8
|
+
import type { MigrationEdge, MigrationGraph } from '@prisma-next/migration-tools/graph';
|
|
3
9
|
import {
|
|
4
10
|
findPath,
|
|
5
11
|
findPathWithDecision,
|
|
6
12
|
findReachableLeaves,
|
|
7
|
-
} from '@prisma-next/migration-tools/
|
|
8
|
-
import type {
|
|
13
|
+
} from '@prisma-next/migration-tools/migration-graph';
|
|
14
|
+
import type { MigrationPackage } from '@prisma-next/migration-tools/package';
|
|
15
|
+
import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
|
|
9
16
|
import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
|
|
10
|
-
import type {
|
|
11
|
-
MigrationBundle,
|
|
12
|
-
MigrationChainEntry,
|
|
13
|
-
MigrationGraph,
|
|
14
|
-
} from '@prisma-next/migration-tools/types';
|
|
15
|
-
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
16
17
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
17
18
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
18
19
|
import { cyan, dim, magenta, yellow } from 'colorette';
|
|
@@ -20,16 +21,23 @@ import { Command } from 'commander';
|
|
|
20
21
|
|
|
21
22
|
import { loadConfig } from '../config-loader';
|
|
22
23
|
import { createControlClient } from '../control-api/client';
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
type CliStructuredError,
|
|
26
|
+
errorRuntime,
|
|
27
|
+
errorUnexpected,
|
|
28
|
+
mapMigrationToolsError,
|
|
29
|
+
} from '../utils/cli-errors';
|
|
24
30
|
import {
|
|
25
31
|
addGlobalOptions,
|
|
26
|
-
|
|
32
|
+
collectDeclaredInvariants,
|
|
33
|
+
loadMigrationPackages,
|
|
27
34
|
maskConnectionUrl,
|
|
28
35
|
readContractEnvelope,
|
|
29
36
|
resolveMigrationPaths,
|
|
30
37
|
setCommandDescriptions,
|
|
31
38
|
setCommandExamples,
|
|
32
39
|
toPathDecisionResult,
|
|
40
|
+
toStructuralEdge,
|
|
33
41
|
} from '../utils/command-helpers';
|
|
34
42
|
import {
|
|
35
43
|
type EdgeStatus,
|
|
@@ -61,7 +69,7 @@ export interface MigrationStatusEntry {
|
|
|
61
69
|
readonly dirName: string;
|
|
62
70
|
readonly from: string;
|
|
63
71
|
readonly to: string;
|
|
64
|
-
readonly
|
|
72
|
+
readonly migrationHash: string;
|
|
65
73
|
readonly operationCount: number;
|
|
66
74
|
readonly operationSummary: string;
|
|
67
75
|
readonly hasDestructive: boolean;
|
|
@@ -78,23 +86,39 @@ export interface MigrationStatusResult {
|
|
|
78
86
|
readonly targetHash: string;
|
|
79
87
|
readonly contractHash: string;
|
|
80
88
|
readonly refs?: readonly StatusRef[];
|
|
89
|
+
/** Required invariants from the active ref, sorted ascending. Always present (`[]` when no `--ref` or the ref declares none) — knowable offline. */
|
|
90
|
+
readonly requiredInvariants: readonly string[];
|
|
91
|
+
/**
|
|
92
|
+
* Invariants the marker has applied at least once, intersected with
|
|
93
|
+
* `requiredInvariants` for display relevance. JSON consumers see only the
|
|
94
|
+
* subset overlapping the active ref's required set — the full unfiltered
|
|
95
|
+
* marker invariant list lives on `marker.invariants` (control plane) and
|
|
96
|
+
* is not surfaced here. Present only in `mode === 'online'`; absent when
|
|
97
|
+
* offline (the marker is unknown, not empty).
|
|
98
|
+
*/
|
|
99
|
+
readonly appliedInvariants?: readonly string[];
|
|
100
|
+
/** required − applied. Present only in `mode === 'online'`; absent when offline. */
|
|
101
|
+
readonly missingInvariants?: readonly string[];
|
|
81
102
|
readonly pathDecision?: {
|
|
82
103
|
readonly fromHash: string;
|
|
83
104
|
readonly toHash: string;
|
|
84
105
|
readonly alternativeCount: number;
|
|
85
106
|
readonly tieBreakReasons: readonly string[];
|
|
86
107
|
readonly refName?: string;
|
|
108
|
+
readonly requiredInvariants: readonly string[];
|
|
109
|
+
readonly satisfiedInvariants: readonly string[];
|
|
87
110
|
readonly selectedPath: readonly {
|
|
88
111
|
readonly dirName: string;
|
|
89
|
-
readonly
|
|
112
|
+
readonly migrationHash: string;
|
|
90
113
|
readonly from: string;
|
|
91
114
|
readonly to: string;
|
|
115
|
+
readonly invariants: readonly string[];
|
|
92
116
|
}[];
|
|
93
117
|
};
|
|
94
118
|
readonly summary: string;
|
|
95
119
|
readonly diagnostics: readonly StatusDiagnostic[];
|
|
96
120
|
readonly graph?: MigrationGraph;
|
|
97
|
-
readonly bundles?: readonly
|
|
121
|
+
readonly bundles?: readonly MigrationPackage[];
|
|
98
122
|
readonly edgeStatuses?: readonly EdgeStatus[];
|
|
99
123
|
readonly activeRefHash?: string;
|
|
100
124
|
readonly activeRefName?: string;
|
|
@@ -154,7 +178,7 @@ export function deriveEdgeStatuses(
|
|
|
154
178
|
): EdgeStatus[] {
|
|
155
179
|
if (mode === 'offline') return [];
|
|
156
180
|
|
|
157
|
-
const edgeKey = (e:
|
|
181
|
+
const edgeKey = (e: MigrationEdge) => `${e.from}\0${e.to}`;
|
|
158
182
|
|
|
159
183
|
// No marker = empty DB — treat root as the marker (nothing applied, everything pending)
|
|
160
184
|
const effectiveMarker = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
@@ -224,8 +248,8 @@ export function deriveEdgeStatuses(
|
|
|
224
248
|
* @param markerHash — the marker hash from the database, or undefined if no marker row / offline
|
|
225
249
|
*/
|
|
226
250
|
function buildMigrationEntries(
|
|
227
|
-
chain: readonly
|
|
228
|
-
packages: readonly
|
|
251
|
+
chain: readonly MigrationEdge[],
|
|
252
|
+
packages: readonly MigrationPackage[],
|
|
229
253
|
mode: 'online' | 'offline',
|
|
230
254
|
markerHash: string | undefined,
|
|
231
255
|
edgeStatuses?: readonly EdgeStatus[],
|
|
@@ -261,7 +285,7 @@ function buildMigrationEntries(
|
|
|
261
285
|
dirName: migration.dirName,
|
|
262
286
|
from: migration.from,
|
|
263
287
|
to: migration.to,
|
|
264
|
-
|
|
288
|
+
migrationHash: migration.migrationHash,
|
|
265
289
|
operationCount: ops.length,
|
|
266
290
|
operationSummary: summary,
|
|
267
291
|
hasDestructive,
|
|
@@ -294,7 +318,7 @@ function resolveDisplayChain(
|
|
|
294
318
|
graph: MigrationGraph,
|
|
295
319
|
targetHash: string,
|
|
296
320
|
markerHash: string | undefined,
|
|
297
|
-
): readonly
|
|
321
|
+
): readonly MigrationEdge[] | null {
|
|
298
322
|
if (markerHash === undefined) {
|
|
299
323
|
return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
300
324
|
}
|
|
@@ -355,18 +379,13 @@ async function executeMigrationStatusCommand(
|
|
|
355
379
|
|
|
356
380
|
let activeRefName: string | undefined;
|
|
357
381
|
let activeRefHash: string | undefined;
|
|
382
|
+
let activeRefEntry: RefEntry | undefined;
|
|
358
383
|
let allRefs: Refs = {};
|
|
359
384
|
try {
|
|
360
385
|
allRefs = await readRefs(refsDir);
|
|
361
386
|
} catch (error) {
|
|
362
387
|
if (MigrationToolsError.is(error)) {
|
|
363
|
-
return notOk(
|
|
364
|
-
errorRuntime(error.message, {
|
|
365
|
-
why: error.why,
|
|
366
|
-
fix: error.fix,
|
|
367
|
-
meta: { code: error.code },
|
|
368
|
-
}),
|
|
369
|
-
);
|
|
388
|
+
return notOk(mapMigrationToolsError(error));
|
|
370
389
|
}
|
|
371
390
|
throw error;
|
|
372
391
|
}
|
|
@@ -374,21 +393,18 @@ async function executeMigrationStatusCommand(
|
|
|
374
393
|
if (options.ref) {
|
|
375
394
|
activeRefName = options.ref;
|
|
376
395
|
try {
|
|
377
|
-
|
|
396
|
+
activeRefEntry = resolveRef(allRefs, activeRefName);
|
|
397
|
+
activeRefHash = activeRefEntry.hash;
|
|
378
398
|
} catch (error) {
|
|
379
399
|
if (MigrationToolsError.is(error)) {
|
|
380
|
-
return notOk(
|
|
381
|
-
errorRuntime(error.message, {
|
|
382
|
-
why: error.why,
|
|
383
|
-
fix: error.fix,
|
|
384
|
-
meta: { code: error.code },
|
|
385
|
-
}),
|
|
386
|
-
);
|
|
400
|
+
return notOk(mapMigrationToolsError(error));
|
|
387
401
|
}
|
|
388
402
|
throw error;
|
|
389
403
|
}
|
|
390
404
|
}
|
|
391
405
|
|
|
406
|
+
const requiredInvariants: readonly string[] = [...(activeRefEntry?.invariants ?? [])].sort();
|
|
407
|
+
|
|
392
408
|
const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, entry]) => ({
|
|
393
409
|
name,
|
|
394
410
|
hash: entry.hash,
|
|
@@ -406,6 +422,12 @@ async function executeMigrationStatusCommand(
|
|
|
406
422
|
if (activeRefName) {
|
|
407
423
|
details.push({ label: 'ref', value: activeRefName });
|
|
408
424
|
}
|
|
425
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
426
|
+
details.push({
|
|
427
|
+
label: 'required',
|
|
428
|
+
value: formatInvariantList(activeRefEntry.invariants),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
409
431
|
const header = formatStyledHeader({
|
|
410
432
|
command: 'migration status',
|
|
411
433
|
description: 'Show migration history and applied status',
|
|
@@ -429,15 +451,13 @@ async function executeMigrationStatusCommand(
|
|
|
429
451
|
});
|
|
430
452
|
}
|
|
431
453
|
|
|
432
|
-
let bundles: readonly
|
|
454
|
+
let bundles: readonly MigrationPackage[];
|
|
433
455
|
let graph: MigrationGraph;
|
|
434
456
|
try {
|
|
435
|
-
({ bundles, graph } = await
|
|
457
|
+
({ bundles, graph } = await loadMigrationPackages(migrationsDir));
|
|
436
458
|
} catch (error) {
|
|
437
459
|
if (MigrationToolsError.is(error)) {
|
|
438
|
-
return notOk(
|
|
439
|
-
errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
|
|
440
|
-
);
|
|
460
|
+
return notOk(mapMigrationToolsError(error));
|
|
441
461
|
}
|
|
442
462
|
return notOk(
|
|
443
463
|
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
@@ -465,6 +485,7 @@ async function executeMigrationStatusCommand(
|
|
|
465
485
|
contractHash,
|
|
466
486
|
summary: 'No migrations found',
|
|
467
487
|
diagnostics,
|
|
488
|
+
requiredInvariants,
|
|
468
489
|
});
|
|
469
490
|
}
|
|
470
491
|
|
|
@@ -492,6 +513,7 @@ async function executeMigrationStatusCommand(
|
|
|
492
513
|
}
|
|
493
514
|
|
|
494
515
|
let markerHash: string | undefined;
|
|
516
|
+
let markerInvariants: readonly string[] = [];
|
|
495
517
|
let mode: 'online' | 'offline' = 'offline';
|
|
496
518
|
|
|
497
519
|
if (dbConnection && hasDriver) {
|
|
@@ -504,7 +526,9 @@ async function executeMigrationStatusCommand(
|
|
|
504
526
|
});
|
|
505
527
|
try {
|
|
506
528
|
await client.connect(dbConnection);
|
|
507
|
-
|
|
529
|
+
const marker = await client.readMarker();
|
|
530
|
+
markerHash = marker?.storageHash;
|
|
531
|
+
markerInvariants = marker?.invariants ?? [];
|
|
508
532
|
mode = 'online';
|
|
509
533
|
} catch {
|
|
510
534
|
if (!flags.json && !flags.quiet) {
|
|
@@ -515,6 +539,32 @@ async function executeMigrationStatusCommand(
|
|
|
515
539
|
}
|
|
516
540
|
}
|
|
517
541
|
|
|
542
|
+
// Pre-check unknown invariants. Online: union the graph's declared
|
|
543
|
+
// invariants with the marker's recorded set so a retired-but-applied
|
|
544
|
+
// invariant doesn't surface as MIGRATION.UNKNOWN_INVARIANT — apply would
|
|
545
|
+
// route fine because marker subtraction empties `effectiveRequired`.
|
|
546
|
+
// Offline: keep the check graph-strict (the marker is unknown, and a
|
|
547
|
+
// missing declarer is the dominant signal we can offer).
|
|
548
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
549
|
+
const declared = collectDeclaredInvariants(graph);
|
|
550
|
+
const known = new Set<string>(declared);
|
|
551
|
+
if (mode === 'online') {
|
|
552
|
+
for (const id of markerInvariants) known.add(id);
|
|
553
|
+
}
|
|
554
|
+
const unknown = activeRefEntry.invariants.filter((id) => !known.has(id));
|
|
555
|
+
if (unknown.length > 0) {
|
|
556
|
+
return notOk(
|
|
557
|
+
mapMigrationToolsError(
|
|
558
|
+
errorUnknownInvariant({
|
|
559
|
+
...ifDefined('refName', activeRefName),
|
|
560
|
+
unknown,
|
|
561
|
+
declared: [...declared].sort(),
|
|
562
|
+
}),
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
518
568
|
// Marker exists but is not in the migration graph and doesn't match the
|
|
519
569
|
// contract hash. The DB is at an unknown state relative to the graph.
|
|
520
570
|
// Bail out early with a clear diagnostic instead of rendering a confusing
|
|
@@ -560,6 +610,7 @@ async function executeMigrationStatusCommand(
|
|
|
560
610
|
summary: `${bundles.length} migration(s) on disk`,
|
|
561
611
|
diagnostics,
|
|
562
612
|
markerHash,
|
|
613
|
+
requiredInvariants,
|
|
563
614
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
564
615
|
});
|
|
565
616
|
}
|
|
@@ -600,6 +651,7 @@ async function executeMigrationStatusCommand(
|
|
|
600
651
|
summary: `${bundles.length} migration(s) on disk`,
|
|
601
652
|
diagnostics,
|
|
602
653
|
...ifDefined('markerHash', markerHash),
|
|
654
|
+
requiredInvariants,
|
|
603
655
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
604
656
|
graph,
|
|
605
657
|
bundles,
|
|
@@ -624,14 +676,39 @@ async function executeMigrationStatusCommand(
|
|
|
624
676
|
const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
|
|
625
677
|
const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
|
|
626
678
|
|
|
679
|
+
let appliedInvariants: readonly string[] | undefined;
|
|
680
|
+
let missingInvariants: readonly string[] | undefined;
|
|
681
|
+
let effectiveRequired = new Set<string>();
|
|
682
|
+
if (mode === 'online') {
|
|
683
|
+
// Mirrors `migration-apply.ts`: compute `effectiveRequired = required −
|
|
684
|
+
// marker.invariants` directly, then derive the display fields from it.
|
|
685
|
+
// `appliedInvariants` is the intersection (`required ∩ marker`), which
|
|
686
|
+
// is what JSON consumers see for the active ref; the unfiltered set
|
|
687
|
+
// lives on `marker.invariants`.
|
|
688
|
+
const markerSet = new Set(markerInvariants);
|
|
689
|
+
effectiveRequired = new Set(requiredInvariants.filter((id) => !markerSet.has(id)));
|
|
690
|
+
appliedInvariants = requiredInvariants.filter((id) => markerSet.has(id));
|
|
691
|
+
missingInvariants = [...effectiveRequired].sort();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// The marker can match the structural target while still missing required
|
|
695
|
+
// invariants — for example, a self-edge that provides X, applied via a ref
|
|
696
|
+
// declaring X. `pendingCount` (structural) says zero in that case but
|
|
697
|
+
// `effectiveRequired` is non-empty, so up-to-date messaging would mislead.
|
|
698
|
+
const hasInvariantWork = effectiveRequired.size > 0;
|
|
699
|
+
const missingList = [...effectiveRequired].sort().join(', ');
|
|
700
|
+
|
|
627
701
|
let summary: string;
|
|
628
702
|
if (mode === 'online') {
|
|
629
703
|
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
630
704
|
summary = `${bundles.length} migration(s) on disk`;
|
|
631
705
|
} else if (activeRefHash && markerHash !== undefined) {
|
|
632
|
-
|
|
633
|
-
|
|
706
|
+
const distance = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
|
|
707
|
+
summary = hasInvariantWork ? `${distance} — missing invariant(s): ${missingList}` : distance;
|
|
708
|
+
} else if (pendingCount === 0 && !hasInvariantWork) {
|
|
634
709
|
summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? 's' : ''} applied)`;
|
|
710
|
+
} else if (pendingCount === 0 && hasInvariantWork) {
|
|
711
|
+
summary = `Missing invariant(s): ${missingList} — run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply`;
|
|
635
712
|
} else if (markerHash === undefined) {
|
|
636
713
|
summary = `${pendingCount} pending migration(s) — database has no marker`;
|
|
637
714
|
} else {
|
|
@@ -641,6 +718,37 @@ async function executeMigrationStatusCommand(
|
|
|
641
718
|
summary = `${entries.length} migration(s) on disk`;
|
|
642
719
|
}
|
|
643
720
|
|
|
721
|
+
let pathDecision: MigrationStatusResult['pathDecision'];
|
|
722
|
+
let routingUnreachable = false;
|
|
723
|
+
if (mode === 'online') {
|
|
724
|
+
const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
725
|
+
const outcome = findPathWithDecision(graph, originHash, targetHash, {
|
|
726
|
+
...ifDefined('refName', activeRefName),
|
|
727
|
+
required: effectiveRequired,
|
|
728
|
+
});
|
|
729
|
+
if (outcome.kind === 'ok') {
|
|
730
|
+
pathDecision = toPathDecisionResult(outcome.decision);
|
|
731
|
+
} else if (outcome.kind === 'unsatisfiable') {
|
|
732
|
+
return notOk(
|
|
733
|
+
mapMigrationToolsError(
|
|
734
|
+
errorNoInvariantPath({
|
|
735
|
+
...ifDefined('refName', activeRefName),
|
|
736
|
+
required: [...effectiveRequired].sort(),
|
|
737
|
+
missing: outcome.missing,
|
|
738
|
+
structuralPath: outcome.structuralPath.map(toStructuralEdge),
|
|
739
|
+
}),
|
|
740
|
+
),
|
|
741
|
+
);
|
|
742
|
+
} else {
|
|
743
|
+
// outcome.kind === 'unreachable' — origin (marker) has no structural
|
|
744
|
+
// path to the active target. `pendingCount` and `hasInvariantWork`
|
|
745
|
+
// both report zero in this case, but emitting MIGRATION.UP_TO_DATE
|
|
746
|
+
// would be wrong: the database simply cannot reach the requested
|
|
747
|
+
// ref/contract from its current state. Suppress UP_TO_DATE below.
|
|
748
|
+
routingUnreachable = true;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
644
752
|
if (mode === 'online') {
|
|
645
753
|
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
646
754
|
diagnostics.push({
|
|
@@ -657,7 +765,16 @@ async function executeMigrationStatusCommand(
|
|
|
657
765
|
message: `${pendingCount} migration(s) pending`,
|
|
658
766
|
hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
|
|
659
767
|
});
|
|
660
|
-
} else {
|
|
768
|
+
} else if (hasInvariantWork) {
|
|
769
|
+
diagnostics.push({
|
|
770
|
+
code: 'MIGRATION.INVARIANTS_PENDING',
|
|
771
|
+
severity: 'info',
|
|
772
|
+
message: `Missing required invariant(s): ${missingList}`,
|
|
773
|
+
hints: [
|
|
774
|
+
`Run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply a path that covers the required invariants`,
|
|
775
|
+
],
|
|
776
|
+
});
|
|
777
|
+
} else if (!routingUnreachable) {
|
|
661
778
|
diagnostics.push({
|
|
662
779
|
code: 'MIGRATION.UP_TO_DATE',
|
|
663
780
|
severity: 'info',
|
|
@@ -667,14 +784,6 @@ async function executeMigrationStatusCommand(
|
|
|
667
784
|
}
|
|
668
785
|
}
|
|
669
786
|
|
|
670
|
-
let pathDecision: MigrationStatusResult['pathDecision'];
|
|
671
|
-
if (mode === 'online' && markerHash !== undefined) {
|
|
672
|
-
const decision = findPathWithDecision(graph, markerHash, targetHash, activeRefName);
|
|
673
|
-
if (decision) {
|
|
674
|
-
pathDecision = toPathDecisionResult(decision);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
787
|
const result: MigrationStatusResult = {
|
|
679
788
|
ok: true,
|
|
680
789
|
mode,
|
|
@@ -684,6 +793,9 @@ async function executeMigrationStatusCommand(
|
|
|
684
793
|
summary,
|
|
685
794
|
diagnostics,
|
|
686
795
|
...ifDefined('markerHash', markerHash),
|
|
796
|
+
requiredInvariants,
|
|
797
|
+
...ifDefined('appliedInvariants', appliedInvariants),
|
|
798
|
+
...ifDefined('missingInvariants', missingInvariants),
|
|
687
799
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
688
800
|
...ifDefined('pathDecision', pathDecision),
|
|
689
801
|
graph,
|
|
@@ -724,13 +836,19 @@ export function createMigrationStatusCommand(): Command {
|
|
|
724
836
|
|
|
725
837
|
const exitCode = handleResult(result, flags, ui, (statusResult) => {
|
|
726
838
|
if (flags.json) {
|
|
839
|
+
// Strip non-JSON-shape fields before emitting. These belong to
|
|
840
|
+
// the in-memory result so the human renderer can avoid
|
|
841
|
+
// recomputing them, but they would either bloat the wire format
|
|
842
|
+
// (graph, bundles, edgeStatuses) or expose internals
|
|
843
|
+
// (activeRefHash, activeRefName, diverged) that consumers should
|
|
844
|
+
// read off `pathDecision` / `refs` instead.
|
|
727
845
|
const {
|
|
728
|
-
graph:
|
|
729
|
-
bundles:
|
|
730
|
-
edgeStatuses:
|
|
731
|
-
activeRefHash:
|
|
732
|
-
activeRefName:
|
|
733
|
-
diverged:
|
|
846
|
+
graph: _graph,
|
|
847
|
+
bundles: _bundles,
|
|
848
|
+
edgeStatuses: _edgeStatuses,
|
|
849
|
+
activeRefHash: _activeRefHash,
|
|
850
|
+
activeRefName: _activeRefName,
|
|
851
|
+
diverged: _diverged,
|
|
734
852
|
...jsonResult
|
|
735
853
|
} = statusResult;
|
|
736
854
|
ui.output(JSON.stringify(jsonResult, null, 2));
|
|
@@ -789,7 +907,7 @@ function formatLegend(colorize: boolean): string {
|
|
|
789
907
|
return c(dim, parts.join(' '));
|
|
790
908
|
}
|
|
791
909
|
|
|
792
|
-
function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
910
|
+
export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
793
911
|
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
794
912
|
const lines: string[] = [];
|
|
795
913
|
|
|
@@ -797,11 +915,16 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
797
915
|
const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
|
|
798
916
|
|
|
799
917
|
const hasWarnings = result.diagnostics?.some((d) => d.severity === 'warn') ?? false;
|
|
918
|
+
// INVARIANTS_PENDING is filed at severity 'info' (per ADR 208) so the
|
|
919
|
+
// warn-severity check above doesn't see it. It still represents pending
|
|
920
|
+
// work, so it must promote the summary off the success icon.
|
|
921
|
+
const hasInvariantPending =
|
|
922
|
+
result.diagnostics?.some((d) => d.code === 'MIGRATION.INVARIANTS_PENDING') ?? false;
|
|
800
923
|
|
|
801
924
|
if (result.mode === 'online') {
|
|
802
925
|
if (hasUnknown || hasWarnings) {
|
|
803
926
|
lines.push(`${c(yellow, '⚠')} ${result.summary}`);
|
|
804
|
-
} else if (pendingCount === 0) {
|
|
927
|
+
} else if (pendingCount === 0 && !hasInvariantPending) {
|
|
805
928
|
lines.push(`${c(cyan, '✔')} ${result.summary}`);
|
|
806
929
|
} else {
|
|
807
930
|
lines.push(`${c(yellow, '⧗')} ${result.summary}`);
|
|
@@ -810,6 +933,15 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
810
933
|
lines.push(result.summary);
|
|
811
934
|
}
|
|
812
935
|
|
|
936
|
+
if (result.requiredInvariants.length > 0) {
|
|
937
|
+
if (result.appliedInvariants !== undefined && result.missingInvariants !== undefined) {
|
|
938
|
+
lines.push(`${c(dim, 'applied ')}${formatInvariantList(result.appliedInvariants)}`);
|
|
939
|
+
lines.push(`${c(dim, 'missing ')}${formatInvariantList(result.missingInvariants)}`);
|
|
940
|
+
} else {
|
|
941
|
+
lines.push(`${c(dim, 'applied ')}(unknown — connect a database to evaluate)`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
813
945
|
const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
|
|
814
946
|
for (const diag of warnings) {
|
|
815
947
|
lines.push(`${c(yellow, '⚠')} ${diag.message}`);
|
|
@@ -821,6 +953,10 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
821
953
|
return lines.join('\n');
|
|
822
954
|
}
|
|
823
955
|
|
|
956
|
+
function formatInvariantList(ids: readonly string[]): string {
|
|
957
|
+
return ids.length === 0 ? '(none)' : ids.join(', ');
|
|
958
|
+
}
|
|
959
|
+
|
|
824
960
|
function summarizeRefDistance(
|
|
825
961
|
graph: MigrationGraph,
|
|
826
962
|
markerHash: string,
|
|
@@ -57,7 +57,6 @@ export function finalizeConfig(config: PrismaNextConfig, configDir: string): Pri
|
|
|
57
57
|
if (!config.contract) {
|
|
58
58
|
return config;
|
|
59
59
|
}
|
|
60
|
-
|
|
61
60
|
const contract = normalizeContractConfig(config.contract);
|
|
62
61
|
const source = finalizeContractSource(contract.source, configDir);
|
|
63
62
|
const output = resolve(configDir, contract.output);
|
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
ControlFamilyInstance,
|
|
7
7
|
ControlStack,
|
|
8
8
|
CoreSchemaView,
|
|
9
|
+
MigrationPlanOperation,
|
|
10
|
+
OperationPreview,
|
|
9
11
|
SignDatabaseResult,
|
|
10
12
|
VerifyDatabaseResult,
|
|
11
13
|
VerifyDatabaseSchemaResult,
|
|
@@ -13,8 +15,11 @@ import type {
|
|
|
13
15
|
import {
|
|
14
16
|
createControlStack,
|
|
15
17
|
hasMigrations,
|
|
18
|
+
hasOperationPreview,
|
|
19
|
+
hasPslContractInfer,
|
|
16
20
|
hasSchemaView,
|
|
17
21
|
} from '@prisma-next/framework-components/control';
|
|
22
|
+
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
18
23
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
19
24
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
20
25
|
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
@@ -469,6 +474,22 @@ class ControlClientImpl implements ControlClient {
|
|
|
469
474
|
return undefined;
|
|
470
475
|
}
|
|
471
476
|
|
|
477
|
+
inferPslContract(schemaIR: unknown): PslDocumentAst | undefined {
|
|
478
|
+
this.init();
|
|
479
|
+
if (this.familyInstance && hasPslContractInfer(this.familyInstance)) {
|
|
480
|
+
return this.familyInstance.inferPslContract(schemaIR);
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview | undefined {
|
|
486
|
+
this.init();
|
|
487
|
+
if (this.familyInstance && hasOperationPreview(this.familyInstance)) {
|
|
488
|
+
return this.familyInstance.toOperationPreview(operations);
|
|
489
|
+
}
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
472
493
|
async emit(options: EmitOptions): Promise<EmitResult> {
|
|
473
494
|
const { onProgress, contractConfig } = options;
|
|
474
495
|
|