@prisma-next/cli 0.5.0-dev.74 → 0.5.0-dev.76
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 +8 -8
- package/dist/{client-0ZX24FXF.mjs → client-qVH-rEgd.mjs} +433 -236
- package/dist/client-qVH-rEgd.mjs.map +1 -0
- package/dist/{result-handler-DWb1rFS-.mjs → command-helpers-BeZHkxV8.mjs} +22 -24
- package/dist/command-helpers-BeZHkxV8.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +7 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +5 -4
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +6 -5
- 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 +7 -5
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migration-apply.d.mts +29 -17
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +35 -129
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.mjs +4 -3
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +19 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +2 -2
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +3 -2
- package/dist/commands/migration-ref.mjs.map +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +5 -4
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +104 -1
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -2
- package/dist/{contract-emit-DkMqO7f2.mjs → contract-emit-9DBda5Ou.mjs} +7 -5
- package/dist/{contract-emit-DkMqO7f2.mjs.map → contract-emit-9DBda5Ou.mjs.map} +1 -1
- package/dist/{contract-emit-B3ChISB_.mjs → contract-emit-B77TsJqf.mjs} +4 -15
- package/dist/{contract-emit-B3ChISB_.mjs.map → contract-emit-B77TsJqf.mjs.map} +1 -1
- package/dist/{contract-enrichment-CF6ogEJ_.mjs → contract-enrichment-Dani0mMW.mjs} +1 -1
- package/dist/{contract-enrichment-CF6ogEJ_.mjs.map → contract-enrichment-Dani0mMW.mjs.map} +1 -1
- package/dist/{contract-infer-BDKAE0B0.mjs → contract-infer-BK9YFGEG.mjs} +5 -4
- package/dist/{contract-infer-BDKAE0B0.mjs.map → contract-infer-BK9YFGEG.mjs.map} +1 -1
- package/dist/{db-verify-B4TdDKOI.mjs → db-verify-C0y1PCO2.mjs} +7 -6
- package/dist/{db-verify-B4TdDKOI.mjs.map → db-verify-C0y1PCO2.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +3 -746
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -3
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/extension-pack-inputs-C7xgE-vv.mjs +74 -0
- package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +1 -0
- package/dist/{framework-components-gwAHl7ml.mjs → framework-components-ChqVUxR-.mjs} +1 -1
- package/dist/{framework-components-gwAHl7ml.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-Deo7U8_U.mjs → init-CoDVPvQ4.mjs} +4 -4
- package/dist/{init-Deo7U8_U.mjs.map → init-CoDVPvQ4.mjs.map} +1 -1
- package/dist/{inspect-live-schema-BAgQMYpD.mjs → inspect-live-schema-CWYxGKlb.mjs} +4 -4
- package/dist/{inspect-live-schema-BAgQMYpD.mjs.map → inspect-live-schema-CWYxGKlb.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-B8J702Uh.mjs → migration-command-scaffold-B5dORFEv.mjs} +4 -4
- package/dist/{migration-command-scaffold-B8J702Uh.mjs.map → migration-command-scaffold-B5dORFEv.mjs.map} +1 -1
- package/dist/{migration-plan-BcKNnTM7.mjs → migration-plan-C6lVaHsO.mjs} +47 -23
- package/dist/migration-plan-C6lVaHsO.mjs.map +1 -0
- package/dist/{migration-status-CjwB2of-.mjs → migration-status-CZ-D5k7k.mjs} +161 -7
- package/dist/migration-status-CZ-D5k7k.mjs.map +1 -0
- package/dist/{migrations-CIK94AJf.mjs → migrations-D_UJnpuW.mjs} +67 -24
- package/dist/migrations-D_UJnpuW.mjs.map +1 -0
- package/dist/{output-DnjfCC_u.mjs → output-B16Kefzx.mjs} +1 -1
- package/dist/{output-DnjfCC_u.mjs.map → output-B16Kefzx.mjs.map} +1 -1
- package/dist/{progress-adapter-xASh41wr.mjs → progress-adapter-DFfvZcYL.mjs} +1 -1
- package/dist/{progress-adapter-xASh41wr.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-zaRDhJnP.mjs → terminal-ui-C_hFNbAn.mjs} +3 -23
- package/dist/terminal-ui-C_hFNbAn.mjs.map +1 -0
- package/dist/types-D7x-IFLO.d.mts +858 -0
- package/dist/types-D7x-IFLO.d.mts.map +1 -0
- package/dist/{verify-BEIa9638.mjs → verify-CiwNWM9N.mjs} +2 -2
- package/dist/{verify-BEIa9638.mjs.map → verify-CiwNWM9N.mjs.map} +1 -1
- package/package.json +14 -14
- package/src/commands/db-init.ts +1 -0
- package/src/commands/db-update.ts +1 -0
- package/src/commands/migration-apply.ts +94 -213
- package/src/commands/migration-plan.ts +89 -32
- package/src/commands/migration-status.ts +288 -5
- package/src/control-api/client.ts +16 -4
- package/src/control-api/operations/apply-aggregate.ts +290 -0
- package/src/control-api/operations/db-apply-aggregate.ts +42 -91
- package/src/control-api/operations/migration-apply.ts +420 -155
- package/src/control-api/types.ts +165 -32
- package/src/utils/contract-space-aggregate-loader.ts +24 -56
- package/src/utils/extension-pack-inputs.ts +170 -0
- package/src/utils/formatters/migrations.ts +135 -35
- package/dist/client-0ZX24FXF.mjs.map +0 -1
- package/dist/migration-plan-BcKNnTM7.mjs.map +0 -1
- package/dist/migration-status-CjwB2of-.mjs.map +0 -1
- package/dist/migrations-CIK94AJf.mjs.map +0 -1
- package/dist/result-handler-DWb1rFS-.mjs.map +0 -1
- package/dist/terminal-ui-zaRDhJnP.mjs.map +0 -1
- /package/dist/{cli-errors-QH8kf-C2.d.mts → cli-errors-B9OBbled.d.mts} +0 -0
|
@@ -1,4 +1,11 @@
|
|
|
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';
|
|
3
10
|
import {
|
|
4
11
|
errorNoInvariantPath,
|
|
@@ -39,6 +46,10 @@ import {
|
|
|
39
46
|
toPathDecisionResult,
|
|
40
47
|
toStructuralEdge,
|
|
41
48
|
} from '../utils/command-helpers';
|
|
49
|
+
import {
|
|
50
|
+
type BuildAggregateInputs,
|
|
51
|
+
buildContractSpaceAggregate,
|
|
52
|
+
} from '../utils/contract-space-aggregate-loader';
|
|
42
53
|
import {
|
|
43
54
|
type EdgeStatus,
|
|
44
55
|
type EdgeStatusKind,
|
|
@@ -76,6 +87,53 @@ export interface MigrationStatusEntry {
|
|
|
76
87
|
readonly status: EdgeStatusKind | 'unknown';
|
|
77
88
|
}
|
|
78
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
|
+
|
|
79
137
|
export type { StatusDiagnostic, StatusRef } from '../utils/migration-types';
|
|
80
138
|
|
|
81
139
|
export interface MigrationStatusResult {
|
|
@@ -117,6 +175,22 @@ export interface MigrationStatusResult {
|
|
|
117
175
|
};
|
|
118
176
|
readonly summary: string;
|
|
119
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;
|
|
120
194
|
readonly graph?: MigrationGraph;
|
|
121
195
|
readonly bundles?: readonly OnDiskMigrationPackage[];
|
|
122
196
|
readonly edgeStatuses?: readonly EdgeStatus[];
|
|
@@ -363,16 +437,130 @@ function determineLimit(opts: MigrationStatusOptions) {
|
|
|
363
437
|
return parsed;
|
|
364
438
|
}
|
|
365
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Build the aggregate enumeration of contract spaces for the status
|
|
442
|
+
* output. Loads the aggregate from disk (lossy on failure — extension
|
|
443
|
+
* spaces are simply omitted, the existing single-space app behaviour
|
|
444
|
+
* keeps working), reads per-space marker rows when online, and uses
|
|
445
|
+
* {@link graphWalkStrategy} to compute each space's pending count.
|
|
446
|
+
*
|
|
447
|
+
* Sub-spec § `migration status` semantics — the aggregate-walking
|
|
448
|
+
* version reports per-space marker + pending state alongside the
|
|
449
|
+
* cross-space totals.
|
|
450
|
+
*/
|
|
451
|
+
export async function loadAggregateStatusSpaces(args: {
|
|
452
|
+
readonly targetId: string;
|
|
453
|
+
readonly migrationsDir: string;
|
|
454
|
+
readonly appContractRaw: unknown;
|
|
455
|
+
readonly extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'];
|
|
456
|
+
readonly validateContract: BuildAggregateInputs<string, string>['validateContract'];
|
|
457
|
+
readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
|
|
458
|
+
}): Promise<readonly MigrationStatusSpaceEntry[]> {
|
|
459
|
+
const loadInputs: BuildAggregateInputs<string, string> = {
|
|
460
|
+
targetId: args.targetId,
|
|
461
|
+
migrationsDir: args.migrationsDir,
|
|
462
|
+
appContract: args.validateContract(args.appContractRaw),
|
|
463
|
+
extensionPacks: args.extensionPacks,
|
|
464
|
+
validateContract: args.validateContract,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const loaded = await buildContractSpaceAggregate(loadInputs);
|
|
468
|
+
if (!loaded.ok) {
|
|
469
|
+
// Loader failure (drift, layout violation, etc.) — surfacing it
|
|
470
|
+
// as a status diagnostic would duplicate `migration plan`'s job.
|
|
471
|
+
// The single-space app pipeline still runs; extensions are simply
|
|
472
|
+
// not enumerated.
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
const aggregate = loaded.value;
|
|
476
|
+
|
|
477
|
+
const orderedMembers = [...aggregate.extensions, aggregate.app];
|
|
478
|
+
const rows: MigrationStatusSpaceEntry[] = [];
|
|
479
|
+
for (const member of orderedMembers) {
|
|
480
|
+
const liveMarker = args.markersBySpace?.get(member.spaceId) ?? null;
|
|
481
|
+
const isApp = member.spaceId === aggregate.app.spaceId;
|
|
482
|
+
|
|
483
|
+
if (member.migrations.graph.nodes.size === 0) {
|
|
484
|
+
rows.push({
|
|
485
|
+
spaceId: member.spaceId,
|
|
486
|
+
kind: isApp ? 'app' : 'extension',
|
|
487
|
+
headHash: member.headRef.hash,
|
|
488
|
+
...(args.markersBySpace !== null
|
|
489
|
+
? {
|
|
490
|
+
markerHash: liveMarker?.storageHash ?? null,
|
|
491
|
+
status: member.headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
|
|
492
|
+
pendingCount: 0,
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
495
|
+
});
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (args.markersBySpace === null) {
|
|
500
|
+
rows.push({
|
|
501
|
+
spaceId: member.spaceId,
|
|
502
|
+
kind: isApp ? 'app' : 'extension',
|
|
503
|
+
headHash: member.headRef.hash,
|
|
504
|
+
});
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const walked = graphWalkStrategy({
|
|
509
|
+
aggregateTargetId: aggregate.targetId,
|
|
510
|
+
member,
|
|
511
|
+
currentMarker: liveMarker,
|
|
512
|
+
});
|
|
513
|
+
let pendingCount = 0;
|
|
514
|
+
let status: MigrationStatusSpaceEntry['status'];
|
|
515
|
+
if (walked.kind === 'ok') {
|
|
516
|
+
pendingCount = walked.result.plan.operations.length;
|
|
517
|
+
if (liveMarker === null) {
|
|
518
|
+
status = pendingCount === 0 ? 'no-marker' : 'pending';
|
|
519
|
+
} else {
|
|
520
|
+
status = pendingCount === 0 ? 'up-to-date' : 'pending';
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
status = 'unreachable';
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
rows.push({
|
|
527
|
+
spaceId: member.spaceId,
|
|
528
|
+
kind: isApp ? 'app' : 'extension',
|
|
529
|
+
headHash: member.headRef.hash,
|
|
530
|
+
markerHash: liveMarker?.storageHash ?? null,
|
|
531
|
+
pendingCount,
|
|
532
|
+
...(status ? { status } : {}),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return rows;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Read the raw contract.json bytes from disk for the aggregate
|
|
540
|
+
* loader. Returns `null` if the file is missing or unparseable —
|
|
541
|
+
* the existing `readContractEnvelope` path will report the same
|
|
542
|
+
* problem via a status diagnostic, no need to double-surface.
|
|
543
|
+
*/
|
|
544
|
+
async function loadContractRawSafely(config: {
|
|
545
|
+
contract?: { output?: string };
|
|
546
|
+
}): Promise<unknown | null> {
|
|
547
|
+
try {
|
|
548
|
+
const path = (await import('../utils/command-helpers')).resolveContractPath(config);
|
|
549
|
+
const raw = await (await import('node:fs/promises')).readFile(path, 'utf-8');
|
|
550
|
+
return JSON.parse(raw) as unknown;
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
366
556
|
async function executeMigrationStatusCommand(
|
|
367
557
|
options: MigrationStatusOptions,
|
|
368
558
|
flags: GlobalFlags,
|
|
369
559
|
ui: TerminalUI,
|
|
370
560
|
): Promise<Result<MigrationStatusResult, CliStructuredError>> {
|
|
371
561
|
const config = await loadConfig(options.config);
|
|
372
|
-
const { configPath, appMigrationsDir, appMigrationsRelative, refsDir } =
|
|
373
|
-
options.config,
|
|
374
|
-
config,
|
|
375
|
-
);
|
|
562
|
+
const { configPath, appMigrationsDir, appMigrationsRelative, migrationsDir, refsDir } =
|
|
563
|
+
resolveMigrationPaths(options.config, config);
|
|
376
564
|
|
|
377
565
|
const dbConnection = options.db ?? config.db?.connection;
|
|
378
566
|
const hasDriver = !!config.driver;
|
|
@@ -515,6 +703,7 @@ async function executeMigrationStatusCommand(
|
|
|
515
703
|
let markerHash: string | undefined;
|
|
516
704
|
let markerInvariants: readonly string[] = [];
|
|
517
705
|
let mode: 'online' | 'offline' = 'offline';
|
|
706
|
+
let allMarkers: ReadonlyMap<string, ContractMarkerRecordLike> | null = null;
|
|
518
707
|
|
|
519
708
|
if (dbConnection && hasDriver) {
|
|
520
709
|
const client = createControlClient({
|
|
@@ -530,6 +719,21 @@ async function executeMigrationStatusCommand(
|
|
|
530
719
|
markerHash = marker?.storageHash;
|
|
531
720
|
markerInvariants = marker?.invariants ?? [];
|
|
532
721
|
mode = 'online';
|
|
722
|
+
// Read every space's marker so the aggregate enumeration can
|
|
723
|
+
// surface per-space marker state. `readAllMarkers` mirrors what
|
|
724
|
+
// `db init` / `db update` already use to drive the multi-space
|
|
725
|
+
// planner; here it powers the aggregate status output.
|
|
726
|
+
try {
|
|
727
|
+
allMarkers = await client.readAllMarkers();
|
|
728
|
+
} catch {
|
|
729
|
+
// Older family instances may not implement `readAllMarkers`.
|
|
730
|
+
// Per-space enumeration falls back to "marker unknown" rather
|
|
731
|
+
// than failing the whole status command — leaving
|
|
732
|
+
// `allMarkers` as `null` signals "unknown" to the aggregate
|
|
733
|
+
// loader (an empty `Map` would instead mean "every space has
|
|
734
|
+
// no marker", which is a different condition).
|
|
735
|
+
allMarkers = null;
|
|
736
|
+
}
|
|
533
737
|
} catch {
|
|
534
738
|
if (!flags.json && !flags.quiet) {
|
|
535
739
|
ui.warn('Could not connect to database — showing offline status');
|
|
@@ -539,6 +743,37 @@ async function executeMigrationStatusCommand(
|
|
|
539
743
|
}
|
|
540
744
|
}
|
|
541
745
|
|
|
746
|
+
// Build the aggregate enumeration of contract spaces. Lossy on
|
|
747
|
+
// failure (extensions are simply omitted) so the existing
|
|
748
|
+
// single-space app pipeline below still runs even if extensions
|
|
749
|
+
// can't be loaded — a strict failure here would degrade the
|
|
750
|
+
// load-bearing app-space output for unrelated reasons.
|
|
751
|
+
const contractRawForAggregate = await loadContractRawSafely(config);
|
|
752
|
+
let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
|
|
753
|
+
if (contractRawForAggregate !== null) {
|
|
754
|
+
// The aggregate loader needs a typed-Contract producer. Build a
|
|
755
|
+
// real control stack so `validateContract` runs against a fully
|
|
756
|
+
// composed family instance — descriptors that read stack members
|
|
757
|
+
// during construction (e.g. codec lookups) get a consistent view.
|
|
758
|
+
const stack = createControlStack(config);
|
|
759
|
+
const familyInstance = config.family.create(stack);
|
|
760
|
+
try {
|
|
761
|
+
aggregateSpaces = await loadAggregateStatusSpaces({
|
|
762
|
+
targetId: config.target.targetId,
|
|
763
|
+
migrationsDir,
|
|
764
|
+
appContractRaw: contractRawForAggregate,
|
|
765
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
766
|
+
validateContract: (json: unknown) => familyInstance.validateContract(json),
|
|
767
|
+
markersBySpace: allMarkers,
|
|
768
|
+
});
|
|
769
|
+
} catch {
|
|
770
|
+
// Loader failure short-circuits silently — the existing
|
|
771
|
+
// single-space app pipeline below still runs.
|
|
772
|
+
aggregateSpaces = [];
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const totalPendingAcrossSpaces = computeTotalPendingAcrossSpaces(aggregateSpaces);
|
|
776
|
+
|
|
542
777
|
// Pre-check unknown invariants. Online: union the graph's declared
|
|
543
778
|
// invariants with the marker's recorded set so a retired-but-applied
|
|
544
779
|
// invariant doesn't surface as MIGRATION.UNKNOWN_INVARIANT — apply would
|
|
@@ -803,6 +1038,8 @@ async function executeMigrationStatusCommand(
|
|
|
803
1038
|
edgeStatuses,
|
|
804
1039
|
...ifDefined('activeRefHash', activeRefHash),
|
|
805
1040
|
...ifDefined('activeRefName', activeRefName),
|
|
1041
|
+
spaces: aggregateSpaces,
|
|
1042
|
+
...ifDefined('totalPendingAcrossSpaces', totalPendingAcrossSpaces),
|
|
806
1043
|
};
|
|
807
1044
|
return ok(result);
|
|
808
1045
|
}
|
|
@@ -950,9 +1187,55 @@ export function formatStatusSummary(result: MigrationStatusResult, colorize: boo
|
|
|
950
1187
|
}
|
|
951
1188
|
}
|
|
952
1189
|
|
|
1190
|
+
// Per-space section. Suppressed when there's no extension space —
|
|
1191
|
+
// the legacy single-space output already covers the app member.
|
|
1192
|
+
// When extensions exist, render every space (including the app)
|
|
1193
|
+
// for consistency, plus a cross-space pending total + apply hint.
|
|
1194
|
+
if (result.spaces?.some((s) => s.kind === 'extension')) {
|
|
1195
|
+
const total = result.totalPendingAcrossSpaces ?? 0;
|
|
1196
|
+
lines.push('');
|
|
1197
|
+
lines.push(c(dim, 'spaces'));
|
|
1198
|
+
for (const space of result.spaces) {
|
|
1199
|
+
lines.push(formatSpaceLine(space, c));
|
|
1200
|
+
}
|
|
1201
|
+
if (total > 0) {
|
|
1202
|
+
lines.push('');
|
|
1203
|
+
lines.push(
|
|
1204
|
+
`${c(yellow, '⧗')} ${total} pending migration(s) across ${result.spaces.length} space(s) — run 'prisma-next migration apply' to apply`,
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
953
1209
|
return lines.join('\n');
|
|
954
1210
|
}
|
|
955
1211
|
|
|
1212
|
+
function formatSpaceLine(
|
|
1213
|
+
space: MigrationStatusSpaceEntry,
|
|
1214
|
+
c: (fn: (s: string) => string, s: string) => string,
|
|
1215
|
+
): string {
|
|
1216
|
+
const glyph = (() => {
|
|
1217
|
+
if (space.status === 'up-to-date' || space.status === 'no-marker') return c(cyan, '✓');
|
|
1218
|
+
if (space.status === 'pending') return c(yellow, '⧗');
|
|
1219
|
+
if (space.status === 'unreachable' || space.status === 'never-planned') return c(magenta, '✗');
|
|
1220
|
+
return ' ';
|
|
1221
|
+
})();
|
|
1222
|
+
const tag = space.kind === 'app' ? '[app]' : '[ext]';
|
|
1223
|
+
const head = space.headHash.slice(0, 8);
|
|
1224
|
+
const marker =
|
|
1225
|
+
space.markerHash === undefined
|
|
1226
|
+
? '(unknown)'
|
|
1227
|
+
: space.markerHash === null
|
|
1228
|
+
? '(no marker)'
|
|
1229
|
+
: space.markerHash.slice(0, 8);
|
|
1230
|
+
const pending =
|
|
1231
|
+
space.pendingCount === undefined
|
|
1232
|
+
? ''
|
|
1233
|
+
: space.pendingCount === 0
|
|
1234
|
+
? c(dim, ' (up to date)')
|
|
1235
|
+
: c(yellow, ` (${space.pendingCount} pending)`);
|
|
1236
|
+
return ` ${glyph} ${c(dim, tag)} ${space.spaceId} → head ${c(dim, head)}, marker ${c(dim, marker)}${pending}`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
956
1239
|
function formatInvariantList(ids: readonly string[]): string {
|
|
957
1240
|
return ids.length === 0 ? '(none)' : ids.join(', ');
|
|
958
1241
|
}
|
|
@@ -461,16 +461,28 @@ class ControlClientImpl implements ControlClient {
|
|
|
461
461
|
throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
let contract: Contract;
|
|
465
|
+
try {
|
|
466
|
+
contract = familyInstance.validateContract(options.contract);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
469
|
+
throw new ContractValidationError(message, error);
|
|
470
|
+
}
|
|
471
|
+
|
|
464
472
|
return executeMigrationApply({
|
|
465
473
|
driver,
|
|
466
474
|
familyInstance,
|
|
467
|
-
|
|
468
|
-
destinationHash: options.destinationHash,
|
|
469
|
-
pendingMigrations: options.pendingMigrations,
|
|
475
|
+
contract,
|
|
470
476
|
migrations: this.options.target.migrations,
|
|
471
477
|
frameworkComponents,
|
|
478
|
+
migrationsDir: options.migrationsDir,
|
|
479
|
+
extensionPacks: this.options.extensionPacks ?? [],
|
|
472
480
|
targetId: this.options.target.targetId,
|
|
473
|
-
|
|
481
|
+
appMigrationPackages: options.appMigrationPackages,
|
|
482
|
+
...ifDefined('refHash', options.refHash),
|
|
483
|
+
...ifDefined('refInvariants', options.refInvariants),
|
|
484
|
+
...ifDefined('refName', options.refName),
|
|
485
|
+
...ifDefined('onProgress', onProgress),
|
|
474
486
|
});
|
|
475
487
|
}
|
|
476
488
|
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
2
|
+
import type {
|
|
3
|
+
ControlDriverInstance,
|
|
4
|
+
ControlFamilyInstance,
|
|
5
|
+
MigrationOperationPolicy,
|
|
6
|
+
MultiSpaceCapableRunner,
|
|
7
|
+
MultiSpaceRunnerPerSpaceOptions,
|
|
8
|
+
TargetMigrationsCapability,
|
|
9
|
+
} from '@prisma-next/framework-components/control';
|
|
10
|
+
import { hasMultiSpaceRunner } from '@prisma-next/framework-components/control';
|
|
11
|
+
import type {
|
|
12
|
+
AggregatePerSpacePlan,
|
|
13
|
+
ContractSpaceAggregate,
|
|
14
|
+
} from '@prisma-next/migration-tools/aggregate';
|
|
15
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
16
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
17
|
+
import { errorRunnerFailed } from '../../utils/cli-errors';
|
|
18
|
+
import type { AggregatePerSpaceExecutionEntry, OnControlProgress } from '../types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Span id emitted via `onProgress` for the apply phase. Stable
|
|
22
|
+
* identifier consumed by the structured-output renderer and by tests.
|
|
23
|
+
*/
|
|
24
|
+
const APPLY_SPAN_ID = 'apply' as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Action that originated this apply call. Threaded into `OnControlProgress`
|
|
28
|
+
* events so the parent CLI command can attribute the span correctly,
|
|
29
|
+
* and used to compose action-specific summary phrasing.
|
|
30
|
+
*/
|
|
31
|
+
export type AggregateApplyAction = 'dbInit' | 'dbUpdate' | 'migrationApply';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Failure variant emitted by {@link applyAggregate} when the multi-space
|
|
35
|
+
* runner itself rejects the apply. Mirrors the failure shape callers
|
|
36
|
+
* already wrap into their own action-specific failure envelopes
|
|
37
|
+
* (`DbInitFailure`, `DbUpdateFailure`, `MigrationApplyFailure`) so each
|
|
38
|
+
* caller keeps owning its own discriminated failure code.
|
|
39
|
+
*/
|
|
40
|
+
export interface AggregateApplyRunnerFailure {
|
|
41
|
+
readonly summary: string;
|
|
42
|
+
readonly why?: string;
|
|
43
|
+
readonly meta: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ApplyAggregateInputs<TFamilyId extends string, TTargetId extends string> {
|
|
47
|
+
readonly aggregate: ContractSpaceAggregate;
|
|
48
|
+
/**
|
|
49
|
+
* Per-space plans, keyed by `spaceId`. Produced by either the full
|
|
50
|
+
* {@link planAggregate} pipeline (`db init` / `db update` — synth
|
|
51
|
+
* for the app, graph-walk for extensions) or by direct
|
|
52
|
+
* {@link graphWalkStrategy} calls (`migration apply` — graph-walk
|
|
53
|
+
* for every member). Either way, the runner consumes the same shape.
|
|
54
|
+
*/
|
|
55
|
+
readonly perSpacePlans: ReadonlyMap<string, AggregatePerSpacePlan>;
|
|
56
|
+
/**
|
|
57
|
+
* Canonical schedule order — extensions alphabetically by `spaceId`,
|
|
58
|
+
* then app. Mirrors {@link import('@prisma-next/migration-tools/concatenate-space-apply-inputs').concatenateSpaceApplyInputs}'s
|
|
59
|
+
* convention so `MultiSpaceRunnerFailure.failingSpace` attribution
|
|
60
|
+
* stays byte-for-byte stable across callers.
|
|
61
|
+
*/
|
|
62
|
+
readonly applyOrder: readonly string[];
|
|
63
|
+
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
64
|
+
readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
|
|
65
|
+
readonly migrations: TargetMigrationsCapability<
|
|
66
|
+
TFamilyId,
|
|
67
|
+
TTargetId,
|
|
68
|
+
ControlFamilyInstance<TFamilyId, unknown>
|
|
69
|
+
>;
|
|
70
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
|
|
71
|
+
readonly policy: MigrationOperationPolicy;
|
|
72
|
+
readonly action: AggregateApplyAction;
|
|
73
|
+
readonly onProgress?: OnControlProgress;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolved per-space plan in canonical schedule order. Surfaced from
|
|
78
|
+
* {@link applyAggregate} to callers so each one can build its own
|
|
79
|
+
* action-specific success envelope (e.g. `DbInitSuccess` vs
|
|
80
|
+
* `MigrationApplySuccess`) without re-deriving the ordering.
|
|
81
|
+
*/
|
|
82
|
+
export interface OrderedResolution {
|
|
83
|
+
readonly spaceId: string;
|
|
84
|
+
readonly entry: AggregatePerSpacePlan;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ApplyAggregateValue {
|
|
88
|
+
readonly orderedResolutions: readonly OrderedResolution[];
|
|
89
|
+
readonly totalOpsPlanned: number;
|
|
90
|
+
readonly totalOpsExecuted: number;
|
|
91
|
+
/**
|
|
92
|
+
* Per-space breakdown ready to thread into action-specific success
|
|
93
|
+
* envelopes. Each entry carries the post-apply marker (live storage hash
|
|
94
|
+
* plus invariants) so callers can render it directly without re-reading.
|
|
95
|
+
*/
|
|
96
|
+
readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ApplyAggregateResult = Result<ApplyAggregateValue, AggregateApplyRunnerFailure>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Runner-driving tail shared by every aggregate apply caller — `db init`,
|
|
103
|
+
* `db update`, and `migration apply`. Consumes already-resolved per-space
|
|
104
|
+
* plans (the planner-vs-replay distinction is owned by the caller) and
|
|
105
|
+
* dispatches them to the multi-space runner in canonical order.
|
|
106
|
+
*
|
|
107
|
+
* Marker advancement is part of the runner's per-space transaction
|
|
108
|
+
* (the SQL family runner writes the marker as the last step of each
|
|
109
|
+
* space's transaction), so this primitive does not advance markers
|
|
110
|
+
* separately — by the time `executeAcrossSpaces` returns ok, every
|
|
111
|
+
* space's marker has been advanced to its plan's destination.
|
|
112
|
+
*
|
|
113
|
+
* Span emission (`spanStart 'apply'` / `spanEnd 'apply'`) is owned here
|
|
114
|
+
* so callers don't have to duplicate it; the `action` field on each
|
|
115
|
+
* progress event is taken from the caller's `action` argument.
|
|
116
|
+
*/
|
|
117
|
+
export async function applyAggregate<TFamilyId extends string, TTargetId extends string>(
|
|
118
|
+
inputs: ApplyAggregateInputs<TFamilyId, TTargetId>,
|
|
119
|
+
): Promise<ApplyAggregateResult> {
|
|
120
|
+
const {
|
|
121
|
+
aggregate,
|
|
122
|
+
perSpacePlans,
|
|
123
|
+
applyOrder,
|
|
124
|
+
driver,
|
|
125
|
+
familyInstance,
|
|
126
|
+
migrations,
|
|
127
|
+
frameworkComponents,
|
|
128
|
+
policy,
|
|
129
|
+
action,
|
|
130
|
+
onProgress,
|
|
131
|
+
} = inputs;
|
|
132
|
+
|
|
133
|
+
const orderedResolutions = collectOrdered(applyOrder, perSpacePlans);
|
|
134
|
+
|
|
135
|
+
const runner = migrations.createRunner(familyInstance);
|
|
136
|
+
if (!hasMultiSpaceRunner(runner)) {
|
|
137
|
+
throw errorRunnerFailed(
|
|
138
|
+
`Runner for target "${aggregate.targetId}" does not implement \`executeAcrossSpaces\``,
|
|
139
|
+
{
|
|
140
|
+
why: `${labelForAction(action)} requires multi-space-capable runners (today: every SQL family runner).`,
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onProgress?.({
|
|
146
|
+
action,
|
|
147
|
+
kind: 'spanStart',
|
|
148
|
+
spanId: APPLY_SPAN_ID,
|
|
149
|
+
label: progressLabelForAction(action),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const perSpaceOptions: MultiSpaceRunnerPerSpaceOptions<TFamilyId, TTargetId>[] =
|
|
153
|
+
orderedResolutions.map((r) => ({
|
|
154
|
+
space: r.spaceId,
|
|
155
|
+
plan: r.entry.plan,
|
|
156
|
+
driver,
|
|
157
|
+
destinationContract: r.entry.destinationContract,
|
|
158
|
+
policy,
|
|
159
|
+
frameworkComponents,
|
|
160
|
+
// Per-space post-apply schema verification is non-strict: each
|
|
161
|
+
// space's `destinationContract` describes only its own slice; a
|
|
162
|
+
// strict verifier would treat every other space's tables as
|
|
163
|
+
// `extras`. Tolerant mode still catches missing tables / columns.
|
|
164
|
+
// SQL family runners read `strictVerification` via structural
|
|
165
|
+
// typing.
|
|
166
|
+
strictVerification: false,
|
|
167
|
+
})) as MultiSpaceRunnerPerSpaceOptions<TFamilyId, TTargetId>[];
|
|
168
|
+
|
|
169
|
+
const runnerResult = await (
|
|
170
|
+
runner as MultiSpaceCapableRunner<TFamilyId, TTargetId>
|
|
171
|
+
).executeAcrossSpaces({ driver, perSpaceOptions });
|
|
172
|
+
|
|
173
|
+
if (!runnerResult.ok) {
|
|
174
|
+
onProgress?.({ action, kind: 'spanEnd', spanId: APPLY_SPAN_ID, outcome: 'error' });
|
|
175
|
+
return notOk({
|
|
176
|
+
summary: runnerResult.failure.summary,
|
|
177
|
+
...ifDefined('why', runnerResult.failure.why),
|
|
178
|
+
meta: {
|
|
179
|
+
...(runnerResult.failure.meta ?? {}),
|
|
180
|
+
failingSpace: runnerResult.failure.failingSpace,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
onProgress?.({ action, kind: 'spanEnd', spanId: APPLY_SPAN_ID, outcome: 'ok' });
|
|
185
|
+
|
|
186
|
+
const totalOpsPlanned = runnerResult.value.perSpaceResults.reduce(
|
|
187
|
+
(sum, r) => sum + r.value.operationsPlanned,
|
|
188
|
+
0,
|
|
189
|
+
);
|
|
190
|
+
const totalOpsExecuted = runnerResult.value.perSpaceResults.reduce(
|
|
191
|
+
(sum, r) => sum + r.value.operationsExecuted,
|
|
192
|
+
0,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const perSpace = buildPerSpaceBreakdown(orderedResolutions, aggregate.app.spaceId, {
|
|
196
|
+
includeMarkers: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return ok({
|
|
200
|
+
orderedResolutions,
|
|
201
|
+
totalOpsPlanned,
|
|
202
|
+
totalOpsExecuted,
|
|
203
|
+
perSpace,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Project the planner's per-space resolutions into the
|
|
209
|
+
* `AggregatePerSpaceExecutionEntry[]` shape the CLI surfaces.
|
|
210
|
+
*
|
|
211
|
+
* `includeMarkers` is `true` for apply-mode (each space's marker is
|
|
212
|
+
* the `destination.storageHash` of its plan, which the runner
|
|
213
|
+
* advances as the last step of each space's transaction) and `false`
|
|
214
|
+
* for plan-mode (no marker has been written yet).
|
|
215
|
+
*
|
|
216
|
+
* Exported alongside {@link applyAggregate} so plan-mode callers can
|
|
217
|
+
* assemble the same per-space block without going through the runner.
|
|
218
|
+
*/
|
|
219
|
+
export function buildPerSpaceBreakdown(
|
|
220
|
+
orderedResolutions: readonly OrderedResolution[],
|
|
221
|
+
appSpaceId: string,
|
|
222
|
+
options: { readonly includeMarkers: boolean },
|
|
223
|
+
): readonly AggregatePerSpaceExecutionEntry[] {
|
|
224
|
+
return orderedResolutions.map((r) => {
|
|
225
|
+
const operations = r.entry.displayOps.map((op) => ({
|
|
226
|
+
id: op.id,
|
|
227
|
+
label: op.label,
|
|
228
|
+
operationClass: op.operationClass,
|
|
229
|
+
}));
|
|
230
|
+
const base: AggregatePerSpaceExecutionEntry = {
|
|
231
|
+
spaceId: r.spaceId,
|
|
232
|
+
kind: r.spaceId === appSpaceId ? 'app' : 'extension',
|
|
233
|
+
operations,
|
|
234
|
+
};
|
|
235
|
+
if (!options.includeMarkers) return base;
|
|
236
|
+
return {
|
|
237
|
+
...base,
|
|
238
|
+
marker: { storageHash: r.entry.plan.destination.storageHash },
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Materialise the `applyOrder` ordering into resolved per-space
|
|
245
|
+
* entries. Throws if the planner output is missing a member listed
|
|
246
|
+
* in `applyOrder` — a wiring bug that should never reach runtime.
|
|
247
|
+
*
|
|
248
|
+
* Exported so callers building their own success envelopes after a
|
|
249
|
+
* plan-mode dispatch can replay the same ordering.
|
|
250
|
+
*/
|
|
251
|
+
export function collectOrdered(
|
|
252
|
+
applyOrder: readonly string[],
|
|
253
|
+
perSpace: ReadonlyMap<string, AggregatePerSpacePlan>,
|
|
254
|
+
): readonly OrderedResolution[] {
|
|
255
|
+
return applyOrder.map((spaceId) => {
|
|
256
|
+
const entry = perSpace.get(spaceId);
|
|
257
|
+
if (!entry) {
|
|
258
|
+
throw new Error(`Aggregate planner output missing per-space plan for "${spaceId}"`);
|
|
259
|
+
}
|
|
260
|
+
return { spaceId, entry };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Action-appropriate label for the `spanStart` event the apply
|
|
266
|
+
* primitive emits. `applyAggregate` is shared by `db init`, `db update`,
|
|
267
|
+
* and `migration apply`; the span label tracks the user-visible action
|
|
268
|
+
* so structured-progress output reads naturally for each surface.
|
|
269
|
+
*/
|
|
270
|
+
export function progressLabelForAction(action: AggregateApplyAction): string {
|
|
271
|
+
switch (action) {
|
|
272
|
+
case 'dbInit':
|
|
273
|
+
return 'Initialising database across spaces';
|
|
274
|
+
case 'dbUpdate':
|
|
275
|
+
return 'Updating database across spaces';
|
|
276
|
+
case 'migrationApply':
|
|
277
|
+
return 'Applying migration plan across spaces';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function labelForAction(action: AggregateApplyAction): string {
|
|
282
|
+
switch (action) {
|
|
283
|
+
case 'dbInit':
|
|
284
|
+
return 'db init';
|
|
285
|
+
case 'dbUpdate':
|
|
286
|
+
return 'db update';
|
|
287
|
+
case 'migrationApply':
|
|
288
|
+
return 'migration apply';
|
|
289
|
+
}
|
|
290
|
+
}
|