@prisma-next/cli 0.5.0-dev.9 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -26
- 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-BCnP7cHo.mjs +1485 -0
- package/dist/client-BCnP7cHo.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 +64 -10
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +166 -60
- 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-ByxhPjpW.mjs} +13 -22
- package/dist/contract-infer-ByxhPjpW.mjs.map +1 -0
- package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs +160 -0
- package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs.map +1 -0
- package/dist/db-verify-Czm5T-J4.mjs +404 -0
- package/dist/db-verify-Czm5T-J4.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/{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-DxdBd4Er.mjs} +10 -11
- package/dist/inspect-live-schema-DxdBd4Er.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-BdV8JYXV.mjs} +8 -9
- package/dist/migration-command-scaffold-BdV8JYXV.mjs.map +1 -0
- package/dist/migration-plan-mRu5K81L.mjs +494 -0
- package/dist/migration-plan-mRu5K81L.mjs.map +1 -0
- package/dist/{migration-status-DUMiH8_G.mjs → migration-status-By9G5p2H.mjs} +270 -65
- package/dist/migration-status-By9G5p2H.mjs.map +1 -0
- package/dist/migrations-CTsyBXCA.mjs +229 -0
- package/dist/migrations-CTsyBXCA.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-LItU7E4l.d.mts +856 -0
- package/dist/types-LItU7E4l.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 +213 -75
- package/src/commands/migration-ref.ts +8 -7
- package/src/commands/migration-show.ts +274 -70
- package/src/commands/migration-status.ts +491 -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 +399 -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 +430 -131
- package/src/control-api/types.ts +278 -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 +177 -0
- package/src/utils/contract-space-seed-phase.ts +201 -0
- package/src/utils/emit-queue.ts +26 -0
- package/src/utils/extension-pack-inputs.ts +162 -0
- package/src/utils/formatters/graph-migration-mapper.ts +7 -3
- package/src/utils/formatters/migrations.ts +255 -77
- 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,146 @@ 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
|
+
// Count pending *migrations* (graph edges), not operations: a
|
|
517
|
+
// single authored migration that lowers to N ops or zero ops
|
|
518
|
+
// both count as exactly one pending unit of work for the user.
|
|
519
|
+
pendingCount = walked.result.migrationEdges?.length ?? 0;
|
|
520
|
+
if (liveMarker === null) {
|
|
521
|
+
status = pendingCount === 0 ? 'no-marker' : 'pending';
|
|
522
|
+
} else {
|
|
523
|
+
status = pendingCount === 0 ? 'up-to-date' : 'pending';
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
status = 'unreachable';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
rows.push({
|
|
530
|
+
spaceId: member.spaceId,
|
|
531
|
+
kind: isApp ? 'app' : 'extension',
|
|
532
|
+
headHash: member.headRef.hash,
|
|
533
|
+
markerHash: liveMarker?.storageHash ?? null,
|
|
534
|
+
pendingCount,
|
|
535
|
+
...(status ? { status } : {}),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return rows;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Read the raw contract.json bytes from disk for the aggregate
|
|
543
|
+
* loader. Returns `null` if the file is missing or unparseable —
|
|
544
|
+
* the existing `readContractEnvelope` path will report the same
|
|
545
|
+
* problem via a status diagnostic, no need to double-surface.
|
|
546
|
+
*/
|
|
547
|
+
async function loadContractRawSafely(config: {
|
|
548
|
+
contract?: { output?: string };
|
|
549
|
+
}): Promise<unknown | null> {
|
|
550
|
+
try {
|
|
551
|
+
const path = (await import('../utils/command-helpers')).resolveContractPath(config);
|
|
552
|
+
const raw = await (await import('node:fs/promises')).readFile(path, 'utf-8');
|
|
553
|
+
return JSON.parse(raw) as unknown;
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
342
559
|
async function executeMigrationStatusCommand(
|
|
343
560
|
options: MigrationStatusOptions,
|
|
344
561
|
flags: GlobalFlags,
|
|
345
562
|
ui: TerminalUI,
|
|
346
563
|
): Promise<Result<MigrationStatusResult, CliStructuredError>> {
|
|
347
564
|
const config = await loadConfig(options.config);
|
|
348
|
-
const { configPath,
|
|
349
|
-
options.config,
|
|
350
|
-
config,
|
|
351
|
-
);
|
|
565
|
+
const { configPath, appMigrationsDir, appMigrationsRelative, migrationsDir, refsDir } =
|
|
566
|
+
resolveMigrationPaths(options.config, config);
|
|
352
567
|
|
|
353
568
|
const dbConnection = options.db ?? config.db?.connection;
|
|
354
569
|
const hasDriver = !!config.driver;
|
|
355
570
|
|
|
356
571
|
let activeRefName: string | undefined;
|
|
357
572
|
let activeRefHash: string | undefined;
|
|
573
|
+
let activeRefEntry: RefEntry | undefined;
|
|
358
574
|
let allRefs: Refs = {};
|
|
359
575
|
try {
|
|
360
576
|
allRefs = await readRefs(refsDir);
|
|
361
577
|
} catch (error) {
|
|
362
578
|
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
|
-
);
|
|
579
|
+
return notOk(mapMigrationToolsError(error));
|
|
370
580
|
}
|
|
371
581
|
throw error;
|
|
372
582
|
}
|
|
@@ -374,21 +584,18 @@ async function executeMigrationStatusCommand(
|
|
|
374
584
|
if (options.ref) {
|
|
375
585
|
activeRefName = options.ref;
|
|
376
586
|
try {
|
|
377
|
-
|
|
587
|
+
activeRefEntry = resolveRef(allRefs, activeRefName);
|
|
588
|
+
activeRefHash = activeRefEntry.hash;
|
|
378
589
|
} catch (error) {
|
|
379
590
|
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
|
-
);
|
|
591
|
+
return notOk(mapMigrationToolsError(error));
|
|
387
592
|
}
|
|
388
593
|
throw error;
|
|
389
594
|
}
|
|
390
595
|
}
|
|
391
596
|
|
|
597
|
+
const requiredInvariants: readonly string[] = [...(activeRefEntry?.invariants ?? [])].sort();
|
|
598
|
+
|
|
392
599
|
const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, entry]) => ({
|
|
393
600
|
name,
|
|
394
601
|
hash: entry.hash,
|
|
@@ -398,7 +605,7 @@ async function executeMigrationStatusCommand(
|
|
|
398
605
|
if (!flags.json && !flags.quiet) {
|
|
399
606
|
const details: Array<{ label: string; value: string }> = [
|
|
400
607
|
{ label: 'config', value: configPath },
|
|
401
|
-
{ label: 'migrations', value:
|
|
608
|
+
{ label: 'migrations', value: appMigrationsRelative },
|
|
402
609
|
];
|
|
403
610
|
if (dbConnection && hasDriver) {
|
|
404
611
|
details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
|
|
@@ -406,6 +613,12 @@ async function executeMigrationStatusCommand(
|
|
|
406
613
|
if (activeRefName) {
|
|
407
614
|
details.push({ label: 'ref', value: activeRefName });
|
|
408
615
|
}
|
|
616
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
617
|
+
details.push({
|
|
618
|
+
label: 'required',
|
|
619
|
+
value: formatInvariantList(activeRefEntry.invariants),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
409
622
|
const header = formatStyledHeader({
|
|
410
623
|
command: 'migration status',
|
|
411
624
|
description: 'Show migration history and applied status',
|
|
@@ -429,15 +642,13 @@ async function executeMigrationStatusCommand(
|
|
|
429
642
|
});
|
|
430
643
|
}
|
|
431
644
|
|
|
432
|
-
let bundles: readonly
|
|
645
|
+
let bundles: readonly OnDiskMigrationPackage[];
|
|
433
646
|
let graph: MigrationGraph;
|
|
434
647
|
try {
|
|
435
|
-
({ bundles, graph } = await
|
|
648
|
+
({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
|
|
436
649
|
} catch (error) {
|
|
437
650
|
if (MigrationToolsError.is(error)) {
|
|
438
|
-
return notOk(
|
|
439
|
-
errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
|
|
440
|
-
);
|
|
651
|
+
return notOk(mapMigrationToolsError(error));
|
|
441
652
|
}
|
|
442
653
|
return notOk(
|
|
443
654
|
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
@@ -465,6 +676,7 @@ async function executeMigrationStatusCommand(
|
|
|
465
676
|
contractHash,
|
|
466
677
|
summary: 'No migrations found',
|
|
467
678
|
diagnostics,
|
|
679
|
+
requiredInvariants,
|
|
468
680
|
});
|
|
469
681
|
}
|
|
470
682
|
|
|
@@ -492,7 +704,9 @@ async function executeMigrationStatusCommand(
|
|
|
492
704
|
}
|
|
493
705
|
|
|
494
706
|
let markerHash: string | undefined;
|
|
707
|
+
let markerInvariants: readonly string[] = [];
|
|
495
708
|
let mode: 'online' | 'offline' = 'offline';
|
|
709
|
+
let allMarkers: ReadonlyMap<string, ContractMarkerRecordLike> | null = null;
|
|
496
710
|
|
|
497
711
|
if (dbConnection && hasDriver) {
|
|
498
712
|
const client = createControlClient({
|
|
@@ -504,8 +718,30 @@ async function executeMigrationStatusCommand(
|
|
|
504
718
|
});
|
|
505
719
|
try {
|
|
506
720
|
await client.connect(dbConnection);
|
|
507
|
-
|
|
721
|
+
const marker = await client.readMarker();
|
|
722
|
+
markerHash = marker?.storageHash;
|
|
723
|
+
markerInvariants = marker?.invariants ?? [];
|
|
508
724
|
mode = 'online';
|
|
725
|
+
// Read every space's marker so the aggregate enumeration can
|
|
726
|
+
// surface per-space marker state. `readAllMarkers` mirrors what
|
|
727
|
+
// `db init` / `db update` already use to drive the multi-space
|
|
728
|
+
// planner; here it powers the aggregate status output.
|
|
729
|
+
//
|
|
730
|
+
// Probe for the method first so we only swallow the
|
|
731
|
+
// unsupported-method case: older family instances may not
|
|
732
|
+
// implement `readAllMarkers` (per-space enumeration then falls
|
|
733
|
+
// back to "marker unknown"). Real query / runtime errors from
|
|
734
|
+
// an instance that *does* expose the method must propagate up
|
|
735
|
+
// — otherwise transient DB failures would silently degrade
|
|
736
|
+
// status to "markers unknown".
|
|
737
|
+
if (typeof client.readAllMarkers === 'function') {
|
|
738
|
+
allMarkers = await client.readAllMarkers();
|
|
739
|
+
} else {
|
|
740
|
+
// Leaving `allMarkers` as `null` signals "unknown" to the
|
|
741
|
+
// aggregate loader (an empty `Map` would instead mean "every
|
|
742
|
+
// space has no marker", which is a different condition).
|
|
743
|
+
allMarkers = null;
|
|
744
|
+
}
|
|
509
745
|
} catch {
|
|
510
746
|
if (!flags.json && !flags.quiet) {
|
|
511
747
|
ui.warn('Could not connect to database — showing offline status');
|
|
@@ -515,6 +751,63 @@ async function executeMigrationStatusCommand(
|
|
|
515
751
|
}
|
|
516
752
|
}
|
|
517
753
|
|
|
754
|
+
// Build the aggregate enumeration of contract spaces. Lossy on
|
|
755
|
+
// failure (extensions are simply omitted) so the existing
|
|
756
|
+
// single-space app pipeline below still runs even if extensions
|
|
757
|
+
// can't be loaded — a strict failure here would degrade the
|
|
758
|
+
// load-bearing app-space output for unrelated reasons.
|
|
759
|
+
const contractRawForAggregate = await loadContractRawSafely(config);
|
|
760
|
+
let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
|
|
761
|
+
if (contractRawForAggregate !== null) {
|
|
762
|
+
// The aggregate loader needs a typed-Contract producer. Build a
|
|
763
|
+
// real control stack so `validateContract` runs against a fully
|
|
764
|
+
// composed family instance — descriptors that read stack members
|
|
765
|
+
// during construction (e.g. codec lookups) get a consistent view.
|
|
766
|
+
const stack = createControlStack(config);
|
|
767
|
+
const familyInstance = config.family.create(stack);
|
|
768
|
+
try {
|
|
769
|
+
aggregateSpaces = await loadAggregateStatusSpaces({
|
|
770
|
+
targetId: config.target.targetId,
|
|
771
|
+
migrationsDir,
|
|
772
|
+
appContractRaw: contractRawForAggregate,
|
|
773
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
774
|
+
validateContract: (json: unknown) => familyInstance.validateContract(json),
|
|
775
|
+
markersBySpace: allMarkers,
|
|
776
|
+
});
|
|
777
|
+
} catch {
|
|
778
|
+
// Loader failure short-circuits silently — the existing
|
|
779
|
+
// single-space app pipeline below still runs.
|
|
780
|
+
aggregateSpaces = [];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const totalPendingAcrossSpaces = computeTotalPendingAcrossSpaces(aggregateSpaces);
|
|
784
|
+
|
|
785
|
+
// Pre-check unknown invariants. Online: union the graph's declared
|
|
786
|
+
// invariants with the marker's recorded set so a retired-but-applied
|
|
787
|
+
// invariant doesn't surface as MIGRATION.UNKNOWN_INVARIANT — apply would
|
|
788
|
+
// route fine because marker subtraction empties `effectiveRequired`.
|
|
789
|
+
// Offline: keep the check graph-strict (the marker is unknown, and a
|
|
790
|
+
// missing declarer is the dominant signal we can offer).
|
|
791
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
792
|
+
const declared = collectDeclaredInvariants(graph);
|
|
793
|
+
const known = new Set<string>(declared);
|
|
794
|
+
if (mode === 'online') {
|
|
795
|
+
for (const id of markerInvariants) known.add(id);
|
|
796
|
+
}
|
|
797
|
+
const unknown = activeRefEntry.invariants.filter((id) => !known.has(id));
|
|
798
|
+
if (unknown.length > 0) {
|
|
799
|
+
return notOk(
|
|
800
|
+
mapMigrationToolsError(
|
|
801
|
+
errorUnknownInvariant({
|
|
802
|
+
...ifDefined('refName', activeRefName),
|
|
803
|
+
unknown,
|
|
804
|
+
declared: [...declared].sort(),
|
|
805
|
+
}),
|
|
806
|
+
),
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
518
811
|
// Marker exists but is not in the migration graph and doesn't match the
|
|
519
812
|
// contract hash. The DB is at an unknown state relative to the graph.
|
|
520
813
|
// Bail out early with a clear diagnostic instead of rendering a confusing
|
|
@@ -560,6 +853,7 @@ async function executeMigrationStatusCommand(
|
|
|
560
853
|
summary: `${bundles.length} migration(s) on disk`,
|
|
561
854
|
diagnostics,
|
|
562
855
|
markerHash,
|
|
856
|
+
requiredInvariants,
|
|
563
857
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
564
858
|
});
|
|
565
859
|
}
|
|
@@ -600,6 +894,7 @@ async function executeMigrationStatusCommand(
|
|
|
600
894
|
summary: `${bundles.length} migration(s) on disk`,
|
|
601
895
|
diagnostics,
|
|
602
896
|
...ifDefined('markerHash', markerHash),
|
|
897
|
+
requiredInvariants,
|
|
603
898
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
604
899
|
graph,
|
|
605
900
|
bundles,
|
|
@@ -624,14 +919,39 @@ async function executeMigrationStatusCommand(
|
|
|
624
919
|
const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
|
|
625
920
|
const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
|
|
626
921
|
|
|
922
|
+
let appliedInvariants: readonly string[] | undefined;
|
|
923
|
+
let missingInvariants: readonly string[] | undefined;
|
|
924
|
+
let effectiveRequired = new Set<string>();
|
|
925
|
+
if (mode === 'online') {
|
|
926
|
+
// Mirrors `migration-apply.ts`: compute `effectiveRequired = required −
|
|
927
|
+
// marker.invariants` directly, then derive the display fields from it.
|
|
928
|
+
// `appliedInvariants` is the intersection (`required ∩ marker`), which
|
|
929
|
+
// is what JSON consumers see for the active ref; the unfiltered set
|
|
930
|
+
// lives on `marker.invariants`.
|
|
931
|
+
const markerSet = new Set(markerInvariants);
|
|
932
|
+
effectiveRequired = new Set(requiredInvariants.filter((id) => !markerSet.has(id)));
|
|
933
|
+
appliedInvariants = requiredInvariants.filter((id) => markerSet.has(id));
|
|
934
|
+
missingInvariants = [...effectiveRequired].sort();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// The marker can match the structural target while still missing required
|
|
938
|
+
// invariants — for example, a self-edge that provides X, applied via a ref
|
|
939
|
+
// declaring X. `pendingCount` (structural) says zero in that case but
|
|
940
|
+
// `effectiveRequired` is non-empty, so up-to-date messaging would mislead.
|
|
941
|
+
const hasInvariantWork = effectiveRequired.size > 0;
|
|
942
|
+
const missingList = [...effectiveRequired].sort().join(', ');
|
|
943
|
+
|
|
627
944
|
let summary: string;
|
|
628
945
|
if (mode === 'online') {
|
|
629
946
|
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
630
947
|
summary = `${bundles.length} migration(s) on disk`;
|
|
631
948
|
} else if (activeRefHash && markerHash !== undefined) {
|
|
632
|
-
|
|
633
|
-
|
|
949
|
+
const distance = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
|
|
950
|
+
summary = hasInvariantWork ? `${distance} — missing invariant(s): ${missingList}` : distance;
|
|
951
|
+
} else if (pendingCount === 0 && !hasInvariantWork) {
|
|
634
952
|
summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? 's' : ''} applied)`;
|
|
953
|
+
} else if (pendingCount === 0 && hasInvariantWork) {
|
|
954
|
+
summary = `Missing invariant(s): ${missingList} — run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply`;
|
|
635
955
|
} else if (markerHash === undefined) {
|
|
636
956
|
summary = `${pendingCount} pending migration(s) — database has no marker`;
|
|
637
957
|
} else {
|
|
@@ -641,6 +961,37 @@ async function executeMigrationStatusCommand(
|
|
|
641
961
|
summary = `${entries.length} migration(s) on disk`;
|
|
642
962
|
}
|
|
643
963
|
|
|
964
|
+
let pathDecision: MigrationStatusResult['pathDecision'];
|
|
965
|
+
let routingUnreachable = false;
|
|
966
|
+
if (mode === 'online') {
|
|
967
|
+
const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
968
|
+
const outcome = findPathWithDecision(graph, originHash, targetHash, {
|
|
969
|
+
...ifDefined('refName', activeRefName),
|
|
970
|
+
required: effectiveRequired,
|
|
971
|
+
});
|
|
972
|
+
if (outcome.kind === 'ok') {
|
|
973
|
+
pathDecision = toPathDecisionResult(outcome.decision);
|
|
974
|
+
} else if (outcome.kind === 'unsatisfiable') {
|
|
975
|
+
return notOk(
|
|
976
|
+
mapMigrationToolsError(
|
|
977
|
+
errorNoInvariantPath({
|
|
978
|
+
...ifDefined('refName', activeRefName),
|
|
979
|
+
required: [...effectiveRequired].sort(),
|
|
980
|
+
missing: outcome.missing,
|
|
981
|
+
structuralPath: outcome.structuralPath.map(toStructuralEdge),
|
|
982
|
+
}),
|
|
983
|
+
),
|
|
984
|
+
);
|
|
985
|
+
} else {
|
|
986
|
+
// outcome.kind === 'unreachable' — origin (marker) has no structural
|
|
987
|
+
// path to the active target. `pendingCount` and `hasInvariantWork`
|
|
988
|
+
// both report zero in this case, but emitting MIGRATION.UP_TO_DATE
|
|
989
|
+
// would be wrong: the database simply cannot reach the requested
|
|
990
|
+
// ref/contract from its current state. Suppress UP_TO_DATE below.
|
|
991
|
+
routingUnreachable = true;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
644
995
|
if (mode === 'online') {
|
|
645
996
|
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
646
997
|
diagnostics.push({
|
|
@@ -657,7 +1008,16 @@ async function executeMigrationStatusCommand(
|
|
|
657
1008
|
message: `${pendingCount} migration(s) pending`,
|
|
658
1009
|
hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
|
|
659
1010
|
});
|
|
660
|
-
} else {
|
|
1011
|
+
} else if (hasInvariantWork) {
|
|
1012
|
+
diagnostics.push({
|
|
1013
|
+
code: 'MIGRATION.INVARIANTS_PENDING',
|
|
1014
|
+
severity: 'info',
|
|
1015
|
+
message: `Missing required invariant(s): ${missingList}`,
|
|
1016
|
+
hints: [
|
|
1017
|
+
`Run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply a path that covers the required invariants`,
|
|
1018
|
+
],
|
|
1019
|
+
});
|
|
1020
|
+
} else if (!routingUnreachable) {
|
|
661
1021
|
diagnostics.push({
|
|
662
1022
|
code: 'MIGRATION.UP_TO_DATE',
|
|
663
1023
|
severity: 'info',
|
|
@@ -667,14 +1027,6 @@ async function executeMigrationStatusCommand(
|
|
|
667
1027
|
}
|
|
668
1028
|
}
|
|
669
1029
|
|
|
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
1030
|
const result: MigrationStatusResult = {
|
|
679
1031
|
ok: true,
|
|
680
1032
|
mode,
|
|
@@ -684,6 +1036,9 @@ async function executeMigrationStatusCommand(
|
|
|
684
1036
|
summary,
|
|
685
1037
|
diagnostics,
|
|
686
1038
|
...ifDefined('markerHash', markerHash),
|
|
1039
|
+
requiredInvariants,
|
|
1040
|
+
...ifDefined('appliedInvariants', appliedInvariants),
|
|
1041
|
+
...ifDefined('missingInvariants', missingInvariants),
|
|
687
1042
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
688
1043
|
...ifDefined('pathDecision', pathDecision),
|
|
689
1044
|
graph,
|
|
@@ -691,6 +1046,8 @@ async function executeMigrationStatusCommand(
|
|
|
691
1046
|
edgeStatuses,
|
|
692
1047
|
...ifDefined('activeRefHash', activeRefHash),
|
|
693
1048
|
...ifDefined('activeRefName', activeRefName),
|
|
1049
|
+
spaces: aggregateSpaces,
|
|
1050
|
+
...ifDefined('totalPendingAcrossSpaces', totalPendingAcrossSpaces),
|
|
694
1051
|
};
|
|
695
1052
|
return ok(result);
|
|
696
1053
|
}
|
|
@@ -724,13 +1081,19 @@ export function createMigrationStatusCommand(): Command {
|
|
|
724
1081
|
|
|
725
1082
|
const exitCode = handleResult(result, flags, ui, (statusResult) => {
|
|
726
1083
|
if (flags.json) {
|
|
1084
|
+
// Strip non-JSON-shape fields before emitting. These belong to
|
|
1085
|
+
// the in-memory result so the human renderer can avoid
|
|
1086
|
+
// recomputing them, but they would either bloat the wire format
|
|
1087
|
+
// (graph, bundles, edgeStatuses) or expose internals
|
|
1088
|
+
// (activeRefHash, activeRefName, diverged) that consumers should
|
|
1089
|
+
// read off `pathDecision` / `refs` instead.
|
|
727
1090
|
const {
|
|
728
|
-
graph:
|
|
729
|
-
bundles:
|
|
730
|
-
edgeStatuses:
|
|
731
|
-
activeRefHash:
|
|
732
|
-
activeRefName:
|
|
733
|
-
diverged:
|
|
1091
|
+
graph: _graph,
|
|
1092
|
+
bundles: _bundles,
|
|
1093
|
+
edgeStatuses: _edgeStatuses,
|
|
1094
|
+
activeRefHash: _activeRefHash,
|
|
1095
|
+
activeRefName: _activeRefName,
|
|
1096
|
+
diverged: _diverged,
|
|
734
1097
|
...jsonResult
|
|
735
1098
|
} = statusResult;
|
|
736
1099
|
ui.output(JSON.stringify(jsonResult, null, 2));
|
|
@@ -789,7 +1152,7 @@ function formatLegend(colorize: boolean): string {
|
|
|
789
1152
|
return c(dim, parts.join(' '));
|
|
790
1153
|
}
|
|
791
1154
|
|
|
792
|
-
function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
1155
|
+
export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
793
1156
|
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
794
1157
|
const lines: string[] = [];
|
|
795
1158
|
|
|
@@ -797,11 +1160,16 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
797
1160
|
const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
|
|
798
1161
|
|
|
799
1162
|
const hasWarnings = result.diagnostics?.some((d) => d.severity === 'warn') ?? false;
|
|
1163
|
+
// INVARIANTS_PENDING is filed at severity 'info' (per ADR 208) so the
|
|
1164
|
+
// warn-severity check above doesn't see it. It still represents pending
|
|
1165
|
+
// work, so it must promote the summary off the success icon.
|
|
1166
|
+
const hasInvariantPending =
|
|
1167
|
+
result.diagnostics?.some((d) => d.code === 'MIGRATION.INVARIANTS_PENDING') ?? false;
|
|
800
1168
|
|
|
801
1169
|
if (result.mode === 'online') {
|
|
802
1170
|
if (hasUnknown || hasWarnings) {
|
|
803
1171
|
lines.push(`${c(yellow, '⚠')} ${result.summary}`);
|
|
804
|
-
} else if (pendingCount === 0) {
|
|
1172
|
+
} else if (pendingCount === 0 && !hasInvariantPending) {
|
|
805
1173
|
lines.push(`${c(cyan, '✔')} ${result.summary}`);
|
|
806
1174
|
} else {
|
|
807
1175
|
lines.push(`${c(yellow, '⧗')} ${result.summary}`);
|
|
@@ -810,6 +1178,15 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
810
1178
|
lines.push(result.summary);
|
|
811
1179
|
}
|
|
812
1180
|
|
|
1181
|
+
if (result.requiredInvariants.length > 0) {
|
|
1182
|
+
if (result.appliedInvariants !== undefined && result.missingInvariants !== undefined) {
|
|
1183
|
+
lines.push(`${c(dim, 'applied ')}${formatInvariantList(result.appliedInvariants)}`);
|
|
1184
|
+
lines.push(`${c(dim, 'missing ')}${formatInvariantList(result.missingInvariants)}`);
|
|
1185
|
+
} else {
|
|
1186
|
+
lines.push(`${c(dim, 'applied ')}(unknown — connect a database to evaluate)`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
813
1190
|
const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
|
|
814
1191
|
for (const diag of warnings) {
|
|
815
1192
|
lines.push(`${c(yellow, '⚠')} ${diag.message}`);
|
|
@@ -818,9 +1195,59 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
|
|
|
818
1195
|
}
|
|
819
1196
|
}
|
|
820
1197
|
|
|
1198
|
+
// Per-space section. Suppressed when there's no extension space —
|
|
1199
|
+
// the legacy single-space output already covers the app member.
|
|
1200
|
+
// When extensions exist, render every space (including the app)
|
|
1201
|
+
// for consistency, plus a cross-space pending total + apply hint.
|
|
1202
|
+
if (result.spaces?.some((s) => s.kind === 'extension')) {
|
|
1203
|
+
const total = result.totalPendingAcrossSpaces ?? 0;
|
|
1204
|
+
lines.push('');
|
|
1205
|
+
lines.push(c(dim, 'spaces'));
|
|
1206
|
+
for (const space of result.spaces) {
|
|
1207
|
+
lines.push(formatSpaceLine(space, c));
|
|
1208
|
+
}
|
|
1209
|
+
if (total > 0) {
|
|
1210
|
+
lines.push('');
|
|
1211
|
+
lines.push(
|
|
1212
|
+
`${c(yellow, '⧗')} ${total} pending migration(s) across ${result.spaces.length} space(s) — run 'prisma-next migration apply' to apply`,
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
821
1217
|
return lines.join('\n');
|
|
822
1218
|
}
|
|
823
1219
|
|
|
1220
|
+
function formatSpaceLine(
|
|
1221
|
+
space: MigrationStatusSpaceEntry,
|
|
1222
|
+
c: (fn: (s: string) => string, s: string) => string,
|
|
1223
|
+
): string {
|
|
1224
|
+
const glyph = (() => {
|
|
1225
|
+
if (space.status === 'up-to-date' || space.status === 'no-marker') return c(cyan, '✓');
|
|
1226
|
+
if (space.status === 'pending') return c(yellow, '⧗');
|
|
1227
|
+
if (space.status === 'unreachable' || space.status === 'never-planned') return c(magenta, '✗');
|
|
1228
|
+
return ' ';
|
|
1229
|
+
})();
|
|
1230
|
+
const tag = space.kind === 'app' ? '[app]' : '[ext]';
|
|
1231
|
+
const head = space.headHash.slice(0, 8);
|
|
1232
|
+
const marker =
|
|
1233
|
+
space.markerHash === undefined
|
|
1234
|
+
? '(unknown)'
|
|
1235
|
+
: space.markerHash === null
|
|
1236
|
+
? '(no marker)'
|
|
1237
|
+
: space.markerHash.slice(0, 8);
|
|
1238
|
+
const pending =
|
|
1239
|
+
space.pendingCount === undefined
|
|
1240
|
+
? ''
|
|
1241
|
+
: space.pendingCount === 0
|
|
1242
|
+
? c(dim, ' (up to date)')
|
|
1243
|
+
: c(yellow, ` (${space.pendingCount} pending)`);
|
|
1244
|
+
return ` ${glyph} ${c(dim, tag)} ${space.spaceId} → head ${c(dim, head)}, marker ${c(dim, marker)}${pending}`;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function formatInvariantList(ids: readonly string[]): string {
|
|
1248
|
+
return ids.length === 0 ? '(none)' : ids.join(', ');
|
|
1249
|
+
}
|
|
1250
|
+
|
|
824
1251
|
function summarizeRefDistance(
|
|
825
1252
|
graph: MigrationGraph,
|
|
826
1253
|
markerHash: string,
|