@prisma-next/cli 0.12.0-dev.25 → 0.12.0-dev.27
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 +11 -11
- package/dist/{client-BHe8szOW.mjs → client-V7BkIQrQ.mjs} +4 -4
- package/dist/{client-BHe8szOW.mjs.map → client-V7BkIQrQ.mjs.map} +1 -1
- package/dist/{command-helpers-Cmdqyhz9.mjs → command-helpers-DlrUCI7s.mjs} +3 -24
- package/dist/{command-helpers-Cmdqyhz9.mjs.map → command-helpers-DlrUCI7s.mjs.map} +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +4 -4
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +4 -4
- package/dist/commands/db-update.mjs +5 -5
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.mjs +5 -5
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +2 -11
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +8 -35
- package/dist/commands/migration-graph.mjs.map +1 -1
- package/dist/commands/migration-list.d.mts +13 -24
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +4 -4
- package/dist/commands/migration-list.mjs.map +1 -1
- package/dist/commands/migration-log.d.mts +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-new.mjs +3 -3
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +3 -3
- package/dist/commands/migration-status.d.mts +24 -142
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +3 -759
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.mjs +2 -2
- package/dist/commands/telemetry/index.mjs +1 -1
- package/dist/{contract-at-errors-Cz0z5PJi.mjs → contract-at-errors-DlZHXSkI.mjs} +2 -2
- package/dist/{contract-at-errors-Cz0z5PJi.mjs.map → contract-at-errors-DlZHXSkI.mjs.map} +1 -1
- package/dist/{contract-emit-DPMij44i.mjs → contract-emit-CaKp92-Q.mjs} +3 -3
- package/dist/{contract-emit-DPMij44i.mjs.map → contract-emit-CaKp92-Q.mjs.map} +1 -1
- package/dist/{contract-emit-CC9jDOmu.mjs → contract-emit-S53EyBRV.mjs} +3 -3
- package/dist/{contract-emit-CC9jDOmu.mjs.map → contract-emit-S53EyBRV.mjs.map} +1 -1
- package/dist/{contract-infer-OCn12Zvn.mjs → contract-infer-Cebb-_Qx.mjs} +3 -3
- package/dist/{contract-infer-OCn12Zvn.mjs.map → contract-infer-Cebb-_Qx.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-CirAEsM8.mjs → contract-space-aggregate-loader-Dvl1SJ4C.mjs} +3 -3
- package/dist/{contract-space-aggregate-loader-CirAEsM8.mjs.map → contract-space-aggregate-loader-Dvl1SJ4C.mjs.map} +1 -1
- package/dist/{db-verify-DJxengYP.mjs → db-verify-B1OoWEWn.mjs} +4 -4
- package/dist/{db-verify-DJxengYP.mjs.map → db-verify-B1OoWEWn.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.mjs +1 -1
- package/dist/{framework-components-DynSvww4.mjs → framework-components-DCAT1uUC.mjs} +2 -2
- package/dist/{framework-components-DynSvww4.mjs.map → framework-components-DCAT1uUC.mjs.map} +1 -1
- package/dist/{init-B6kKrmf7.mjs → init-Kf3T4A4W.mjs} +3 -3
- package/dist/{init-B6kKrmf7.mjs.map → init-Kf3T4A4W.mjs.map} +1 -1
- package/dist/{inspect-live-schema-DVZlDlnF.mjs → inspect-live-schema-DTqflZ8X.mjs} +3 -3
- package/dist/{inspect-live-schema-DVZlDlnF.mjs.map → inspect-live-schema-DTqflZ8X.mjs.map} +1 -1
- package/dist/{migration-check-DzH1u-O1.mjs → migration-check-Ccyd0QKb.mjs} +2 -2
- package/dist/{migration-check-DzH1u-O1.mjs.map → migration-check-Ccyd0QKb.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-Cs7Ky-m5.mjs → migration-command-scaffold-DI7_SFL0.mjs} +3 -3
- package/dist/{migration-command-scaffold-Cs7Ky-m5.mjs.map → migration-command-scaffold-DI7_SFL0.mjs.map} +1 -1
- package/dist/{migration-graph-tree-render-BQdhKBO8.mjs → migration-graph-tree-render-DyDBuJEX.mjs} +25 -2
- package/dist/{migration-graph-tree-render-BQdhKBO8.mjs.map → migration-graph-tree-render-DyDBuJEX.mjs.map} +1 -1
- package/dist/migration-list-types-DV9PBc7Z.d.mts +23 -0
- package/dist/migration-list-types-DV9PBc7Z.d.mts.map +1 -0
- package/dist/{migration-log-BzPmks3c.mjs → migration-log-Des4seHP.mjs} +4 -4
- package/dist/{migration-log-BzPmks3c.mjs.map → migration-log-Des4seHP.mjs.map} +1 -1
- package/dist/{migration-plan-CaeKCKp4.mjs → migration-plan-DxDTBzGS.mjs} +5 -5
- package/dist/{migration-plan-CaeKCKp4.mjs.map → migration-plan-DxDTBzGS.mjs.map} +1 -1
- package/dist/migration-status-CCwqA-vi.mjs +416 -0
- package/dist/migration-status-CCwqA-vi.mjs.map +1 -0
- package/dist/{migrations-DQ1t3XFL.mjs → migrations-B3H6RTXb.mjs} +2 -2
- package/dist/{migrations-DQ1t3XFL.mjs.map → migrations-B3H6RTXb.mjs.map} +1 -1
- package/dist/{telemetry-Q88WHwlv.mjs → telemetry-LFFQmqHd.mjs} +2 -2
- package/dist/{telemetry-Q88WHwlv.mjs.map → telemetry-LFFQmqHd.mjs.map} +1 -1
- package/dist/{types-DiC683UW.d.mts → types-BdS8PoKM.d.mts} +2 -1
- package/dist/types-BdS8PoKM.d.mts.map +1 -0
- package/dist/{verify-CreSJ1Mz.mjs → verify-C0TARc6h.mjs} +2 -2
- package/dist/{verify-CreSJ1Mz.mjs.map → verify-C0TARc6h.mjs.map} +1 -1
- package/package.json +18 -19
- package/src/commands/migration-graph.ts +27 -74
- package/src/commands/migration-list.ts +1 -1
- package/src/commands/migration-status-overlay.ts +61 -0
- package/src/commands/migration-status.ts +391 -1047
- package/src/utils/formatters/migration-graph-tree-render.ts +27 -2
- package/dist/commands/migration-status.mjs.map +0 -1
- package/dist/graph-render-rFAqZujX.mjs +0 -1081
- package/dist/graph-render-rFAqZujX.mjs.map +0 -1
- package/dist/types-DiC683UW.d.mts.map +0 -1
- package/src/utils/formatters/graph-migration-mapper.ts +0 -235
- package/src/utils/formatters/graph-render.ts +0 -1323
- package/src/utils/formatters/graph-types.ts +0 -120
|
@@ -1,41 +1,32 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from '@prisma-next/framework-components/control';
|
|
6
|
-
import {
|
|
7
|
-
type ContractMarkerRecordLike,
|
|
8
|
-
type ContractSpaceAggregate,
|
|
9
|
-
graphWalkStrategy,
|
|
10
|
-
loadContractSpaceAggregate,
|
|
11
|
-
requireHeadRef,
|
|
1
|
+
import type { LedgerEntryRecord } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
ContractMarkerRecordLike,
|
|
4
|
+
ContractSpaceMember,
|
|
12
5
|
} from '@prisma-next/migration-tools/aggregate';
|
|
6
|
+
import { requireHeadRef } from '@prisma-next/migration-tools/aggregate';
|
|
13
7
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
14
8
|
import {
|
|
15
9
|
errorNoInvariantPath,
|
|
16
10
|
errorUnknownInvariant,
|
|
17
11
|
MigrationToolsError,
|
|
18
12
|
} from '@prisma-next/migration-tools/errors';
|
|
19
|
-
import type { MigrationEdge, MigrationGraph } from '@prisma-next/migration-tools/graph';
|
|
20
13
|
import {
|
|
21
14
|
findPath,
|
|
22
15
|
findPathWithDecision,
|
|
23
16
|
findReachableLeaves,
|
|
24
17
|
} from '@prisma-next/migration-tools/migration-graph';
|
|
25
|
-
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
26
18
|
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
27
19
|
import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
|
|
28
20
|
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
29
21
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
30
22
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
31
|
-
import {
|
|
23
|
+
import { dim, yellow } from 'colorette';
|
|
32
24
|
import { Command } from 'commander';
|
|
33
|
-
|
|
34
25
|
import { loadConfig } from '../config-loader';
|
|
35
26
|
import { createControlClient } from '../control-api/client';
|
|
36
27
|
import {
|
|
37
28
|
CliStructuredError,
|
|
38
|
-
|
|
29
|
+
errorDatabaseConnectionRequired,
|
|
39
30
|
errorUnexpected,
|
|
40
31
|
mapMigrationToolsError,
|
|
41
32
|
mapRefResolutionError,
|
|
@@ -49,518 +40,211 @@ import {
|
|
|
49
40
|
setCommandDescriptions,
|
|
50
41
|
setCommandExamples,
|
|
51
42
|
setCommandSeeAlso,
|
|
52
|
-
toPathDecisionResult,
|
|
53
43
|
toStructuralEdge,
|
|
54
44
|
} from '../utils/command-helpers';
|
|
55
45
|
import {
|
|
56
|
-
|
|
46
|
+
buildReadAggregate,
|
|
57
47
|
loadContractRawSafely,
|
|
58
|
-
refuseContractSpaceIntegrity,
|
|
59
48
|
refusePackageCorruptionOnAggregate,
|
|
60
49
|
} from '../utils/contract-space-aggregate-loader';
|
|
61
|
-
import {
|
|
50
|
+
import { buildMigrationGraphLayout } from '../utils/formatters/migration-graph-layout';
|
|
51
|
+
import { buildMigrationGraphRows } from '../utils/formatters/migration-graph-rows';
|
|
62
52
|
import {
|
|
63
|
-
type
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} from '../utils/formatters/
|
|
67
|
-
import {
|
|
68
|
-
extractRelevantSubgraph,
|
|
69
|
-
graphRenderer,
|
|
70
|
-
isLinearGraph,
|
|
71
|
-
} from '../utils/formatters/graph-render';
|
|
53
|
+
type MigrationEdgeAnnotation,
|
|
54
|
+
renderMigrationGraphTree,
|
|
55
|
+
} from '../utils/formatters/migration-graph-tree-render';
|
|
56
|
+
import type { MigrationListEntry } from '../utils/formatters/migration-list-types';
|
|
72
57
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
73
58
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
74
59
|
import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
|
|
75
|
-
import type { StatusDiagnostic
|
|
60
|
+
import type { StatusDiagnostic } from '../utils/migration-types';
|
|
76
61
|
import { handleResult } from '../utils/result-handler';
|
|
77
62
|
import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
|
|
63
|
+
import {
|
|
64
|
+
listRefsByContractHash,
|
|
65
|
+
migrationSpaceListEntriesFromAggregate,
|
|
66
|
+
runMigrationList,
|
|
67
|
+
} from './migration-list';
|
|
68
|
+
import {
|
|
69
|
+
appliedHashesFromLedger,
|
|
70
|
+
deriveStatusEdgeAnnotations,
|
|
71
|
+
statusForMigrationHash,
|
|
72
|
+
} from './migration-status-overlay';
|
|
78
73
|
|
|
79
74
|
interface MigrationStatusOptions extends CommonCommandOptions {
|
|
80
75
|
readonly db?: string;
|
|
81
76
|
readonly config?: string;
|
|
82
77
|
readonly to?: string;
|
|
83
78
|
readonly from?: string;
|
|
79
|
+
readonly space?: string;
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
export interface
|
|
87
|
-
readonly
|
|
88
|
-
readonly from: string;
|
|
89
|
-
readonly to: string;
|
|
90
|
-
readonly migrationHash: string;
|
|
91
|
-
readonly operationCount: number;
|
|
92
|
-
readonly operationSummary: string;
|
|
93
|
-
readonly hasDestructive: boolean;
|
|
94
|
-
readonly status: EdgeStatusKind | 'unknown';
|
|
82
|
+
export interface MigrationStatusMigrationEntry extends MigrationListEntry {
|
|
83
|
+
readonly status: 'applied' | 'pending' | null;
|
|
95
84
|
}
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
* Per-space status row in the aggregate-shaped status output.
|
|
99
|
-
*
|
|
100
|
-
* Surfaces, for each contract space:
|
|
101
|
-
*
|
|
102
|
-
* - `headHash`: the on-disk head ref's hash (where the space is going).
|
|
103
|
-
* - `markerHash`: the live marker hash for the space, or null if no
|
|
104
|
-
* marker has been written yet (greenfield, or pre-`migrate`).
|
|
105
|
-
* - `pendingCount`: number of migration edges between marker and head.
|
|
106
|
-
* Computed via {@link graphWalkStrategy}; 0 means the space is
|
|
107
|
-
* already at head.
|
|
108
|
-
* - `status`: convenience tag the formatter uses to pick a glyph.
|
|
109
|
-
* `'never-planned'` is reserved for spaces with non-empty head but
|
|
110
|
-
* no on-disk migrations — which shouldn't happen if the loader's
|
|
111
|
-
* integrity check passes.
|
|
112
|
-
*
|
|
113
|
-
* Online-only fields (`markerHash`, `status`) are absent when the
|
|
114
|
-
* command runs without a database connection.
|
|
115
|
-
*/
|
|
116
|
-
export interface MigrationStatusSpaceEntry {
|
|
86
|
+
export interface MigrationStatusSpaceResult {
|
|
117
87
|
readonly spaceId: string;
|
|
118
|
-
readonly
|
|
119
|
-
readonly
|
|
120
|
-
readonly
|
|
121
|
-
readonly pendingCount?: number;
|
|
122
|
-
readonly status?: 'up-to-date' | 'pending' | 'no-marker' | 'never-planned' | 'unreachable';
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Sum per-space `pendingCount` into a cross-space total, but only when
|
|
127
|
-
* every loaded space reports a defined `pendingCount`. Returns
|
|
128
|
-
* `undefined` if any space is on the marker-unknown / offline path
|
|
129
|
-
* (where `pendingCount` is intentionally absent), so JSON consumers can
|
|
130
|
-
* distinguish "no pending" from "unknown".
|
|
131
|
-
*/
|
|
132
|
-
export function computeTotalPendingAcrossSpaces(
|
|
133
|
-
spaces: readonly MigrationStatusSpaceEntry[],
|
|
134
|
-
): number | undefined {
|
|
135
|
-
if (spaces.length === 0) return undefined;
|
|
136
|
-
let total = 0;
|
|
137
|
-
for (const s of spaces) {
|
|
138
|
-
if (s.pendingCount === undefined) return undefined;
|
|
139
|
-
total += s.pendingCount;
|
|
140
|
-
}
|
|
141
|
-
return total;
|
|
88
|
+
readonly markerHash: string | null;
|
|
89
|
+
readonly targetHash: string;
|
|
90
|
+
readonly migrations: readonly MigrationStatusMigrationEntry[];
|
|
142
91
|
}
|
|
143
92
|
|
|
144
|
-
export type { StatusDiagnostic, StatusRef } from '../utils/migration-types';
|
|
145
|
-
|
|
146
93
|
export interface MigrationStatusResult {
|
|
147
94
|
readonly ok: true;
|
|
148
|
-
readonly
|
|
149
|
-
readonly migrations: readonly MigrationStatusEntry[];
|
|
150
|
-
readonly markerHash?: string;
|
|
151
|
-
readonly targetHash: string;
|
|
152
|
-
readonly contractHash: string;
|
|
153
|
-
readonly refs?: readonly StatusRef[];
|
|
154
|
-
/** Required invariants from the active ref, sorted ascending. Always present (`[]` when no `--ref` or the ref declares none) — knowable offline. */
|
|
155
|
-
readonly requiredInvariants: readonly string[];
|
|
156
|
-
/**
|
|
157
|
-
* Invariants the marker has applied at least once, intersected with
|
|
158
|
-
* `requiredInvariants` for display relevance. JSON consumers see only the
|
|
159
|
-
* subset overlapping the active ref's required set — the full unfiltered
|
|
160
|
-
* marker invariant list lives on `marker.invariants` (control plane) and
|
|
161
|
-
* is not surfaced here. Present only in `mode === 'online'`; absent when
|
|
162
|
-
* offline (the marker is unknown, not empty).
|
|
163
|
-
*/
|
|
164
|
-
readonly appliedInvariants?: readonly string[];
|
|
165
|
-
/** required − applied. Present only in `mode === 'online'`; absent when offline. */
|
|
166
|
-
readonly missingInvariants?: readonly string[];
|
|
167
|
-
readonly pathDecision?: {
|
|
168
|
-
readonly fromHash: string;
|
|
169
|
-
readonly toHash: string;
|
|
170
|
-
readonly alternativeCount: number;
|
|
171
|
-
readonly tieBreakReasons: readonly string[];
|
|
172
|
-
readonly refName?: string;
|
|
173
|
-
readonly requiredInvariants: readonly string[];
|
|
174
|
-
readonly satisfiedInvariants: readonly string[];
|
|
175
|
-
readonly selectedPath: readonly {
|
|
176
|
-
readonly dirName: string;
|
|
177
|
-
readonly migrationHash: string;
|
|
178
|
-
readonly from: string;
|
|
179
|
-
readonly to: string;
|
|
180
|
-
readonly invariants: readonly string[];
|
|
181
|
-
}[];
|
|
182
|
-
};
|
|
95
|
+
readonly spaces: readonly MigrationStatusSpaceResult[];
|
|
183
96
|
readonly summary: string;
|
|
97
|
+
readonly missingInvariantsLine?: string;
|
|
184
98
|
readonly diagnostics: readonly StatusDiagnostic[];
|
|
185
|
-
|
|
186
|
-
* Aggregate enumeration of every on-disk contract space (app +
|
|
187
|
-
* extensions), in canonical schedule order (extensions
|
|
188
|
-
* alphabetically, then app). Present whenever the aggregate loader
|
|
189
|
-
* succeeded; absent in early-error returns (e.g. unreadable
|
|
190
|
-
* migrations directory) where the existing diagnostics already
|
|
191
|
-
* surface the failure.
|
|
192
|
-
*
|
|
193
|
-
* The top-level fields (`migrations`, `markerHash`, `targetHash`,
|
|
194
|
-
* `pathDecision`, …) describe the **app member** specifically.
|
|
195
|
-
* Per-space detail for extension members lives only on this list.
|
|
196
|
-
*/
|
|
197
|
-
readonly spaces?: readonly MigrationStatusSpaceEntry[];
|
|
198
|
-
/** Cross-space pending-migration total (sum of `spaces[].pendingCount`). Present when `spaces` is. */
|
|
199
|
-
readonly totalPendingAcrossSpaces?: number;
|
|
200
|
-
readonly graph?: MigrationGraph;
|
|
201
|
-
readonly bundles?: readonly OnDiskMigrationPackage[];
|
|
202
|
-
readonly edgeStatuses?: readonly EdgeStatus[];
|
|
203
|
-
readonly activeRefHash?: string;
|
|
204
|
-
readonly activeRefName?: string;
|
|
205
|
-
readonly diverged?: boolean;
|
|
99
|
+
readonly treeSections: readonly MigrationStatusTreeSection[];
|
|
206
100
|
}
|
|
207
101
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const classes = new Map<string, number>();
|
|
215
|
-
for (const op of ops) {
|
|
216
|
-
classes.set(op.operationClass, (classes.get(op.operationClass) ?? 0) + 1);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const hasDestructive = classes.has('destructive');
|
|
220
|
-
const count = ops.length;
|
|
221
|
-
const noun = count === 1 ? 'op' : 'ops';
|
|
222
|
-
|
|
223
|
-
if (classes.size === 1) {
|
|
224
|
-
const cls = [...classes.keys()][0]!;
|
|
225
|
-
return { summary: `${count} ${noun} (all ${cls})`, hasDestructive };
|
|
226
|
-
}
|
|
102
|
+
export interface MigrationStatusTreeSection {
|
|
103
|
+
readonly spaceId: string;
|
|
104
|
+
readonly tree: string;
|
|
105
|
+
readonly showHeading: boolean;
|
|
106
|
+
}
|
|
227
107
|
|
|
228
|
-
|
|
229
|
-
if (destructiveCount) {
|
|
230
|
-
return { summary: `${count} ${noun} (${destructiveCount} destructive)`, hasDestructive };
|
|
231
|
-
}
|
|
108
|
+
export type { StatusDiagnostic, StatusRef } from '../utils/migration-types';
|
|
232
109
|
|
|
233
|
-
|
|
234
|
-
|
|
110
|
+
function shortDisplayHash(hash: string): string {
|
|
111
|
+
const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
112
|
+
return stripped.slice(0, 12);
|
|
235
113
|
}
|
|
236
114
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
*
|
|
240
|
-
* - **applied**: edge is on the path from root to the DB marker
|
|
241
|
-
* - **pending**: edge is on the path from the DB marker to the target
|
|
242
|
-
* (and the marker is reachable from root, i.e. it's on the same branch)
|
|
243
|
-
* - **unreachable**: edge is on the path from root to the target but the DB
|
|
244
|
-
* marker is on a different branch — `apply` can't reach these edges
|
|
245
|
-
* without the DB first moving to this branch
|
|
246
|
-
*
|
|
247
|
-
* Returns statuses only for edges that have a known status (skips offline
|
|
248
|
-
* and edges not on any relevant path).
|
|
249
|
-
*
|
|
250
|
-
* @internal Exported for testing only.
|
|
251
|
-
*/
|
|
252
|
-
export function deriveEdgeStatuses(
|
|
253
|
-
graph: MigrationGraph,
|
|
254
|
-
targetHash: string,
|
|
115
|
+
function resolveTargetHashForSpace(
|
|
116
|
+
member: ContractSpaceMember,
|
|
255
117
|
contractHash: string,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
const edgeKey = (e: MigrationEdge) => `${e.from}\0${e.to}`;
|
|
262
|
-
|
|
263
|
-
// No marker = empty DB — treat root as the marker (nothing applied, everything pending)
|
|
264
|
-
const effectiveMarker = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
265
|
-
|
|
266
|
-
const appliedPath =
|
|
267
|
-
markerHash !== undefined ? findPath(graph, EMPTY_CONTRACT_HASH, markerHash) : null;
|
|
268
|
-
|
|
269
|
-
const pendingPath = findPath(graph, effectiveMarker, targetHash);
|
|
270
|
-
const targetPath = findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
271
|
-
|
|
272
|
-
const statuses: EdgeStatus[] = [];
|
|
273
|
-
const assignedKeys = new Set<string>();
|
|
274
|
-
|
|
275
|
-
// Applied edges (root → marker)
|
|
276
|
-
if (appliedPath) {
|
|
277
|
-
for (const e of appliedPath) {
|
|
278
|
-
assignedKeys.add(edgeKey(e));
|
|
279
|
-
statuses.push({ dirName: e.dirName, status: 'applied' });
|
|
280
|
-
}
|
|
118
|
+
activeRefHash: string | undefined,
|
|
119
|
+
): string | undefined {
|
|
120
|
+
const graph = member.graph();
|
|
121
|
+
if (activeRefHash !== undefined && graph.nodes.has(activeRefHash)) {
|
|
122
|
+
return activeRefHash;
|
|
281
123
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (pendingPath) {
|
|
285
|
-
for (const e of pendingPath) {
|
|
286
|
-
assignedKeys.add(edgeKey(e));
|
|
287
|
-
statuses.push({ dirName: e.dirName, status: 'pending' });
|
|
288
|
-
}
|
|
124
|
+
if (graph.nodes.has(contractHash)) {
|
|
125
|
+
return contractHash;
|
|
289
126
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// and the contract is reachable from it)
|
|
293
|
-
if (
|
|
294
|
-
contractHash !== EMPTY_CONTRACT_HASH &&
|
|
295
|
-
contractHash !== targetHash &&
|
|
296
|
-
graph.nodes.has(contractHash)
|
|
297
|
-
) {
|
|
298
|
-
const beyondTarget = findPath(graph, targetHash, contractHash);
|
|
299
|
-
if (beyondTarget) {
|
|
300
|
-
for (const e of beyondTarget) {
|
|
301
|
-
if (!assignedKeys.has(edgeKey(e))) {
|
|
302
|
-
assignedKeys.add(edgeKey(e));
|
|
303
|
-
statuses.push({ dirName: e.dirName, status: 'pending' });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
127
|
+
if (graph.nodes.size === 0) {
|
|
128
|
+
return requireHeadRef(member).hash;
|
|
307
129
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// 1. Marker can't reach target at all (different branch entirely)
|
|
312
|
-
// 2. Marker reaches target via a different route, leaving some root→target
|
|
313
|
-
// edges orphaned (e.g. a fork where one branch was applied and apply
|
|
314
|
-
// will continue through the other)
|
|
315
|
-
if (targetPath) {
|
|
316
|
-
for (const e of targetPath) {
|
|
317
|
-
if (!assignedKeys.has(edgeKey(e))) {
|
|
318
|
-
statuses.push({ dirName: e.dirName, status: 'unreachable' });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
130
|
+
const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
|
|
131
|
+
if (leaves.length === 1) {
|
|
132
|
+
return leaves[0];
|
|
321
133
|
}
|
|
322
|
-
|
|
323
|
-
return statuses;
|
|
134
|
+
return undefined;
|
|
324
135
|
}
|
|
325
136
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
edgeStatuses?: readonly EdgeStatus[],
|
|
336
|
-
): MigrationStatusEntry[] {
|
|
337
|
-
const pkgByDirName = new Map(packages.map((p) => [p.dirName, p]));
|
|
338
|
-
const statusByDirName = edgeStatuses
|
|
339
|
-
? new Map(edgeStatuses.map((e) => [e.dirName, e.status]))
|
|
340
|
-
: undefined;
|
|
341
|
-
|
|
342
|
-
const markerInChain = markerHash === undefined || chain.some((e) => e.to === markerHash);
|
|
343
|
-
|
|
344
|
-
const entries: MigrationStatusEntry[] = [];
|
|
345
|
-
let reachedMarker = mode === 'online' && markerHash === undefined;
|
|
346
|
-
|
|
347
|
-
for (const migration of chain) {
|
|
348
|
-
const pkg = pkgByDirName.get(migration.dirName);
|
|
349
|
-
const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
|
|
350
|
-
const { summary, hasDestructive } = summarizeOps(ops);
|
|
351
|
-
|
|
352
|
-
let status: EdgeStatusKind | 'unknown';
|
|
353
|
-
const edgeStatus = statusByDirName?.get(migration.dirName);
|
|
354
|
-
if (edgeStatus) {
|
|
355
|
-
status = edgeStatus;
|
|
356
|
-
} else if (mode === 'offline' || !markerInChain) {
|
|
357
|
-
status = 'unknown';
|
|
358
|
-
} else if (reachedMarker) {
|
|
359
|
-
status = 'pending';
|
|
360
|
-
} else {
|
|
361
|
-
status = 'applied';
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
entries.push({
|
|
365
|
-
dirName: migration.dirName,
|
|
366
|
-
from: migration.from,
|
|
367
|
-
to: migration.to,
|
|
368
|
-
migrationHash: migration.migrationHash,
|
|
369
|
-
operationCount: ops.length,
|
|
370
|
-
operationSummary: summary,
|
|
371
|
-
hasDestructive,
|
|
372
|
-
status,
|
|
373
|
-
});
|
|
137
|
+
function buildStatusMigrations(
|
|
138
|
+
listMigrations: readonly MigrationListEntry[],
|
|
139
|
+
annotations: ReadonlyMap<string, MigrationEdgeAnnotation>,
|
|
140
|
+
): readonly MigrationStatusMigrationEntry[] {
|
|
141
|
+
return listMigrations.map((migration) => ({
|
|
142
|
+
...migration,
|
|
143
|
+
status: statusForMigrationHash(migration.migrationHash, annotations),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
374
146
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
147
|
+
function renderSpaceTree(args: {
|
|
148
|
+
readonly member: ContractSpaceMember;
|
|
149
|
+
readonly contractHash: string;
|
|
150
|
+
readonly markerHash: string | undefined;
|
|
151
|
+
readonly showDbMarker: boolean;
|
|
152
|
+
readonly targetHash: string;
|
|
153
|
+
readonly edgeAnnotations: ReadonlyMap<string, MigrationEdgeAnnotation>;
|
|
154
|
+
readonly colorize: boolean;
|
|
155
|
+
readonly glyphMode: 'unicode' | 'ascii';
|
|
156
|
+
}): string {
|
|
157
|
+
const graph = args.member.graph();
|
|
158
|
+
if (graph.nodes.size === 0) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
const refsByHash = listRefsByContractHash(args.member);
|
|
162
|
+
const rowModel = buildMigrationGraphRows(graph, { contractHash: args.contractHash });
|
|
163
|
+
const layout = buildMigrationGraphLayout(rowModel);
|
|
164
|
+
return renderMigrationGraphTree(layout, {
|
|
165
|
+
refsByHash,
|
|
166
|
+
...(args.showDbMarker && args.markerHash !== undefined ? { dbHash: args.markerHash } : {}),
|
|
167
|
+
contractHash: args.contractHash,
|
|
168
|
+
edgeAnnotationsByHash: args.edgeAnnotations,
|
|
169
|
+
colorize: args.colorize,
|
|
170
|
+
glyphMode: args.glyphMode,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
379
173
|
|
|
380
|
-
|
|
174
|
+
function countPending(migrations: readonly MigrationStatusMigrationEntry[]): number {
|
|
175
|
+
return migrations.filter((m) => m.status === 'pending').length;
|
|
381
176
|
}
|
|
382
177
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
* EMPTY→marker (applied history) + marker→target (pending edges). This ensures
|
|
392
|
-
* the displayed chain includes the marker node so applied/pending status is
|
|
393
|
-
* correct. Without this, BFS from EMPTY to target could pick a shortest path
|
|
394
|
-
* that bypasses the marker entirely (e.g. in a diamond graph), causing the
|
|
395
|
-
* marker to appear "diverged" when it isn't.
|
|
396
|
-
*/
|
|
397
|
-
function resolveDisplayChain(
|
|
398
|
-
graph: MigrationGraph,
|
|
399
|
-
targetHash: string,
|
|
400
|
-
markerHash: string | undefined,
|
|
401
|
-
): readonly MigrationEdge[] | null {
|
|
402
|
-
if (markerHash === undefined) {
|
|
403
|
-
return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
178
|
+
export function buildStatusHeadline(args: {
|
|
179
|
+
readonly pendingCount: number;
|
|
180
|
+
readonly targetHash: string;
|
|
181
|
+
readonly markerDiverged: boolean;
|
|
182
|
+
readonly markerHash: string | undefined;
|
|
183
|
+
}): string {
|
|
184
|
+
if (args.markerDiverged && args.markerHash !== undefined) {
|
|
185
|
+
return `Database marker ${shortDisplayHash(args.markerHash)} is not in the on-disk migration graph`;
|
|
404
186
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (!toMarker) return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
410
|
-
|
|
411
|
-
if (markerHash === targetHash) return toMarker;
|
|
412
|
-
|
|
413
|
-
const fromMarker = findPath(graph, markerHash, targetHash);
|
|
414
|
-
if (fromMarker) return [...toMarker, ...fromMarker];
|
|
415
|
-
|
|
416
|
-
// Marker is ahead of target (or on a disconnected branch).
|
|
417
|
-
// Try the inverse: target→marker. If it succeeds, the marker is ahead —
|
|
418
|
-
// show the full chain from EMPTY through the target and on to the marker.
|
|
419
|
-
const toTarget = findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
420
|
-
if (!toTarget) return null;
|
|
421
|
-
|
|
422
|
-
const targetToMarker = findPath(graph, targetHash, markerHash);
|
|
423
|
-
if (targetToMarker) return [...toTarget, ...targetToMarker];
|
|
424
|
-
|
|
425
|
-
// Genuinely disconnected — show EMPTY→target; caller handles divergence diagnostic.
|
|
426
|
-
return toTarget;
|
|
187
|
+
if (args.pendingCount === 0) {
|
|
188
|
+
return 'up to date';
|
|
189
|
+
}
|
|
190
|
+
return `${args.pendingCount} pending — run \`prisma-next migrate --to ${shortDisplayHash(args.targetHash)}\``;
|
|
427
191
|
}
|
|
428
192
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}): Promise<readonly MigrationStatusSpaceEntry[]> {
|
|
444
|
-
const declaredExtensions = toDeclaredExtensionsFromRaw(args.extensionPacks);
|
|
445
|
-
if (
|
|
446
|
-
refuseContractSpaceIntegrity(args.aggregate, {
|
|
447
|
-
declaredExtensions,
|
|
448
|
-
checkContracts: true,
|
|
449
|
-
})
|
|
450
|
-
) {
|
|
451
|
-
// Full integrity refusal (drift, layout violation, etc.) — surfacing
|
|
452
|
-
// it as a status diagnostic would duplicate `migration plan`'s job.
|
|
453
|
-
// The app pipeline still runs; extensions are simply not enumerated.
|
|
454
|
-
return [];
|
|
193
|
+
export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
194
|
+
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
195
|
+
const lines: string[] = [];
|
|
196
|
+
const pendingTotal = result.spaces.reduce(
|
|
197
|
+
(sum, space) => sum + countPending(space.migrations),
|
|
198
|
+
0,
|
|
199
|
+
);
|
|
200
|
+
const hasDivergence = result.diagnostics.some(
|
|
201
|
+
(d) => d.code === 'MIGRATION.MARKER_NOT_IN_HISTORY',
|
|
202
|
+
);
|
|
203
|
+
if (hasDivergence || pendingTotal > 0) {
|
|
204
|
+
lines.push(c(yellow, result.summary));
|
|
205
|
+
} else {
|
|
206
|
+
lines.push(result.summary);
|
|
455
207
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const liveMarker = args.markersBySpace?.get(member.spaceId) ?? null;
|
|
462
|
-
const isApp = member.spaceId === aggregate.app.spaceId;
|
|
463
|
-
// The aggregate passed the integrity gate above, so every member has
|
|
464
|
-
// a resolved head ref (a missing one would have refused the load).
|
|
465
|
-
const headRef = requireHeadRef(member);
|
|
466
|
-
|
|
467
|
-
if (member.graph().nodes.size === 0) {
|
|
468
|
-
rows.push({
|
|
469
|
-
spaceId: member.spaceId,
|
|
470
|
-
kind: isApp ? 'app' : 'extension',
|
|
471
|
-
headHash: headRef.hash,
|
|
472
|
-
...(args.markersBySpace !== null
|
|
473
|
-
? {
|
|
474
|
-
markerHash: liveMarker?.storageHash ?? null,
|
|
475
|
-
status: headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
|
|
476
|
-
pendingCount: 0,
|
|
477
|
-
}
|
|
478
|
-
: {}),
|
|
479
|
-
});
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
208
|
+
if (result.missingInvariantsLine !== undefined) {
|
|
209
|
+
lines.push(c(dim, result.missingInvariantsLine));
|
|
210
|
+
}
|
|
211
|
+
return lines.join('\n');
|
|
212
|
+
}
|
|
482
213
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
});
|
|
489
|
-
continue;
|
|
214
|
+
export function formatStatusHumanOutput(result: MigrationStatusResult, colorize: boolean): string {
|
|
215
|
+
const sections: string[] = [];
|
|
216
|
+
for (const section of result.treeSections) {
|
|
217
|
+
if (section.showHeading) {
|
|
218
|
+
sections.push(`${section.spaceId}:`);
|
|
490
219
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
aggregateTargetId: aggregate.targetId,
|
|
494
|
-
member,
|
|
495
|
-
currentMarker: liveMarker,
|
|
496
|
-
});
|
|
497
|
-
let pendingCount = 0;
|
|
498
|
-
let status: MigrationStatusSpaceEntry['status'];
|
|
499
|
-
if (walked.kind === 'ok') {
|
|
500
|
-
// Count pending *migrations* (graph edges), not operations: a
|
|
501
|
-
// single authored migration that lowers to N ops or zero ops
|
|
502
|
-
// both count as exactly one pending unit of work for the user.
|
|
503
|
-
pendingCount = walked.result.migrationEdges.length;
|
|
504
|
-
if (liveMarker === null) {
|
|
505
|
-
status = pendingCount === 0 ? 'no-marker' : 'pending';
|
|
506
|
-
} else {
|
|
507
|
-
status = pendingCount === 0 ? 'up-to-date' : 'pending';
|
|
508
|
-
}
|
|
220
|
+
if (section.tree.length > 0) {
|
|
221
|
+
sections.push(section.tree);
|
|
509
222
|
} else {
|
|
510
|
-
|
|
223
|
+
sections.push('(no migrations)');
|
|
511
224
|
}
|
|
512
|
-
|
|
513
|
-
rows.push({
|
|
514
|
-
spaceId: member.spaceId,
|
|
515
|
-
kind: isApp ? 'app' : 'extension',
|
|
516
|
-
headHash: headRef.hash,
|
|
517
|
-
markerHash: liveMarker?.storageHash ?? null,
|
|
518
|
-
pendingCount,
|
|
519
|
-
...(status ? { status } : {}),
|
|
520
|
-
});
|
|
225
|
+
sections.push('');
|
|
521
226
|
}
|
|
522
|
-
|
|
227
|
+
sections.push(formatStatusSummary(result, colorize));
|
|
228
|
+
return sections.join('\n').trimEnd();
|
|
523
229
|
}
|
|
524
230
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
family: config.family,
|
|
543
|
-
target: config.target,
|
|
544
|
-
adapter: config.adapter,
|
|
545
|
-
driver,
|
|
546
|
-
extensionPacks: config.extensionPacks ?? [],
|
|
547
|
-
});
|
|
548
|
-
try {
|
|
549
|
-
await client.connect(dbConnection);
|
|
550
|
-
await client.readMarker();
|
|
551
|
-
return ok(undefined);
|
|
552
|
-
} catch (error) {
|
|
553
|
-
if (CliStructuredError.is(error)) {
|
|
554
|
-
return notOk(error);
|
|
555
|
-
}
|
|
556
|
-
return notOk(
|
|
557
|
-
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
558
|
-
why: `Failed to read database marker: ${error instanceof Error ? error.message : String(error)}`,
|
|
559
|
-
}),
|
|
560
|
-
);
|
|
561
|
-
} finally {
|
|
562
|
-
await client.close();
|
|
563
|
-
}
|
|
231
|
+
async function readMarkersAndLedgers(args: {
|
|
232
|
+
readonly client: ReturnType<typeof createControlClient>;
|
|
233
|
+
readonly spaceIds: readonly string[];
|
|
234
|
+
}): Promise<{
|
|
235
|
+
readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike>;
|
|
236
|
+
readonly ledgersBySpace: ReadonlyMap<string, readonly LedgerEntryRecord[]>;
|
|
237
|
+
}> {
|
|
238
|
+
const markersBySpace = new Map<string, ContractMarkerRecordLike>();
|
|
239
|
+
const all = await args.client.readAllMarkers();
|
|
240
|
+
for (const [spaceId, marker] of all) {
|
|
241
|
+
markersBySpace.set(spaceId, marker);
|
|
242
|
+
}
|
|
243
|
+
const ledgersBySpace = new Map<string, readonly LedgerEntryRecord[]>();
|
|
244
|
+
for (const spaceId of args.spaceIds) {
|
|
245
|
+
ledgersBySpace.set(spaceId, await args.client.readLedger(spaceId));
|
|
246
|
+
}
|
|
247
|
+
return { markersBySpace, ledgersBySpace };
|
|
564
248
|
}
|
|
565
249
|
|
|
566
250
|
async function executeMigrationStatusCommand(
|
|
@@ -569,17 +253,24 @@ async function executeMigrationStatusCommand(
|
|
|
569
253
|
ui: TerminalUI,
|
|
570
254
|
): Promise<Result<MigrationStatusResult, CliStructuredError>> {
|
|
571
255
|
const config = await loadConfig(options.config);
|
|
572
|
-
const { configPath,
|
|
256
|
+
const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
|
|
573
257
|
options.config,
|
|
574
258
|
config,
|
|
575
259
|
);
|
|
576
260
|
|
|
577
261
|
const dbConnection = options.db ?? config.db?.connection;
|
|
578
262
|
const hasDriver = !!config.driver;
|
|
263
|
+
const usingFromOverride = options.from !== undefined;
|
|
264
|
+
|
|
265
|
+
if (!usingFromOverride && (!dbConnection || !hasDriver)) {
|
|
266
|
+
return notOk(
|
|
267
|
+
errorDatabaseConnectionRequired({
|
|
268
|
+
why: 'migration status needs a database connection to read the marker and ledger (or pass --from for offline path preview)',
|
|
269
|
+
retryCommand: 'prisma-next migration status --from <contract>',
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
579
273
|
|
|
580
|
-
let activeRefName: string | undefined;
|
|
581
|
-
let activeRefHash: string | undefined;
|
|
582
|
-
let activeRefEntry: RefEntry | undefined;
|
|
583
274
|
let allRefs: Refs = {};
|
|
584
275
|
try {
|
|
585
276
|
allRefs = await readRefs(refsDir);
|
|
@@ -604,93 +295,52 @@ async function executeMigrationStatusCommand(
|
|
|
604
295
|
});
|
|
605
296
|
}
|
|
606
297
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const deserializeContract = (json: unknown): Contract => familyInstance.deserializeContract(json);
|
|
611
|
-
const appContractStandIn = appContractStandInFromIdentity({
|
|
612
|
-
contractHash,
|
|
613
|
-
targetId: config.target.id,
|
|
614
|
-
targetFamily: config.target.familyId,
|
|
615
|
-
});
|
|
616
|
-
let appContractForLoad: Contract = appContractStandIn;
|
|
617
|
-
if (contractRawForAggregate !== null) {
|
|
618
|
-
try {
|
|
619
|
-
appContractForLoad = deserializeContract(contractRawForAggregate);
|
|
620
|
-
} catch (error) {
|
|
621
|
-
diagnostics.push({
|
|
622
|
-
code: 'CONTRACT.UNREADABLE',
|
|
623
|
-
severity: 'warn',
|
|
624
|
-
message: `Could not deserialize contract: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
625
|
-
hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
let aggregate: ContractSpaceAggregate;
|
|
631
|
-
try {
|
|
632
|
-
aggregate = await loadContractSpaceAggregate({
|
|
633
|
-
migrationsDir,
|
|
634
|
-
deserializeContract,
|
|
635
|
-
appContract: appContractForLoad,
|
|
636
|
-
});
|
|
637
|
-
} catch (error) {
|
|
638
|
-
if (MigrationToolsError.is(error)) {
|
|
639
|
-
return notOk(mapMigrationToolsError(error));
|
|
640
|
-
}
|
|
641
|
-
return notOk(
|
|
642
|
-
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
643
|
-
why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
|
|
644
|
-
}),
|
|
645
|
-
);
|
|
298
|
+
const loaded = await buildReadAggregate(config, { migrationsDir });
|
|
299
|
+
if (!loaded.ok) {
|
|
300
|
+
return notOk(loaded.failure);
|
|
646
301
|
}
|
|
647
302
|
|
|
303
|
+
const { aggregate } = loaded.value;
|
|
304
|
+
const contractRawForAggregate = await loadContractRawSafely(config);
|
|
648
305
|
if (contractRawForAggregate !== null) {
|
|
649
306
|
const corruptionFailure = refusePackageCorruptionOnAggregate(aggregate);
|
|
650
307
|
if (corruptionFailure) {
|
|
651
308
|
return notOk(corruptionFailure);
|
|
652
309
|
}
|
|
653
310
|
}
|
|
654
|
-
|
|
655
311
|
const appGraph = aggregate.app.graph();
|
|
656
312
|
|
|
313
|
+
let activeRefHash: string | undefined;
|
|
314
|
+
let activeRefName: string | undefined;
|
|
315
|
+
let activeRefEntry: RefEntry | undefined;
|
|
657
316
|
let fromOverrideHash: string | undefined;
|
|
658
317
|
|
|
659
|
-
if (options.to
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
activeRefName = resolvedRefName;
|
|
669
|
-
activeRefEntry = allRefs[resolvedRefName];
|
|
670
|
-
}
|
|
318
|
+
if (options.to) {
|
|
319
|
+
const refResult = parseContractRef(options.to, { graph: appGraph, refs: allRefs });
|
|
320
|
+
if (!refResult.ok) {
|
|
321
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
322
|
+
}
|
|
323
|
+
activeRefHash = refResult.value.hash;
|
|
324
|
+
if (refResult.value.provenance.kind === 'ref') {
|
|
325
|
+
activeRefName = refResult.value.provenance.refName;
|
|
326
|
+
activeRefEntry = allRefs[activeRefName];
|
|
671
327
|
}
|
|
328
|
+
}
|
|
672
329
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
fromOverrideHash = fromResult.value.hash;
|
|
330
|
+
if (options.from) {
|
|
331
|
+
const fromResult = parseContractRef(options.from, { graph: appGraph, refs: allRefs });
|
|
332
|
+
if (!fromResult.ok) {
|
|
333
|
+
return notOk(mapRefResolutionError(fromResult.failure));
|
|
679
334
|
}
|
|
335
|
+
fromOverrideHash = fromResult.value.hash;
|
|
680
336
|
}
|
|
681
337
|
|
|
682
338
|
const requiredInvariants: readonly string[] = [...(activeRefEntry?.invariants ?? [])].sort();
|
|
683
339
|
|
|
684
|
-
const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, entry]) => ({
|
|
685
|
-
name,
|
|
686
|
-
hash: entry.hash,
|
|
687
|
-
active: name === activeRefName,
|
|
688
|
-
}));
|
|
689
|
-
|
|
690
340
|
if (!flags.json && !flags.quiet) {
|
|
691
341
|
const details: Array<{ label: string; value: string }> = [
|
|
692
342
|
{ label: 'config', value: configPath },
|
|
693
|
-
{ label: 'migrations', value:
|
|
343
|
+
{ label: 'migrations', value: migrationsRelative },
|
|
694
344
|
];
|
|
695
345
|
if (dbConnection && hasDriver) {
|
|
696
346
|
details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
|
|
@@ -701,11 +351,8 @@ async function executeMigrationStatusCommand(
|
|
|
701
351
|
if (options.from) {
|
|
702
352
|
details.push({ label: 'from', value: options.from });
|
|
703
353
|
}
|
|
704
|
-
if (
|
|
705
|
-
details.push({
|
|
706
|
-
label: 'required',
|
|
707
|
-
value: formatInvariantList(activeRefEntry.invariants),
|
|
708
|
-
});
|
|
354
|
+
if (options.space) {
|
|
355
|
+
details.push({ label: 'space', value: options.space });
|
|
709
356
|
}
|
|
710
357
|
const header = formatStyledHeader({
|
|
711
358
|
command: 'migration status',
|
|
@@ -716,67 +363,23 @@ async function executeMigrationStatusCommand(
|
|
|
716
363
|
ui.stderr(header);
|
|
717
364
|
}
|
|
718
365
|
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return markerProbe;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
if (contractHash !== EMPTY_CONTRACT_HASH) {
|
|
730
|
-
diagnostics.push({
|
|
731
|
-
code: 'CONTRACT.AHEAD',
|
|
732
|
-
severity: 'warn',
|
|
733
|
-
message: 'No migration exists for the current contract',
|
|
734
|
-
hints: [
|
|
735
|
-
"Run 'prisma-next migration plan' to generate a migration for the current contract",
|
|
736
|
-
],
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
return ok({
|
|
740
|
-
ok: true,
|
|
741
|
-
mode: dbConnection && hasDriver ? 'online' : 'offline',
|
|
742
|
-
migrations: [],
|
|
743
|
-
targetHash: EMPTY_CONTRACT_HASH,
|
|
744
|
-
contractHash,
|
|
745
|
-
summary: 'No migrations found',
|
|
746
|
-
diagnostics,
|
|
747
|
-
requiredInvariants,
|
|
748
|
-
});
|
|
366
|
+
const listSpaces = await migrationSpaceListEntriesFromAggregate(aggregate, migrationsDir);
|
|
367
|
+
const listResult = runMigrationList({
|
|
368
|
+
spaces: listSpaces,
|
|
369
|
+
...ifDefined('spaceFilter', options.space),
|
|
370
|
+
});
|
|
371
|
+
if (!listResult.ok) {
|
|
372
|
+
return listResult;
|
|
749
373
|
}
|
|
750
374
|
|
|
751
|
-
|
|
375
|
+
const scopedSpaces = listResult.value.spaces;
|
|
376
|
+
const showSpaceHeadings = scopedSpaces.length > 1;
|
|
752
377
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
targetHash = contractHash;
|
|
757
|
-
} else {
|
|
758
|
-
const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
|
|
759
|
-
if (leaves.length === 1) {
|
|
760
|
-
targetHash = leaves[0];
|
|
761
|
-
} else {
|
|
762
|
-
diagnostics.push({
|
|
763
|
-
code: 'MIGRATION.DIVERGED',
|
|
764
|
-
severity: 'warn',
|
|
765
|
-
message: 'There are multiple valid migration paths — you must select a target',
|
|
766
|
-
hints: [
|
|
767
|
-
"Use '--to <contract>' to select a target",
|
|
768
|
-
"Or 'prisma-next ref set <name> <hash>' to create one",
|
|
769
|
-
],
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
}
|
|
378
|
+
let markersBySpace = new Map<string, ContractMarkerRecordLike>();
|
|
379
|
+
let ledgersBySpace = new Map<string, readonly LedgerEntryRecord[]>();
|
|
380
|
+
let connected = false;
|
|
773
381
|
|
|
774
|
-
|
|
775
|
-
let markerInvariants: readonly string[] = [];
|
|
776
|
-
let mode: 'online' | 'offline' = 'offline';
|
|
777
|
-
let allMarkers: ReadonlyMap<string, ContractMarkerRecordLike> | null = null;
|
|
778
|
-
|
|
779
|
-
if (dbConnection && hasDriver) {
|
|
382
|
+
if (dbConnection && hasDriver && !usingFromOverride) {
|
|
780
383
|
const client = createControlClient({
|
|
781
384
|
family: config.family,
|
|
782
385
|
target: config.target,
|
|
@@ -786,74 +389,32 @@ async function executeMigrationStatusCommand(
|
|
|
786
389
|
});
|
|
787
390
|
try {
|
|
788
391
|
await client.connect(dbConnection);
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// here it powers the aggregate status output.
|
|
797
|
-
//
|
|
798
|
-
// Probe for the method first so we only swallow the
|
|
799
|
-
// unsupported-method case: older family instances may not
|
|
800
|
-
// implement `readAllMarkers` (per-space enumeration then falls
|
|
801
|
-
// back to "marker unknown"). Real query / runtime errors from
|
|
802
|
-
// an instance that *does* expose the method must propagate up
|
|
803
|
-
// — otherwise transient DB failures would silently degrade
|
|
804
|
-
// status to "markers unknown".
|
|
805
|
-
if (typeof client.readAllMarkers === 'function') {
|
|
806
|
-
allMarkers = await client.readAllMarkers();
|
|
807
|
-
} else {
|
|
808
|
-
// Leaving `allMarkers` as `null` signals "unknown" to the
|
|
809
|
-
// aggregate loader (an empty `Map` would instead mean "every
|
|
810
|
-
// space has no marker", which is a different condition).
|
|
811
|
-
allMarkers = null;
|
|
812
|
-
}
|
|
392
|
+
connected = true;
|
|
393
|
+
const read = await readMarkersAndLedgers({
|
|
394
|
+
client,
|
|
395
|
+
spaceIds: scopedSpaces.map((s) => s.spaceId),
|
|
396
|
+
});
|
|
397
|
+
markersBySpace = new Map(read.markersBySpace);
|
|
398
|
+
ledgersBySpace = new Map(read.ledgersBySpace);
|
|
813
399
|
} catch (error) {
|
|
814
400
|
if (CliStructuredError.is(error)) {
|
|
815
401
|
return notOk(error);
|
|
816
402
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
403
|
+
return notOk(
|
|
404
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
405
|
+
why: `Failed to read database state: ${error instanceof Error ? error.message : String(error)}`,
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
820
408
|
} finally {
|
|
821
409
|
await client.close();
|
|
822
410
|
}
|
|
823
411
|
}
|
|
824
412
|
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
allMarkers = null;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
|
|
832
|
-
if (contractRawForAggregate !== null) {
|
|
833
|
-
try {
|
|
834
|
-
aggregateSpaces = await loadAggregateStatusSpaces({
|
|
835
|
-
aggregate,
|
|
836
|
-
extensionPacks: config.extensionPacks ?? [],
|
|
837
|
-
markersBySpace: allMarkers,
|
|
838
|
-
});
|
|
839
|
-
} catch {
|
|
840
|
-
aggregateSpaces = [];
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
const totalPendingAcrossSpaces = computeTotalPendingAcrossSpaces(aggregateSpaces);
|
|
844
|
-
|
|
845
|
-
// Pre-check unknown invariants. Online: union the graph's declared
|
|
846
|
-
// invariants with the marker's recorded set so a retired-but-applied
|
|
847
|
-
// invariant doesn't surface as MIGRATION.UNKNOWN_INVARIANT — apply would
|
|
848
|
-
// route fine because marker subtraction empties `effectiveRequired`.
|
|
849
|
-
// Offline: keep the check graph-strict (the marker is unknown, and a
|
|
850
|
-
// missing declarer is the dominant signal we can offer).
|
|
851
|
-
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
852
|
-
const declared = collectDeclaredInvariants(graph);
|
|
413
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0 && connected) {
|
|
414
|
+
const declared = collectDeclaredInvariants(appGraph);
|
|
415
|
+
const markerInvariants = markersBySpace.get(aggregate.app.spaceId)?.invariants ?? [];
|
|
853
416
|
const known = new Set<string>(declared);
|
|
854
|
-
|
|
855
|
-
for (const id of markerInvariants) known.add(id);
|
|
856
|
-
}
|
|
417
|
+
for (const id of markerInvariants) known.add(id);
|
|
857
418
|
const unknown = activeRefEntry.invariants.filter((id) => !known.has(id));
|
|
858
419
|
if (unknown.length > 0) {
|
|
859
420
|
return notOk(
|
|
@@ -868,247 +429,174 @@ async function executeMigrationStatusCommand(
|
|
|
868
429
|
}
|
|
869
430
|
}
|
|
870
431
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
) {
|
|
885
|
-
const
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
432
|
+
const showAppliedOverlay = connected && !usingFromOverride;
|
|
433
|
+
const showDbMarker = connected && !usingFromOverride;
|
|
434
|
+
const glyphMode = ui.resolveGlyphMode(false);
|
|
435
|
+
const colorize = flags.color !== false;
|
|
436
|
+
|
|
437
|
+
const statusSpaces: MigrationStatusSpaceResult[] = [];
|
|
438
|
+
const treeSections: MigrationStatusTreeSection[] = [];
|
|
439
|
+
let markerDiverged = false;
|
|
440
|
+
let markerCannotReachTarget = false;
|
|
441
|
+
let headlineTargetHash = activeRefHash ?? contractHash;
|
|
442
|
+
let totalPending = 0;
|
|
443
|
+
let hasAmbiguousTarget = false;
|
|
444
|
+
|
|
445
|
+
for (const spaceEntry of scopedSpaces) {
|
|
446
|
+
const member = aggregate.space(spaceEntry.spaceId);
|
|
447
|
+
if (member === undefined) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const graph = member.graph();
|
|
451
|
+
const spaceContractHash = member.contract().storage.storageHash;
|
|
452
|
+
const targetHash = resolveTargetHashForSpace(member, spaceContractHash, activeRefHash);
|
|
453
|
+
if (targetHash === undefined) {
|
|
454
|
+
hasAmbiguousTarget = true;
|
|
455
|
+
diagnostics.push({
|
|
456
|
+
code: 'MIGRATION.DIVERGED',
|
|
457
|
+
severity: 'warn',
|
|
458
|
+
message: 'There are multiple valid migration paths — you must select a target',
|
|
459
|
+
hints: [
|
|
460
|
+
"Use '--to <contract>' to select a target",
|
|
461
|
+
"Or 'prisma-next ref set <name> <hash>' to create one",
|
|
462
|
+
],
|
|
463
|
+
});
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (spaceEntry.spaceId === aggregate.app.spaceId) {
|
|
467
|
+
headlineTargetHash = targetHash;
|
|
899
468
|
}
|
|
900
|
-
diagnostics.push({
|
|
901
|
-
code: 'MIGRATION.MARKER_NOT_IN_HISTORY',
|
|
902
|
-
severity: 'warn',
|
|
903
|
-
message:
|
|
904
|
-
'Database was updated outside the migration system (marker does not match any migration)',
|
|
905
|
-
hints,
|
|
906
|
-
});
|
|
907
|
-
return ok({
|
|
908
|
-
ok: true,
|
|
909
|
-
mode,
|
|
910
|
-
migrations: [],
|
|
911
|
-
targetHash: EMPTY_CONTRACT_HASH,
|
|
912
|
-
contractHash,
|
|
913
|
-
summary: `${bundles.length} migration(s) on disk`,
|
|
914
|
-
diagnostics,
|
|
915
|
-
markerHash,
|
|
916
|
-
requiredInvariants,
|
|
917
|
-
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
469
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
470
|
+
const markerRecord = markersBySpace.get(spaceEntry.spaceId);
|
|
471
|
+
const markerHash = usingFromOverride
|
|
472
|
+
? fromOverrideHash
|
|
473
|
+
: (markerRecord?.storageHash ?? undefined);
|
|
474
|
+
const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
475
|
+
const markerInGraph =
|
|
476
|
+
markerHash === undefined || graph.nodes.has(markerHash) || markerHash === spaceContractHash;
|
|
477
|
+
if (
|
|
478
|
+
connected &&
|
|
479
|
+
!usingFromOverride &&
|
|
480
|
+
markerHash !== undefined &&
|
|
481
|
+
markerInGraph &&
|
|
482
|
+
markerHash !== targetHash &&
|
|
483
|
+
findPath(graph, originHash, targetHash) === null
|
|
484
|
+
) {
|
|
485
|
+
markerCannotReachTarget = true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (connected && !usingFromOverride && markerHash !== undefined && !markerInGraph) {
|
|
489
|
+
markerDiverged = true;
|
|
490
|
+
diagnostics.push({
|
|
491
|
+
code: 'MIGRATION.MARKER_NOT_IN_HISTORY',
|
|
492
|
+
severity: 'warn',
|
|
493
|
+
message:
|
|
494
|
+
'Database was updated outside the migration system (marker does not match any migration)',
|
|
495
|
+
hints: [
|
|
496
|
+
"Run 'prisma-next db sign' to overwrite the marker if the database already matches the contract",
|
|
497
|
+
"Run 'prisma-next db update' to push the current contract to the database",
|
|
498
|
+
],
|
|
499
|
+
});
|
|
500
|
+
}
|
|
929
501
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
// (b) marker === contract and both off-graph (marker-not-in-graph diagnostic covers it).
|
|
933
|
-
if (
|
|
934
|
-
targetHash &&
|
|
935
|
-
contractHash !== EMPTY_CONTRACT_HASH &&
|
|
936
|
-
!graph.nodes.has(contractHash) &&
|
|
937
|
-
markerHash !== contractHash
|
|
938
|
-
) {
|
|
939
|
-
diagnostics.push({
|
|
940
|
-
code: 'CONTRACT.AHEAD',
|
|
941
|
-
severity: 'warn',
|
|
942
|
-
message: 'Contract has changed since the last migration was planned',
|
|
943
|
-
hints: ["Run 'prisma-next migration plan' to generate a migration for the current contract"],
|
|
944
|
-
});
|
|
945
|
-
}
|
|
502
|
+
const ledger = ledgersBySpace.get(spaceEntry.spaceId) ?? [];
|
|
503
|
+
const appliedHashes = showAppliedOverlay ? appliedHashesFromLedger(ledger) : new Set<string>();
|
|
946
504
|
|
|
947
|
-
|
|
948
|
-
return ok({
|
|
949
|
-
ok: true,
|
|
950
|
-
mode,
|
|
951
|
-
migrations: [],
|
|
952
|
-
targetHash: EMPTY_CONTRACT_HASH,
|
|
953
|
-
contractHash,
|
|
954
|
-
summary: `${bundles.length} migration(s) on disk`,
|
|
955
|
-
diagnostics,
|
|
956
|
-
...ifDefined('markerHash', markerHash),
|
|
957
|
-
requiredInvariants,
|
|
958
|
-
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
505
|
+
const annotations = deriveStatusEdgeAnnotations({
|
|
959
506
|
graph,
|
|
960
|
-
|
|
961
|
-
|
|
507
|
+
targetHash,
|
|
508
|
+
originHash,
|
|
509
|
+
appliedMigrationHashes: appliedHashes,
|
|
510
|
+
showAppliedOverlay,
|
|
511
|
+
});
|
|
512
|
+
const tree = renderSpaceTree({
|
|
513
|
+
member,
|
|
514
|
+
contractHash: spaceContractHash,
|
|
515
|
+
markerHash,
|
|
516
|
+
showDbMarker,
|
|
517
|
+
targetHash,
|
|
518
|
+
edgeAnnotations: annotations,
|
|
519
|
+
colorize,
|
|
520
|
+
glyphMode,
|
|
521
|
+
});
|
|
522
|
+
const migrations = buildStatusMigrations(spaceEntry.migrations, annotations);
|
|
523
|
+
const pending = countPending(migrations);
|
|
524
|
+
totalPending += pending;
|
|
525
|
+
|
|
526
|
+
statusSpaces.push({
|
|
527
|
+
spaceId: spaceEntry.spaceId,
|
|
528
|
+
markerHash: markerHash ?? null,
|
|
529
|
+
targetHash,
|
|
530
|
+
migrations,
|
|
531
|
+
});
|
|
532
|
+
treeSections.push({
|
|
533
|
+
spaceId: spaceEntry.spaceId,
|
|
534
|
+
tree,
|
|
535
|
+
showHeading: showSpaceHeadings,
|
|
962
536
|
});
|
|
963
537
|
}
|
|
964
538
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
return notOk(
|
|
969
|
-
errorRuntime('Cannot reconstruct migration history', {
|
|
970
|
-
why: `No path from ${EMPTY_CONTRACT_HASH} to target ${targetHash}`,
|
|
971
|
-
fix: 'The migration history may have gaps. Check the migrations directory for missing or corrupted packages.',
|
|
972
|
-
}),
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const edgeStatuses = deriveEdgeStatuses(graph, targetHash, contractHash, markerHash, mode);
|
|
977
|
-
const entries = buildMigrationEntries(chain, bundles, mode, markerHash, edgeStatuses);
|
|
978
|
-
|
|
979
|
-
const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
|
|
980
|
-
const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
|
|
981
|
-
|
|
982
|
-
let appliedInvariants: readonly string[] | undefined;
|
|
983
|
-
let missingInvariants: readonly string[] | undefined;
|
|
984
|
-
let effectiveRequired = new Set<string>();
|
|
985
|
-
if (mode === 'online') {
|
|
986
|
-
// Mirrors `migrate.ts`: compute `effectiveRequired = required −
|
|
987
|
-
// marker.invariants` directly, then derive the display fields from it.
|
|
988
|
-
// `appliedInvariants` is the intersection (`required ∩ marker`), which
|
|
989
|
-
// is what JSON consumers see for the active ref; the unfiltered set
|
|
990
|
-
// lives on `marker.invariants`.
|
|
539
|
+
let missingInvariantsLine: string | undefined;
|
|
540
|
+
if (connected && requiredInvariants.length > 0) {
|
|
541
|
+
const markerInvariants = markersBySpace.get(aggregate.app.spaceId)?.invariants ?? [];
|
|
991
542
|
const markerSet = new Set(markerInvariants);
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
} else if (markerHash === undefined) {
|
|
1016
|
-
summary = `${pendingCount} pending migration(s) — database has no marker`;
|
|
1017
|
-
} else {
|
|
1018
|
-
summary = `${pendingCount} pending migration(s) — run 'prisma-next migrate' to apply`;
|
|
543
|
+
const missing = requiredInvariants.filter((id) => !markerSet.has(id));
|
|
544
|
+
if (missing.length > 0) {
|
|
545
|
+
missingInvariantsLine = `missing invariant(s): ${missing.join(', ')}`;
|
|
546
|
+
if (activeRefHash !== undefined) {
|
|
547
|
+
const originHash =
|
|
548
|
+
markersBySpace.get(aggregate.app.spaceId)?.storageHash ?? EMPTY_CONTRACT_HASH;
|
|
549
|
+
const outcome = findPathWithDecision(appGraph, originHash, activeRefHash, {
|
|
550
|
+
...ifDefined('refName', activeRefName),
|
|
551
|
+
required: new Set(missing),
|
|
552
|
+
});
|
|
553
|
+
if (outcome.kind === 'unsatisfiable') {
|
|
554
|
+
return notOk(
|
|
555
|
+
mapMigrationToolsError(
|
|
556
|
+
errorNoInvariantPath({
|
|
557
|
+
...ifDefined('refName', activeRefName),
|
|
558
|
+
required: [...missing].sort(),
|
|
559
|
+
missing: outcome.missing,
|
|
560
|
+
structuralPath: outcome.structuralPath.map(toStructuralEdge),
|
|
561
|
+
}),
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
1019
566
|
}
|
|
1020
|
-
} else {
|
|
1021
|
-
summary = `${entries.length} migration(s) on disk`;
|
|
1022
567
|
}
|
|
1023
568
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return notOk(
|
|
1036
|
-
mapMigrationToolsError(
|
|
1037
|
-
errorNoInvariantPath({
|
|
1038
|
-
...ifDefined('refName', activeRefName),
|
|
1039
|
-
required: [...effectiveRequired].sort(),
|
|
1040
|
-
missing: outcome.missing,
|
|
1041
|
-
structuralPath: outcome.structuralPath.map(toStructuralEdge),
|
|
1042
|
-
}),
|
|
1043
|
-
),
|
|
1044
|
-
);
|
|
1045
|
-
} else {
|
|
1046
|
-
// outcome.kind === 'unreachable' — origin (marker) has no structural
|
|
1047
|
-
// path to the active target. `pendingCount` and `hasInvariantWork`
|
|
1048
|
-
// both report zero in this case, but emitting MIGRATION.UP_TO_DATE
|
|
1049
|
-
// would be wrong: the database simply cannot reach the requested
|
|
1050
|
-
// ref/contract from its current state. Suppress UP_TO_DATE below.
|
|
1051
|
-
routingUnreachable = true;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
569
|
+
const appMarkerHash = markersBySpace.get(aggregate.app.spaceId)?.storageHash;
|
|
570
|
+
const summary = hasAmbiguousTarget
|
|
571
|
+
? 'Multiple valid migration paths — select a target with --to'
|
|
572
|
+
: markerCannotReachTarget
|
|
573
|
+
? 'Database marker cannot reach the selected target'
|
|
574
|
+
: buildStatusHeadline({
|
|
575
|
+
pendingCount: totalPending,
|
|
576
|
+
targetHash: headlineTargetHash,
|
|
577
|
+
markerDiverged,
|
|
578
|
+
markerHash: appMarkerHash,
|
|
579
|
+
});
|
|
1054
580
|
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1064
|
-
diagnostics.push({
|
|
1065
|
-
code: 'MIGRATION.DATABASE_BEHIND',
|
|
1066
|
-
severity: 'info',
|
|
1067
|
-
message: `${pendingCount} migration(s) pending`,
|
|
1068
|
-
hints: ["Run 'prisma-next migrate' to apply pending migrations"],
|
|
1069
|
-
});
|
|
1070
|
-
} else if (hasInvariantWork) {
|
|
1071
|
-
diagnostics.push({
|
|
1072
|
-
code: 'MIGRATION.INVARIANTS_PENDING',
|
|
1073
|
-
severity: 'info',
|
|
1074
|
-
message: `Missing required invariant(s): ${missingList}`,
|
|
1075
|
-
hints: [
|
|
1076
|
-
`Run 'prisma-next migrate --to ${activeRefName ?? '<ref>'}' to apply a path that covers the required invariants`,
|
|
1077
|
-
],
|
|
1078
|
-
});
|
|
1079
|
-
} else if (!routingUnreachable) {
|
|
1080
|
-
diagnostics.push({
|
|
1081
|
-
code: 'MIGRATION.UP_TO_DATE',
|
|
1082
|
-
severity: 'info',
|
|
1083
|
-
message: 'Database is up to date',
|
|
1084
|
-
hints: [],
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
581
|
+
if (scopedSpaces.every((s) => s.migrations.length === 0)) {
|
|
582
|
+
return ok({
|
|
583
|
+
ok: true,
|
|
584
|
+
spaces: statusSpaces,
|
|
585
|
+
summary: 'No migrations found',
|
|
586
|
+
diagnostics,
|
|
587
|
+
treeSections,
|
|
588
|
+
...ifDefined('missingInvariantsLine', missingInvariantsLine),
|
|
589
|
+
});
|
|
1087
590
|
}
|
|
1088
591
|
|
|
1089
|
-
|
|
592
|
+
return ok({
|
|
1090
593
|
ok: true,
|
|
1091
|
-
|
|
1092
|
-
migrations: entries,
|
|
1093
|
-
targetHash,
|
|
1094
|
-
contractHash,
|
|
594
|
+
spaces: statusSpaces,
|
|
1095
595
|
summary,
|
|
1096
596
|
diagnostics,
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
...ifDefined('missingInvariants', missingInvariants),
|
|
1101
|
-
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
1102
|
-
...ifDefined('pathDecision', pathDecision),
|
|
1103
|
-
graph,
|
|
1104
|
-
bundles,
|
|
1105
|
-
edgeStatuses,
|
|
1106
|
-
...ifDefined('activeRefHash', activeRefHash),
|
|
1107
|
-
...ifDefined('activeRefName', activeRefName),
|
|
1108
|
-
spaces: aggregateSpaces,
|
|
1109
|
-
...ifDefined('totalPendingAcrossSpaces', totalPendingAcrossSpaces),
|
|
1110
|
-
};
|
|
1111
|
-
return ok(result);
|
|
597
|
+
treeSections,
|
|
598
|
+
...ifDefined('missingInvariantsLine', missingInvariantsLine),
|
|
599
|
+
});
|
|
1112
600
|
}
|
|
1113
601
|
|
|
1114
602
|
export function createMigrationStatusCommand(): Command {
|
|
@@ -1124,6 +612,7 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1124
612
|
setCommandExamples(command, [
|
|
1125
613
|
'prisma-next migration status --db $DATABASE_URL',
|
|
1126
614
|
'prisma-next migration status --to production --db $DATABASE_URL',
|
|
615
|
+
'prisma-next migration status --from sha256:abc --to production',
|
|
1127
616
|
]);
|
|
1128
617
|
setCommandSeeAlso(command, [
|
|
1129
618
|
{ verb: 'migration log', oneLiner: 'Show executed migration history' },
|
|
@@ -1134,6 +623,7 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1134
623
|
addGlobalOptions(command)
|
|
1135
624
|
.option('--db <url>', 'Database connection string')
|
|
1136
625
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
626
|
+
.option('--space <id>', 'Narrow output to a single contract space')
|
|
1137
627
|
.option(
|
|
1138
628
|
'--to <contract>',
|
|
1139
629
|
'Target contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
@@ -1150,48 +640,25 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1150
640
|
|
|
1151
641
|
const exitCode = handleResult(result, flags, ui, (statusResult) => {
|
|
1152
642
|
if (flags.json) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
643
|
+
ui.output(
|
|
644
|
+
JSON.stringify(
|
|
645
|
+
{
|
|
646
|
+
ok: true,
|
|
647
|
+
spaces: statusResult.spaces,
|
|
648
|
+
summary: statusResult.summary,
|
|
649
|
+
...(statusResult.diagnostics.length > 0
|
|
650
|
+
? { diagnostics: statusResult.diagnostics }
|
|
651
|
+
: {}),
|
|
652
|
+
...(statusResult.missingInvariantsLine
|
|
653
|
+
? { missingInvariants: statusResult.missingInvariantsLine }
|
|
654
|
+
: {}),
|
|
655
|
+
},
|
|
656
|
+
null,
|
|
657
|
+
2,
|
|
658
|
+
),
|
|
659
|
+
);
|
|
1163
660
|
} else if (!flags.quiet) {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
if (statusResult.graph) {
|
|
1167
|
-
const renderInput = migrationGraphToRenderInput({
|
|
1168
|
-
graph: statusResult.graph,
|
|
1169
|
-
mode: statusResult.mode,
|
|
1170
|
-
markerHash: statusResult.markerHash,
|
|
1171
|
-
contractHash: statusResult.contractHash,
|
|
1172
|
-
refs: statusResult.refs,
|
|
1173
|
-
activeRefHash: statusResult.activeRefHash,
|
|
1174
|
-
activeRefName: statusResult.activeRefName,
|
|
1175
|
-
edgeStatuses: statusResult.edgeStatuses,
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
const graphToRender = statusResult.diverged
|
|
1179
|
-
? renderInput.graph
|
|
1180
|
-
: extractRelevantSubgraph(renderInput.graph, renderInput.relevantPaths);
|
|
1181
|
-
const dagreOptions = isLinearGraph(graphToRender) ? { ranksep: 1 } : undefined;
|
|
1182
|
-
const renderOptions = {
|
|
1183
|
-
...renderInput.options,
|
|
1184
|
-
colorize,
|
|
1185
|
-
...ifDefined('dagreOptions', dagreOptions),
|
|
1186
|
-
};
|
|
1187
|
-
const graphOutput = graphRenderer.render(graphToRender, renderOptions);
|
|
1188
|
-
ui.log(graphOutput);
|
|
1189
|
-
if (statusResult.mode === 'online') {
|
|
1190
|
-
ui.log(formatLegend(colorize));
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
ui.log('');
|
|
1194
|
-
ui.log(formatStatusSummary(statusResult, colorize));
|
|
661
|
+
ui.output(formatStatusHumanOutput(statusResult, flags.color !== false));
|
|
1195
662
|
}
|
|
1196
663
|
});
|
|
1197
664
|
|
|
@@ -1200,126 +667,3 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1200
667
|
|
|
1201
668
|
return command;
|
|
1202
669
|
}
|
|
1203
|
-
|
|
1204
|
-
function formatLegend(colorize: boolean): string {
|
|
1205
|
-
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
1206
|
-
const parts = [
|
|
1207
|
-
`${c(cyan, '✓')} applied`,
|
|
1208
|
-
`${c(yellow, '⧗')} pending`,
|
|
1209
|
-
`${c(magenta, '✗')} unreachable`,
|
|
1210
|
-
];
|
|
1211
|
-
return c(dim, parts.join(' '));
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
1215
|
-
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
1216
|
-
const lines: string[] = [];
|
|
1217
|
-
|
|
1218
|
-
const hasUnknown = result.migrations.some((e) => e.status === 'unknown');
|
|
1219
|
-
const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
|
|
1220
|
-
|
|
1221
|
-
const hasWarnings = result.diagnostics?.some((d) => d.severity === 'warn') ?? false;
|
|
1222
|
-
// INVARIANTS_PENDING is filed at severity 'info' (per ADR 208) so the
|
|
1223
|
-
// warn-severity check above doesn't see it. It still represents pending
|
|
1224
|
-
// work, so it must promote the summary off the success icon.
|
|
1225
|
-
const hasInvariantPending =
|
|
1226
|
-
result.diagnostics?.some((d) => d.code === 'MIGRATION.INVARIANTS_PENDING') ?? false;
|
|
1227
|
-
|
|
1228
|
-
if (result.mode === 'online') {
|
|
1229
|
-
if (hasUnknown || hasWarnings) {
|
|
1230
|
-
lines.push(`${c(yellow, '⚠')} ${result.summary}`);
|
|
1231
|
-
} else if (pendingCount === 0 && !hasInvariantPending) {
|
|
1232
|
-
lines.push(`${c(cyan, '✔')} ${result.summary}`);
|
|
1233
|
-
} else {
|
|
1234
|
-
lines.push(`${c(yellow, '⧗')} ${result.summary}`);
|
|
1235
|
-
}
|
|
1236
|
-
} else {
|
|
1237
|
-
lines.push(result.summary);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (result.requiredInvariants.length > 0) {
|
|
1241
|
-
if (result.appliedInvariants !== undefined && result.missingInvariants !== undefined) {
|
|
1242
|
-
lines.push(`${c(dim, 'applied ')}${formatInvariantList(result.appliedInvariants)}`);
|
|
1243
|
-
lines.push(`${c(dim, 'missing ')}${formatInvariantList(result.missingInvariants)}`);
|
|
1244
|
-
} else {
|
|
1245
|
-
lines.push(`${c(dim, 'applied ')}(unknown — connect a database to evaluate)`);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
|
|
1250
|
-
for (const diag of warnings) {
|
|
1251
|
-
lines.push(`${c(yellow, '⚠')} ${diag.message}`);
|
|
1252
|
-
for (const hint of diag.hints) {
|
|
1253
|
-
lines.push(` ${c(dim, hint)}`);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Per-space section. Suppressed when there's no extension space —
|
|
1258
|
-
// the top-level output already covers the app member.
|
|
1259
|
-
// When extensions exist, render every space (including the app)
|
|
1260
|
-
// for consistency, plus a cross-space pending total + apply hint.
|
|
1261
|
-
if (result.spaces?.some((s) => s.kind === 'extension')) {
|
|
1262
|
-
const total = result.totalPendingAcrossSpaces ?? 0;
|
|
1263
|
-
lines.push('');
|
|
1264
|
-
lines.push(c(dim, 'spaces'));
|
|
1265
|
-
for (const space of result.spaces) {
|
|
1266
|
-
lines.push(formatSpaceLine(space, c));
|
|
1267
|
-
}
|
|
1268
|
-
if (total > 0) {
|
|
1269
|
-
lines.push('');
|
|
1270
|
-
lines.push(
|
|
1271
|
-
`${c(yellow, '⧗')} ${total} pending migration(s) across ${result.spaces.length} space(s) — run 'prisma-next migrate' to apply`,
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
return lines.join('\n');
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function formatSpaceLine(
|
|
1280
|
-
space: MigrationStatusSpaceEntry,
|
|
1281
|
-
c: (fn: (s: string) => string, s: string) => string,
|
|
1282
|
-
): string {
|
|
1283
|
-
const glyph = (() => {
|
|
1284
|
-
if (space.status === 'up-to-date' || space.status === 'no-marker') return c(cyan, '✓');
|
|
1285
|
-
if (space.status === 'pending') return c(yellow, '⧗');
|
|
1286
|
-
if (space.status === 'unreachable' || space.status === 'never-planned') return c(magenta, '✗');
|
|
1287
|
-
return ' ';
|
|
1288
|
-
})();
|
|
1289
|
-
const tag = space.kind === 'app' ? '[app]' : '[ext]';
|
|
1290
|
-
const head = space.headHash.slice(0, 8);
|
|
1291
|
-
const marker =
|
|
1292
|
-
space.markerHash === undefined
|
|
1293
|
-
? '(unknown)'
|
|
1294
|
-
: space.markerHash === null
|
|
1295
|
-
? '(no marker)'
|
|
1296
|
-
: space.markerHash.slice(0, 8);
|
|
1297
|
-
const pending =
|
|
1298
|
-
space.pendingCount === undefined
|
|
1299
|
-
? ''
|
|
1300
|
-
: space.pendingCount === 0
|
|
1301
|
-
? c(dim, ' (up to date)')
|
|
1302
|
-
: c(yellow, ` (${space.pendingCount} pending)`);
|
|
1303
|
-
return ` ${glyph} ${c(dim, tag)} ${space.spaceId} → head ${c(dim, head)}, marker ${c(dim, marker)}${pending}`;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function formatInvariantList(ids: readonly string[]): string {
|
|
1307
|
-
return ids.length === 0 ? '(none)' : ids.join(', ');
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
function summarizeRefDistance(
|
|
1311
|
-
graph: MigrationGraph,
|
|
1312
|
-
markerHash: string,
|
|
1313
|
-
refHash: string,
|
|
1314
|
-
refName: string,
|
|
1315
|
-
): string {
|
|
1316
|
-
if (markerHash === refHash) return `At ref "${refName}" target`;
|
|
1317
|
-
|
|
1318
|
-
const pathToRef = findPath(graph, markerHash, refHash);
|
|
1319
|
-
if (pathToRef) return `${pathToRef.length} migration(s) behind ref "${refName}"`;
|
|
1320
|
-
|
|
1321
|
-
const pathFromRef = findPath(graph, refHash, markerHash);
|
|
1322
|
-
if (pathFromRef) return `${pathFromRef.length} migration(s) ahead of ref "${refName}"`;
|
|
1323
|
-
|
|
1324
|
-
return `No path between database marker and ref "${refName}" target`;
|
|
1325
|
-
}
|