@prisma-next/cli 0.5.0-dev.9 → 0.5.0
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 +60 -25
- package/dist/cli-errors-B9OBbled.d.mts +3 -0
- package/dist/cli-errors-D3_sMh2K.mjs +33 -0
- package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
- package/dist/cli.mjs +16 -78
- package/dist/cli.mjs.map +1 -1
- package/dist/client-qVH-rEgd.mjs +1595 -0
- package/dist/client-qVH-rEgd.mjs.map +1 -0
- package/dist/{result-handler-Ba3zWQsI.mjs → command-helpers-BeZHkxV8.mjs} +70 -47
- package/dist/command-helpers-BeZHkxV8.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -4
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +2 -4
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +16 -13
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +6 -7
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +9 -9
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +15 -13
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -321
- package/dist/commands/migration-apply.d.mts +28 -13
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +55 -151
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts +0 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +34 -40
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +33 -6
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +2 -348
- 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 +8 -12
- 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 +35 -36
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +126 -5
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -4
- package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
- package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
- package/dist/config-loader.d.mts +0 -1
- package/dist/config-loader.d.mts.map +1 -1
- package/dist/config-loader.mjs +2 -3
- package/dist/contract-emit-9DBda5Ou.mjs +150 -0
- package/dist/contract-emit-9DBda5Ou.mjs.map +1 -0
- package/dist/contract-emit-B77TsJqf.mjs +327 -0
- package/dist/contract-emit-B77TsJqf.mjs.map +1 -0
- package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-Dani0mMW.mjs} +4 -6
- package/dist/contract-enrichment-Dani0mMW.mjs.map +1 -0
- package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BK9YFGEG.mjs} +13 -22
- package/dist/contract-infer-BK9YFGEG.mjs.map +1 -0
- package/dist/db-verify-C0y1PCO2.mjs +404 -0
- package/dist/db-verify-C0y1PCO2.mjs.map +1 -0
- package/dist/exports/config-types.mjs +1 -2
- package/dist/exports/control-api.d.mts +101 -586
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +4 -6
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +28 -30
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +2 -4
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +2 -3
- package/dist/extension-pack-inputs-C7xgE-vv.mjs +74 -0
- package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +1 -0
- package/dist/{framework-components-Cr--XBKy.mjs → framework-components-ChqVUxR-.mjs} +3 -4
- package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-ChqVUxR-.mjs.map} +1 -1
- package/dist/global-flags-Icqpxk23.d.mts +12 -0
- package/dist/global-flags-Icqpxk23.d.mts.map +1 -0
- package/dist/helpers-eqdN8tH6.mjs +25 -0
- package/dist/helpers-eqdN8tH6.mjs.map +1 -0
- package/dist/{init-C5220SY9.mjs → init-DETSgw3h.mjs} +40 -49
- package/dist/init-DETSgw3h.mjs.map +1 -0
- package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-CWYxGKlb.mjs} +10 -11
- package/dist/inspect-live-schema-CWYxGKlb.mjs.map +1 -0
- package/dist/migration-cli.d.mts +41 -12
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +309 -86
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B5dORFEv.mjs} +8 -9
- package/dist/migration-command-scaffold-B5dORFEv.mjs.map +1 -0
- package/dist/migration-plan-C6lVaHsO.mjs +554 -0
- package/dist/migration-plan-C6lVaHsO.mjs.map +1 -0
- package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CZ-D5k7k.mjs} +272 -65
- package/dist/migration-status-CZ-D5k7k.mjs.map +1 -0
- package/dist/migrations-D_UJnpuW.mjs +216 -0
- package/dist/migrations-D_UJnpuW.mjs.map +1 -0
- package/dist/{output-BpcQrnnq.mjs → output-B16Kefzx.mjs} +9 -3
- package/dist/output-B16Kefzx.mjs.map +1 -0
- package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-DFfvZcYL.mjs} +2 -2
- package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-DFfvZcYL.mjs.map} +1 -1
- package/dist/result-handler-rmPVKIP2.mjs +25 -0
- package/dist/result-handler-rmPVKIP2.mjs.map +1 -0
- package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
- package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-C_hFNbAn.mjs} +4 -28
- package/dist/terminal-ui-C_hFNbAn.mjs.map +1 -0
- package/dist/types-D7x-IFLO.d.mts +858 -0
- package/dist/types-D7x-IFLO.d.mts.map +1 -0
- package/dist/{verify-Bkycc-Tf.mjs → verify-CiwNWM9N.mjs} +3 -4
- package/dist/verify-CiwNWM9N.mjs.map +1 -0
- package/package.json +28 -26
- 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 +15 -3
- package/src/commands/db-update.ts +9 -4
- package/src/commands/db-verify.ts +47 -15
- package/src/commands/init/index.ts +1 -1
- package/src/commands/init/init.ts +2 -2
- package/src/commands/init/templates/code-templates.ts +26 -18
- package/src/commands/inspect-live-schema.ts +10 -5
- package/src/commands/migration-apply.ts +114 -212
- package/src/commands/migration-new.ts +42 -45
- package/src/commands/migration-plan.ts +212 -72
- package/src/commands/migration-ref.ts +8 -7
- package/src/commands/migration-show.ts +60 -41
- package/src/commands/migration-status.ts +483 -64
- package/src/config-path-validation.ts +0 -1
- package/src/control-api/client.ts +85 -5
- package/src/control-api/contract-enrichment.ts +6 -4
- package/src/control-api/operations/apply-aggregate.ts +290 -0
- package/src/control-api/operations/contract-emit.ts +198 -115
- package/src/control-api/operations/db-apply-aggregate.ts +397 -0
- package/src/control-api/operations/db-init.ts +51 -253
- package/src/control-api/operations/db-update.ts +66 -183
- package/src/control-api/operations/db-verify.ts +342 -0
- package/src/control-api/operations/migration-apply.ts +424 -131
- package/src/control-api/types.ts +280 -29
- package/src/exports/control-api.ts +15 -3
- package/src/load-ts-contract.ts +28 -26
- package/src/migration-cli.ts +445 -122
- package/src/utils/cli-errors.ts +49 -2
- package/src/utils/combine-schema-results.ts +84 -0
- package/src/utils/command-helpers.ts +69 -25
- package/src/utils/contract-space-aggregate-loader.ts +204 -0
- package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
- package/src/utils/contract-space-migrate-pass.ts +156 -0
- package/src/utils/emit-queue.ts +26 -0
- package/src/utils/extension-pack-inputs.ts +170 -0
- package/src/utils/formatters/graph-migration-mapper.ts +7 -3
- package/src/utils/formatters/migrations.ts +197 -61
- package/src/utils/publish-contract-artifact-pair.ts +134 -0
- package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
- package/dist/cli-errors-Cd79vmTH.mjs +0 -5
- package/dist/client-CrsnY58k.mjs +0 -997
- package/dist/client-CrsnY58k.mjs.map +0 -1
- package/dist/commands/db-verify.mjs.map +0 -1
- package/dist/commands/migration-plan.mjs.map +0 -1
- package/dist/config-loader-C25b63rJ.mjs.map +0 -1
- package/dist/contract-emit--feXyNd7.mjs +0 -4
- package/dist/contract-emit-NJ01hiiv.mjs +0 -195
- package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
- package/dist/contract-emit-V5SSitUT.mjs +0 -122
- package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
- package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
- package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
- package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
- package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
- package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
- package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
- package/dist/init-C5220SY9.mjs.map +0 -1
- package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
- package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
- package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
- package/dist/migrations-Bo5WtTla.mjs +0 -153
- package/dist/migrations-Bo5WtTla.mjs.map +0 -1
- package/dist/output-BpcQrnnq.mjs.map +0 -1
- package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
- package/dist/terminal-ui-C3ZLwQxK.mjs.map +0 -1
- package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
- package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
- package/dist/verify-Bkycc-Tf.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,26 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
createControlStack,
|
|
3
|
+
type MigrationPlanOperation,
|
|
4
|
+
} from '@prisma-next/framework-components/control';
|
|
5
|
+
import {
|
|
6
|
+
type ContractMarkerRecordLike,
|
|
7
|
+
graphWalkStrategy,
|
|
8
|
+
} from '@prisma-next/migration-tools/aggregate';
|
|
2
9
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
10
|
+
import {
|
|
11
|
+
errorNoInvariantPath,
|
|
12
|
+
errorUnknownInvariant,
|
|
13
|
+
MigrationToolsError,
|
|
14
|
+
} from '@prisma-next/migration-tools/errors';
|
|
15
|
+
import type { MigrationEdge, MigrationGraph } from '@prisma-next/migration-tools/graph';
|
|
3
16
|
import {
|
|
4
17
|
findPath,
|
|
5
18
|
findPathWithDecision,
|
|
6
19
|
findReachableLeaves,
|
|
7
|
-
} from '@prisma-next/migration-tools/
|
|
8
|
-
import type {
|
|
20
|
+
} from '@prisma-next/migration-tools/migration-graph';
|
|
21
|
+
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
22
|
+
import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
|
|
9
23
|
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
24
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
17
25
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
18
26
|
import { cyan, dim, magenta, yellow } from 'colorette';
|
|
@@ -20,17 +28,28 @@ import { Command } from 'commander';
|
|
|
20
28
|
|
|
21
29
|
import { loadConfig } from '../config-loader';
|
|
22
30
|
import { createControlClient } from '../control-api/client';
|
|
23
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
type CliStructuredError,
|
|
33
|
+
errorRuntime,
|
|
34
|
+
errorUnexpected,
|
|
35
|
+
mapMigrationToolsError,
|
|
36
|
+
} from '../utils/cli-errors';
|
|
24
37
|
import {
|
|
25
38
|
addGlobalOptions,
|
|
26
|
-
|
|
39
|
+
collectDeclaredInvariants,
|
|
40
|
+
loadMigrationPackages,
|
|
27
41
|
maskConnectionUrl,
|
|
28
42
|
readContractEnvelope,
|
|
29
43
|
resolveMigrationPaths,
|
|
30
44
|
setCommandDescriptions,
|
|
31
45
|
setCommandExamples,
|
|
32
46
|
toPathDecisionResult,
|
|
47
|
+
toStructuralEdge,
|
|
33
48
|
} from '../utils/command-helpers';
|
|
49
|
+
import {
|
|
50
|
+
type BuildAggregateInputs,
|
|
51
|
+
buildContractSpaceAggregate,
|
|
52
|
+
} from '../utils/contract-space-aggregate-loader';
|
|
34
53
|
import {
|
|
35
54
|
type EdgeStatus,
|
|
36
55
|
type EdgeStatusKind,
|
|
@@ -61,13 +80,60 @@ export interface MigrationStatusEntry {
|
|
|
61
80
|
readonly dirName: string;
|
|
62
81
|
readonly from: string;
|
|
63
82
|
readonly to: string;
|
|
64
|
-
readonly
|
|
83
|
+
readonly migrationHash: string;
|
|
65
84
|
readonly operationCount: number;
|
|
66
85
|
readonly operationSummary: string;
|
|
67
86
|
readonly hasDestructive: boolean;
|
|
68
87
|
readonly status: EdgeStatusKind | 'unknown';
|
|
69
88
|
}
|
|
70
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Per-space status row in the aggregate-shaped status output.
|
|
92
|
+
*
|
|
93
|
+
* Surfaces, for each contract space:
|
|
94
|
+
*
|
|
95
|
+
* - `headHash`: the on-disk head ref's hash (where the space is going).
|
|
96
|
+
* - `markerHash`: the live marker hash for the space, or null if no
|
|
97
|
+
* marker has been written yet (greenfield, or pre-`migration apply`).
|
|
98
|
+
* - `pendingCount`: number of migration edges between marker and head.
|
|
99
|
+
* Computed via {@link graphWalkStrategy}; 0 means the space is
|
|
100
|
+
* already at head.
|
|
101
|
+
* - `status`: convenience tag the formatter uses to pick a glyph.
|
|
102
|
+
* `'never-planned'` is reserved for spaces with non-empty head but
|
|
103
|
+
* no on-disk migrations — which shouldn't happen if the loader's
|
|
104
|
+
* integrity check passes.
|
|
105
|
+
*
|
|
106
|
+
* Online-only fields (`markerHash`, `status`) are absent when the
|
|
107
|
+
* command runs without a database connection.
|
|
108
|
+
*/
|
|
109
|
+
export interface MigrationStatusSpaceEntry {
|
|
110
|
+
readonly spaceId: string;
|
|
111
|
+
readonly kind: 'app' | 'extension';
|
|
112
|
+
readonly headHash: string;
|
|
113
|
+
readonly markerHash?: string | null;
|
|
114
|
+
readonly pendingCount?: number;
|
|
115
|
+
readonly status?: 'up-to-date' | 'pending' | 'no-marker' | 'never-planned' | 'unreachable';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sum per-space `pendingCount` into a cross-space total, but only when
|
|
120
|
+
* every loaded space reports a defined `pendingCount`. Returns
|
|
121
|
+
* `undefined` if any space is on the marker-unknown / offline path
|
|
122
|
+
* (where `pendingCount` is intentionally absent), so JSON consumers can
|
|
123
|
+
* distinguish "no pending" from "unknown".
|
|
124
|
+
*/
|
|
125
|
+
export function computeTotalPendingAcrossSpaces(
|
|
126
|
+
spaces: readonly MigrationStatusSpaceEntry[],
|
|
127
|
+
): number | undefined {
|
|
128
|
+
if (spaces.length === 0) return undefined;
|
|
129
|
+
let total = 0;
|
|
130
|
+
for (const s of spaces) {
|
|
131
|
+
if (s.pendingCount === undefined) return undefined;
|
|
132
|
+
total += s.pendingCount;
|
|
133
|
+
}
|
|
134
|
+
return total;
|
|
135
|
+
}
|
|
136
|
+
|
|
71
137
|
export type { StatusDiagnostic, StatusRef } from '../utils/migration-types';
|
|
72
138
|
|
|
73
139
|
export interface MigrationStatusResult {
|
|
@@ -78,23 +144,55 @@ export interface MigrationStatusResult {
|
|
|
78
144
|
readonly targetHash: string;
|
|
79
145
|
readonly contractHash: string;
|
|
80
146
|
readonly refs?: readonly StatusRef[];
|
|
147
|
+
/** Required invariants from the active ref, sorted ascending. Always present (`[]` when no `--ref` or the ref declares none) — knowable offline. */
|
|
148
|
+
readonly requiredInvariants: readonly string[];
|
|
149
|
+
/**
|
|
150
|
+
* Invariants the marker has applied at least once, intersected with
|
|
151
|
+
* `requiredInvariants` for display relevance. JSON consumers see only the
|
|
152
|
+
* subset overlapping the active ref's required set — the full unfiltered
|
|
153
|
+
* marker invariant list lives on `marker.invariants` (control plane) and
|
|
154
|
+
* is not surfaced here. Present only in `mode === 'online'`; absent when
|
|
155
|
+
* offline (the marker is unknown, not empty).
|
|
156
|
+
*/
|
|
157
|
+
readonly appliedInvariants?: readonly string[];
|
|
158
|
+
/** required − applied. Present only in `mode === 'online'`; absent when offline. */
|
|
159
|
+
readonly missingInvariants?: readonly string[];
|
|
81
160
|
readonly pathDecision?: {
|
|
82
161
|
readonly fromHash: string;
|
|
83
162
|
readonly toHash: string;
|
|
84
163
|
readonly alternativeCount: number;
|
|
85
164
|
readonly tieBreakReasons: readonly string[];
|
|
86
165
|
readonly refName?: string;
|
|
166
|
+
readonly requiredInvariants: readonly string[];
|
|
167
|
+
readonly satisfiedInvariants: readonly string[];
|
|
87
168
|
readonly selectedPath: readonly {
|
|
88
169
|
readonly dirName: string;
|
|
89
|
-
readonly
|
|
170
|
+
readonly migrationHash: string;
|
|
90
171
|
readonly from: string;
|
|
91
172
|
readonly to: string;
|
|
173
|
+
readonly invariants: readonly string[];
|
|
92
174
|
}[];
|
|
93
175
|
};
|
|
94
176
|
readonly summary: string;
|
|
95
177
|
readonly diagnostics: readonly StatusDiagnostic[];
|
|
178
|
+
/**
|
|
179
|
+
* Aggregate enumeration of every on-disk contract space (app +
|
|
180
|
+
* extensions), in canonical schedule order (extensions
|
|
181
|
+
* alphabetically, then app). Present whenever the aggregate loader
|
|
182
|
+
* succeeded; absent in early-error returns (e.g. unreadable
|
|
183
|
+
* migrations directory) where the existing diagnostics already
|
|
184
|
+
* surface the failure.
|
|
185
|
+
*
|
|
186
|
+
* The legacy top-level fields (`migrations`, `markerHash`,
|
|
187
|
+
* `targetHash`, `pathDecision`, …) describe the **app member**
|
|
188
|
+
* specifically — back-compat with single-space callers. Per-space
|
|
189
|
+
* detail for extension members lives only on this list.
|
|
190
|
+
*/
|
|
191
|
+
readonly spaces?: readonly MigrationStatusSpaceEntry[];
|
|
192
|
+
/** Cross-space pending-migration total (sum of `spaces[].pendingCount`). Present when `spaces` is. */
|
|
193
|
+
readonly totalPendingAcrossSpaces?: number;
|
|
96
194
|
readonly graph?: MigrationGraph;
|
|
97
|
-
readonly bundles?: readonly
|
|
195
|
+
readonly bundles?: readonly OnDiskMigrationPackage[];
|
|
98
196
|
readonly edgeStatuses?: readonly EdgeStatus[];
|
|
99
197
|
readonly activeRefHash?: string;
|
|
100
198
|
readonly activeRefName?: string;
|
|
@@ -154,7 +252,7 @@ export function deriveEdgeStatuses(
|
|
|
154
252
|
): EdgeStatus[] {
|
|
155
253
|
if (mode === 'offline') return [];
|
|
156
254
|
|
|
157
|
-
const edgeKey = (e:
|
|
255
|
+
const edgeKey = (e: MigrationEdge) => `${e.from}\0${e.to}`;
|
|
158
256
|
|
|
159
257
|
// No marker = empty DB — treat root as the marker (nothing applied, everything pending)
|
|
160
258
|
const effectiveMarker = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
@@ -224,8 +322,8 @@ export function deriveEdgeStatuses(
|
|
|
224
322
|
* @param markerHash — the marker hash from the database, or undefined if no marker row / offline
|
|
225
323
|
*/
|
|
226
324
|
function buildMigrationEntries(
|
|
227
|
-
chain: readonly
|
|
228
|
-
packages: readonly
|
|
325
|
+
chain: readonly MigrationEdge[],
|
|
326
|
+
packages: readonly OnDiskMigrationPackage[],
|
|
229
327
|
mode: 'online' | 'offline',
|
|
230
328
|
markerHash: string | undefined,
|
|
231
329
|
edgeStatuses?: readonly EdgeStatus[],
|
|
@@ -261,7 +359,7 @@ function buildMigrationEntries(
|
|
|
261
359
|
dirName: migration.dirName,
|
|
262
360
|
from: migration.from,
|
|
263
361
|
to: migration.to,
|
|
264
|
-
|
|
362
|
+
migrationHash: migration.migrationHash,
|
|
265
363
|
operationCount: ops.length,
|
|
266
364
|
operationSummary: summary,
|
|
267
365
|
hasDestructive,
|
|
@@ -294,7 +392,7 @@ function resolveDisplayChain(
|
|
|
294
392
|
graph: MigrationGraph,
|
|
295
393
|
targetHash: string,
|
|
296
394
|
markerHash: string | undefined,
|
|
297
|
-
): readonly
|
|
395
|
+
): readonly MigrationEdge[] | null {
|
|
298
396
|
if (markerHash === undefined) {
|
|
299
397
|
return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
300
398
|
}
|
|
@@ -339,34 +437,143 @@ function determineLimit(opts: MigrationStatusOptions) {
|
|
|
339
437
|
return parsed;
|
|
340
438
|
}
|
|
341
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Build the aggregate enumeration of contract spaces for the status
|
|
442
|
+
* output. Loads the aggregate from disk (lossy on failure — extension
|
|
443
|
+
* spaces are simply omitted, the existing single-space app behaviour
|
|
444
|
+
* keeps working), reads per-space marker rows when online, and uses
|
|
445
|
+
* {@link graphWalkStrategy} to compute each space's pending count.
|
|
446
|
+
*
|
|
447
|
+
* Sub-spec § `migration status` semantics — the aggregate-walking
|
|
448
|
+
* version reports per-space marker + pending state alongside the
|
|
449
|
+
* cross-space totals.
|
|
450
|
+
*/
|
|
451
|
+
export async function loadAggregateStatusSpaces(args: {
|
|
452
|
+
readonly targetId: string;
|
|
453
|
+
readonly migrationsDir: string;
|
|
454
|
+
readonly appContractRaw: unknown;
|
|
455
|
+
readonly extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'];
|
|
456
|
+
readonly validateContract: BuildAggregateInputs<string, string>['validateContract'];
|
|
457
|
+
readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
|
|
458
|
+
}): Promise<readonly MigrationStatusSpaceEntry[]> {
|
|
459
|
+
const loadInputs: BuildAggregateInputs<string, string> = {
|
|
460
|
+
targetId: args.targetId,
|
|
461
|
+
migrationsDir: args.migrationsDir,
|
|
462
|
+
appContract: args.validateContract(args.appContractRaw),
|
|
463
|
+
extensionPacks: args.extensionPacks,
|
|
464
|
+
validateContract: args.validateContract,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const loaded = await buildContractSpaceAggregate(loadInputs);
|
|
468
|
+
if (!loaded.ok) {
|
|
469
|
+
// Loader failure (drift, layout violation, etc.) — surfacing it
|
|
470
|
+
// as a status diagnostic would duplicate `migration plan`'s job.
|
|
471
|
+
// The single-space app pipeline still runs; extensions are simply
|
|
472
|
+
// not enumerated.
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
const aggregate = loaded.value;
|
|
476
|
+
|
|
477
|
+
const orderedMembers = [...aggregate.extensions, aggregate.app];
|
|
478
|
+
const rows: MigrationStatusSpaceEntry[] = [];
|
|
479
|
+
for (const member of orderedMembers) {
|
|
480
|
+
const liveMarker = args.markersBySpace?.get(member.spaceId) ?? null;
|
|
481
|
+
const isApp = member.spaceId === aggregate.app.spaceId;
|
|
482
|
+
|
|
483
|
+
if (member.migrations.graph.nodes.size === 0) {
|
|
484
|
+
rows.push({
|
|
485
|
+
spaceId: member.spaceId,
|
|
486
|
+
kind: isApp ? 'app' : 'extension',
|
|
487
|
+
headHash: member.headRef.hash,
|
|
488
|
+
...(args.markersBySpace !== null
|
|
489
|
+
? {
|
|
490
|
+
markerHash: liveMarker?.storageHash ?? null,
|
|
491
|
+
status: member.headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
|
|
492
|
+
pendingCount: 0,
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
495
|
+
});
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (args.markersBySpace === null) {
|
|
500
|
+
rows.push({
|
|
501
|
+
spaceId: member.spaceId,
|
|
502
|
+
kind: isApp ? 'app' : 'extension',
|
|
503
|
+
headHash: member.headRef.hash,
|
|
504
|
+
});
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const walked = graphWalkStrategy({
|
|
509
|
+
aggregateTargetId: aggregate.targetId,
|
|
510
|
+
member,
|
|
511
|
+
currentMarker: liveMarker,
|
|
512
|
+
});
|
|
513
|
+
let pendingCount = 0;
|
|
514
|
+
let status: MigrationStatusSpaceEntry['status'];
|
|
515
|
+
if (walked.kind === 'ok') {
|
|
516
|
+
pendingCount = walked.result.plan.operations.length;
|
|
517
|
+
if (liveMarker === null) {
|
|
518
|
+
status = pendingCount === 0 ? 'no-marker' : 'pending';
|
|
519
|
+
} else {
|
|
520
|
+
status = pendingCount === 0 ? 'up-to-date' : 'pending';
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
status = 'unreachable';
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
rows.push({
|
|
527
|
+
spaceId: member.spaceId,
|
|
528
|
+
kind: isApp ? 'app' : 'extension',
|
|
529
|
+
headHash: member.headRef.hash,
|
|
530
|
+
markerHash: liveMarker?.storageHash ?? null,
|
|
531
|
+
pendingCount,
|
|
532
|
+
...(status ? { status } : {}),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return rows;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Read the raw contract.json bytes from disk for the aggregate
|
|
540
|
+
* loader. Returns `null` if the file is missing or unparseable —
|
|
541
|
+
* the existing `readContractEnvelope` path will report the same
|
|
542
|
+
* problem via a status diagnostic, no need to double-surface.
|
|
543
|
+
*/
|
|
544
|
+
async function loadContractRawSafely(config: {
|
|
545
|
+
contract?: { output?: string };
|
|
546
|
+
}): Promise<unknown | null> {
|
|
547
|
+
try {
|
|
548
|
+
const path = (await import('../utils/command-helpers')).resolveContractPath(config);
|
|
549
|
+
const raw = await (await import('node:fs/promises')).readFile(path, 'utf-8');
|
|
550
|
+
return JSON.parse(raw) as unknown;
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
342
556
|
async function executeMigrationStatusCommand(
|
|
343
557
|
options: MigrationStatusOptions,
|
|
344
558
|
flags: GlobalFlags,
|
|
345
559
|
ui: TerminalUI,
|
|
346
560
|
): Promise<Result<MigrationStatusResult, CliStructuredError>> {
|
|
347
561
|
const config = await loadConfig(options.config);
|
|
348
|
-
const { configPath,
|
|
349
|
-
options.config,
|
|
350
|
-
config,
|
|
351
|
-
);
|
|
562
|
+
const { configPath, appMigrationsDir, appMigrationsRelative, migrationsDir, refsDir } =
|
|
563
|
+
resolveMigrationPaths(options.config, config);
|
|
352
564
|
|
|
353
565
|
const dbConnection = options.db ?? config.db?.connection;
|
|
354
566
|
const hasDriver = !!config.driver;
|
|
355
567
|
|
|
356
568
|
let activeRefName: string | undefined;
|
|
357
569
|
let activeRefHash: string | undefined;
|
|
570
|
+
let activeRefEntry: RefEntry | undefined;
|
|
358
571
|
let allRefs: Refs = {};
|
|
359
572
|
try {
|
|
360
573
|
allRefs = await readRefs(refsDir);
|
|
361
574
|
} catch (error) {
|
|
362
575
|
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
|
-
);
|
|
576
|
+
return notOk(mapMigrationToolsError(error));
|
|
370
577
|
}
|
|
371
578
|
throw error;
|
|
372
579
|
}
|
|
@@ -374,21 +581,18 @@ async function executeMigrationStatusCommand(
|
|
|
374
581
|
if (options.ref) {
|
|
375
582
|
activeRefName = options.ref;
|
|
376
583
|
try {
|
|
377
|
-
|
|
584
|
+
activeRefEntry = resolveRef(allRefs, activeRefName);
|
|
585
|
+
activeRefHash = activeRefEntry.hash;
|
|
378
586
|
} catch (error) {
|
|
379
587
|
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
|
-
);
|
|
588
|
+
return notOk(mapMigrationToolsError(error));
|
|
387
589
|
}
|
|
388
590
|
throw error;
|
|
389
591
|
}
|
|
390
592
|
}
|
|
391
593
|
|
|
594
|
+
const requiredInvariants: readonly string[] = [...(activeRefEntry?.invariants ?? [])].sort();
|
|
595
|
+
|
|
392
596
|
const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, entry]) => ({
|
|
393
597
|
name,
|
|
394
598
|
hash: entry.hash,
|
|
@@ -398,7 +602,7 @@ async function executeMigrationStatusCommand(
|
|
|
398
602
|
if (!flags.json && !flags.quiet) {
|
|
399
603
|
const details: Array<{ label: string; value: string }> = [
|
|
400
604
|
{ label: 'config', value: configPath },
|
|
401
|
-
{ label: 'migrations', value:
|
|
605
|
+
{ label: 'migrations', value: appMigrationsRelative },
|
|
402
606
|
];
|
|
403
607
|
if (dbConnection && hasDriver) {
|
|
404
608
|
details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
|
|
@@ -406,6 +610,12 @@ async function executeMigrationStatusCommand(
|
|
|
406
610
|
if (activeRefName) {
|
|
407
611
|
details.push({ label: 'ref', value: activeRefName });
|
|
408
612
|
}
|
|
613
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
614
|
+
details.push({
|
|
615
|
+
label: 'required',
|
|
616
|
+
value: formatInvariantList(activeRefEntry.invariants),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
409
619
|
const header = formatStyledHeader({
|
|
410
620
|
command: 'migration status',
|
|
411
621
|
description: 'Show migration history and applied status',
|
|
@@ -429,15 +639,13 @@ async function executeMigrationStatusCommand(
|
|
|
429
639
|
});
|
|
430
640
|
}
|
|
431
641
|
|
|
432
|
-
let bundles: readonly
|
|
642
|
+
let bundles: readonly OnDiskMigrationPackage[];
|
|
433
643
|
let graph: MigrationGraph;
|
|
434
644
|
try {
|
|
435
|
-
({ bundles, graph } = await
|
|
645
|
+
({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
|
|
436
646
|
} catch (error) {
|
|
437
647
|
if (MigrationToolsError.is(error)) {
|
|
438
|
-
return notOk(
|
|
439
|
-
errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
|
|
440
|
-
);
|
|
648
|
+
return notOk(mapMigrationToolsError(error));
|
|
441
649
|
}
|
|
442
650
|
return notOk(
|
|
443
651
|
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
@@ -465,6 +673,7 @@ async function executeMigrationStatusCommand(
|
|
|
465
673
|
contractHash,
|
|
466
674
|
summary: 'No migrations found',
|
|
467
675
|
diagnostics,
|
|
676
|
+
requiredInvariants,
|
|
468
677
|
});
|
|
469
678
|
}
|
|
470
679
|
|
|
@@ -492,7 +701,9 @@ async function executeMigrationStatusCommand(
|
|
|
492
701
|
}
|
|
493
702
|
|
|
494
703
|
let markerHash: string | undefined;
|
|
704
|
+
let markerInvariants: readonly string[] = [];
|
|
495
705
|
let mode: 'online' | 'offline' = 'offline';
|
|
706
|
+
let allMarkers: ReadonlyMap<string, ContractMarkerRecordLike> | null = null;
|
|
496
707
|
|
|
497
708
|
if (dbConnection && hasDriver) {
|
|
498
709
|
const client = createControlClient({
|
|
@@ -504,8 +715,25 @@ async function executeMigrationStatusCommand(
|
|
|
504
715
|
});
|
|
505
716
|
try {
|
|
506
717
|
await client.connect(dbConnection);
|
|
507
|
-
|
|
718
|
+
const marker = await client.readMarker();
|
|
719
|
+
markerHash = marker?.storageHash;
|
|
720
|
+
markerInvariants = marker?.invariants ?? [];
|
|
508
721
|
mode = 'online';
|
|
722
|
+
// Read every space's marker so the aggregate enumeration can
|
|
723
|
+
// surface per-space marker state. `readAllMarkers` mirrors what
|
|
724
|
+
// `db init` / `db update` already use to drive the multi-space
|
|
725
|
+
// planner; here it powers the aggregate status output.
|
|
726
|
+
try {
|
|
727
|
+
allMarkers = await client.readAllMarkers();
|
|
728
|
+
} catch {
|
|
729
|
+
// Older family instances may not implement `readAllMarkers`.
|
|
730
|
+
// Per-space enumeration falls back to "marker unknown" rather
|
|
731
|
+
// than failing the whole status command — leaving
|
|
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).
|
|
735
|
+
allMarkers = null;
|
|
736
|
+
}
|
|
509
737
|
} catch {
|
|
510
738
|
if (!flags.json && !flags.quiet) {
|
|
511
739
|
ui.warn('Could not connect to database — showing offline status');
|
|
@@ -515,6 +743,63 @@ async function executeMigrationStatusCommand(
|
|
|
515
743
|
}
|
|
516
744
|
}
|
|
517
745
|
|
|
746
|
+
// Build the aggregate enumeration of contract spaces. Lossy on
|
|
747
|
+
// failure (extensions are simply omitted) so the existing
|
|
748
|
+
// single-space app pipeline below still runs even if extensions
|
|
749
|
+
// can't be loaded — a strict failure here would degrade the
|
|
750
|
+
// load-bearing app-space output for unrelated reasons.
|
|
751
|
+
const contractRawForAggregate = await loadContractRawSafely(config);
|
|
752
|
+
let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
|
|
753
|
+
if (contractRawForAggregate !== null) {
|
|
754
|
+
// The aggregate loader needs a typed-Contract producer. Build a
|
|
755
|
+
// real control stack so `validateContract` runs against a fully
|
|
756
|
+
// composed family instance — descriptors that read stack members
|
|
757
|
+
// during construction (e.g. codec lookups) get a consistent view.
|
|
758
|
+
const stack = createControlStack(config);
|
|
759
|
+
const familyInstance = config.family.create(stack);
|
|
760
|
+
try {
|
|
761
|
+
aggregateSpaces = await loadAggregateStatusSpaces({
|
|
762
|
+
targetId: config.target.targetId,
|
|
763
|
+
migrationsDir,
|
|
764
|
+
appContractRaw: contractRawForAggregate,
|
|
765
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
766
|
+
validateContract: (json: unknown) => familyInstance.validateContract(json),
|
|
767
|
+
markersBySpace: allMarkers,
|
|
768
|
+
});
|
|
769
|
+
} catch {
|
|
770
|
+
// Loader failure short-circuits silently — the existing
|
|
771
|
+
// single-space app pipeline below still runs.
|
|
772
|
+
aggregateSpaces = [];
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const totalPendingAcrossSpaces = computeTotalPendingAcrossSpaces(aggregateSpaces);
|
|
776
|
+
|
|
777
|
+
// Pre-check unknown invariants. Online: union the graph's declared
|
|
778
|
+
// invariants with the marker's recorded set so a retired-but-applied
|
|
779
|
+
// invariant doesn't surface as MIGRATION.UNKNOWN_INVARIANT — apply would
|
|
780
|
+
// route fine because marker subtraction empties `effectiveRequired`.
|
|
781
|
+
// Offline: keep the check graph-strict (the marker is unknown, and a
|
|
782
|
+
// missing declarer is the dominant signal we can offer).
|
|
783
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
784
|
+
const declared = collectDeclaredInvariants(graph);
|
|
785
|
+
const known = new Set<string>(declared);
|
|
786
|
+
if (mode === 'online') {
|
|
787
|
+
for (const id of markerInvariants) known.add(id);
|
|
788
|
+
}
|
|
789
|
+
const unknown = activeRefEntry.invariants.filter((id) => !known.has(id));
|
|
790
|
+
if (unknown.length > 0) {
|
|
791
|
+
return notOk(
|
|
792
|
+
mapMigrationToolsError(
|
|
793
|
+
errorUnknownInvariant({
|
|
794
|
+
...ifDefined('refName', activeRefName),
|
|
795
|
+
unknown,
|
|
796
|
+
declared: [...declared].sort(),
|
|
797
|
+
}),
|
|
798
|
+
),
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
518
803
|
// Marker exists but is not in the migration graph and doesn't match the
|
|
519
804
|
// contract hash. The DB is at an unknown state relative to the graph.
|
|
520
805
|
// Bail out early with a clear diagnostic instead of rendering a confusing
|
|
@@ -560,6 +845,7 @@ async function executeMigrationStatusCommand(
|
|
|
560
845
|
summary: `${bundles.length} migration(s) on disk`,
|
|
561
846
|
diagnostics,
|
|
562
847
|
markerHash,
|
|
848
|
+
requiredInvariants,
|
|
563
849
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
564
850
|
});
|
|
565
851
|
}
|
|
@@ -600,6 +886,7 @@ async function executeMigrationStatusCommand(
|
|
|
600
886
|
summary: `${bundles.length} migration(s) on disk`,
|
|
601
887
|
diagnostics,
|
|
602
888
|
...ifDefined('markerHash', markerHash),
|
|
889
|
+
requiredInvariants,
|
|
603
890
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
604
891
|
graph,
|
|
605
892
|
bundles,
|
|
@@ -624,14 +911,39 @@ async function executeMigrationStatusCommand(
|
|
|
624
911
|
const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
|
|
625
912
|
const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
|
|
626
913
|
|
|
914
|
+
let appliedInvariants: readonly string[] | undefined;
|
|
915
|
+
let missingInvariants: readonly string[] | undefined;
|
|
916
|
+
let effectiveRequired = new Set<string>();
|
|
917
|
+
if (mode === 'online') {
|
|
918
|
+
// Mirrors `migration-apply.ts`: compute `effectiveRequired = required −
|
|
919
|
+
// marker.invariants` directly, then derive the display fields from it.
|
|
920
|
+
// `appliedInvariants` is the intersection (`required ∩ marker`), which
|
|
921
|
+
// is what JSON consumers see for the active ref; the unfiltered set
|
|
922
|
+
// lives on `marker.invariants`.
|
|
923
|
+
const markerSet = new Set(markerInvariants);
|
|
924
|
+
effectiveRequired = new Set(requiredInvariants.filter((id) => !markerSet.has(id)));
|
|
925
|
+
appliedInvariants = requiredInvariants.filter((id) => markerSet.has(id));
|
|
926
|
+
missingInvariants = [...effectiveRequired].sort();
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// The marker can match the structural target while still missing required
|
|
930
|
+
// invariants — for example, a self-edge that provides X, applied via a ref
|
|
931
|
+
// declaring X. `pendingCount` (structural) says zero in that case but
|
|
932
|
+
// `effectiveRequired` is non-empty, so up-to-date messaging would mislead.
|
|
933
|
+
const hasInvariantWork = effectiveRequired.size > 0;
|
|
934
|
+
const missingList = [...effectiveRequired].sort().join(', ');
|
|
935
|
+
|
|
627
936
|
let summary: string;
|
|
628
937
|
if (mode === 'online') {
|
|
629
938
|
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
630
939
|
summary = `${bundles.length} migration(s) on disk`;
|
|
631
940
|
} else if (activeRefHash && markerHash !== undefined) {
|
|
632
|
-
|
|
633
|
-
|
|
941
|
+
const distance = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
|
|
942
|
+
summary = hasInvariantWork ? `${distance} — missing invariant(s): ${missingList}` : distance;
|
|
943
|
+
} else if (pendingCount === 0 && !hasInvariantWork) {
|
|
634
944
|
summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? 's' : ''} applied)`;
|
|
945
|
+
} else if (pendingCount === 0 && hasInvariantWork) {
|
|
946
|
+
summary = `Missing invariant(s): ${missingList} — run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply`;
|
|
635
947
|
} else if (markerHash === undefined) {
|
|
636
948
|
summary = `${pendingCount} pending migration(s) — database has no marker`;
|
|
637
949
|
} else {
|
|
@@ -641,6 +953,37 @@ async function executeMigrationStatusCommand(
|
|
|
641
953
|
summary = `${entries.length} migration(s) on disk`;
|
|
642
954
|
}
|
|
643
955
|
|
|
956
|
+
let pathDecision: MigrationStatusResult['pathDecision'];
|
|
957
|
+
let routingUnreachable = false;
|
|
958
|
+
if (mode === 'online') {
|
|
959
|
+
const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
960
|
+
const outcome = findPathWithDecision(graph, originHash, targetHash, {
|
|
961
|
+
...ifDefined('refName', activeRefName),
|
|
962
|
+
required: effectiveRequired,
|
|
963
|
+
});
|
|
964
|
+
if (outcome.kind === 'ok') {
|
|
965
|
+
pathDecision = toPathDecisionResult(outcome.decision);
|
|
966
|
+
} else if (outcome.kind === 'unsatisfiable') {
|
|
967
|
+
return notOk(
|
|
968
|
+
mapMigrationToolsError(
|
|
969
|
+
errorNoInvariantPath({
|
|
970
|
+
...ifDefined('refName', activeRefName),
|
|
971
|
+
required: [...effectiveRequired].sort(),
|
|
972
|
+
missing: outcome.missing,
|
|
973
|
+
structuralPath: outcome.structuralPath.map(toStructuralEdge),
|
|
974
|
+
}),
|
|
975
|
+
),
|
|
976
|
+
);
|
|
977
|
+
} else {
|
|
978
|
+
// outcome.kind === 'unreachable' — origin (marker) has no structural
|
|
979
|
+
// path to the active target. `pendingCount` and `hasInvariantWork`
|
|
980
|
+
// both report zero in this case, but emitting MIGRATION.UP_TO_DATE
|
|
981
|
+
// would be wrong: the database simply cannot reach the requested
|
|
982
|
+
// ref/contract from its current state. Suppress UP_TO_DATE below.
|
|
983
|
+
routingUnreachable = true;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
644
987
|
if (mode === 'online') {
|
|
645
988
|
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
646
989
|
diagnostics.push({
|
|
@@ -657,7 +1000,16 @@ async function executeMigrationStatusCommand(
|
|
|
657
1000
|
message: `${pendingCount} migration(s) pending`,
|
|
658
1001
|
hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
|
|
659
1002
|
});
|
|
660
|
-
} else {
|
|
1003
|
+
} else if (hasInvariantWork) {
|
|
1004
|
+
diagnostics.push({
|
|
1005
|
+
code: 'MIGRATION.INVARIANTS_PENDING',
|
|
1006
|
+
severity: 'info',
|
|
1007
|
+
message: `Missing required invariant(s): ${missingList}`,
|
|
1008
|
+
hints: [
|
|
1009
|
+
`Run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply a path that covers the required invariants`,
|
|
1010
|
+
],
|
|
1011
|
+
});
|
|
1012
|
+
} else if (!routingUnreachable) {
|
|
661
1013
|
diagnostics.push({
|
|
662
1014
|
code: 'MIGRATION.UP_TO_DATE',
|
|
663
1015
|
severity: 'info',
|
|
@@ -667,14 +1019,6 @@ async function executeMigrationStatusCommand(
|
|
|
667
1019
|
}
|
|
668
1020
|
}
|
|
669
1021
|
|
|
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
1022
|
const result: MigrationStatusResult = {
|
|
679
1023
|
ok: true,
|
|
680
1024
|
mode,
|
|
@@ -684,6 +1028,9 @@ async function executeMigrationStatusCommand(
|
|
|
684
1028
|
summary,
|
|
685
1029
|
diagnostics,
|
|
686
1030
|
...ifDefined('markerHash', markerHash),
|
|
1031
|
+
requiredInvariants,
|
|
1032
|
+
...ifDefined('appliedInvariants', appliedInvariants),
|
|
1033
|
+
...ifDefined('missingInvariants', missingInvariants),
|
|
687
1034
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
688
1035
|
...ifDefined('pathDecision', pathDecision),
|
|
689
1036
|
graph,
|
|
@@ -691,6 +1038,8 @@ async function executeMigrationStatusCommand(
|
|
|
691
1038
|
edgeStatuses,
|
|
692
1039
|
...ifDefined('activeRefHash', activeRefHash),
|
|
693
1040
|
...ifDefined('activeRefName', activeRefName),
|
|
1041
|
+
spaces: aggregateSpaces,
|
|
1042
|
+
...ifDefined('totalPendingAcrossSpaces', totalPendingAcrossSpaces),
|
|
694
1043
|
};
|
|
695
1044
|
return ok(result);
|
|
696
1045
|
}
|
|
@@ -724,13 +1073,19 @@ export function createMigrationStatusCommand(): Command {
|
|
|
724
1073
|
|
|
725
1074
|
const exitCode = handleResult(result, flags, ui, (statusResult) => {
|
|
726
1075
|
if (flags.json) {
|
|
1076
|
+
// Strip non-JSON-shape fields before emitting. These belong to
|
|
1077
|
+
// the in-memory result so the human renderer can avoid
|
|
1078
|
+
// recomputing them, but they would either bloat the wire format
|
|
1079
|
+
// (graph, bundles, edgeStatuses) or expose internals
|
|
1080
|
+
// (activeRefHash, activeRefName, diverged) that consumers should
|
|
1081
|
+
// read off `pathDecision` / `refs` instead.
|
|
727
1082
|
const {
|
|
728
|
-
graph:
|
|
729
|
-
bundles:
|
|
730
|
-
edgeStatuses:
|
|
731
|
-
activeRefHash:
|
|
732
|
-
activeRefName:
|
|
733
|
-
diverged:
|
|
1083
|
+
graph: _graph,
|
|
1084
|
+
bundles: _bundles,
|
|
1085
|
+
edgeStatuses: _edgeStatuses,
|
|
1086
|
+
activeRefHash: _activeRefHash,
|
|
1087
|
+
activeRefName: _activeRefName,
|
|
1088
|
+
diverged: _diverged,
|
|
734
1089
|
...jsonResult
|
|
735
1090
|
} = statusResult;
|
|
736
1091
|
ui.output(JSON.stringify(jsonResult, null, 2));
|
|
@@ -789,7 +1144,7 @@ function formatLegend(colorize: boolean): string {
|
|
|
789
1144
|
return c(dim, parts.join(' '));
|
|
790
1145
|
}
|
|
791
1146
|
|
|
792
|
-
function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
1147
|
+
export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
793
1148
|
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
794
1149
|
const lines: string[] = [];
|
|
795
1150
|
|
|
@@ -797,11 +1152,16 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
797
1152
|
const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
|
|
798
1153
|
|
|
799
1154
|
const hasWarnings = result.diagnostics?.some((d) => d.severity === 'warn') ?? false;
|
|
1155
|
+
// INVARIANTS_PENDING is filed at severity 'info' (per ADR 208) so the
|
|
1156
|
+
// warn-severity check above doesn't see it. It still represents pending
|
|
1157
|
+
// work, so it must promote the summary off the success icon.
|
|
1158
|
+
const hasInvariantPending =
|
|
1159
|
+
result.diagnostics?.some((d) => d.code === 'MIGRATION.INVARIANTS_PENDING') ?? false;
|
|
800
1160
|
|
|
801
1161
|
if (result.mode === 'online') {
|
|
802
1162
|
if (hasUnknown || hasWarnings) {
|
|
803
1163
|
lines.push(`${c(yellow, '⚠')} ${result.summary}`);
|
|
804
|
-
} else if (pendingCount === 0) {
|
|
1164
|
+
} else if (pendingCount === 0 && !hasInvariantPending) {
|
|
805
1165
|
lines.push(`${c(cyan, '✔')} ${result.summary}`);
|
|
806
1166
|
} else {
|
|
807
1167
|
lines.push(`${c(yellow, '⧗')} ${result.summary}`);
|
|
@@ -810,6 +1170,15 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
810
1170
|
lines.push(result.summary);
|
|
811
1171
|
}
|
|
812
1172
|
|
|
1173
|
+
if (result.requiredInvariants.length > 0) {
|
|
1174
|
+
if (result.appliedInvariants !== undefined && result.missingInvariants !== undefined) {
|
|
1175
|
+
lines.push(`${c(dim, 'applied ')}${formatInvariantList(result.appliedInvariants)}`);
|
|
1176
|
+
lines.push(`${c(dim, 'missing ')}${formatInvariantList(result.missingInvariants)}`);
|
|
1177
|
+
} else {
|
|
1178
|
+
lines.push(`${c(dim, 'applied ')}(unknown — connect a database to evaluate)`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
813
1182
|
const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
|
|
814
1183
|
for (const diag of warnings) {
|
|
815
1184
|
lines.push(`${c(yellow, '⚠')} ${diag.message}`);
|
|
@@ -818,9 +1187,59 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
818
1187
|
}
|
|
819
1188
|
}
|
|
820
1189
|
|
|
1190
|
+
// Per-space section. Suppressed when there's no extension space —
|
|
1191
|
+
// the legacy single-space output already covers the app member.
|
|
1192
|
+
// When extensions exist, render every space (including the app)
|
|
1193
|
+
// for consistency, plus a cross-space pending total + apply hint.
|
|
1194
|
+
if (result.spaces?.some((s) => s.kind === 'extension')) {
|
|
1195
|
+
const total = result.totalPendingAcrossSpaces ?? 0;
|
|
1196
|
+
lines.push('');
|
|
1197
|
+
lines.push(c(dim, 'spaces'));
|
|
1198
|
+
for (const space of result.spaces) {
|
|
1199
|
+
lines.push(formatSpaceLine(space, c));
|
|
1200
|
+
}
|
|
1201
|
+
if (total > 0) {
|
|
1202
|
+
lines.push('');
|
|
1203
|
+
lines.push(
|
|
1204
|
+
`${c(yellow, '⧗')} ${total} pending migration(s) across ${result.spaces.length} space(s) — run 'prisma-next migration apply' to apply`,
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
821
1209
|
return lines.join('\n');
|
|
822
1210
|
}
|
|
823
1211
|
|
|
1212
|
+
function formatSpaceLine(
|
|
1213
|
+
space: MigrationStatusSpaceEntry,
|
|
1214
|
+
c: (fn: (s: string) => string, s: string) => string,
|
|
1215
|
+
): string {
|
|
1216
|
+
const glyph = (() => {
|
|
1217
|
+
if (space.status === 'up-to-date' || space.status === 'no-marker') return c(cyan, '✓');
|
|
1218
|
+
if (space.status === 'pending') return c(yellow, '⧗');
|
|
1219
|
+
if (space.status === 'unreachable' || space.status === 'never-planned') return c(magenta, '✗');
|
|
1220
|
+
return ' ';
|
|
1221
|
+
})();
|
|
1222
|
+
const tag = space.kind === 'app' ? '[app]' : '[ext]';
|
|
1223
|
+
const head = space.headHash.slice(0, 8);
|
|
1224
|
+
const marker =
|
|
1225
|
+
space.markerHash === undefined
|
|
1226
|
+
? '(unknown)'
|
|
1227
|
+
: space.markerHash === null
|
|
1228
|
+
? '(no marker)'
|
|
1229
|
+
: space.markerHash.slice(0, 8);
|
|
1230
|
+
const pending =
|
|
1231
|
+
space.pendingCount === undefined
|
|
1232
|
+
? ''
|
|
1233
|
+
: space.pendingCount === 0
|
|
1234
|
+
? c(dim, ' (up to date)')
|
|
1235
|
+
: c(yellow, ` (${space.pendingCount} pending)`);
|
|
1236
|
+
return ` ${glyph} ${c(dim, tag)} ${space.spaceId} → head ${c(dim, head)}, marker ${c(dim, marker)}${pending}`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function formatInvariantList(ids: readonly string[]): string {
|
|
1240
|
+
return ids.length === 0 ? '(none)' : ids.join(', ');
|
|
1241
|
+
}
|
|
1242
|
+
|
|
824
1243
|
function summarizeRefDistance(
|
|
825
1244
|
graph: MigrationGraph,
|
|
826
1245
|
markerHash: string,
|