@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
|
@@ -3,23 +3,25 @@ import type { Contract } from '@prisma-next/contract/types';
|
|
|
3
3
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import {
|
|
5
5
|
createControlStack,
|
|
6
|
+
hasOperationPreview,
|
|
6
7
|
type MigrationPlanOperation,
|
|
8
|
+
type OperationPreview,
|
|
7
9
|
} from '@prisma-next/framework-components/control';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
11
|
+
import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
12
|
+
import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
|
|
11
13
|
import {
|
|
12
14
|
copyFilesWithRename,
|
|
13
15
|
formatMigrationDirName,
|
|
14
16
|
writeMigrationPackage,
|
|
15
17
|
} from '@prisma-next/migration-tools/io';
|
|
18
|
+
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
19
|
+
import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
|
|
16
20
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
17
|
-
import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
18
21
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
19
22
|
import { Command } from 'commander';
|
|
20
23
|
import { join, relative } from 'pathe';
|
|
21
24
|
import { loadConfig } from '../config-loader';
|
|
22
|
-
import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
|
|
23
25
|
import {
|
|
24
26
|
type CliErrorConflict,
|
|
25
27
|
CliStructuredError,
|
|
@@ -29,16 +31,20 @@ import {
|
|
|
29
31
|
errorRuntime,
|
|
30
32
|
errorTargetMigrationNotSupported,
|
|
31
33
|
errorUnexpected,
|
|
34
|
+
mapMigrationToolsError,
|
|
32
35
|
} from '../utils/cli-errors';
|
|
33
36
|
import {
|
|
34
37
|
addGlobalOptions,
|
|
35
38
|
getTargetMigrations,
|
|
36
|
-
|
|
39
|
+
loadMigrationPackages,
|
|
37
40
|
resolveContractPath,
|
|
38
41
|
resolveMigrationPaths,
|
|
39
42
|
setCommandDescriptions,
|
|
40
43
|
setCommandExamples,
|
|
41
44
|
} from '../utils/command-helpers';
|
|
45
|
+
import { buildContractSpaceAggregate } from '../utils/contract-space-aggregate-loader';
|
|
46
|
+
import { runContractSpaceSeedPhase } from '../utils/contract-space-seed-phase';
|
|
47
|
+
import { toExtensionInputs } from '../utils/extension-pack-inputs';
|
|
42
48
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
43
49
|
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
44
50
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
@@ -55,15 +61,33 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
55
61
|
export interface MigrationPlanResult {
|
|
56
62
|
readonly ok: boolean;
|
|
57
63
|
readonly noOp: boolean;
|
|
58
|
-
readonly from: string;
|
|
64
|
+
readonly from: string | null;
|
|
59
65
|
readonly to: string;
|
|
60
66
|
readonly dir?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Extension-space migration packages materialised onto disk during this
|
|
69
|
+
* `plan` run. Each entry names a `migrations/<spaceId>/<dirName>/`
|
|
70
|
+
* tree the framework wrote alongside the app-space migration directory.
|
|
71
|
+
* Empty when the project has no extension packs declaring a contract
|
|
72
|
+
* space, or when every extension-space package is already on disk.
|
|
73
|
+
*
|
|
74
|
+
* Surfacing these in the result (rather than only via `ui.step` log
|
|
75
|
+
* lines) makes the cross-space side effect explicit to JSON consumers
|
|
76
|
+
* and the success-summary renderer — the same multi-space side effect
|
|
77
|
+
* that `migration apply` will replay.
|
|
78
|
+
*/
|
|
79
|
+
readonly emittedExtensionDirs: readonly { readonly spaceId: string; readonly dirName: string }[];
|
|
61
80
|
readonly operations: readonly {
|
|
62
81
|
readonly id: string;
|
|
63
82
|
readonly label: string;
|
|
64
83
|
readonly operationClass: string;
|
|
65
84
|
}[];
|
|
66
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Family-agnostic textual preview of the migration plan operations.
|
|
87
|
+
* Replaces the previous `sql?: readonly string[]` field; consumers should
|
|
88
|
+
* read `result.preview?.statements`.
|
|
89
|
+
*/
|
|
90
|
+
readonly preview?: OperationPreview;
|
|
67
91
|
readonly summary: string;
|
|
68
92
|
/**
|
|
69
93
|
* When true, `migration.ts` was written but contains unfilled
|
|
@@ -76,22 +100,6 @@ export interface MigrationPlanResult {
|
|
|
76
100
|
};
|
|
77
101
|
}
|
|
78
102
|
|
|
79
|
-
function mapMigrationToolsError(error: unknown): CliStructuredError {
|
|
80
|
-
if (CliStructuredError.is(error)) {
|
|
81
|
-
return error;
|
|
82
|
-
}
|
|
83
|
-
if (MigrationToolsError.is(error)) {
|
|
84
|
-
return errorRuntime(error.message, {
|
|
85
|
-
why: error.why,
|
|
86
|
-
fix: error.fix,
|
|
87
|
-
meta: { code: error.code, ...(error.details ?? {}) },
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
91
|
-
why: `Unexpected error during migration plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
103
|
async function executeMigrationPlanCommand(
|
|
96
104
|
options: MigrationPlanOptions,
|
|
97
105
|
flags: GlobalFlags,
|
|
@@ -99,10 +107,8 @@ async function executeMigrationPlanCommand(
|
|
|
99
107
|
startTime: number,
|
|
100
108
|
): Promise<Result<MigrationPlanResult, CliStructuredError>> {
|
|
101
109
|
const config = await loadConfig(options.config);
|
|
102
|
-
const { configPath, migrationsDir,
|
|
103
|
-
options.config,
|
|
104
|
-
config,
|
|
105
|
-
);
|
|
110
|
+
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
|
|
111
|
+
resolveMigrationPaths(options.config, config);
|
|
106
112
|
|
|
107
113
|
const contractPathAbsolute = resolveContractPath(config);
|
|
108
114
|
const contractPath = relative(process.cwd(), contractPathAbsolute);
|
|
@@ -111,7 +117,7 @@ async function executeMigrationPlanCommand(
|
|
|
111
117
|
const details: Array<{ label: string; value: string }> = [
|
|
112
118
|
{ label: 'config', value: configPath },
|
|
113
119
|
{ label: 'contract', value: contractPath },
|
|
114
|
-
{ label: 'migrations', value:
|
|
120
|
+
{ label: 'migrations', value: appMigrationsRelative },
|
|
115
121
|
];
|
|
116
122
|
if (options.from) {
|
|
117
123
|
details.push({ label: 'from', value: options.from });
|
|
@@ -173,11 +179,11 @@ async function executeMigrationPlanCommand(
|
|
|
173
179
|
|
|
174
180
|
// Read existing migrations and determine "from" contract
|
|
175
181
|
let fromContract: Contract | null = null;
|
|
176
|
-
let fromHash: string =
|
|
182
|
+
let fromHash: string | null = null;
|
|
177
183
|
let fromContractSourceDir: string | null = null;
|
|
178
184
|
|
|
179
185
|
try {
|
|
180
|
-
const { bundles, graph } = await
|
|
186
|
+
const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
|
|
181
187
|
|
|
182
188
|
if (options.from) {
|
|
183
189
|
const resolved = resolveBundleByPrefix(bundles, options.from);
|
|
@@ -186,25 +192,27 @@ async function executeMigrationPlanCommand(
|
|
|
186
192
|
return notOk(
|
|
187
193
|
f.reason === 'ambiguous'
|
|
188
194
|
? errorRuntime('Multiple matching migrations found', {
|
|
189
|
-
why: `Prefix "${options.from}" matches ${f.count} migrations in ${
|
|
195
|
+
why: `Prefix "${options.from}" matches ${f.count} migrations in ${appMigrationsRelative}`,
|
|
190
196
|
fix: 'Provide a longer prefix to disambiguate, or omit --from to use the latest migration target.',
|
|
191
197
|
})
|
|
192
198
|
: errorRuntime('Starting contract not found', {
|
|
193
|
-
why: `No migration with to hash matching "${options.from}" exists in ${
|
|
199
|
+
why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
|
|
194
200
|
fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target.',
|
|
195
201
|
}),
|
|
196
202
|
);
|
|
197
203
|
}
|
|
198
|
-
fromHash = resolved.value.
|
|
199
|
-
fromContract = resolved.value.
|
|
204
|
+
fromHash = resolved.value.metadata.to;
|
|
205
|
+
fromContract = resolved.value.metadata.toContract;
|
|
200
206
|
fromContractSourceDir = resolved.value.dirPath;
|
|
201
207
|
} else {
|
|
202
208
|
const latestMigration = findLatestMigration(graph);
|
|
203
209
|
if (latestMigration) {
|
|
204
210
|
fromHash = latestMigration.to;
|
|
205
|
-
const leafPkg = bundles.find(
|
|
211
|
+
const leafPkg = bundles.find(
|
|
212
|
+
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
213
|
+
);
|
|
206
214
|
if (leafPkg) {
|
|
207
|
-
fromContract = leafPkg.
|
|
215
|
+
fromContract = leafPkg.metadata.toContract;
|
|
208
216
|
fromContractSourceDir = leafPkg.dirPath;
|
|
209
217
|
}
|
|
210
218
|
}
|
|
@@ -213,8 +221,42 @@ async function executeMigrationPlanCommand(
|
|
|
213
221
|
if (MigrationToolsError.is(error)) {
|
|
214
222
|
return notOk(mapMigrationToolsError(error));
|
|
215
223
|
}
|
|
216
|
-
|
|
224
|
+
// Wrap unexpected (non-MigrationToolsError) failures from the migration
|
|
225
|
+
// load phase in a structured CLI envelope. Letting them throw would
|
|
226
|
+
// bypass `handleResult()` and crash the command — see CLI structured-
|
|
227
|
+
// errors guideline (CliStructuredError + Result pattern).
|
|
228
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
229
|
+
return notOk(
|
|
230
|
+
errorUnexpected(message, {
|
|
231
|
+
why: `Unexpected error while loading migrations: ${message}`,
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Phase 1 — seed: unconditionally re-emit per-space pinned artefacts
|
|
237
|
+
// (contract.json / contract.d.ts / refs/head.json) and materialise any
|
|
238
|
+
// descriptor-shipped migration packages not yet on disk. Runs before
|
|
239
|
+
// the no-op check so that an extension bump alone (with no structural
|
|
240
|
+
// app-space change) still re-pins extension artefacts on disk.
|
|
241
|
+
const canonicalExtensionInputs = toExtensionInputs(config.extensionPacks ?? []);
|
|
242
|
+
const seedResult = await runContractSpaceSeedPhase({
|
|
243
|
+
migrationsDir,
|
|
244
|
+
extensionPacks: canonicalExtensionInputs,
|
|
245
|
+
});
|
|
246
|
+
if (!flags.json && !flags.quiet) {
|
|
247
|
+
for (const record of seedResult.seeded) {
|
|
248
|
+
if (record.action === 'updated') {
|
|
249
|
+
const pkgSuffix =
|
|
250
|
+
record.newMigrationDirs.length > 0
|
|
251
|
+
? `; ${record.newMigrationDirs.length} new migration package(s) materialised`
|
|
252
|
+
: '';
|
|
253
|
+
ui.step(`Updated ${record.spaceId} to ${record.newHash}${pkgSuffix}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
217
256
|
}
|
|
257
|
+
const emittedExtensionDirs = seedResult.seeded.flatMap((r) =>
|
|
258
|
+
r.newMigrationDirs.map((dirName) => ({ spaceId: r.spaceId, dirName })),
|
|
259
|
+
);
|
|
218
260
|
|
|
219
261
|
// Check for no-op (same hash means no changes)
|
|
220
262
|
if (fromHash === toStorageHash) {
|
|
@@ -224,6 +266,7 @@ async function executeMigrationPlanCommand(
|
|
|
224
266
|
from: fromHash,
|
|
225
267
|
to: toStorageHash,
|
|
226
268
|
operations: [],
|
|
269
|
+
emittedExtensionDirs,
|
|
227
270
|
summary: 'No changes detected between contracts',
|
|
228
271
|
timings: { total: Date.now() - startTime },
|
|
229
272
|
};
|
|
@@ -239,6 +282,25 @@ async function executeMigrationPlanCommand(
|
|
|
239
282
|
}),
|
|
240
283
|
);
|
|
241
284
|
}
|
|
285
|
+
|
|
286
|
+
// Phase 2 — load: build the aggregate against the now-consistent disk
|
|
287
|
+
// state that phase 1 just seeded. The seed phase guarantees every
|
|
288
|
+
// declared extension has its head ref pinned, so the loader's
|
|
289
|
+
// declaredButUnmigrated precheck always passes here.
|
|
290
|
+
const stack = createControlStack(config);
|
|
291
|
+
const familyInstance = config.family.create(stack);
|
|
292
|
+
const aggregateResult = await buildContractSpaceAggregate({
|
|
293
|
+
targetId: config.target.targetId,
|
|
294
|
+
migrationsDir,
|
|
295
|
+
appContract: toContractJson,
|
|
296
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
297
|
+
validateContract: (json: unknown) => familyInstance.validateContract(json),
|
|
298
|
+
});
|
|
299
|
+
if (!aggregateResult.ok) {
|
|
300
|
+
return notOk(aggregateResult.failure);
|
|
301
|
+
}
|
|
302
|
+
const aggregate = aggregateResult.value;
|
|
303
|
+
|
|
242
304
|
const frameworkComponents = assertFrameworkComponentsCompatible(
|
|
243
305
|
config.family.familyId,
|
|
244
306
|
config.target.targetId,
|
|
@@ -249,12 +311,11 @@ async function executeMigrationPlanCommand(
|
|
|
249
311
|
const timestamp = new Date();
|
|
250
312
|
const slug = options.name ?? 'migration';
|
|
251
313
|
const dirName = formatMigrationDirName(timestamp, slug);
|
|
252
|
-
const packageDir = join(
|
|
314
|
+
const packageDir = join(appMigrationsDir, dirName);
|
|
253
315
|
|
|
254
|
-
const
|
|
316
|
+
const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
|
|
255
317
|
from: fromHash,
|
|
256
318
|
to: toStorageHash,
|
|
257
|
-
kind: 'regular',
|
|
258
319
|
fromContract,
|
|
259
320
|
toContract: toContractJson,
|
|
260
321
|
hints: {
|
|
@@ -267,17 +328,15 @@ async function executeMigrationPlanCommand(
|
|
|
267
328
|
};
|
|
268
329
|
|
|
269
330
|
try {
|
|
270
|
-
const stack = createControlStack(config);
|
|
271
|
-
const familyInstance = config.family.create(stack);
|
|
272
331
|
const planner = migrations.createPlanner(familyInstance);
|
|
273
332
|
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
274
333
|
const plannerResult = planner.plan({
|
|
275
|
-
contract:
|
|
334
|
+
contract: aggregate.app.contract,
|
|
276
335
|
schema: fromSchema,
|
|
277
336
|
policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
|
|
278
|
-
fromHash,
|
|
279
337
|
fromContract,
|
|
280
338
|
frameworkComponents,
|
|
339
|
+
spaceId: aggregate.app.spaceId,
|
|
281
340
|
});
|
|
282
341
|
if (plannerResult.kind === 'failure') {
|
|
283
342
|
return notOk(
|
|
@@ -320,18 +379,22 @@ async function executeMigrationPlanCommand(
|
|
|
320
379
|
|
|
321
380
|
const migrationTsContent = plannerResult.plan.renderTypeScript();
|
|
322
381
|
|
|
323
|
-
// Always-attest: compute
|
|
324
|
-
// placeholders blocked lowering, ops is `[]` and the
|
|
325
|
-
// the empty list — re-emitting after the user fills the placeholder
|
|
326
|
-
// produces a different
|
|
382
|
+
// Always-attest: compute migrationHash over (metadata, ops). When
|
|
383
|
+
// placeholders blocked lowering, ops is `[]` and the hash is computed
|
|
384
|
+
// over the empty list — re-emitting after the user fills the placeholder
|
|
385
|
+
// produces a different hash (over the real ops). This is intentional;
|
|
327
386
|
// there is no on-disk "draft" state.
|
|
328
387
|
const opsForWrite = hasPlaceholders ? [] : plannedOps;
|
|
329
|
-
const
|
|
330
|
-
...
|
|
331
|
-
|
|
388
|
+
const metadataWithInvariants: Omit<MigrationMetadata, 'migrationHash'> = {
|
|
389
|
+
...baseMetadata,
|
|
390
|
+
providedInvariants: deriveProvidedInvariants(opsForWrite),
|
|
391
|
+
};
|
|
392
|
+
const metadata: MigrationMetadata = {
|
|
393
|
+
...metadataWithInvariants,
|
|
394
|
+
migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
|
|
332
395
|
};
|
|
333
396
|
|
|
334
|
-
await writeMigrationPackage(packageDir,
|
|
397
|
+
await writeMigrationPackage(packageDir, metadata, opsForWrite);
|
|
335
398
|
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
336
399
|
await copyFilesWithRename(packageDir, [
|
|
337
400
|
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
@@ -356,6 +419,7 @@ async function executeMigrationPlanCommand(
|
|
|
356
419
|
to: toStorageHash,
|
|
357
420
|
dir: relative(process.cwd(), packageDir),
|
|
358
421
|
operations: [],
|
|
422
|
+
emittedExtensionDirs,
|
|
359
423
|
pendingPlaceholders: true,
|
|
360
424
|
summary:
|
|
361
425
|
'Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
|
|
@@ -364,7 +428,9 @@ async function executeMigrationPlanCommand(
|
|
|
364
428
|
return ok(result);
|
|
365
429
|
}
|
|
366
430
|
|
|
367
|
-
const
|
|
431
|
+
const preview = hasOperationPreview(familyInstance)
|
|
432
|
+
? familyInstance.toOperationPreview(plannedOps)
|
|
433
|
+
: undefined;
|
|
368
434
|
const result: MigrationPlanResult = {
|
|
369
435
|
ok: true,
|
|
370
436
|
noOp: false,
|
|
@@ -376,13 +442,25 @@ async function executeMigrationPlanCommand(
|
|
|
376
442
|
label: op.label,
|
|
377
443
|
operationClass: op.operationClass,
|
|
378
444
|
})),
|
|
379
|
-
|
|
380
|
-
|
|
445
|
+
emittedExtensionDirs,
|
|
446
|
+
...(preview !== undefined ? { preview } : {}),
|
|
447
|
+
summary: buildPlanSummary(plannedOps.length, emittedExtensionDirs.length),
|
|
381
448
|
timings: { total: Date.now() - startTime },
|
|
382
449
|
};
|
|
383
450
|
return ok(result);
|
|
384
451
|
} catch (error) {
|
|
385
|
-
|
|
452
|
+
if (CliStructuredError.is(error)) {
|
|
453
|
+
return notOk(error);
|
|
454
|
+
}
|
|
455
|
+
if (MigrationToolsError.is(error)) {
|
|
456
|
+
return notOk(mapMigrationToolsError(error));
|
|
457
|
+
}
|
|
458
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
459
|
+
return notOk(
|
|
460
|
+
errorUnexpected(message, {
|
|
461
|
+
why: `Unexpected error during migration plan: ${message}`,
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
386
464
|
}
|
|
387
465
|
}
|
|
388
466
|
|
|
@@ -424,7 +502,29 @@ export function createMigrationPlanCommand(): Command {
|
|
|
424
502
|
return command;
|
|
425
503
|
}
|
|
426
504
|
|
|
427
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Compose the success-line summary so the cross-space side effect
|
|
507
|
+
* (extension-space migration packages materialised on disk during
|
|
508
|
+
* this `plan` run) is visible in the top line — not just in the
|
|
509
|
+
* step log above it.
|
|
510
|
+
*
|
|
511
|
+
* Example outputs:
|
|
512
|
+
* - `Planned 3 operation(s)` (app-space-only project)
|
|
513
|
+
* - `Planned 3 operation(s); materialised 1 extension-space migration` (one extension)
|
|
514
|
+
* - `Planned 3 operation(s); materialised 2 extension-space migrations` (two extensions)
|
|
515
|
+
*
|
|
516
|
+
* Locks AC3 at the summary-line level: a reader of the success line
|
|
517
|
+
* can tell that something happened beyond the app space.
|
|
518
|
+
*/
|
|
519
|
+
function buildPlanSummary(plannedOpsCount: number, emittedExtensionDirsCount: number): string {
|
|
520
|
+
const base = `Planned ${plannedOpsCount} operation(s)`;
|
|
521
|
+
if (emittedExtensionDirsCount === 0) return base;
|
|
522
|
+
const noun =
|
|
523
|
+
emittedExtensionDirsCount === 1 ? 'extension-space migration' : 'extension-space migrations';
|
|
524
|
+
return `${base}; materialised ${emittedExtensionDirsCount} ${noun}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
|
|
428
528
|
const lines: string[] = [];
|
|
429
529
|
const useColor = flags.color !== false;
|
|
430
530
|
|
|
@@ -432,10 +532,29 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
432
532
|
const yellow_ = useColor ? (s: string) => `\x1b[33m${s}\x1b[0m` : (s: string) => s;
|
|
433
533
|
const dim_ = useColor ? (s: string) => `\x1b[2m${s}\x1b[0m` : (s: string) => s;
|
|
434
534
|
|
|
535
|
+
// Renders the extension-space materialisation block + canonical apply-step
|
|
536
|
+
// hint shared by the no-op, placeholder, and full-plan branches. The app
|
|
537
|
+
// space short-circuits do not skip it: an extension-only bump emits new
|
|
538
|
+
// `migrations/<spaceId>/<dirName>/` directories on disk that the user
|
|
539
|
+
// still has to apply, so the success line must surface them.
|
|
540
|
+
function appendEmittedExtensions(): void {
|
|
541
|
+
if (result.emittedExtensionDirs.length === 0) return;
|
|
542
|
+
lines.push('');
|
|
543
|
+
lines.push(dim_('Emitted extension migrations:'));
|
|
544
|
+
for (const entry of result.emittedExtensionDirs) {
|
|
545
|
+
lines.push(dim_(` ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`));
|
|
546
|
+
}
|
|
547
|
+
lines.push('');
|
|
548
|
+
lines.push(
|
|
549
|
+
`Next: review the extension migrations above, then run ${green_('prisma-next migration apply')}.`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
435
553
|
if (result.noOp) {
|
|
436
554
|
lines.push(`${green_('✔')} No changes detected`);
|
|
437
555
|
lines.push(dim_(` from: ${result.from}`));
|
|
438
556
|
lines.push(dim_(` to: ${result.to}`));
|
|
557
|
+
appendEmittedExtensions();
|
|
439
558
|
return lines.join('\n');
|
|
440
559
|
}
|
|
441
560
|
|
|
@@ -452,6 +571,7 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
452
571
|
'Open migration.ts and replace each `placeholder(...)` call with your actual query.',
|
|
453
572
|
);
|
|
454
573
|
lines.push(`Then run: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)}`);
|
|
574
|
+
appendEmittedExtensions();
|
|
455
575
|
return lines.join('\n');
|
|
456
576
|
}
|
|
457
577
|
|
|
@@ -464,11 +584,11 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
464
584
|
const op = result.operations[i]!;
|
|
465
585
|
const isLast = i === result.operations.length - 1;
|
|
466
586
|
const treeChar = isLast ? '└' : '├';
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
lines.push(`${dim_(treeChar)}─ ${op.label}
|
|
587
|
+
// operationClass tag is intentionally NOT inlined per spec:
|
|
588
|
+
// a destructive footer warning still surfaces below this list.
|
|
589
|
+
const destructiveMarker =
|
|
590
|
+
op.operationClass === 'destructive' ? ` ${yellow_('(destructive)')}` : '';
|
|
591
|
+
lines.push(`${dim_(treeChar)}─ ${op.label}${destructiveMarker}`);
|
|
472
592
|
}
|
|
473
593
|
|
|
474
594
|
const hasDestructive = result.operations.some((op) => op.operationClass === 'destructive');
|
|
@@ -484,22 +604,37 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
484
604
|
lines.push(dim_(`from: ${result.from}`));
|
|
485
605
|
lines.push(dim_(`to: ${result.to}`));
|
|
486
606
|
if (result.dir) {
|
|
487
|
-
lines.push(dim_(`
|
|
607
|
+
lines.push(dim_(`App space → ${result.dir}`));
|
|
608
|
+
}
|
|
609
|
+
// Per-space block: surface the extension-space directories materialised
|
|
610
|
+
// alongside the app-space migration. Without this block the cross-space
|
|
611
|
+
// side effect is invisible in the success summary (e2e finding F1).
|
|
612
|
+
for (const entry of result.emittedExtensionDirs) {
|
|
613
|
+
lines.push(
|
|
614
|
+
dim_(`Extension space ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`),
|
|
615
|
+
);
|
|
488
616
|
}
|
|
489
617
|
|
|
490
618
|
lines.push('');
|
|
619
|
+
// The "Next:" hint always points at the canonical apply path
|
|
620
|
+
// (`prisma-next migration apply`) regardless of how many spaces
|
|
621
|
+
// were materialised — `db update` is a dev-time convenience, not
|
|
622
|
+
// the canonical replay step.
|
|
491
623
|
lines.push(
|
|
492
|
-
`Next: ${green_(
|
|
624
|
+
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
|
|
493
625
|
);
|
|
494
626
|
|
|
495
|
-
if (result.
|
|
627
|
+
if (result.preview && result.preview.statements.length > 0) {
|
|
628
|
+
// The non-empty length is already guaranteed by the surrounding check, so
|
|
629
|
+
// a plain `every` here is equivalent to the helper in formatters/migrations.ts.
|
|
630
|
+
const allSql = result.preview.statements.every((s) => s.language === 'sql');
|
|
496
631
|
lines.push('');
|
|
497
|
-
lines.push(dim_('DDL preview'));
|
|
632
|
+
lines.push(dim_(allSql ? 'DDL preview' : 'Operation preview'));
|
|
498
633
|
lines.push('');
|
|
499
|
-
for (const statement of result.
|
|
500
|
-
const trimmed = statement.trim();
|
|
634
|
+
for (const statement of result.preview.statements) {
|
|
635
|
+
const trimmed = statement.text.trim();
|
|
501
636
|
if (!trimmed) continue;
|
|
502
|
-
const line = trimmed.endsWith(';') ? trimmed :
|
|
637
|
+
const line = statement.language === 'sql' && !trimmed.endsWith(';') ? `${trimmed};` : trimmed;
|
|
503
638
|
lines.push(line);
|
|
504
639
|
}
|
|
505
640
|
}
|
|
@@ -517,24 +652,27 @@ export type PrefixResolutionFailure =
|
|
|
517
652
|
| { reason: 'not-found' };
|
|
518
653
|
|
|
519
654
|
/**
|
|
520
|
-
* Resolve a migration
|
|
655
|
+
* Resolve a migration package by **target contract hash** (`metadata.to`)
|
|
656
|
+
* using exact match or prefix match.
|
|
521
657
|
*
|
|
658
|
+
* Note: matches `metadata.to` (the contract hash this migration produces),
|
|
659
|
+
* not `metadata.migrationHash` (the package's content-addressed identity).
|
|
522
660
|
* Tries exact match first, then prefix match (auto-prepending `sha256:` when
|
|
523
|
-
* the needle omits the scheme). Returns the matched
|
|
661
|
+
* the needle omits the scheme). Returns the matched package on success, or a
|
|
524
662
|
* discriminated failure indicating whether the prefix was ambiguous or simply
|
|
525
663
|
* not found.
|
|
526
664
|
*
|
|
527
665
|
* @internal Exported for testing only.
|
|
528
666
|
*/
|
|
529
|
-
export function resolveBundleByPrefix<T extends {
|
|
667
|
+
export function resolveBundleByPrefix<T extends { metadata: { to: string } }>(
|
|
530
668
|
bundles: readonly T[],
|
|
531
669
|
needle: string,
|
|
532
670
|
): Result<T, PrefixResolutionFailure> {
|
|
533
|
-
const exact = bundles.find((p) => p.
|
|
671
|
+
const exact = bundles.find((p) => p.metadata.to === needle);
|
|
534
672
|
if (exact) return ok(exact);
|
|
535
673
|
|
|
536
674
|
const prefixWithScheme = needle.startsWith('sha256:') ? needle : `sha256:${needle}`;
|
|
537
|
-
const candidates = bundles.filter((p) => p.
|
|
675
|
+
const candidates = bundles.filter((p) => p.metadata.to.startsWith(prefixWithScheme));
|
|
538
676
|
|
|
539
677
|
if (candidates.length === 1) return ok(candidates[0]!);
|
|
540
678
|
if (candidates.length > 1) return notOk({ reason: 'ambiguous', count: candidates.length });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
1
2
|
import type { RefEntry } from '@prisma-next/migration-tools/refs';
|
|
2
3
|
import {
|
|
3
4
|
deleteRef,
|
|
@@ -7,11 +8,15 @@ import {
|
|
|
7
8
|
validateRefValue,
|
|
8
9
|
writeRef,
|
|
9
10
|
} from '@prisma-next/migration-tools/refs';
|
|
10
|
-
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
11
11
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
12
12
|
import { Command } from 'commander';
|
|
13
13
|
import { loadConfig } from '../config-loader';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CliStructuredError,
|
|
16
|
+
errorRuntime,
|
|
17
|
+
errorUnexpected,
|
|
18
|
+
mapMigrationToolsError,
|
|
19
|
+
} from '../utils/cli-errors';
|
|
15
20
|
import {
|
|
16
21
|
addGlobalOptions,
|
|
17
22
|
resolveMigrationPaths,
|
|
@@ -49,11 +54,7 @@ interface RefListResult {
|
|
|
49
54
|
|
|
50
55
|
function mapError(error: unknown): CliStructuredError {
|
|
51
56
|
if (MigrationToolsError.is(error)) {
|
|
52
|
-
return
|
|
53
|
-
why: error.why,
|
|
54
|
-
fix: error.fix,
|
|
55
|
-
meta: { code: error.code },
|
|
56
|
-
});
|
|
57
|
+
return mapMigrationToolsError(error);
|
|
57
58
|
}
|
|
58
59
|
return errorUnexpected(error instanceof Error ? error.message : String(error));
|
|
59
60
|
}
|