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