@prisma-next/cli 0.5.0 → 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 +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-qVH-rEgd.mjs → client-BCnP7cHo.mjs} +9 -119
- package/dist/client-BCnP7cHo.mjs.map +1 -0
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +3 -3
- package/dist/commands/db-schema.mjs +1 -1
- package/dist/commands/db-sign.mjs +1 -1
- package/dist/commands/db-update.mjs +3 -3
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migration-apply.d.mts +1 -1
- package/dist/commands/migration-apply.mjs +2 -2
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +55 -7
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +153 -46
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/{contract-infer-BK9YFGEG.mjs → contract-infer-ByxhPjpW.mjs} +2 -2
- package/dist/{contract-infer-BK9YFGEG.mjs.map → contract-infer-ByxhPjpW.mjs.map} +1 -1
- package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs +160 -0
- package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs.map +1 -0
- package/dist/{db-verify-C0y1PCO2.mjs → db-verify-Czm5T-J4.mjs} +2 -2
- package/dist/{db-verify-C0y1PCO2.mjs.map → db-verify-Czm5T-J4.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.mjs +1 -1
- package/dist/{inspect-live-schema-CWYxGKlb.mjs → inspect-live-schema-DxdBd4Er.mjs} +2 -2
- package/dist/{inspect-live-schema-CWYxGKlb.mjs.map → inspect-live-schema-DxdBd4Er.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-B5dORFEv.mjs → migration-command-scaffold-BdV8JYXV.mjs} +2 -2
- package/dist/{migration-command-scaffold-B5dORFEv.mjs.map → migration-command-scaffold-BdV8JYXV.mjs.map} +1 -1
- package/dist/{migration-plan-C6lVaHsO.mjs → migration-plan-mRu5K81L.mjs} +89 -149
- package/dist/migration-plan-mRu5K81L.mjs.map +1 -0
- package/dist/{migration-status-CZ-D5k7k.mjs → migration-status-By9G5p2H.mjs} +6 -8
- package/dist/{migration-status-CZ-D5k7k.mjs.map → migration-status-By9G5p2H.mjs.map} +1 -1
- package/dist/{migrations-D_UJnpuW.mjs → migrations-CTsyBXCA.mjs} +42 -29
- package/dist/migrations-CTsyBXCA.mjs.map +1 -0
- package/dist/{types-D7x-IFLO.d.mts → types-LItU7E4l.d.mts} +7 -9
- package/dist/{types-D7x-IFLO.d.mts.map → types-LItU7E4l.d.mts.map} +1 -1
- package/package.json +14 -14
- package/src/commands/migration-plan.ts +45 -47
- package/src/commands/migration-show.ts +245 -60
- package/src/commands/migration-status.ts +17 -9
- package/src/control-api/operations/db-apply-aggregate.ts +12 -10
- package/src/control-api/operations/migration-apply.ts +7 -1
- package/src/control-api/types.ts +6 -8
- package/src/utils/contract-space-aggregate-loader.ts +7 -34
- package/src/utils/contract-space-seed-phase.ts +201 -0
- package/src/utils/extension-pack-inputs.ts +47 -55
- package/src/utils/formatters/migrations.ts +80 -38
- package/dist/client-qVH-rEgd.mjs.map +0 -1
- package/dist/extension-pack-inputs-C7xgE-vv.mjs +0 -74
- package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +0 -1
- package/dist/migration-plan-C6lVaHsO.mjs.map +0 -1
- package/dist/migrations-D_UJnpuW.mjs.map +0 -1
- package/src/utils/contract-space-extension-migrations-pass.ts +0 -120
- package/src/utils/contract-space-migrate-pass.ts +0 -156
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { materialiseExtensionMigrationPackageIfMissing } from '@prisma-next/migration-tools/io';
|
|
2
|
+
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
3
|
+
import type { MigrationOps } from '@prisma-next/migration-tools/package';
|
|
4
|
+
import {
|
|
5
|
+
emitContractSpaceArtefacts,
|
|
6
|
+
planAllSpaces,
|
|
7
|
+
readContractSpaceHeadRef,
|
|
8
|
+
type SpacePlanOutput,
|
|
9
|
+
spaceMigrationDirectory,
|
|
10
|
+
} from '@prisma-next/migration-tools/spaces';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* In-memory authored migration package shipped by an extension descriptor.
|
|
14
|
+
* Mirrors `MigrationPackage` from `@prisma-next/migration-tools/io` (the
|
|
15
|
+
* on-disk shape minus `dirPath`); redeclared structurally here so the
|
|
16
|
+
* CLI helper does not couple to any family's `ExtensionMigrationPackage`
|
|
17
|
+
* type — any family that ships pre-built migration packages can pass
|
|
18
|
+
* them through unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export interface DescriptorMigrationPackage {
|
|
21
|
+
readonly dirName: string;
|
|
22
|
+
readonly metadata: MigrationMetadata;
|
|
23
|
+
readonly ops: MigrationOps;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Minimal descriptor view consumed by the seed phase. Mirrors the shape
|
|
28
|
+
* the SQL family ships on each declared extension entry; only the fields
|
|
29
|
+
* the seed phase needs are surfaced.
|
|
30
|
+
*/
|
|
31
|
+
export interface SeedPhaseExtensionInput {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly contractSpace?: {
|
|
34
|
+
readonly contractJson: unknown;
|
|
35
|
+
readonly headRef: { readonly hash: string; readonly invariants: readonly string[] };
|
|
36
|
+
readonly migrations: readonly DescriptorMigrationPackage[];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ContractSpaceSeedPhaseInputs {
|
|
41
|
+
readonly migrationsDir: string;
|
|
42
|
+
readonly extensionPacks: ReadonlyArray<SeedPhaseExtensionInput>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One per-space record describing what the seed phase did for an
|
|
47
|
+
* extension contract space. Surfaced verbatim by the caller (typically
|
|
48
|
+
* `migration plan`) so users see a single line per touched extension.
|
|
49
|
+
*
|
|
50
|
+
* - `action: 'updated'` — either the on-disk head pointer changed, or
|
|
51
|
+
* one or more new descriptor-shipped migration packages were
|
|
52
|
+
* materialised into `migrations/<spaceId>/<dirName>/`.
|
|
53
|
+
* - `action: 'unchanged'` — the on-disk head already matched the
|
|
54
|
+
* descriptor and no new migration packages needed to be written.
|
|
55
|
+
*
|
|
56
|
+
* Either way, the artefacts (`contract.json`, `contract.d.ts`,
|
|
57
|
+
* `refs/head.json`) are re-emitted: the framework owns those files and
|
|
58
|
+
* makes the re-emit observably idempotent at the byte level.
|
|
59
|
+
*/
|
|
60
|
+
export interface ContractSpaceSeedPhaseRecord {
|
|
61
|
+
readonly spaceId: string;
|
|
62
|
+
readonly action: 'updated' | 'unchanged';
|
|
63
|
+
readonly priorHash: string | null;
|
|
64
|
+
readonly newHash: string;
|
|
65
|
+
readonly newMigrationDirs: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ContractSpaceSeedPhaseResult {
|
|
69
|
+
readonly seeded: readonly ContractSpaceSeedPhaseRecord[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Phase-1 of the two-phase `migration plan` pipeline (sub-spec § 4).
|
|
74
|
+
*
|
|
75
|
+
* For every extension that exposes a `contractSpace`:
|
|
76
|
+
*
|
|
77
|
+
* 1. Read the on-disk head ref (returns `null` on first emit).
|
|
78
|
+
* 2. Re-emit `contract.json` / `contract.d.ts` / `refs/head.json`
|
|
79
|
+
* unconditionally via {@link emitContractSpaceArtefacts}. The
|
|
80
|
+
* framework owns these files; re-emit is the contract.
|
|
81
|
+
* 3. Materialise any descriptor-shipped migration packages not yet on
|
|
82
|
+
* disk via {@link materialiseExtensionMigrationPackageIfMissing}.
|
|
83
|
+
* Existing packages are left untouched (by-existence skip).
|
|
84
|
+
*
|
|
85
|
+
* The return value lets the caller render a per-space status line and
|
|
86
|
+
* lets the phase-2 aggregate loader run on a now-consistent disk state
|
|
87
|
+
* (every loaded extension is guaranteed to have its head ref pinned
|
|
88
|
+
* to the descriptor's hash and to ship every package the descriptor
|
|
89
|
+
* declares).
|
|
90
|
+
*
|
|
91
|
+
* Output ordering is deterministic and alphabetical by spaceId (via
|
|
92
|
+
* {@link planAllSpaces}, which also detects duplicate spaceIds). This
|
|
93
|
+
* matches the canonical sort order used by every other aggregate
|
|
94
|
+
* surface (`migration apply`, `migration status`, the runner).
|
|
95
|
+
*/
|
|
96
|
+
export async function runContractSpaceSeedPhase(
|
|
97
|
+
inputs: ContractSpaceSeedPhaseInputs,
|
|
98
|
+
): Promise<ContractSpaceSeedPhaseResult> {
|
|
99
|
+
const planInputs = inputs.extensionPacks
|
|
100
|
+
.filter(
|
|
101
|
+
(
|
|
102
|
+
pack,
|
|
103
|
+
): pack is SeedPhaseExtensionInput & {
|
|
104
|
+
contractSpace: NonNullable<SeedPhaseExtensionInput['contractSpace']>;
|
|
105
|
+
} => pack.contractSpace !== undefined,
|
|
106
|
+
)
|
|
107
|
+
.map((pack) => ({
|
|
108
|
+
spaceId: pack.id,
|
|
109
|
+
priorContract: null,
|
|
110
|
+
newContract: pack.contractSpace.contractJson,
|
|
111
|
+
__pack: pack.contractSpace,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// `planAllSpaces` brings deterministic alphabetical ordering and
|
|
115
|
+
// duplicate-spaceId detection. The "planner" callback is a no-op
|
|
116
|
+
// pass-through that simply returns the descriptor's pre-built
|
|
117
|
+
// migration packages.
|
|
118
|
+
const planned: readonly SpacePlanOutput<DescriptorMigrationPackage>[] = planAllSpaces(
|
|
119
|
+
planInputs,
|
|
120
|
+
(input) =>
|
|
121
|
+
(
|
|
122
|
+
input as typeof input & {
|
|
123
|
+
readonly __pack: NonNullable<SeedPhaseExtensionInput['contractSpace']>;
|
|
124
|
+
}
|
|
125
|
+
).__pack.migrations,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Reassemble a spaceId → descriptor lookup so the loop below can read
|
|
129
|
+
// the contractJson / headRef without leaking the typed-cast back into
|
|
130
|
+
// `planAllSpaces`'s output shape.
|
|
131
|
+
const descriptorBySpace = new Map<
|
|
132
|
+
string,
|
|
133
|
+
NonNullable<SeedPhaseExtensionInput['contractSpace']>
|
|
134
|
+
>();
|
|
135
|
+
for (const pack of inputs.extensionPacks) {
|
|
136
|
+
if (pack.contractSpace !== undefined) descriptorBySpace.set(pack.id, pack.contractSpace);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const seeded: ContractSpaceSeedPhaseRecord[] = [];
|
|
140
|
+
for (const space of planned) {
|
|
141
|
+
const descriptor = descriptorBySpace.get(space.spaceId);
|
|
142
|
+
if (descriptor === undefined) continue;
|
|
143
|
+
|
|
144
|
+
const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, space.spaceId);
|
|
145
|
+
const priorHash = onDiskHeadRef?.hash ?? null;
|
|
146
|
+
|
|
147
|
+
await emitContractSpaceArtefacts(inputs.migrationsDir, space.spaceId, {
|
|
148
|
+
contract: descriptor.contractJson,
|
|
149
|
+
contractDts: buildPlaceholderContractDts(space.spaceId),
|
|
150
|
+
headRef: { hash: descriptor.headRef.hash, invariants: descriptor.headRef.invariants },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const spaceDir = spaceMigrationDirectory(inputs.migrationsDir, space.spaceId);
|
|
154
|
+
const newMigrationDirs: string[] = [];
|
|
155
|
+
for (const pkg of space.migrationPackages) {
|
|
156
|
+
const { written } = await materialiseExtensionMigrationPackageIfMissing(spaceDir, pkg);
|
|
157
|
+
if (written) newMigrationDirs.push(pkg.dirName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const action: ContractSpaceSeedPhaseRecord['action'] =
|
|
161
|
+
priorHash !== descriptor.headRef.hash || newMigrationDirs.length > 0
|
|
162
|
+
? 'updated'
|
|
163
|
+
: 'unchanged';
|
|
164
|
+
|
|
165
|
+
seeded.push({
|
|
166
|
+
spaceId: space.spaceId,
|
|
167
|
+
action,
|
|
168
|
+
priorHash,
|
|
169
|
+
newHash: descriptor.headRef.hash,
|
|
170
|
+
newMigrationDirs,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { seeded };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Placeholder `.d.ts` content for an extension space's on-disk mirror.
|
|
179
|
+
*
|
|
180
|
+
* Rendering a fully-typed `.d.ts` for an extension contract requires
|
|
181
|
+
* the SQL-family renderer with the codec / typemap registry threaded
|
|
182
|
+
* through; until that integration ships, the on-disk `.d.ts` is a
|
|
183
|
+
* stub `export {};` module that documents how consumers should
|
|
184
|
+
* validate the sibling `contract.json`. The stub typechecks on its
|
|
185
|
+
* own and does not need any TypeScript suppressions.
|
|
186
|
+
*/
|
|
187
|
+
function buildPlaceholderContractDts(spaceId: string): string {
|
|
188
|
+
return [
|
|
189
|
+
'/**',
|
|
190
|
+
` * Placeholder \`.d.ts\` for extension space "${spaceId}".`,
|
|
191
|
+
' *',
|
|
192
|
+
' * The framework re-emits this file on every `migration plan` run',
|
|
193
|
+
' * alongside `contract.json` and `refs/head.json`. A typed `.d.ts`',
|
|
194
|
+
' * rendering pass for extension contracts is tracked separately;',
|
|
195
|
+
' * until that ships, consumers should import `contract.json`',
|
|
196
|
+
' * directly with `validateContract<…>(…)`.',
|
|
197
|
+
' */',
|
|
198
|
+
'export {};',
|
|
199
|
+
'',
|
|
200
|
+
].join('\n');
|
|
201
|
+
}
|
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
import type { DeclaredExtensionEntry } from '@prisma-next/migration-tools/aggregate';
|
|
17
17
|
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
18
18
|
import type { MigrationOps } from '@prisma-next/migration-tools/package';
|
|
19
|
-
import type { ExtensionMigrationsExtensionInput } from './contract-space-extension-migrations-pass';
|
|
20
|
-
import type { MigrateExtensionInput } from './contract-space-migrate-pass';
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
21
|
* In-memory authored migration package shipped by an extension descriptor.
|
|
@@ -108,63 +106,57 @@ export function toExtensionInputs(
|
|
|
108
106
|
// ---------------------------------------------------------------------------
|
|
109
107
|
|
|
110
108
|
/**
|
|
111
|
-
* Aggregate-loader projection
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
109
|
+
* Aggregate-loader projection. Surfaces `id` + `targetId` per
|
|
110
|
+
* contract-space-bearing extension to
|
|
111
|
+
* {@link import('./contract-space-aggregate-loader').buildContractSpaceAggregate}.
|
|
112
|
+
*
|
|
113
|
+
* Codec-only extensions (no `contractSpace` declaration) are filtered
|
|
114
|
+
* out: they are not contract-space members, so the aggregate loader
|
|
115
|
+
* has nothing to do with them. Filtering happens at this descriptor-
|
|
116
|
+
* import boundary so the loader stays oblivious to that distinction —
|
|
117
|
+
* every entry it sees expects an on-disk `migrations/<id>/` directory.
|
|
115
118
|
*/
|
|
116
|
-
export function toDeclaredExtensions(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} {
|
|
119
|
+
export function toDeclaredExtensions(
|
|
120
|
+
inputs: ReadonlyArray<ExtensionPackInput>,
|
|
121
|
+
): readonly DeclaredExtensionEntry[] {
|
|
120
122
|
const entries: DeclaredExtensionEntry[] = [];
|
|
121
|
-
const hashByContractJson = new Map<unknown, string>();
|
|
122
123
|
for (const pack of inputs) {
|
|
123
|
-
if (pack.contractSpace)
|
|
124
|
-
|
|
125
|
-
id: pack.id,
|
|
126
|
-
targetId: pack.targetId,
|
|
127
|
-
contractSpace: { contractJson: pack.contractSpace.contractJson },
|
|
128
|
-
});
|
|
129
|
-
hashByContractJson.set(pack.contractSpace.contractJson, pack.contractSpace.headRef.hash);
|
|
130
|
-
} else {
|
|
131
|
-
entries.push({ id: pack.id, targetId: pack.targetId });
|
|
132
|
-
}
|
|
124
|
+
if (pack.contractSpace === undefined) continue;
|
|
125
|
+
entries.push({ id: pack.id, targetId: pack.targetId });
|
|
133
126
|
}
|
|
134
|
-
return
|
|
127
|
+
return entries;
|
|
135
128
|
}
|
|
136
129
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
130
|
+
/**
|
|
131
|
+
* Minimal aggregate-loader projection that extracts `id` + `targetId`
|
|
132
|
+
* from raw extension pack descriptors **without invoking any
|
|
133
|
+
* `contractSpace` accessor**. Inspects the own-property descriptor so
|
|
134
|
+
* that getter-backed `contractSpace` declarations are detected but
|
|
135
|
+
* never called.
|
|
136
|
+
*
|
|
137
|
+
* Inclusion semantics match {@link toDeclaredExtensions}: a data
|
|
138
|
+
* property whose value is explicitly `undefined` is treated as "no
|
|
139
|
+
* contract-space declaration" and skipped, mirroring the
|
|
140
|
+
* `pack.contractSpace === undefined` check used on canonicalised
|
|
141
|
+
* inputs. Prototype-chain `contractSpace` properties (no own
|
|
142
|
+
* descriptor) are also skipped.
|
|
143
|
+
*
|
|
144
|
+
* This variant must be used by `buildContractSpaceAggregate` so that
|
|
145
|
+
* the aggregate path (including `db verify`) never reads
|
|
146
|
+
* `contractSpace.contractJson` from extension descriptors — the loader
|
|
147
|
+
* always reads the contract from on-disk artefacts instead.
|
|
148
|
+
*/
|
|
149
|
+
export function toDeclaredExtensionsFromRaw(
|
|
150
|
+
extensionPacks: ReadonlyArray<unknown>,
|
|
151
|
+
): readonly DeclaredExtensionEntry[] {
|
|
152
|
+
const entries: DeclaredExtensionEntry[] = [];
|
|
153
|
+
for (const raw of extensionPacks) {
|
|
154
|
+
if (typeof raw !== 'object' || raw === null) continue;
|
|
155
|
+
const descriptor = Object.getOwnPropertyDescriptor(raw, 'contractSpace');
|
|
156
|
+
if (descriptor === undefined) continue;
|
|
157
|
+
if ('value' in descriptor && descriptor.value === undefined) continue;
|
|
158
|
+
const pack = raw as { readonly id: string; readonly targetId: string };
|
|
159
|
+
entries.push({ id: pack.id, targetId: pack.targetId });
|
|
160
|
+
}
|
|
161
|
+
return entries;
|
|
170
162
|
}
|
|
@@ -7,9 +7,9 @@ import { createColorFormatter, formatDim, isVerbose } from './helpers';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Render a single statement of an `OperationPreview` for the human-readable
|
|
10
|
-
* preview block. SQL statements get a trailing `;` if missing
|
|
11
|
-
* legacy `string[]`-based renderer
|
|
12
|
-
* languages (`'mongodb-shell'`) render verbatim.
|
|
10
|
+
* preview block. SQL statements get a trailing `;` if missing so the rendered
|
|
11
|
+
* preview is byte-identical to the legacy `string[]`-based renderer for SQL
|
|
12
|
+
* targets. Other languages (`'mongodb-shell'`) render verbatim.
|
|
13
13
|
*/
|
|
14
14
|
function renderPreviewStatement(text: string, language: string): string | undefined {
|
|
15
15
|
const trimmed = text.trim();
|
|
@@ -22,9 +22,10 @@ function renderPreviewStatement(text: string, language: string): string | undefi
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Choose the header label for a preview block. SQL-only previews keep the
|
|
25
|
-
* legacy `DDL preview` label
|
|
26
|
-
*
|
|
27
|
-
* non-SQL language — use the family-agnostic `Operation preview`
|
|
25
|
+
* legacy `DDL preview` label so the rendered output is byte-identical to the
|
|
26
|
+
* pre-aggregate SQL CLI; previews from any other family — or a mix that
|
|
27
|
+
* includes any non-SQL language — use the family-agnostic `Operation preview`
|
|
28
|
+
* label.
|
|
28
29
|
*
|
|
29
30
|
* An empty `statements` array deliberately renders as `Operation preview`
|
|
30
31
|
* rather than `DDL preview`: `Array.prototype.every` is vacuously true for
|
|
@@ -76,8 +77,9 @@ export interface MigrationCommandResult {
|
|
|
76
77
|
/**
|
|
77
78
|
* Per-space execution breakdown in canonical schedule order
|
|
78
79
|
* (extensions alphabetically, then app). Surfaces per-space markers
|
|
79
|
-
*
|
|
80
|
-
*
|
|
80
|
+
* and the ops grouped by space, so the CLI summary can name which
|
|
81
|
+
* space each op and marker belongs to instead of flattening them
|
|
82
|
+
* into a single ambiguous list. See {@link AggregatePerSpaceExecutionEntry}.
|
|
81
83
|
*/
|
|
82
84
|
readonly perSpace?: ReadonlyArray<AggregatePerSpaceExecutionEntry>;
|
|
83
85
|
readonly summary: string;
|
|
@@ -88,10 +90,9 @@ export interface MigrationCommandResult {
|
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
92
|
* Render the shared per-space execution block consumed by the `db init`
|
|
91
|
-
* / `db update` / `migration apply` summaries
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* hash (when known).
|
|
93
|
+
* / `db update` / `migration apply` summaries. Always shows: space
|
|
94
|
+
* label (`Extension space: <id>` or `App space`) → per-op lines under
|
|
95
|
+
* each space → per-space marker hash (when known).
|
|
95
96
|
*
|
|
96
97
|
* `mode` controls the marker label phrasing — `'apply'` shows
|
|
97
98
|
* `marker → <hash>` (post-apply), `'plan'` omits the marker line
|
|
@@ -165,7 +166,7 @@ export function formatMigrationPlanOutput(
|
|
|
165
166
|
const formatYellow = createColorFormatter(useColor, yellow);
|
|
166
167
|
|
|
167
168
|
// Per-space breakdown takes precedence over the flat ops tree when
|
|
168
|
-
// the aggregate flow surfaced one
|
|
169
|
+
// the aggregate flow surfaced one.
|
|
169
170
|
if (result.perSpace && result.perSpace.length > 0) {
|
|
170
171
|
lines.push('');
|
|
171
172
|
lines.push(...formatPerSpaceBlock(result.perSpace, 'plan', useColor));
|
|
@@ -294,7 +295,9 @@ export function formatMigrationApplyCommandOutput(
|
|
|
294
295
|
return lines.join('\n');
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
interface
|
|
298
|
+
interface MigrationShowSpacePresent {
|
|
299
|
+
readonly kind: 'present';
|
|
300
|
+
readonly spaceId: string;
|
|
298
301
|
readonly dirName: string;
|
|
299
302
|
readonly dirPath: string;
|
|
300
303
|
readonly from: string | null;
|
|
@@ -310,39 +313,48 @@ interface MigrationShowResult {
|
|
|
310
313
|
readonly summary: string;
|
|
311
314
|
}
|
|
312
315
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
316
|
+
interface MigrationShowSpaceMissing {
|
|
317
|
+
readonly kind: 'missing';
|
|
318
|
+
readonly spaceId: string;
|
|
319
|
+
readonly summary: string;
|
|
320
|
+
}
|
|
317
321
|
|
|
318
|
-
|
|
322
|
+
type MigrationShowSpaceResult = MigrationShowSpacePresent | MigrationShowSpaceMissing;
|
|
319
323
|
|
|
320
|
-
|
|
324
|
+
interface MigrationShowResult {
|
|
325
|
+
readonly spaces: readonly MigrationShowSpaceResult[];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatSpaceShowBlock(
|
|
329
|
+
space: MigrationShowSpacePresent,
|
|
330
|
+
useColor: boolean,
|
|
331
|
+
): readonly string[] {
|
|
321
332
|
const formatGreen = createColorFormatter(useColor, green);
|
|
322
333
|
const formatYellow = createColorFormatter(useColor, yellow);
|
|
323
334
|
const formatDimText = (text: string) => formatDim(useColor, text);
|
|
324
335
|
|
|
325
|
-
lines
|
|
326
|
-
lines.push(`${
|
|
327
|
-
lines.push(`${formatDimText(`
|
|
328
|
-
lines.push(`${formatDimText(`
|
|
329
|
-
lines.push(`${formatDimText(`
|
|
336
|
+
const lines: string[] = [];
|
|
337
|
+
lines.push(`${formatGreen('✔')} ${space.dirName}`);
|
|
338
|
+
lines.push(`${formatDimText(` from: ${space.from ?? '(baseline)'}`)}`);
|
|
339
|
+
lines.push(`${formatDimText(` to: ${space.to}`)}`);
|
|
340
|
+
lines.push(`${formatDimText(` migrationHash: ${space.migrationHash}`)}`);
|
|
341
|
+
lines.push(`${formatDimText(` created: ${space.createdAt}`)}`);
|
|
330
342
|
|
|
331
343
|
lines.push('');
|
|
332
|
-
lines.push(`${
|
|
344
|
+
lines.push(`${space.operations.length} operation(s)`);
|
|
333
345
|
|
|
334
|
-
if (
|
|
346
|
+
if (space.operations.length > 0) {
|
|
335
347
|
lines.push(`${formatDimText('│')}`);
|
|
336
|
-
for (let i = 0; i <
|
|
337
|
-
const op =
|
|
338
|
-
const isLast = i ===
|
|
348
|
+
for (let i = 0; i < space.operations.length; i++) {
|
|
349
|
+
const op = space.operations[i]!;
|
|
350
|
+
const isLast = i === space.operations.length - 1;
|
|
339
351
|
const treeChar = isLast ? '└' : '├';
|
|
340
352
|
const destructiveMarker =
|
|
341
353
|
op.operationClass === 'destructive' ? ` ${formatYellow('(destructive)')}` : '';
|
|
342
354
|
lines.push(`${formatDimText(treeChar)}─ ${op.label}${destructiveMarker}`);
|
|
343
355
|
}
|
|
344
356
|
|
|
345
|
-
const hasDestructive =
|
|
357
|
+
const hasDestructive = space.operations.some((op) => op.operationClass === 'destructive');
|
|
346
358
|
if (hasDestructive) {
|
|
347
359
|
lines.push('');
|
|
348
360
|
lines.push(
|
|
@@ -351,11 +363,11 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
|
|
|
351
363
|
}
|
|
352
364
|
}
|
|
353
365
|
|
|
354
|
-
if (
|
|
366
|
+
if (space.preview.statements.length > 0) {
|
|
355
367
|
lines.push('');
|
|
356
|
-
lines.push(`${formatDimText(previewBlockHeader(
|
|
368
|
+
lines.push(`${formatDimText(previewBlockHeader(space.preview))}`);
|
|
357
369
|
lines.push('');
|
|
358
|
-
for (const statement of
|
|
370
|
+
for (const statement of space.preview.statements) {
|
|
359
371
|
const rendered = renderPreviewStatement(statement.text, statement.language);
|
|
360
372
|
if (rendered) {
|
|
361
373
|
lines.push(rendered);
|
|
@@ -363,6 +375,36 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
|
|
|
363
375
|
}
|
|
364
376
|
}
|
|
365
377
|
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function formatMigrationShowOutput(result: MigrationShowResult, flags: GlobalFlags): string {
|
|
382
|
+
if (flags.quiet) {
|
|
383
|
+
return '';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const useColor = flags.color !== false;
|
|
387
|
+
const formatDimText = (text: string) => formatDim(useColor, text);
|
|
388
|
+
const multipleSpaces = result.spaces.length > 1;
|
|
389
|
+
const lines: string[] = [];
|
|
390
|
+
|
|
391
|
+
for (let i = 0; i < result.spaces.length; i++) {
|
|
392
|
+
const space = result.spaces[i]!;
|
|
393
|
+
if (multipleSpaces) {
|
|
394
|
+
lines.push(formatDimText(`── ${space.spaceId} ──`));
|
|
395
|
+
}
|
|
396
|
+
if (space.kind === 'missing') {
|
|
397
|
+
lines.push(formatDimText(` ${space.summary}`));
|
|
398
|
+
} else {
|
|
399
|
+
for (const line of formatSpaceShowBlock(space, useColor)) {
|
|
400
|
+
lines.push(line);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (i < result.spaces.length - 1) {
|
|
404
|
+
lines.push('');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
366
408
|
return lines.join('\n');
|
|
367
409
|
}
|
|
368
410
|
|
|
@@ -401,7 +443,7 @@ export function formatMigrationApplyOutput(
|
|
|
401
443
|
}
|
|
402
444
|
|
|
403
445
|
// Per-space breakdown — replaces the single ambiguous `Signature:`
|
|
404
|
-
// line
|
|
446
|
+
// line with a per-space marker + ops listing.
|
|
405
447
|
if (result.perSpace && result.perSpace.length > 0) {
|
|
406
448
|
lines.push('');
|
|
407
449
|
lines.push(...formatPerSpaceBlock(result.perSpace, 'apply', useColor));
|
|
@@ -415,9 +457,9 @@ export function formatMigrationApplyOutput(
|
|
|
415
457
|
);
|
|
416
458
|
} else if (result.marker) {
|
|
417
459
|
// Single-space fallback (no aggregate breakdown surfaced — e.g.
|
|
418
|
-
// older callers / non-aggregate code paths).
|
|
419
|
-
// `
|
|
420
|
-
// marker is observable
|
|
460
|
+
// older callers / non-aggregate code paths). The label is
|
|
461
|
+
// `App-space marker` (not `Signature`) so that when only one
|
|
462
|
+
// marker is observable we still name what it covers explicitly.
|
|
421
463
|
lines.push(`${formatDimText(` App-space marker: ${result.marker.storageHash}`)}`);
|
|
422
464
|
if (result.marker.profileHash) {
|
|
423
465
|
lines.push(`${formatDimText(` Profile hash: ${result.marker.profileHash}`)}`);
|