@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,191 +1,490 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
1
2
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
2
3
|
import type {
|
|
3
4
|
ControlDriverInstance,
|
|
5
|
+
ControlExtensionDescriptor,
|
|
4
6
|
ControlFamilyInstance,
|
|
5
|
-
MigrationRunnerResult,
|
|
6
7
|
TargetMigrationsCapability,
|
|
7
8
|
} from '@prisma-next/framework-components/control';
|
|
9
|
+
import {
|
|
10
|
+
type AggregatePerSpacePlan,
|
|
11
|
+
type ContractMarkerRecordLike,
|
|
12
|
+
type ContractSpaceAggregate,
|
|
13
|
+
type ContractSpaceMember,
|
|
14
|
+
graphWalkStrategy,
|
|
15
|
+
} from '@prisma-next/migration-tools/aggregate';
|
|
8
16
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
17
|
+
import { errorNoInvariantPath } from '@prisma-next/migration-tools/errors';
|
|
18
|
+
import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
|
|
19
|
+
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
20
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
9
21
|
import { notOk, ok } from '@prisma-next/utils/result';
|
|
22
|
+
import {
|
|
23
|
+
type BuildAggregateInputs,
|
|
24
|
+
buildContractSpaceAggregate,
|
|
25
|
+
} from '../../utils/contract-space-aggregate-loader';
|
|
10
26
|
import type {
|
|
11
|
-
|
|
27
|
+
AggregatePerSpaceExecutionEntry,
|
|
28
|
+
MigrationApplyFailure,
|
|
29
|
+
MigrationApplyPathDecision,
|
|
12
30
|
MigrationApplyResult,
|
|
13
|
-
|
|
31
|
+
MigrationApplySuccess,
|
|
14
32
|
OnControlProgress,
|
|
15
33
|
} from '../types';
|
|
34
|
+
import { applyAggregate, buildPerSpaceBreakdown } from './apply-aggregate';
|
|
16
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Inputs for the aggregate-walking `migration apply` control-api
|
|
38
|
+
* operation.
|
|
39
|
+
*
|
|
40
|
+
* The CLI command resolves the descriptor surface (config, refs,
|
|
41
|
+
* contract envelope) and hands a flat input through. The operation
|
|
42
|
+
* is the single descriptor-free seam between the CLI and the
|
|
43
|
+
* aggregate runtime.
|
|
44
|
+
*/
|
|
17
45
|
export interface ExecuteMigrationApplyOptions<TFamilyId extends string, TTargetId extends string> {
|
|
18
46
|
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
|
|
19
47
|
readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
|
|
20
|
-
|
|
21
|
-
readonly
|
|
22
|
-
readonly pendingMigrations: readonly MigrationApplyStep[];
|
|
48
|
+
/** Already-validated app contract (the canonical "where we are heading" hash). */
|
|
49
|
+
readonly contract: Contract;
|
|
23
50
|
readonly migrations: TargetMigrationsCapability<
|
|
24
51
|
TFamilyId,
|
|
25
52
|
TTargetId,
|
|
26
53
|
ControlFamilyInstance<TFamilyId, unknown>
|
|
27
54
|
>;
|
|
28
55
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
|
|
29
|
-
readonly
|
|
56
|
+
readonly migrationsDir: string;
|
|
57
|
+
readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
|
|
58
|
+
readonly targetId: TTargetId;
|
|
59
|
+
/**
|
|
60
|
+
* Already-loaded app-space migration packages. The CLI command
|
|
61
|
+
* loads these via `loadMigrationPackages(appMigrationsDir)`; the
|
|
62
|
+
* operation hydrates the app member's graph with them. Required
|
|
63
|
+
* because the framework-neutral aggregate loader doesn't know how
|
|
64
|
+
* to read the user's `migrations/` directory layout (it's family-
|
|
65
|
+
* aware: ops.json shape, manifest keys, etc.).
|
|
66
|
+
*/
|
|
67
|
+
readonly appMigrationPackages: ReadonlyArray<OnDiskMigrationPackage>;
|
|
68
|
+
/**
|
|
69
|
+
* Optional app-space ref override. When provided, the app member's
|
|
70
|
+
* graph-walk targets this hash instead of `member.headRef.hash`.
|
|
71
|
+
* Extensions are unaffected — they always walk to their own head.
|
|
72
|
+
*
|
|
73
|
+
* Sub-spec § `--ref <hash>` semantics under multi-space.
|
|
74
|
+
*/
|
|
75
|
+
readonly refHash?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Required invariants attached to the user-supplied app-space ref.
|
|
78
|
+
* Threaded into the graph-walk's `required` calculation so the
|
|
79
|
+
* planner picks an invariant-bearing path and surfaces the
|
|
80
|
+
* required/satisfied set on the success envelope. When `refHash`
|
|
81
|
+
* is absent the file's `member.headRef.invariants` are used.
|
|
82
|
+
*/
|
|
83
|
+
readonly refInvariants?: readonly string[];
|
|
84
|
+
/**
|
|
85
|
+
* Resolved name of the user-supplied app-space ref. Surfaces in
|
|
86
|
+
* `pathDecision.refName` and in `MIGRATION.NO_INVARIANT_PATH`
|
|
87
|
+
* error envelopes so diagnostics name what the user actually
|
|
88
|
+
* passed (`--ref prod`) instead of a synthetic placeholder.
|
|
89
|
+
* Ignored when `refHash` is absent.
|
|
90
|
+
*/
|
|
91
|
+
readonly refName?: string;
|
|
30
92
|
readonly onProgress?: OnControlProgress;
|
|
31
93
|
}
|
|
32
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Apply pending migrations across every contract space (app +
|
|
97
|
+
* extensions). Replay-only: graph-walk against the on-disk graph for
|
|
98
|
+
* every member; no synth, no introspection.
|
|
99
|
+
*
|
|
100
|
+
* Pipeline:
|
|
101
|
+
*
|
|
102
|
+
* 1. Load aggregate from disk (loader hydrates extension graphs;
|
|
103
|
+
* caller provides app-space packages).
|
|
104
|
+
* 2. Read live marker rows per space (`familyInstance.readAllMarkers`).
|
|
105
|
+
* 3. Per member: `graphWalkStrategy` plots the path from the live
|
|
106
|
+
* marker to `member.headRef.hash` (or `refHash` for the app
|
|
107
|
+
* member when provided). Empty-graph members fail loudly — a
|
|
108
|
+
* "never planned" space is a user-error condition for replay.
|
|
109
|
+
* 4. Hand off to {@link applyAggregate} (the runner-driving tail
|
|
110
|
+
* shared with `db init` / `db update`). Marker advancement is
|
|
111
|
+
* inside the per-space transaction.
|
|
112
|
+
*
|
|
113
|
+
* Sub-spec § `migration apply` semantics + § Required changes 1.
|
|
114
|
+
*/
|
|
33
115
|
export async function executeMigrationApply<TFamilyId extends string, TTargetId extends string>(
|
|
34
116
|
options: ExecuteMigrationApplyOptions<TFamilyId, TTargetId>,
|
|
35
117
|
): Promise<MigrationApplyResult> {
|
|
36
118
|
const {
|
|
37
119
|
driver,
|
|
38
120
|
familyInstance,
|
|
39
|
-
|
|
40
|
-
destinationHash,
|
|
41
|
-
pendingMigrations,
|
|
121
|
+
contract,
|
|
42
122
|
migrations,
|
|
43
123
|
frameworkComponents,
|
|
124
|
+
migrationsDir,
|
|
125
|
+
extensionPacks,
|
|
44
126
|
targetId,
|
|
127
|
+
appMigrationPackages,
|
|
128
|
+
refHash,
|
|
129
|
+
refInvariants,
|
|
130
|
+
refName,
|
|
45
131
|
onProgress,
|
|
46
132
|
} = options;
|
|
47
133
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
markerHash: originHash,
|
|
60
|
-
applied: [],
|
|
61
|
-
summary: 'Already up to date',
|
|
62
|
-
});
|
|
134
|
+
const loadInputs: BuildAggregateInputs<TFamilyId, TTargetId> = {
|
|
135
|
+
targetId,
|
|
136
|
+
migrationsDir,
|
|
137
|
+
appContract: contract,
|
|
138
|
+
extensionPacks,
|
|
139
|
+
validateContract: (json) => familyInstance.validateContract(json),
|
|
140
|
+
appMigrationPackages,
|
|
141
|
+
};
|
|
142
|
+
const loaded = await buildContractSpaceAggregate(loadInputs);
|
|
143
|
+
if (!loaded.ok) {
|
|
144
|
+
throw loaded.failure;
|
|
63
145
|
}
|
|
146
|
+
const aggregate = loaded.value;
|
|
64
147
|
|
|
65
|
-
const
|
|
66
|
-
const lastMigration = pendingMigrations[pendingMigrations.length - 1]!;
|
|
67
|
-
if (firstMigration.from !== originHash || lastMigration.to !== destinationHash) {
|
|
68
|
-
return notOk({
|
|
69
|
-
code: 'MIGRATION_PATH_NOT_FOUND' as const,
|
|
70
|
-
summary: 'Migration apply path does not match requested origin and destination',
|
|
71
|
-
why: `Path resolved as ${firstMigration.from} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
|
|
72
|
-
meta: {
|
|
73
|
-
originHash,
|
|
74
|
-
destinationHash,
|
|
75
|
-
pathOrigin: firstMigration.from,
|
|
76
|
-
pathDestination: lastMigration.to,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
}
|
|
148
|
+
const markerRows = await familyInstance.readAllMarkers({ driver });
|
|
80
149
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
150
|
+
// Plan every member via graph-walk. App member targets `refHash`
|
|
151
|
+
// when provided, otherwise its own head; extensions always walk
|
|
152
|
+
// to their own head ref.
|
|
153
|
+
const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
|
|
154
|
+
const perSpacePlans = new Map<string, AggregatePerSpacePlan>();
|
|
155
|
+
// Already-at-head empty-graph members (typically extensions whose
|
|
156
|
+
// head ref is the empty sentinel, or whose live marker already
|
|
157
|
+
// matches the target). Kept out of the runner schedule so we don't
|
|
158
|
+
// write spurious markers for greenfield extensions, but merged back
|
|
159
|
+
// into the success envelope so every loaded member is represented.
|
|
160
|
+
const atHeadResolutions = new Map<string, AggregatePerSpacePlan>();
|
|
161
|
+
for (const member of allMembers) {
|
|
162
|
+
const isAppMember = member.spaceId === aggregate.app.spaceId;
|
|
163
|
+
const targetHash = isAppMember && refHash !== undefined ? refHash : member.headRef.hash;
|
|
164
|
+
const liveMarker = markerRows.get(member.spaceId) ?? null;
|
|
165
|
+
|
|
166
|
+
// Empty-graph members fail loudly: replay needs an on-disk path
|
|
167
|
+
// and an empty graph means the user has never planned this space.
|
|
168
|
+
if (member.migrations.graph.nodes.size === 0) {
|
|
169
|
+
// Edge case: target == EMPTY (greenfield, nothing to do) or
|
|
170
|
+
// the live marker already matches the target. Loader integrity
|
|
171
|
+
// allows this for extensions whose head ref is the empty
|
|
172
|
+
// sentinel. Record a zero-op resolution so the aggregate result
|
|
173
|
+
// still surfaces the member in `perSpace[]` as already-at-head;
|
|
174
|
+
// the runner is not invoked for these members because they have
|
|
175
|
+
// no authored ops and (for greenfield extensions) no marker to
|
|
176
|
+
// advance.
|
|
177
|
+
const liveHash = liveMarker?.storageHash;
|
|
178
|
+
if (
|
|
179
|
+
targetHash === liveHash ||
|
|
180
|
+
(liveHash === undefined && targetHash === EMPTY_CONTRACT_HASH)
|
|
181
|
+
) {
|
|
182
|
+
atHeadResolutions.set(
|
|
183
|
+
member.spaceId,
|
|
184
|
+
buildAtHeadResolution({
|
|
185
|
+
aggregateTargetId: aggregate.targetId,
|
|
186
|
+
member,
|
|
187
|
+
targetHash,
|
|
188
|
+
liveMarker,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
return notOk(buildNeverPlannedFailure(member.spaceId, targetHash));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const targetInvariants =
|
|
197
|
+
isAppMember && refHash !== undefined && refInvariants !== undefined
|
|
198
|
+
? refInvariants
|
|
199
|
+
: member.headRef.invariants;
|
|
200
|
+
const targetMember: ContractSpaceMember =
|
|
201
|
+
targetHash === member.headRef.hash && targetInvariants === member.headRef.invariants
|
|
202
|
+
? member
|
|
203
|
+
: { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
|
|
204
|
+
|
|
205
|
+
const walked = graphWalkStrategy({
|
|
206
|
+
aggregateTargetId: aggregate.targetId,
|
|
207
|
+
member: targetMember,
|
|
208
|
+
currentMarker: liveMarker,
|
|
209
|
+
...(isAppMember && refName !== undefined ? { refName } : {}),
|
|
210
|
+
});
|
|
211
|
+
if (walked.kind === 'unreachable') {
|
|
212
|
+
return notOk(buildPathNotFoundFailure(member.spaceId, liveMarker, targetHash));
|
|
213
|
+
}
|
|
214
|
+
if (walked.kind === 'unsatisfiable') {
|
|
215
|
+
// Surface the canonical MIGRATION.NO_INVARIANT_PATH envelope
|
|
216
|
+
// (the error rendering pipeline maps it to meta.code +
|
|
217
|
+
// meta.required + meta.missing + meta.structuralPath that the
|
|
218
|
+
// cli-journeys invariant suite asserts on).
|
|
219
|
+
// Greenfield runs (no marker yet) use the canonical empty-hash
|
|
220
|
+
// sentinel so the structural path stays attached to the
|
|
221
|
+
// `MIGRATION.NO_INVARIANT_PATH` error envelope. Using an empty
|
|
222
|
+
// string here would leave the structural lookup with a hash that
|
|
223
|
+
// is never a graph node, producing an empty `structuralPath` and
|
|
224
|
+
// a less actionable diagnostic.
|
|
225
|
+
const fromHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
|
|
226
|
+
const structural = findPathWithDecision(targetMember.migrations.graph, fromHash, targetHash, {
|
|
227
|
+
required: new Set<string>(),
|
|
228
|
+
});
|
|
229
|
+
const structuralPath =
|
|
230
|
+
structural.kind === 'ok'
|
|
231
|
+
? structural.decision.selectedPath.map((edge) => ({
|
|
232
|
+
dirName: edge.dirName,
|
|
233
|
+
migrationHash: edge.migrationHash,
|
|
234
|
+
from: edge.from,
|
|
235
|
+
to: edge.to,
|
|
236
|
+
invariants: edge.invariants,
|
|
237
|
+
}))
|
|
238
|
+
: [];
|
|
239
|
+
throw errorNoInvariantPath({
|
|
240
|
+
...(isAppMember && refName !== undefined ? { refName } : {}),
|
|
241
|
+
required: targetInvariants,
|
|
242
|
+
missing: walked.missing,
|
|
243
|
+
structuralPath,
|
|
98
244
|
});
|
|
99
245
|
}
|
|
246
|
+
|
|
247
|
+
perSpacePlans.set(member.spaceId, walked.result);
|
|
100
248
|
}
|
|
101
249
|
|
|
102
|
-
const
|
|
103
|
-
const
|
|
250
|
+
const canonicalOrder = [...aggregate.extensions.map((m) => m.spaceId), aggregate.app.spaceId];
|
|
251
|
+
const applyOrder = canonicalOrder.filter((spaceId) => perSpacePlans.has(spaceId));
|
|
104
252
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
253
|
+
// Short-circuit: nothing pending across any space (no runner-bound
|
|
254
|
+
// plans). Surfaces every loaded member — including at-head empty-
|
|
255
|
+
// graph extensions — in `perSpace[]` so the result reflects the
|
|
256
|
+
// full aggregate, not just the spaces the runner would have touched.
|
|
257
|
+
const totalPlannedOps = sumPlannedOps(applyOrder, perSpacePlans);
|
|
258
|
+
if (totalPlannedOps === 0) {
|
|
259
|
+
const ordered = canonicalOrder
|
|
260
|
+
.filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId))
|
|
261
|
+
.map((spaceId) => {
|
|
262
|
+
const entry = perSpacePlans.get(spaceId) ?? atHeadResolutions.get(spaceId);
|
|
263
|
+
if (entry === undefined) {
|
|
264
|
+
throw new Error(`Unreachable: missing per-space plan for "${spaceId}"`);
|
|
265
|
+
}
|
|
266
|
+
return { spaceId, entry };
|
|
267
|
+
});
|
|
268
|
+
const perSpace = buildPerSpaceBreakdown(ordered, aggregate.app.spaceId, {
|
|
269
|
+
includeMarkers: true,
|
|
112
270
|
});
|
|
271
|
+
const totalSpaces = ordered.length;
|
|
272
|
+
return ok(
|
|
273
|
+
buildSuccess({
|
|
274
|
+
aggregate,
|
|
275
|
+
orderedResolutions: ordered,
|
|
276
|
+
perSpace,
|
|
277
|
+
totalOpsExecuted: 0,
|
|
278
|
+
summary:
|
|
279
|
+
totalSpaces === 0
|
|
280
|
+
? 'Already up to date — no contract spaces are loaded'
|
|
281
|
+
: totalSpaces === 1
|
|
282
|
+
? 'Already up to date'
|
|
283
|
+
: `Already up to date across ${totalSpaces} space(s)`,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
113
287
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
288
|
+
const applied = await applyAggregate({
|
|
289
|
+
aggregate,
|
|
290
|
+
perSpacePlans,
|
|
291
|
+
applyOrder,
|
|
292
|
+
driver,
|
|
293
|
+
familyInstance,
|
|
294
|
+
migrations,
|
|
295
|
+
frameworkComponents,
|
|
296
|
+
policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
|
|
297
|
+
action: 'migrationApply',
|
|
298
|
+
...ifDefined('onProgress', onProgress),
|
|
299
|
+
});
|
|
122
300
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
operations,
|
|
301
|
+
if (!applied.ok) {
|
|
302
|
+
const failure: MigrationApplyFailure = {
|
|
303
|
+
code: 'RUNNER_FAILED',
|
|
304
|
+
summary: applied.failure.summary,
|
|
305
|
+
why: applied.failure.why,
|
|
306
|
+
meta: applied.failure.meta,
|
|
130
307
|
};
|
|
308
|
+
return notOk(failure);
|
|
309
|
+
}
|
|
131
310
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
311
|
+
// Merge at-head zero-op resolutions back into the canonical order
|
|
312
|
+
// so the success envelope surfaces every loaded member, not just
|
|
313
|
+
// those the runner executed.
|
|
314
|
+
const orderedAll = canonicalOrder
|
|
315
|
+
.filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId))
|
|
316
|
+
.map((spaceId) => {
|
|
317
|
+
if (perSpacePlans.has(spaceId)) {
|
|
318
|
+
const fromRunner = applied.value.orderedResolutions.find((r) => r.spaceId === spaceId);
|
|
319
|
+
if (fromRunner !== undefined) return fromRunner;
|
|
320
|
+
}
|
|
321
|
+
const entry = atHeadResolutions.get(spaceId);
|
|
322
|
+
if (entry === undefined) {
|
|
323
|
+
throw new Error(`Unreachable: missing per-space plan for "${spaceId}"`);
|
|
324
|
+
}
|
|
325
|
+
return { spaceId, entry };
|
|
145
326
|
});
|
|
327
|
+
const perSpaceAll = buildPerSpaceBreakdown(orderedAll, aggregate.app.spaceId, {
|
|
328
|
+
includeMarkers: true,
|
|
329
|
+
});
|
|
330
|
+
const totalMigrationsApplied = applied.value.orderedResolutions.reduce(
|
|
331
|
+
(sum, r) => sum + (r.entry.migrationEdges?.length ?? 0),
|
|
332
|
+
0,
|
|
333
|
+
);
|
|
334
|
+
const summary = `Applied ${totalMigrationsApplied} migration(s) (${applied.value.totalOpsExecuted} operation(s)) across ${orderedAll.length} contract space(s)`;
|
|
146
335
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
why: runnerResult.failure.why,
|
|
158
|
-
meta: {
|
|
159
|
-
migration: migration.dirName,
|
|
160
|
-
from: migration.from,
|
|
161
|
-
to: migration.to,
|
|
162
|
-
...(runnerResult.failure.meta ?? {}),
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
}
|
|
336
|
+
return ok(
|
|
337
|
+
buildSuccess({
|
|
338
|
+
aggregate,
|
|
339
|
+
orderedResolutions: orderedAll,
|
|
340
|
+
perSpace: perSpaceAll,
|
|
341
|
+
totalOpsExecuted: applied.value.totalOpsExecuted,
|
|
342
|
+
summary,
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
166
346
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Build a zero-op {@link AggregatePerSpacePlan} for an empty-graph
|
|
349
|
+
* member whose live marker already matches the target. Lets the apply
|
|
350
|
+
* pipeline thread the member through `perSpacePlans` -> `applyOrder`
|
|
351
|
+
* -> the success envelope's `perSpace[]` block so the result reflects
|
|
352
|
+
* every loaded space, even when there is nothing to execute.
|
|
353
|
+
*/
|
|
354
|
+
function buildAtHeadResolution(args: {
|
|
355
|
+
readonly aggregateTargetId: string;
|
|
356
|
+
readonly member: ContractSpaceMember;
|
|
357
|
+
readonly targetHash: string;
|
|
358
|
+
readonly liveMarker: ContractMarkerRecordLike | null;
|
|
359
|
+
}): AggregatePerSpacePlan {
|
|
360
|
+
const { aggregateTargetId, member, targetHash, liveMarker } = args;
|
|
361
|
+
return {
|
|
362
|
+
plan: {
|
|
363
|
+
targetId: aggregateTargetId,
|
|
364
|
+
spaceId: member.spaceId,
|
|
365
|
+
origin: liveMarker === null ? null : { storageHash: liveMarker.storageHash },
|
|
366
|
+
destination: { storageHash: targetHash },
|
|
367
|
+
operations: [],
|
|
368
|
+
providedInvariants: [],
|
|
369
|
+
},
|
|
370
|
+
displayOps: [],
|
|
371
|
+
destinationContract: member.contract,
|
|
372
|
+
strategy: 'graph-walk',
|
|
373
|
+
migrationEdges: [],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
173
376
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
377
|
+
function sumPlannedOps(
|
|
378
|
+
applyOrder: readonly string[],
|
|
379
|
+
perSpacePlans: ReadonlyMap<string, AggregatePerSpacePlan>,
|
|
380
|
+
): number {
|
|
381
|
+
let total = 0;
|
|
382
|
+
for (const spaceId of applyOrder) {
|
|
383
|
+
const entry = perSpacePlans.get(spaceId);
|
|
384
|
+
if (!entry) continue;
|
|
385
|
+
total += entry.plan.operations.length;
|
|
180
386
|
}
|
|
387
|
+
return total;
|
|
388
|
+
}
|
|
181
389
|
|
|
182
|
-
|
|
183
|
-
|
|
390
|
+
interface BuildSuccessArgs {
|
|
391
|
+
readonly aggregate: ContractSpaceAggregate;
|
|
392
|
+
readonly orderedResolutions: ReadonlyArray<{
|
|
393
|
+
readonly spaceId: string;
|
|
394
|
+
readonly entry: AggregatePerSpacePlan;
|
|
395
|
+
}>;
|
|
396
|
+
readonly perSpace: ReadonlyArray<AggregatePerSpaceExecutionEntry>;
|
|
397
|
+
readonly totalOpsExecuted: number;
|
|
398
|
+
readonly summary: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function buildSuccess(args: BuildSuccessArgs): MigrationApplySuccess {
|
|
402
|
+
// The marker hash surfaced at the top level is the **app member's**
|
|
403
|
+
// post-apply marker (today's single-space `markerHash` field).
|
|
404
|
+
// Per-space markers live on `perSpace[].marker.storageHash`.
|
|
405
|
+
const appResolution = args.orderedResolutions.find(
|
|
406
|
+
(r) => r.spaceId === args.aggregate.app.spaceId,
|
|
407
|
+
);
|
|
408
|
+
const appMarkerHash =
|
|
409
|
+
appResolution?.entry.plan.destination.storageHash ?? args.aggregate.app.headRef.hash;
|
|
410
|
+
|
|
411
|
+
// Per-migration entries (one per authored edge) preserve the
|
|
412
|
+
// single-space `migrationsApplied` count semantics for back-compat
|
|
413
|
+
// with existing JSON-shape consumers (e.g. `parsed.applied.length`
|
|
414
|
+
// in integration tests). The aggregate per-space breakdown lives on
|
|
415
|
+
// `perSpace[]`.
|
|
416
|
+
const applied = args.orderedResolutions.flatMap((r) => {
|
|
417
|
+
const edges = r.entry.migrationEdges ?? [];
|
|
418
|
+
return edges.map((edge) => ({
|
|
419
|
+
spaceId: r.spaceId,
|
|
420
|
+
dirName: edge.dirName,
|
|
421
|
+
migrationHash: edge.migrationHash,
|
|
422
|
+
from: edge.from,
|
|
423
|
+
to: edge.to,
|
|
424
|
+
operationsExecuted: edge.operationCount,
|
|
425
|
+
}));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const appPlan = appResolution?.entry;
|
|
429
|
+
const pathDecision: MigrationApplyPathDecision | undefined = appPlan?.pathDecision
|
|
430
|
+
? {
|
|
431
|
+
fromHash: appPlan.pathDecision.fromHash,
|
|
432
|
+
toHash: appPlan.pathDecision.toHash,
|
|
433
|
+
alternativeCount: appPlan.pathDecision.alternativeCount,
|
|
434
|
+
tieBreakReasons: appPlan.pathDecision.tieBreakReasons,
|
|
435
|
+
...(appPlan.pathDecision.refName !== undefined
|
|
436
|
+
? { refName: appPlan.pathDecision.refName }
|
|
437
|
+
: {}),
|
|
438
|
+
requiredInvariants: appPlan.pathDecision.requiredInvariants ?? [],
|
|
439
|
+
satisfiedInvariants: appPlan.pathDecision.satisfiedInvariants ?? [],
|
|
440
|
+
selectedPath: appPlan.pathDecision.selectedPath.map((entry) => ({
|
|
441
|
+
dirName: entry.dirName,
|
|
442
|
+
migrationHash: entry.migrationHash,
|
|
443
|
+
from: entry.from,
|
|
444
|
+
to: entry.to,
|
|
445
|
+
invariants: entry.invariants,
|
|
446
|
+
})),
|
|
447
|
+
}
|
|
448
|
+
: undefined;
|
|
184
449
|
|
|
185
|
-
return
|
|
450
|
+
return {
|
|
186
451
|
migrationsApplied: applied.length,
|
|
187
|
-
markerHash:
|
|
452
|
+
markerHash: appMarkerHash,
|
|
188
453
|
applied,
|
|
189
|
-
summary:
|
|
190
|
-
|
|
454
|
+
summary: args.summary,
|
|
455
|
+
perSpace: args.perSpace,
|
|
456
|
+
...(pathDecision !== undefined ? { pathDecision } : {}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildNeverPlannedFailure(spaceId: string, targetHash: string): MigrationApplyFailure {
|
|
461
|
+
return {
|
|
462
|
+
code: 'MIGRATION_PATH_NOT_FOUND',
|
|
463
|
+
summary: `No on-disk migrations for contract space "${spaceId}"`,
|
|
464
|
+
why: `migration apply is replay-only: every contract space must have an authored migration graph on disk. Space "${spaceId}" has no migrations under \`migrations/${spaceId}/\` but its head ref targets "${targetHash}". Run \`prisma-next migration plan\` first to materialise the path.`,
|
|
465
|
+
meta: { spaceId, target: targetHash, kind: 'neverPlanned' },
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function buildPathNotFoundFailure(
|
|
470
|
+
spaceId: string,
|
|
471
|
+
marker: ContractMarkerRecordLike | null,
|
|
472
|
+
targetHash: string,
|
|
473
|
+
): MigrationApplyFailure {
|
|
474
|
+
const fromHash = marker?.storageHash ?? '<empty>';
|
|
475
|
+
// The single-space-degenerate phrasing names the user-visible
|
|
476
|
+
// condition (a contract has been emitted that no on-disk
|
|
477
|
+
// migration reaches) so the error reads naturally for the
|
|
478
|
+
// single-space app case. Multi-space callers see the same
|
|
479
|
+
// condition expressed against the offending space.
|
|
480
|
+
const summary =
|
|
481
|
+
spaceId === 'app'
|
|
482
|
+
? 'Current contract has no planned migration path'
|
|
483
|
+
: `Current contract has no planned migration path for contract space "${spaceId}"`;
|
|
484
|
+
return {
|
|
485
|
+
code: 'MIGRATION_PATH_NOT_FOUND',
|
|
486
|
+
summary,
|
|
487
|
+
why: `Cannot reach target "${targetHash}" from current marker "${fromHash}" in space "${spaceId}". The on-disk migration graph for this space does not connect the two states. Run \`prisma-next migration plan\` to materialise the path.`,
|
|
488
|
+
meta: { spaceId, fromHash, targetHash, kind: 'pathUnreachable' },
|
|
489
|
+
};
|
|
191
490
|
}
|