@prisma-next/cli 0.4.0-dev.6 → 0.4.0-dev.8
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 +2 -2
- package/dist/cli.mjs +12 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-DUs1DH-1.mjs → client-CJxHfhze.mjs} +3 -1
- package/dist/client-CJxHfhze.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +6 -1
- package/dist/commands/contract-infer.mjs +7 -1
- package/dist/commands/db-init.mjs +3 -2
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +4 -1
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +2 -1
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.mjs +3 -2
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +2 -1
- package/dist/commands/db-verify.mjs.map +1 -1
- package/dist/commands/migration-apply.mjs +2 -1
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-emit.mjs +2 -2
- package/dist/commands/migration-emit.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +31 -8
- 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 +91 -60
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-status.mjs +6 -1
- package/dist/{contract-emit-C2_J2U7A.mjs → contract-emit-CU-SYNe4.mjs} +2 -0
- package/dist/{contract-emit-7jqH6dq9.mjs → contract-emit-gpJNLGs7.mjs} +2 -2
- package/dist/{contract-emit-7jqH6dq9.mjs.map → contract-emit-gpJNLGs7.mjs.map} +1 -1
- package/dist/{contract-infer-BFlIbyl9.mjs → contract-infer-BDJgg7Xb.mjs} +2 -2
- package/dist/{contract-infer-BFlIbyl9.mjs.map → contract-infer-BDJgg7Xb.mjs.map} +1 -1
- package/dist/exports/control-api.mjs +3 -1
- package/dist/exports/index.mjs +6 -1
- package/dist/exports/index.mjs.map +1 -1
- package/dist/{init-DlFLMBaU.mjs → init-DZWvhEP0.mjs} +2 -2
- package/dist/{init-DlFLMBaU.mjs.map → init-DZWvhEP0.mjs.map} +1 -1
- package/dist/{inspect-live-schema-gjmUZ8xm.mjs → inspect-live-schema-ChqrALmw.mjs} +2 -2
- package/dist/{inspect-live-schema-gjmUZ8xm.mjs.map → inspect-live-schema-ChqrALmw.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-CfllKppa.mjs → migration-command-scaffold-B0oH_hyB.mjs} +2 -2
- package/dist/{migration-command-scaffold-CfllKppa.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-BwKCQB_a.mjs → migration-status-CPamfEPj.mjs} +2 -2
- package/dist/{migration-status-BwKCQB_a.mjs.map → migration-status-CPamfEPj.mjs.map} +1 -1
- package/package.json +16 -16
- package/src/commands/migration-emit.ts +3 -1
- package/src/commands/migration-new.ts +48 -13
- package/src/commands/migration-plan.ts +140 -90
- 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 +36 -24
- package/src/lib/migration-strategy.ts +49 -0
- package/src/utils/cli-errors.ts +2 -0
- package/dist/client-DUs1DH-1.mjs.map +0 -1
- package/dist/migration-emit-dRXV6QSz.mjs +0 -72
- package/dist/migration-emit-dRXV6QSz.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/cli",
|
|
3
|
-
"version": "0.4.0-dev.
|
|
3
|
+
"version": "0.4.0-dev.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
@@ -24,28 +24,28 @@
|
|
|
24
24
|
"string-width": "^8.2.0",
|
|
25
25
|
"strip-ansi": "^7.1.2",
|
|
26
26
|
"wrap-ansi": "^10.0.0",
|
|
27
|
-
"@prisma-next/config": "0.4.0-dev.
|
|
28
|
-
"@prisma-next/contract": "0.4.0-dev.
|
|
29
|
-
"@prisma-next/
|
|
30
|
-
"@prisma-next/framework-components": "0.4.0-dev.
|
|
31
|
-
"@prisma-next/
|
|
32
|
-
"@prisma-next/migration-tools": "0.4.0-dev.
|
|
33
|
-
"@prisma-next/
|
|
34
|
-
"@prisma-next/utils": "0.4.0-dev.
|
|
27
|
+
"@prisma-next/config": "0.4.0-dev.8",
|
|
28
|
+
"@prisma-next/contract": "0.4.0-dev.8",
|
|
29
|
+
"@prisma-next/errors": "0.4.0-dev.8",
|
|
30
|
+
"@prisma-next/framework-components": "0.4.0-dev.8",
|
|
31
|
+
"@prisma-next/emitter": "0.4.0-dev.8",
|
|
32
|
+
"@prisma-next/migration-tools": "0.4.0-dev.8",
|
|
33
|
+
"@prisma-next/psl-printer": "0.4.0-dev.8",
|
|
34
|
+
"@prisma-next/utils": "0.4.0-dev.8"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "24.10.4",
|
|
38
38
|
"tsdown": "0.18.4",
|
|
39
39
|
"typescript": "5.9.3",
|
|
40
40
|
"vitest": "4.0.17",
|
|
41
|
-
"@prisma-next/sql-contract": "0.4.0-dev.
|
|
42
|
-
"@prisma-next/sql-contract-emitter": "0.4.0-dev.
|
|
43
|
-
"@prisma-next/sql-contract-ts": "0.4.0-dev.
|
|
44
|
-
"@prisma-next/sql-operations": "0.4.0-dev.
|
|
45
|
-
"@prisma-next/
|
|
41
|
+
"@prisma-next/sql-contract": "0.4.0-dev.8",
|
|
42
|
+
"@prisma-next/sql-contract-emitter": "0.4.0-dev.8",
|
|
43
|
+
"@prisma-next/sql-contract-ts": "0.4.0-dev.8",
|
|
44
|
+
"@prisma-next/sql-operations": "0.4.0-dev.8",
|
|
45
|
+
"@prisma-next/sql-runtime": "0.4.0-dev.8",
|
|
46
|
+
"@prisma-next/test-utils": "0.0.1",
|
|
46
47
|
"@prisma-next/tsconfig": "0.0.0",
|
|
47
|
-
"@prisma-next/
|
|
48
|
-
"@prisma-next/test-utils": "0.0.1"
|
|
48
|
+
"@prisma-next/tsdown": "0.0.0"
|
|
49
49
|
},
|
|
50
50
|
"exports": {
|
|
51
51
|
".": {
|
|
@@ -105,7 +105,9 @@ export function createMigrationEmitCommand(): Command {
|
|
|
105
105
|
command,
|
|
106
106
|
'Emit ops.json from migration.ts and compute migrationId',
|
|
107
107
|
'Evaluates migration.ts in the package directory, resolves it to ops.json,\n' +
|
|
108
|
-
'then computes and persists the content-addressed migrationId in
|
|
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.',
|
|
109
111
|
);
|
|
110
112
|
setCommandExamples(command, ['prisma-next migration emit --dir migrations/20250101-add-users']);
|
|
111
113
|
addGlobalOptions(command)
|
|
@@ -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)}`,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
+
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
3
4
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
4
5
|
import { findLatestMigration } from '@prisma-next/migration-tools/dag';
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import {
|
|
7
|
+
copyContractToMigrationDir,
|
|
8
|
+
formatMigrationDirName,
|
|
9
|
+
writeMigrationPackage,
|
|
10
|
+
} from '@prisma-next/migration-tools/io';
|
|
11
|
+
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
7
12
|
import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
8
13
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
9
14
|
import { Command } from 'commander';
|
|
@@ -11,6 +16,7 @@ import { join, relative } from 'pathe';
|
|
|
11
16
|
import { loadConfig } from '../config-loader';
|
|
12
17
|
import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
|
|
13
18
|
import { emitMigration } from '../lib/migration-emit';
|
|
19
|
+
import { migrationStrategy } from '../lib/migration-strategy';
|
|
14
20
|
import {
|
|
15
21
|
type CliErrorConflict,
|
|
16
22
|
CliStructuredError,
|
|
@@ -239,104 +245,148 @@ async function executeMigrationPlanCommand(
|
|
|
239
245
|
[config.target, config.adapter, ...(config.extensionPacks ?? [])],
|
|
240
246
|
);
|
|
241
247
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
};
|
|
249
272
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
273
|
+
const scaffoldContext = {
|
|
274
|
+
packageDir,
|
|
275
|
+
contractJsonPath: contractPathAbsolute,
|
|
276
|
+
fromHash,
|
|
277
|
+
toHash: toStorageHash,
|
|
278
|
+
};
|
|
257
279
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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,
|
|
270
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();
|
|
271
353
|
}
|
|
272
354
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
});
|
|
278
368
|
|
|
279
|
-
const
|
|
369
|
+
const sql = extractSqlDdl(operations);
|
|
370
|
+
const result: MigrationPlanResult = {
|
|
371
|
+
ok: true,
|
|
372
|
+
noOp: false,
|
|
280
373
|
from: fromHash,
|
|
281
374
|
to: toStorageHash,
|
|
282
|
-
migrationId
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
},
|
|
292
|
-
labels: [],
|
|
293
|
-
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 },
|
|
294
385
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
await scaffoldMigrationTs(packageDir, {
|
|
299
|
-
descriptors: descriptorResult.descriptors,
|
|
300
|
-
contractJsonPath: contractPathAbsolute,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Always run emit inline; structured errors thrown during evaluation
|
|
304
|
-
// (e.g. invalid migration.ts shape, missing file) propagate through
|
|
305
|
-
// emitMigration to the CLI envelope.
|
|
306
|
-
const { operations, migrationId } = await emitMigration(packageDir, {
|
|
307
|
-
targetId: config.target.targetId,
|
|
308
|
-
migrations,
|
|
309
|
-
frameworkComponents,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const sql = extractSqlDdl(operations);
|
|
313
|
-
const result: MigrationPlanResult = {
|
|
314
|
-
ok: true,
|
|
315
|
-
noOp: false,
|
|
316
|
-
from: fromHash,
|
|
317
|
-
to: toStorageHash,
|
|
318
|
-
migrationId,
|
|
319
|
-
dir: relative(process.cwd(), packageDir),
|
|
320
|
-
operations: operations.map((op) => ({
|
|
321
|
-
id: op.id,
|
|
322
|
-
label: op.label,
|
|
323
|
-
operationClass: op.operationClass,
|
|
324
|
-
})),
|
|
325
|
-
sql,
|
|
326
|
-
summary: `Planned ${operations.length} operation(s)`,
|
|
327
|
-
timings: { total: Date.now() - startTime },
|
|
328
|
-
};
|
|
329
|
-
return ok(result);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
return notOk(mapMigrationToolsError(error));
|
|
332
|
-
}
|
|
386
|
+
return ok(result);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return notOk(mapMigrationToolsError(error));
|
|
333
389
|
}
|
|
334
|
-
|
|
335
|
-
return notOk(
|
|
336
|
-
errorTargetMigrationNotSupported({
|
|
337
|
-
why: `Target "${config.target.id}" does not support planWithDescriptors`,
|
|
338
|
-
}),
|
|
339
|
-
);
|
|
340
390
|
}
|
|
341
391
|
|
|
342
392
|
export function createMigrationPlanCommand(): Command {
|
|
@@ -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') {
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared helper for emitting `ops.json`
|
|
3
|
-
* `migration.ts`.
|
|
2
|
+
* Shared helper for emitting `ops.json` and attesting `migration.json` for a
|
|
3
|
+
* migration package's `migration.ts`.
|
|
4
4
|
*
|
|
5
5
|
* Two flows are dispatched here:
|
|
6
|
-
* - Descriptor flow (Postgres): the framework evaluates `migration.ts`
|
|
7
|
-
* re-exports the planner's descriptor list), calls the target's
|
|
8
|
-
* `resolveDescriptors` to produce display-oriented operations,
|
|
9
|
-
* `ops.json
|
|
10
|
-
* - Class flow (Mongo): the target
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* - Descriptor flow (Postgres): the framework evaluates `migration.ts`
|
|
7
|
+
* (which re-exports the planner's descriptor list), calls the target's
|
|
8
|
+
* `resolveDescriptors` to produce display-oriented operations, writes
|
|
9
|
+
* `ops.json`, and attests `migration.json`.
|
|
10
|
+
* - Class flow (Mongo): the target's `emit` capability dynamic-imports
|
|
11
|
+
* `migration.ts`, instantiates the default-exported `Migration` subclass
|
|
12
|
+
* (or invokes the default-exported factory function), reads `operations`,
|
|
13
|
+
* and writes `ops.json`. This helper then attests `migration.json` once
|
|
14
|
+
* the capability returns.
|
|
15
|
+
*
|
|
16
|
+
* In both cases attestation is owned by this helper so the on-disk artifacts
|
|
17
|
+
* are guaranteed to be fully attested when emit returns.
|
|
18
|
+
*
|
|
19
|
+
* Note that this helper is the CLI-driven emit path. Class-flow `migration.ts`
|
|
20
|
+
* files are also self-emitting via `Migration.run(...)` when run directly;
|
|
21
|
+
* that path attests inside `Migration.run` and produces byte-identical
|
|
22
|
+
* artifacts. This helper exists primarily to bridge descriptor-flow targets
|
|
23
|
+
* and to give `migration plan` a single in-process emit dispatch.
|
|
16
24
|
*
|
|
17
25
|
* Used by `migration emit` (always) and `migration plan` (always, after
|
|
18
26
|
* scaffolding `migration.ts`). Both flows run in-process so that structured
|
|
19
|
-
* errors thrown during evaluation
|
|
20
|
-
*
|
|
27
|
+
* errors thrown during evaluation (notably `errorUnfilledPlaceholder` with
|
|
28
|
+
* code `PN-MIG-2001`) propagate as real exceptions and the CLI's error
|
|
29
|
+
* envelope renders them with full structured metadata.
|
|
21
30
|
*/
|
|
22
31
|
|
|
23
32
|
import assert from 'node:assert/strict';
|
|
@@ -31,6 +40,7 @@ import { attestMigration } from '@prisma-next/migration-tools/attestation';
|
|
|
31
40
|
import { readMigrationPackage, writeMigrationOps } from '@prisma-next/migration-tools/io';
|
|
32
41
|
import { evaluateMigrationTs, hasMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
33
42
|
import { errorMigrationFileMissing, errorTargetMigrationNotSupported } from '../utils/cli-errors';
|
|
43
|
+
import { migrationStrategy } from './migration-strategy';
|
|
34
44
|
|
|
35
45
|
/**
|
|
36
46
|
* Context passed to `emitMigration`. Captures everything the helper needs to
|
|
@@ -45,7 +55,7 @@ export interface EmitMigrationContext {
|
|
|
45
55
|
/**
|
|
46
56
|
* Result of a successful emit: the operations that were written to `ops.json`
|
|
47
57
|
* (display-oriented shape) and the content-addressed migrationId persisted to
|
|
48
|
-
* `
|
|
58
|
+
* `migration.json`.
|
|
49
59
|
*/
|
|
50
60
|
export interface EmitMigrationResult {
|
|
51
61
|
readonly operations: readonly MigrationPlanOperation[];
|
|
@@ -68,22 +78,24 @@ export async function emitMigration(
|
|
|
68
78
|
throw errorMigrationFileMissing(dir);
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
|
|
81
|
+
const strategy = migrationStrategy(ctx.migrations, ctx.targetId);
|
|
82
|
+
|
|
83
|
+
if (strategy === 'descriptor') {
|
|
72
84
|
return emitDescriptorFlow(dir, ctx.migrations, ctx);
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
if (ctx.migrations.emit) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
frameworkComponents: ctx.frameworkComponents,
|
|
87
|
+
if (!ctx.migrations.emit) {
|
|
88
|
+
throw errorTargetMigrationNotSupported({
|
|
89
|
+
why: `Target "${ctx.targetId}" does not implement the class-flow \`emit\` capability; cannot emit a migration package`,
|
|
79
90
|
});
|
|
80
|
-
const migrationId = await attestMigration(dir);
|
|
81
|
-
return { operations, migrationId };
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
const operations = await ctx.migrations.emit({
|
|
94
|
+
dir,
|
|
95
|
+
frameworkComponents: ctx.frameworkComponents,
|
|
86
96
|
});
|
|
97
|
+
const migrationId = await attestMigration(dir);
|
|
98
|
+
return { operations, migrationId };
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration authoring strategy selector.
|
|
3
|
+
*
|
|
4
|
+
* Targets currently use one of two strategies to author `migration.ts`:
|
|
5
|
+
*
|
|
6
|
+
* - **Descriptor flow** — the planner produces an `OperationDescriptor[]`
|
|
7
|
+
* and `migration.ts` is a `export default () => [...]` file that the CLI
|
|
8
|
+
* later replays through `resolveDescriptors` at emit time. Postgres uses
|
|
9
|
+
* this today.
|
|
10
|
+
* - **Class flow** — the planner produces a `MigrationPlanWithAuthoringSurface`
|
|
11
|
+
* that renders itself as a `class M extends Migration { ... }` file. The
|
|
12
|
+
* CLI dispatches to the target's `emit` capability at emit time. Mongo
|
|
13
|
+
* uses this today.
|
|
14
|
+
*
|
|
15
|
+
* The two are mutually exclusive at the target level: a migrations capability
|
|
16
|
+
* either implements the descriptor-flow trio (`planWithDescriptors`,
|
|
17
|
+
* `resolveDescriptors`, `renderDescriptorTypeScript`) or the class-flow
|
|
18
|
+
* `emit` hook. `migrationStrategy` discriminates between them by observing
|
|
19
|
+
* which hooks are present, and is consumed by `migration new`, `migration
|
|
20
|
+
* plan`, and `migration emit` to keep strategy-specific branching in one
|
|
21
|
+
* place.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { errorTargetHasIncompleteMigrationCapabilities } from '@prisma-next/errors/migration';
|
|
25
|
+
import type { TargetMigrationsCapability } from '@prisma-next/framework-components/control';
|
|
26
|
+
|
|
27
|
+
export type MigrationStrategy = 'descriptor' | 'class-based';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determine which authoring strategy a target uses, based on the shape of
|
|
31
|
+
* its `TargetMigrationsCapability`. Callers that need strategy-specific
|
|
32
|
+
* guarantees (e.g. that `resolveDescriptors` is present) should narrow on
|
|
33
|
+
* the returned tag and trust the capability fields directly rather than
|
|
34
|
+
* re-probing.
|
|
35
|
+
*
|
|
36
|
+
* Throws `errorTargetHasIncompleteMigrationCapabilities` (PN-MIG-2011) when
|
|
37
|
+
* the capability registers neither flow. We diagnose this here rather than
|
|
38
|
+
* deferring to the dispatch site so a misconfigured target gets an honest
|
|
39
|
+
* "incomplete capability" error instead of being silently routed to one
|
|
40
|
+
* flow and reported as missing the *other* flow's hook.
|
|
41
|
+
*/
|
|
42
|
+
export function migrationStrategy(
|
|
43
|
+
migrations: TargetMigrationsCapability,
|
|
44
|
+
targetId: string,
|
|
45
|
+
): MigrationStrategy {
|
|
46
|
+
if (migrations.resolveDescriptors) return 'descriptor';
|
|
47
|
+
if (migrations.emit) return 'class-based';
|
|
48
|
+
throw errorTargetHasIncompleteMigrationCapabilities({ targetId });
|
|
49
|
+
}
|