@prisma-next/cli 0.4.0-dev.5 → 0.4.0-dev.7
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 +8 -7
- package/dist/cli-errors-BUuJr6py.mjs +5 -0
- package/dist/{cli-errors-DStABy9d.d.mts → cli-errors-Dic2eADK.d.mts} +1 -0
- package/dist/cli.mjs +9 -16
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-tdnbk0OR.mjs → client-CJxHfhze.mjs} +4 -2
- package/dist/client-CJxHfhze.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -6
- package/dist/commands/contract-infer.mjs +1 -7
- package/dist/commands/db-init.mjs +5 -6
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +3 -6
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +4 -5
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.mjs +5 -6
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +4 -5
- package/dist/commands/db-verify.mjs.map +1 -1
- package/dist/commands/migration-apply.mjs +6 -7
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-emit.d.mts +38 -0
- package/dist/commands/migration-emit.d.mts.map +1 -0
- package/dist/commands/migration-emit.mjs +81 -0
- package/dist/commands/migration-emit.mjs.map +1 -0
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +33 -10
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +93 -81
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +2 -2
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +4 -4
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.mjs +1 -6
- package/dist/contract-emit-C2_J2U7A.mjs +4 -0
- package/dist/{contract-emit-CRoS1nx5.mjs → contract-emit-CKig_Lra.mjs} +4 -4
- package/dist/{contract-emit-CRoS1nx5.mjs.map → contract-emit-CKig_Lra.mjs.map} +1 -1
- package/dist/{contract-emit-Ctn6mH9H.mjs → contract-emit-gpJNLGs7.mjs} +5 -5
- package/dist/{contract-emit-Ctn6mH9H.mjs.map → contract-emit-gpJNLGs7.mjs.map} +1 -1
- package/dist/{contract-infer-Ba1SE57Q.mjs → contract-infer-BDJgg7Xb.mjs} +3 -3
- package/dist/{contract-infer-Ba1SE57Q.mjs.map → contract-infer-BDJgg7Xb.mjs.map} +1 -1
- package/dist/exports/control-api.mjs +2 -4
- package/dist/exports/index.mjs +1 -6
- package/dist/exports/index.mjs.map +1 -1
- package/dist/{framework-components-BAsliT4V.mjs → framework-components-Bsr1GaIj.mjs} +2 -2
- package/dist/{framework-components-BAsliT4V.mjs.map → framework-components-Bsr1GaIj.mjs.map} +1 -1
- package/dist/{init-CYWnL7gq.mjs → init-DlFLMBaU.mjs} +2 -2
- package/dist/{init-CYWnL7gq.mjs.map → init-DlFLMBaU.mjs.map} +1 -1
- package/dist/{inspect-live-schema-gYQiWfpl.mjs → inspect-live-schema-ChqrALmw.mjs} +4 -4
- package/dist/{inspect-live-schema-gYQiWfpl.mjs.map → inspect-live-schema-ChqrALmw.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-x4n_ZhAh.mjs → migration-command-scaffold-B0oH_hyB.mjs} +4 -4
- package/dist/{migration-command-scaffold-x4n_ZhAh.mjs.map → migration-command-scaffold-B0oH_hyB.mjs.map} +1 -1
- package/dist/migration-emit-Du4DBMqz.mjs +125 -0
- package/dist/migration-emit-Du4DBMqz.mjs.map +1 -0
- package/dist/{migration-status-DyVDf5NI.mjs → migration-status-CPamfEPj.mjs} +5 -5
- package/dist/{migration-status-DyVDf5NI.mjs.map → migration-status-CPamfEPj.mjs.map} +1 -1
- package/dist/{migrations-DTZBYXm1.mjs → migrations-BIsjFjSV.mjs} +6 -15
- package/dist/migrations-BIsjFjSV.mjs.map +1 -0
- package/dist/{result-handler-oK_vA-Fn.mjs → result-handler-AFK4hxyX.mjs} +2 -2
- package/dist/result-handler-AFK4hxyX.mjs.map +1 -0
- package/dist/{validate-contract-deps-esa-VQ0h.mjs → validate-contract-deps-DBH6iTAD.mjs} +1 -1
- package/dist/{validate-contract-deps-esa-VQ0h.mjs.map → validate-contract-deps-DBH6iTAD.mjs.map} +1 -1
- package/dist/{verify-DlFQ2FOw.mjs → verify-C56CuQc7.mjs} +2 -2
- package/dist/{verify-DlFQ2FOw.mjs.map → verify-C56CuQc7.mjs.map} +1 -1
- package/package.json +19 -19
- package/src/cli.ts +4 -4
- package/src/commands/migration-apply.ts +2 -2
- package/src/commands/migration-emit.ts +134 -0
- package/src/commands/migration-new.ts +50 -15
- package/src/commands/migration-plan.ts +138 -130
- package/src/commands/migration-show.ts +1 -1
- package/src/commands/migration-status.ts +1 -1
- package/src/control-api/operations/db-init.ts +3 -0
- package/src/control-api/operations/db-update.ts +3 -0
- package/src/lib/migration-emit.ts +125 -0
- package/src/lib/migration-strategy.ts +49 -0
- package/src/utils/cli-errors.ts +7 -0
- package/src/utils/formatters/help.ts +1 -1
- package/src/utils/formatters/migrations.ts +6 -20
- package/dist/cli-errors-BDCYR5ap.mjs +0 -4
- package/dist/client-tdnbk0OR.mjs.map +0 -1
- package/dist/commands/migration-verify.d.mts +0 -16
- package/dist/commands/migration-verify.d.mts.map +0 -1
- package/dist/commands/migration-verify.mjs +0 -110
- package/dist/commands/migration-verify.mjs.map +0 -1
- package/dist/contract-emit-CVUWsFfx.mjs +0 -6
- package/dist/migrations-DTZBYXm1.mjs.map +0 -1
- package/dist/result-handler-oK_vA-Fn.mjs.map +0 -1
- package/src/commands/migration-verify.ts +0 -180
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { loadConfig } from '../config-loader';
|
|
5
|
+
import { emitMigration } from '../lib/migration-emit';
|
|
6
|
+
import {
|
|
7
|
+
CliStructuredError,
|
|
8
|
+
errorRuntime,
|
|
9
|
+
errorTargetMigrationNotSupported,
|
|
10
|
+
errorUnexpected,
|
|
11
|
+
} from '../utils/cli-errors';
|
|
12
|
+
import {
|
|
13
|
+
addGlobalOptions,
|
|
14
|
+
getTargetMigrations,
|
|
15
|
+
setCommandDescriptions,
|
|
16
|
+
setCommandExamples,
|
|
17
|
+
} from '../utils/command-helpers';
|
|
18
|
+
import { formatMigrationEmitCommandOutput } from '../utils/formatters/migrations';
|
|
19
|
+
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
20
|
+
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
21
|
+
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
22
|
+
import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
|
|
23
|
+
import { handleResult } from '../utils/result-handler';
|
|
24
|
+
import { TerminalUI } from '../utils/terminal-ui';
|
|
25
|
+
|
|
26
|
+
export interface MigrationEmitOptions extends CommonCommandOptions {
|
|
27
|
+
readonly dir: string;
|
|
28
|
+
readonly config?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MigrationEmitResult {
|
|
32
|
+
readonly ok: boolean;
|
|
33
|
+
readonly dir: string;
|
|
34
|
+
readonly migrationId: string;
|
|
35
|
+
readonly summary: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function executeMigrationEmitCommand(
|
|
39
|
+
options: MigrationEmitOptions,
|
|
40
|
+
flags: GlobalFlags,
|
|
41
|
+
ui: TerminalUI,
|
|
42
|
+
): Promise<Result<MigrationEmitResult, CliStructuredError>> {
|
|
43
|
+
const dir = options.dir;
|
|
44
|
+
|
|
45
|
+
if (!flags.json && !flags.quiet) {
|
|
46
|
+
const header = formatStyledHeader({
|
|
47
|
+
command: 'migration emit',
|
|
48
|
+
description: 'Emit ops.json from migration.ts and compute migrationId',
|
|
49
|
+
details: [{ label: 'dir', value: dir }],
|
|
50
|
+
flags,
|
|
51
|
+
});
|
|
52
|
+
ui.stderr(header);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const config = await loadConfig(options.config);
|
|
57
|
+
const migrations = getTargetMigrations(config.target);
|
|
58
|
+
if (!migrations) {
|
|
59
|
+
throw errorTargetMigrationNotSupported({
|
|
60
|
+
why: `Target "${config.target.id}" does not support migrations`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const frameworkComponents = assertFrameworkComponentsCompatible(
|
|
64
|
+
config.family.familyId,
|
|
65
|
+
config.target.targetId,
|
|
66
|
+
[config.target, config.adapter, ...(config.extensionPacks ?? [])],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const { migrationId } = await emitMigration(dir, {
|
|
70
|
+
targetId: config.target.targetId,
|
|
71
|
+
migrations,
|
|
72
|
+
frameworkComponents,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return ok({
|
|
76
|
+
ok: true,
|
|
77
|
+
dir,
|
|
78
|
+
migrationId,
|
|
79
|
+
summary: `Emitted ops.json and attested migrationId: ${migrationId}`,
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (CliStructuredError.is(error)) {
|
|
83
|
+
return notOk(error);
|
|
84
|
+
}
|
|
85
|
+
if (MigrationToolsError.is(error)) {
|
|
86
|
+
return notOk(
|
|
87
|
+
errorRuntime(error.message, {
|
|
88
|
+
why: error.why,
|
|
89
|
+
fix: error.fix,
|
|
90
|
+
meta: { code: error.code, ...(error.details ?? {}) },
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return notOk(
|
|
95
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
96
|
+
why: `Failed to emit migration: ${error instanceof Error ? error.message : String(error)}`,
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createMigrationEmitCommand(): Command {
|
|
103
|
+
const command = new Command('emit');
|
|
104
|
+
setCommandDescriptions(
|
|
105
|
+
command,
|
|
106
|
+
'Emit ops.json from migration.ts and compute migrationId',
|
|
107
|
+
'Evaluates migration.ts in the package directory, resolves it to ops.json,\n' +
|
|
108
|
+
'then computes and persists the content-addressed migrationId in migration.json.\n' +
|
|
109
|
+
'If the file contains unfilled placeholder() slots, emit fails with PN-MIG-2001\n' +
|
|
110
|
+
'and reports the slot to fill in.',
|
|
111
|
+
);
|
|
112
|
+
setCommandExamples(command, ['prisma-next migration emit --dir migrations/20250101-add-users']);
|
|
113
|
+
addGlobalOptions(command)
|
|
114
|
+
.requiredOption('--dir <path>', 'Path to the migration package directory')
|
|
115
|
+
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
116
|
+
.action(async (options: MigrationEmitOptions) => {
|
|
117
|
+
const flags = parseGlobalFlags(options);
|
|
118
|
+
const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
|
|
119
|
+
|
|
120
|
+
const result = await executeMigrationEmitCommand(options, flags, ui);
|
|
121
|
+
|
|
122
|
+
const exitCode = handleResult(result, flags, ui, (emitResult) => {
|
|
123
|
+
if (flags.json) {
|
|
124
|
+
ui.output(JSON.stringify(emitResult, null, 2));
|
|
125
|
+
} else if (!flags.quiet) {
|
|
126
|
+
ui.log(formatMigrationEmitCommandOutput(emitResult, flags));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
process.exit(exitCode);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return command;
|
|
134
|
+
}
|
|
@@ -1,33 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `migration new` — scaffolds a migration package with a migration.ts file
|
|
3
|
-
* for manual authoring.
|
|
4
|
-
*
|
|
2
|
+
* `migration new` — scaffolds a migration package with a `migration.ts` file
|
|
3
|
+
* for manual authoring.
|
|
4
|
+
*
|
|
5
|
+
* Both descriptor-flow (Postgres) and class-flow (Mongo) targets go through
|
|
6
|
+
* the same path here: the planner's `emptyMigration(context)` returns a
|
|
7
|
+
* `MigrationPlanWithAuthoringSurface`, whose `renderTypeScript()` produces
|
|
8
|
+
* the target-appropriate empty stub. The CLI writes the returned source
|
|
9
|
+
* verbatim.
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
import { readFileSync } from 'node:fs';
|
|
8
13
|
import type { Contract } from '@prisma-next/contract/types';
|
|
14
|
+
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
9
15
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
10
16
|
import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
|
|
11
17
|
import {
|
|
18
|
+
copyContractToMigrationDir,
|
|
12
19
|
formatMigrationDirName,
|
|
13
20
|
readMigrationsDir,
|
|
14
21
|
writeMigrationPackage,
|
|
15
22
|
} from '@prisma-next/migration-tools/io';
|
|
16
|
-
import {
|
|
23
|
+
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
17
24
|
import type { MigrationManifest } from '@prisma-next/migration-tools/types';
|
|
18
25
|
import { isAttested, MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
19
26
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
20
27
|
import { Command } from 'commander';
|
|
21
28
|
import { join, relative, resolve } from 'pathe';
|
|
22
29
|
import { loadConfig } from '../config-loader';
|
|
23
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
CliStructuredError,
|
|
32
|
+
errorRuntime,
|
|
33
|
+
errorTargetMigrationNotSupported,
|
|
34
|
+
errorUnexpected,
|
|
35
|
+
} from '../utils/cli-errors';
|
|
24
36
|
import {
|
|
25
37
|
addGlobalOptions,
|
|
38
|
+
getTargetMigrations,
|
|
26
39
|
resolveMigrationPaths,
|
|
27
40
|
setCommandDescriptions,
|
|
28
41
|
setCommandExamples,
|
|
29
42
|
} from '../utils/command-helpers';
|
|
30
43
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
44
|
+
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
31
45
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
32
46
|
import { parseGlobalFlags } from '../utils/global-flags';
|
|
33
47
|
import { handleResult } from '../utils/result-handler';
|
|
@@ -53,7 +67,6 @@ async function executeMigrationNewCommand(
|
|
|
53
67
|
const config = await loadConfig(options.config);
|
|
54
68
|
const { migrationsDir, migrationsRelative } = resolveMigrationPaths(options.config, config);
|
|
55
69
|
|
|
56
|
-
// Read the emitted contract (destination)
|
|
57
70
|
const contractPath = config.contract?.output ?? 'contract.json';
|
|
58
71
|
const contractPathAbsolute = resolve(
|
|
59
72
|
options.config ? resolve(options.config, '..') : process.cwd(),
|
|
@@ -101,7 +114,6 @@ async function executeMigrationNewCommand(
|
|
|
101
114
|
);
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
// Determine "from" hash
|
|
105
117
|
let fromContract: Contract | null = null;
|
|
106
118
|
let fromHash: string = EMPTY_CONTRACT_HASH;
|
|
107
119
|
|
|
@@ -113,7 +125,6 @@ async function executeMigrationNewCommand(
|
|
|
113
125
|
const graph = reconstructGraph(attested);
|
|
114
126
|
|
|
115
127
|
if (options.from) {
|
|
116
|
-
// Explicit --from: find the migration with matching to hash
|
|
117
128
|
const match = attested.find((p) => p.manifest.to.startsWith(options.from!));
|
|
118
129
|
if (!match) {
|
|
119
130
|
return notOk(
|
|
@@ -151,7 +162,6 @@ async function executeMigrationNewCommand(
|
|
|
151
162
|
throw error;
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
// Check for no-op
|
|
155
165
|
if (fromHash === toStorageHash) {
|
|
156
166
|
return notOk(
|
|
157
167
|
errorRuntime('No changes detected', {
|
|
@@ -161,7 +171,6 @@ async function executeMigrationNewCommand(
|
|
|
161
171
|
);
|
|
162
172
|
}
|
|
163
173
|
|
|
164
|
-
// Build manifest and write package
|
|
165
174
|
const timestamp = new Date();
|
|
166
175
|
const slug = options.name ?? 'migration';
|
|
167
176
|
const dirName = formatMigrationDirName(timestamp, slug);
|
|
@@ -184,12 +193,35 @@ async function executeMigrationNewCommand(
|
|
|
184
193
|
createdAt: timestamp.toISOString(),
|
|
185
194
|
};
|
|
186
195
|
|
|
196
|
+
const migrations = getTargetMigrations(config.target);
|
|
197
|
+
if (!migrations) {
|
|
198
|
+
return notOk(
|
|
199
|
+
errorTargetMigrationNotSupported({
|
|
200
|
+
why: `Target "${config.target.targetId}" does not support migrations`,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
try {
|
|
188
|
-
|
|
206
|
+
assertFrameworkComponentsCompatible(config.family.familyId, config.target.targetId, [
|
|
207
|
+
config.target,
|
|
208
|
+
config.adapter,
|
|
209
|
+
...(config.extensionPacks ?? []),
|
|
210
|
+
]);
|
|
211
|
+
|
|
189
212
|
await writeMigrationPackage(packageDir, manifest, []);
|
|
213
|
+
await copyContractToMigrationDir(packageDir, contractPathAbsolute);
|
|
190
214
|
|
|
191
|
-
|
|
192
|
-
|
|
215
|
+
const stack = createControlStack(config);
|
|
216
|
+
const familyInstance = config.family.create(stack);
|
|
217
|
+
const planner = migrations.createPlanner(familyInstance);
|
|
218
|
+
const emptyPlan = planner.emptyMigration({
|
|
219
|
+
packageDir,
|
|
220
|
+
contractJsonPath: join(packageDir, 'contract.json'),
|
|
221
|
+
fromHash,
|
|
222
|
+
toHash: toStorageHash,
|
|
223
|
+
});
|
|
224
|
+
await writeMigrationTs(packageDir, emptyPlan.renderTypeScript());
|
|
193
225
|
|
|
194
226
|
return ok({
|
|
195
227
|
ok: true as const,
|
|
@@ -199,6 +231,9 @@ async function executeMigrationNewCommand(
|
|
|
199
231
|
summary: `Scaffolded migration at ${relative(process.cwd(), packageDir)}`,
|
|
200
232
|
});
|
|
201
233
|
} catch (error) {
|
|
234
|
+
if (CliStructuredError.is(error)) {
|
|
235
|
+
return notOk(error);
|
|
236
|
+
}
|
|
202
237
|
return notOk(
|
|
203
238
|
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
204
239
|
why: `Failed to scaffold migration: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -214,7 +249,7 @@ export function createMigrationNewCommand(): Command {
|
|
|
214
249
|
'Scaffold a new migration for manual authoring',
|
|
215
250
|
'Creates a migration package with a migration.ts file for manual authoring.\n' +
|
|
216
251
|
'Write operation descriptors and data transforms in migration.ts, then run\n' +
|
|
217
|
-
'`migration
|
|
252
|
+
'`migration emit` to resolve and attest the package.',
|
|
218
253
|
);
|
|
219
254
|
setCommandExamples(command, [
|
|
220
255
|
'prisma-next migration new --name split-name',
|
|
@@ -248,7 +283,7 @@ export function createMigrationNewCommand(): Command {
|
|
|
248
283
|
ui.output(` from: ${value.from}`);
|
|
249
284
|
ui.output(` to: ${value.to}`);
|
|
250
285
|
ui.output(
|
|
251
|
-
`\nEdit migration.ts, then run \`prisma-next migration
|
|
286
|
+
`\nEdit migration.ts, then run \`prisma-next migration emit --dir "${value.dir}"\` to attest.`,
|
|
252
287
|
);
|
|
253
288
|
}
|
|
254
289
|
});
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
-
import
|
|
4
|
-
import { attestMigration } from '@prisma-next/migration-tools/attestation';
|
|
3
|
+
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
5
4
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
6
5
|
import { findLatestMigration } from '@prisma-next/migration-tools/dag';
|
|
7
6
|
import {
|
|
7
|
+
copyContractToMigrationDir,
|
|
8
8
|
formatMigrationDirName,
|
|
9
|
-
writeMigrationOps,
|
|
10
9
|
writeMigrationPackage,
|
|
11
10
|
} from '@prisma-next/migration-tools/io';
|
|
12
|
-
import {
|
|
13
|
-
evaluateMigrationTs,
|
|
14
|
-
scaffoldMigrationTs,
|
|
15
|
-
} from '@prisma-next/migration-tools/migration-ts';
|
|
11
|
+
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
16
12
|
import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
17
13
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
18
14
|
import { Command } from 'commander';
|
|
19
15
|
import { join, relative } from 'pathe';
|
|
20
16
|
import { loadConfig } from '../config-loader';
|
|
21
17
|
import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
|
|
18
|
+
import { emitMigration } from '../lib/migration-emit';
|
|
19
|
+
import { migrationStrategy } from '../lib/migration-strategy';
|
|
22
20
|
import {
|
|
23
21
|
type CliErrorConflict,
|
|
24
22
|
CliStructuredError,
|
|
@@ -178,7 +176,7 @@ async function executeMigrationPlanCommand(
|
|
|
178
176
|
return notOk(
|
|
179
177
|
errorRuntime('A draft migration to this contract already exists', {
|
|
180
178
|
why: `Draft migration at "${existingDraft.dirName}" already targets ${toStorageHash}`,
|
|
181
|
-
fix: `Run 'prisma-next migration
|
|
179
|
+
fix: `Run 'prisma-next migration emit --dir ${migrationsRelative}/${existingDraft.dirName}' to attest it, or delete it and re-plan.`,
|
|
182
180
|
}),
|
|
183
181
|
);
|
|
184
182
|
}
|
|
@@ -247,138 +245,148 @@ async function executeMigrationPlanCommand(
|
|
|
247
245
|
[config.target, config.adapter, ...(config.extensionPacks ?? [])],
|
|
248
246
|
);
|
|
249
247
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
248
|
+
const strategy = migrationStrategy(migrations, config.target.targetId);
|
|
249
|
+
|
|
250
|
+
// Build manifest and write migration package
|
|
251
|
+
const timestamp = new Date();
|
|
252
|
+
const slug = options.name ?? 'migration';
|
|
253
|
+
const dirName = formatMigrationDirName(timestamp, slug);
|
|
254
|
+
const packageDir = join(migrationsDir, dirName);
|
|
255
|
+
|
|
256
|
+
const manifest: MigrationManifest = {
|
|
257
|
+
from: fromHash,
|
|
258
|
+
to: toStorageHash,
|
|
259
|
+
migrationId: null,
|
|
260
|
+
kind: 'regular',
|
|
261
|
+
fromContract,
|
|
262
|
+
toContract: toContractJson,
|
|
263
|
+
hints: {
|
|
264
|
+
used: [],
|
|
265
|
+
applied: [],
|
|
266
|
+
plannerVersion: '2.0.0',
|
|
267
|
+
planningStrategy: strategy === 'descriptor' ? 'descriptors' : 'class-based',
|
|
268
|
+
},
|
|
269
|
+
labels: [],
|
|
270
|
+
createdAt: timestamp.toISOString(),
|
|
271
|
+
};
|
|
257
272
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
273
|
+
const scaffoldContext = {
|
|
274
|
+
packageDir,
|
|
275
|
+
contractJsonPath: contractPathAbsolute,
|
|
276
|
+
fromHash,
|
|
277
|
+
toHash: toStorageHash,
|
|
278
|
+
};
|
|
265
279
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
try {
|
|
281
|
+
let migrationTsContent: string;
|
|
282
|
+
|
|
283
|
+
if (strategy === 'descriptor') {
|
|
284
|
+
if (!migrations.planWithDescriptors || !migrations.renderDescriptorTypeScript) {
|
|
285
|
+
throw errorTargetMigrationNotSupported({
|
|
286
|
+
why: `Target "${config.target.targetId}" advertises descriptor flow but is missing required hooks`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const descriptorResult = migrations.planWithDescriptors({
|
|
290
|
+
fromContract,
|
|
291
|
+
toContract: toContractJson,
|
|
292
|
+
frameworkComponents,
|
|
293
|
+
});
|
|
294
|
+
if (!descriptorResult.ok) {
|
|
295
|
+
return notOk(
|
|
296
|
+
errorMigrationPlanningFailed({
|
|
297
|
+
conflicts: descriptorResult.conflicts as readonly CliErrorConflict[],
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (descriptorResult.descriptors.length === 0) {
|
|
302
|
+
return notOk(
|
|
303
|
+
errorMigrationPlanningFailed({
|
|
304
|
+
conflicts: [
|
|
305
|
+
{
|
|
306
|
+
kind: 'unsupportedChange',
|
|
307
|
+
summary:
|
|
308
|
+
'Contract changed but planner produced no operations. ' +
|
|
309
|
+
'This indicates unsupported or ignored changes.',
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
migrationTsContent = migrations.renderDescriptorTypeScript(
|
|
316
|
+
descriptorResult.descriptors,
|
|
317
|
+
scaffoldContext,
|
|
278
318
|
);
|
|
319
|
+
} else {
|
|
320
|
+
const stack = createControlStack(config);
|
|
321
|
+
const familyInstance = config.family.create(stack);
|
|
322
|
+
const planner = migrations.createPlanner(familyInstance);
|
|
323
|
+
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
324
|
+
const plannerResult = planner.plan({
|
|
325
|
+
contract: toContractJson,
|
|
326
|
+
schema: fromSchema,
|
|
327
|
+
policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
|
|
328
|
+
fromHash,
|
|
329
|
+
frameworkComponents,
|
|
330
|
+
});
|
|
331
|
+
if (plannerResult.kind === 'failure') {
|
|
332
|
+
return notOk(
|
|
333
|
+
errorMigrationPlanningFailed({
|
|
334
|
+
conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (plannerResult.plan.operations.length === 0) {
|
|
339
|
+
return notOk(
|
|
340
|
+
errorMigrationPlanningFailed({
|
|
341
|
+
conflicts: [
|
|
342
|
+
{
|
|
343
|
+
kind: 'unsupportedChange',
|
|
344
|
+
summary:
|
|
345
|
+
'Contract changed but planner produced no operations. ' +
|
|
346
|
+
'This indicates unsupported or ignored changes.',
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
migrationTsContent = plannerResult.plan.renderTypeScript();
|
|
279
353
|
}
|
|
280
354
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
355
|
+
await writeMigrationPackage(packageDir, manifest, []);
|
|
356
|
+
await copyContractToMigrationDir(packageDir, contractPathAbsolute);
|
|
357
|
+
await writeMigrationTs(packageDir, migrationTsContent);
|
|
358
|
+
|
|
359
|
+
// Always run emit inline. If migration.ts contains unfilled
|
|
360
|
+
// placeholders (e.g. user must hand-author a dataTransform body),
|
|
361
|
+
// emitMigration throws errorUnfilledPlaceholder (PN-MIG-2001) and
|
|
362
|
+
// we propagate that structured error to the user.
|
|
363
|
+
const { operations, migrationId } = await emitMigration(packageDir, {
|
|
364
|
+
targetId: config.target.targetId,
|
|
365
|
+
migrations,
|
|
366
|
+
frameworkComponents,
|
|
367
|
+
});
|
|
286
368
|
|
|
287
|
-
const
|
|
369
|
+
const sql = extractSqlDdl(operations);
|
|
370
|
+
const result: MigrationPlanResult = {
|
|
371
|
+
ok: true,
|
|
372
|
+
noOp: false,
|
|
288
373
|
from: fromHash,
|
|
289
374
|
to: toStorageHash,
|
|
290
|
-
migrationId
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
},
|
|
300
|
-
labels: [],
|
|
301
|
-
createdAt: timestamp.toISOString(),
|
|
375
|
+
migrationId,
|
|
376
|
+
dir: relative(process.cwd(), packageDir),
|
|
377
|
+
operations: operations.map((op) => ({
|
|
378
|
+
id: op.id,
|
|
379
|
+
label: op.label,
|
|
380
|
+
operationClass: op.operationClass,
|
|
381
|
+
})),
|
|
382
|
+
sql,
|
|
383
|
+
summary: `Planned ${operations.length} operation(s)`,
|
|
384
|
+
timings: { total: Date.now() - startTime },
|
|
302
385
|
};
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// Write package with empty ops first (draft)
|
|
307
|
-
await writeMigrationPackage(packageDir, manifest, []);
|
|
308
|
-
await scaffoldMigrationTs(packageDir, {
|
|
309
|
-
descriptors: descriptorResult.descriptors,
|
|
310
|
-
contractJsonPath: contractPathAbsolute,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
if (descriptorResult.needsDataMigration) {
|
|
314
|
-
// Draft — user must fill in dataTransform and run verify
|
|
315
|
-
const result: MigrationPlanResult = {
|
|
316
|
-
ok: true,
|
|
317
|
-
noOp: false,
|
|
318
|
-
from: fromHash,
|
|
319
|
-
to: toStorageHash,
|
|
320
|
-
dir: relative(process.cwd(), packageDir),
|
|
321
|
-
operations: descriptorResult.descriptors.map((d) => ({
|
|
322
|
-
id: (d as { kind: string }).kind,
|
|
323
|
-
label: (d as { kind: string }).kind,
|
|
324
|
-
operationClass: 'data' as const,
|
|
325
|
-
})),
|
|
326
|
-
sql: [],
|
|
327
|
-
summary: `Planned ${descriptorResult.descriptors.length} operation(s) — data migration required. Edit migration.ts and run \`migration verify --dir ${relative(process.cwd(), packageDir)}\` to attest.`,
|
|
328
|
-
timings: { total: Date.now() - startTime },
|
|
329
|
-
};
|
|
330
|
-
return ok(result);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// No data migration — evaluate, resolve, write ops, attest
|
|
334
|
-
const evaluatedDescriptors = await evaluateMigrationTs(packageDir);
|
|
335
|
-
|
|
336
|
-
if (!migrations.resolveDescriptors) {
|
|
337
|
-
throw errorTargetMigrationNotSupported({
|
|
338
|
-
why: `Target "${config.target.targetId}" does not implement resolveDescriptors; cannot finalize migration plan with migration.ts`,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const resolvedOps = migrations.resolveDescriptors(
|
|
343
|
-
evaluatedDescriptors as OperationDescriptor[],
|
|
344
|
-
{
|
|
345
|
-
fromContract,
|
|
346
|
-
toContract: toContractJson,
|
|
347
|
-
frameworkComponents,
|
|
348
|
-
},
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
await writeMigrationOps(packageDir, resolvedOps);
|
|
352
|
-
const migrationId = await attestMigration(packageDir);
|
|
353
|
-
|
|
354
|
-
const sql = extractSqlDdl(resolvedOps);
|
|
355
|
-
const result: MigrationPlanResult = {
|
|
356
|
-
ok: true,
|
|
357
|
-
noOp: false,
|
|
358
|
-
from: fromHash,
|
|
359
|
-
to: toStorageHash,
|
|
360
|
-
migrationId,
|
|
361
|
-
dir: relative(process.cwd(), packageDir),
|
|
362
|
-
operations: resolvedOps.map((op) => ({
|
|
363
|
-
id: op.id,
|
|
364
|
-
label: op.label,
|
|
365
|
-
operationClass: op.operationClass,
|
|
366
|
-
})),
|
|
367
|
-
sql,
|
|
368
|
-
summary: `Planned ${resolvedOps.length} operation(s)`,
|
|
369
|
-
timings: { total: Date.now() - startTime },
|
|
370
|
-
};
|
|
371
|
-
return ok(result);
|
|
372
|
-
} catch (error) {
|
|
373
|
-
return notOk(mapMigrationToolsError(error));
|
|
374
|
-
}
|
|
386
|
+
return ok(result);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return notOk(mapMigrationToolsError(error));
|
|
375
389
|
}
|
|
376
|
-
|
|
377
|
-
return notOk(
|
|
378
|
-
errorTargetMigrationNotSupported({
|
|
379
|
-
why: `Target "${config.target.id}" does not support planWithDescriptors`,
|
|
380
|
-
}),
|
|
381
|
-
);
|
|
382
390
|
}
|
|
383
391
|
|
|
384
392
|
export function createMigrationPlanCommand(): Command {
|
|
@@ -137,7 +137,7 @@ async function executeMigrationShowCommand(
|
|
|
137
137
|
return notOk(
|
|
138
138
|
errorRuntime('No attested migrations found', {
|
|
139
139
|
why: `All migrations in ${migrationsRelative} are drafts (migrationId: null)`,
|
|
140
|
-
fix: 'Run `prisma-next migration
|
|
140
|
+
fix: 'Run `prisma-next migration emit --dir <path>` to attest a draft migration.',
|
|
141
141
|
}),
|
|
142
142
|
);
|
|
143
143
|
}
|
|
@@ -460,7 +460,7 @@ async function executeMigrationStatusCommand(
|
|
|
460
460
|
severity: 'warn',
|
|
461
461
|
message: `${drafts.length} draft migration(s) found: ${drafts.map((d) => d.dirName).join(', ')}`,
|
|
462
462
|
hints: [
|
|
463
|
-
"Run 'prisma-next migration
|
|
463
|
+
"Run 'prisma-next migration emit --dir <path>' to attest draft migrations before applying",
|
|
464
464
|
],
|
|
465
465
|
});
|
|
466
466
|
}
|
|
@@ -83,6 +83,9 @@ export async function executeDbInit<TFamilyId extends string, TTargetId extends
|
|
|
83
83
|
contract,
|
|
84
84
|
schema: schemaIR,
|
|
85
85
|
policy,
|
|
86
|
+
// `db init` does not produce a `migration.ts`, so the from-hash on the
|
|
87
|
+
// resulting plan is never surfaced to authoring — pass empty string.
|
|
88
|
+
fromHash: '',
|
|
86
89
|
frameworkComponents,
|
|
87
90
|
});
|
|
88
91
|
|
|
@@ -83,6 +83,9 @@ export async function executeDbUpdate<TFamilyId extends string, TTargetId extend
|
|
|
83
83
|
contract,
|
|
84
84
|
schema: schemaIR,
|
|
85
85
|
policy,
|
|
86
|
+
// `db update` does not produce a `migration.ts`, so the from-hash on the
|
|
87
|
+
// resulting plan is never surfaced to authoring — pass empty string.
|
|
88
|
+
fromHash: '',
|
|
86
89
|
frameworkComponents,
|
|
87
90
|
});
|
|
88
91
|
if (plannerResult.kind === 'failure') {
|