@prisma-next/cli 0.4.0-dev.9 → 0.5.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -18
- package/dist/cli-errors-C0JhVj0c.d.mts +4 -0
- package/dist/cli-errors-DHq6GQGu.mjs +5 -0
- package/dist/cli.mjs +7 -18
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-CJxHfhze.mjs → client-TG7rbCWT.mjs} +7 -6
- package/dist/{client-CJxHfhze.mjs.map → client-TG7rbCWT.mjs.map} +1 -1
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -7
- package/dist/commands/contract-infer.mjs +2 -8
- package/dist/commands/db-init.mjs +6 -7
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +4 -7
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +5 -6
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.mjs +6 -7
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +6 -7
- package/dist/commands/db-verify.mjs.map +1 -1
- package/dist/commands/migration-apply.d.mts +1 -1
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +33 -25
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +47 -22
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +6 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +92 -69
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +4 -4
- package/dist/commands/migration-show.d.mts +2 -2
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +9 -14
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +4 -5
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -7
- package/dist/config-loader-_W4T21X1.mjs +90 -0
- package/dist/config-loader-_W4T21X1.mjs.map +1 -0
- package/dist/config-loader.d.mts.map +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/{contract-emit-gpJNLGs7.mjs → contract-emit-CNYyzJwF.mjs} +18 -14
- package/dist/contract-emit-CNYyzJwF.mjs.map +1 -0
- package/dist/{contract-emit-CKig_Lra.mjs → contract-emit-CQfj7xJn.mjs} +24 -20
- package/dist/contract-emit-CQfj7xJn.mjs.map +1 -0
- package/dist/contract-emit-fhNwwhkQ.mjs +4 -0
- package/dist/{contract-infer-BDJgg7Xb.mjs → contract-infer-BP3DrGgz.mjs} +3 -3
- package/dist/{contract-infer-BDJgg7Xb.mjs.map → contract-infer-BP3DrGgz.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +2 -2
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -5
- package/dist/exports/index.mjs +2 -7
- package/dist/exports/index.mjs.map +1 -1
- package/dist/{framework-components-Bsr1GaIj.mjs → framework-components-DfZKQBQ2.mjs} +2 -2
- package/dist/{framework-components-Bsr1GaIj.mjs.map → framework-components-DfZKQBQ2.mjs.map} +1 -1
- package/dist/{init-DZWvhEP0.mjs → init-CQfo_4Ro.mjs} +2 -2
- package/dist/{init-DZWvhEP0.mjs.map → init-CQfo_4Ro.mjs.map} +1 -1
- package/dist/{inspect-live-schema-ChqrALmw.mjs → inspect-live-schema-DWzf4Q_m.mjs} +5 -5
- package/dist/{inspect-live-schema-ChqrALmw.mjs.map → inspect-live-schema-DWzf4Q_m.mjs.map} +1 -1
- package/dist/migration-cli.d.mts +50 -0
- package/dist/migration-cli.d.mts.map +1 -0
- package/dist/migration-cli.mjs +184 -0
- package/dist/migration-cli.mjs.map +1 -0
- package/dist/{migration-command-scaffold-B0oH_hyB.mjs → migration-command-scaffold-CLMD302g.mjs} +6 -6
- package/dist/{migration-command-scaffold-B0oH_hyB.mjs.map → migration-command-scaffold-CLMD302g.mjs.map} +1 -1
- package/dist/{migration-status-CPamfEPj.mjs → migration-status-B0HLF7So.mjs} +18 -34
- package/dist/migration-status-B0HLF7So.mjs.map +1 -0
- package/dist/{migrations-BIsjFjSV.mjs → migrations-B0dOQlk0.mjs} +4 -15
- package/dist/migrations-B0dOQlk0.mjs.map +1 -0
- package/dist/{result-handler-AFK4hxyX.mjs → result-handler-CIyu0Pdt.mjs} +22 -11
- package/dist/result-handler-CIyu0Pdt.mjs.map +1 -0
- package/dist/{validate-contract-deps-DBH6iTAD.mjs → validate-contract-deps-esa-VQ0h.mjs} +1 -1
- package/dist/{validate-contract-deps-DBH6iTAD.mjs.map → validate-contract-deps-esa-VQ0h.mjs.map} +1 -1
- package/dist/{verify-C56CuQc7.mjs → verify-BxiVp50b.mjs} +2 -2
- package/dist/{verify-C56CuQc7.mjs.map → verify-BxiVp50b.mjs.map} +1 -1
- package/package.json +19 -19
- package/src/cli.ts +1 -5
- package/src/commands/contract-emit.ts +9 -10
- package/src/commands/migration-apply.ts +34 -23
- package/src/commands/migration-new.ts +39 -17
- package/src/commands/migration-plan.ts +119 -104
- package/src/commands/migration-show.ts +6 -16
- package/src/commands/migration-status.ts +14 -34
- package/src/config-loader.ts +35 -29
- package/src/config-path-validation.ts +75 -0
- package/src/control-api/client.ts +2 -1
- package/src/control-api/operations/contract-emit.ts +24 -23
- package/src/control-api/types.ts +1 -1
- package/src/migration-cli.ts +254 -0
- package/src/utils/cli-errors.ts +1 -0
- package/src/utils/command-helpers.ts +15 -19
- package/src/utils/formatters/graph-migration-mapper.ts +5 -14
- package/src/utils/formatters/help.ts +0 -1
- package/src/utils/formatters/migrations.ts +2 -29
- package/dist/cli-errors-BUuJr6py.mjs +0 -5
- package/dist/cli-errors-Dic2eADK.d.mts +0 -4
- package/dist/commands/migration-emit.d.mts +0 -38
- package/dist/commands/migration-emit.d.mts.map +0 -1
- package/dist/commands/migration-emit.mjs +0 -81
- package/dist/commands/migration-emit.mjs.map +0 -1
- package/dist/config-loader-C4VXKl8f.mjs +0 -43
- package/dist/config-loader-C4VXKl8f.mjs.map +0 -1
- package/dist/contract-emit-CKig_Lra.mjs.map +0 -1
- package/dist/contract-emit-CU-SYNe4.mjs +0 -6
- package/dist/contract-emit-gpJNLGs7.mjs.map +0 -1
- package/dist/migration-emit-Du4DBMqz.mjs +0 -125
- package/dist/migration-emit-Du4DBMqz.mjs.map +0 -1
- package/dist/migration-status-CPamfEPj.mjs.map +0 -1
- package/dist/migrations-BIsjFjSV.mjs.map +0 -1
- package/dist/result-handler-AFK4hxyX.mjs.map +0 -1
- package/src/commands/migration-emit.ts +0 -134
- package/src/lib/migration-emit.ts +0 -125
- package/src/lib/migration-strategy.ts +0 -49
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
-
import {
|
|
3
|
+
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
|
+
import {
|
|
5
|
+
createControlStack,
|
|
6
|
+
type MigrationPlanOperation,
|
|
7
|
+
} from '@prisma-next/framework-components/control';
|
|
8
|
+
import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
|
|
4
9
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
5
10
|
import { findLatestMigration } from '@prisma-next/migration-tools/dag';
|
|
6
11
|
import {
|
|
7
|
-
|
|
12
|
+
copyFilesWithRename,
|
|
8
13
|
formatMigrationDirName,
|
|
9
14
|
writeMigrationPackage,
|
|
10
15
|
} from '@prisma-next/migration-tools/io';
|
|
@@ -15,8 +20,6 @@ import { Command } from 'commander';
|
|
|
15
20
|
import { join, relative } from 'pathe';
|
|
16
21
|
import { loadConfig } from '../config-loader';
|
|
17
22
|
import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
|
|
18
|
-
import { emitMigration } from '../lib/migration-emit';
|
|
19
|
-
import { migrationStrategy } from '../lib/migration-strategy';
|
|
20
23
|
import {
|
|
21
24
|
type CliErrorConflict,
|
|
22
25
|
CliStructuredError,
|
|
@@ -54,7 +57,6 @@ export interface MigrationPlanResult {
|
|
|
54
57
|
readonly noOp: boolean;
|
|
55
58
|
readonly from: string;
|
|
56
59
|
readonly to: string;
|
|
57
|
-
readonly migrationId?: string;
|
|
58
60
|
readonly dir?: string;
|
|
59
61
|
readonly operations: readonly {
|
|
60
62
|
readonly id: string;
|
|
@@ -63,6 +65,12 @@ export interface MigrationPlanResult {
|
|
|
63
65
|
}[];
|
|
64
66
|
readonly sql?: readonly string[];
|
|
65
67
|
readonly summary: string;
|
|
68
|
+
/**
|
|
69
|
+
* When true, `migration.ts` was written but contains unfilled
|
|
70
|
+
* `placeholder(...)` calls. The user must edit the file and then run
|
|
71
|
+
* `node migration.ts` to self-emit `ops.json` / `migration.json`.
|
|
72
|
+
*/
|
|
73
|
+
readonly pendingPlaceholders?: boolean;
|
|
66
74
|
readonly timings: {
|
|
67
75
|
readonly total: number;
|
|
68
76
|
};
|
|
@@ -166,20 +174,10 @@ async function executeMigrationPlanCommand(
|
|
|
166
174
|
// Read existing migrations and determine "from" contract
|
|
167
175
|
let fromContract: Contract | null = null;
|
|
168
176
|
let fromHash: string = EMPTY_CONTRACT_HASH;
|
|
177
|
+
let fromContractSourceDir: string | null = null;
|
|
169
178
|
|
|
170
179
|
try {
|
|
171
|
-
const {
|
|
172
|
-
|
|
173
|
-
// Check if a draft migration already targets this contract
|
|
174
|
-
const existingDraft = drafts.find((d) => d.manifest.to === toStorageHash);
|
|
175
|
-
if (existingDraft) {
|
|
176
|
-
return notOk(
|
|
177
|
-
errorRuntime('A draft migration to this contract already exists', {
|
|
178
|
-
why: `Draft migration at "${existingDraft.dirName}" already targets ${toStorageHash}`,
|
|
179
|
-
fix: `Run 'prisma-next migration emit --dir ${migrationsRelative}/${existingDraft.dirName}' to attest it, or delete it and re-plan.`,
|
|
180
|
-
}),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
180
|
+
const { bundles, graph } = await loadAllBundles(migrationsDir);
|
|
183
181
|
|
|
184
182
|
if (options.from) {
|
|
185
183
|
const resolved = resolveBundleByPrefix(bundles, options.from);
|
|
@@ -199,6 +197,7 @@ async function executeMigrationPlanCommand(
|
|
|
199
197
|
}
|
|
200
198
|
fromHash = resolved.value.manifest.to;
|
|
201
199
|
fromContract = resolved.value.manifest.toContract;
|
|
200
|
+
fromContractSourceDir = resolved.value.dirPath;
|
|
202
201
|
} else {
|
|
203
202
|
const latestMigration = findLatestMigration(graph);
|
|
204
203
|
if (latestMigration) {
|
|
@@ -206,6 +205,7 @@ async function executeMigrationPlanCommand(
|
|
|
206
205
|
const leafPkg = bundles.find((p) => p.manifest.migrationId === latestMigration.migrationId);
|
|
207
206
|
if (leafPkg) {
|
|
208
207
|
fromContract = leafPkg.manifest.toContract;
|
|
208
|
+
fromContractSourceDir = leafPkg.dirPath;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
}
|
|
@@ -245,18 +245,15 @@ async function executeMigrationPlanCommand(
|
|
|
245
245
|
[config.target, config.adapter, ...(config.extensionPacks ?? [])],
|
|
246
246
|
);
|
|
247
247
|
|
|
248
|
-
const strategy = migrationStrategy(migrations, config.target.targetId);
|
|
249
|
-
|
|
250
248
|
// Build manifest and write migration package
|
|
251
249
|
const timestamp = new Date();
|
|
252
250
|
const slug = options.name ?? 'migration';
|
|
253
251
|
const dirName = formatMigrationDirName(timestamp, slug);
|
|
254
252
|
const packageDir = join(migrationsDir, dirName);
|
|
255
253
|
|
|
256
|
-
const
|
|
254
|
+
const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
|
|
257
255
|
from: fromHash,
|
|
258
256
|
to: toStorageHash,
|
|
259
|
-
migrationId: null,
|
|
260
257
|
kind: 'regular',
|
|
261
258
|
fromContract,
|
|
262
259
|
toContract: toContractJson,
|
|
@@ -264,41 +261,42 @@ async function executeMigrationPlanCommand(
|
|
|
264
261
|
used: [],
|
|
265
262
|
applied: [],
|
|
266
263
|
plannerVersion: '2.0.0',
|
|
267
|
-
planningStrategy: strategy === 'descriptor' ? 'descriptors' : 'class-based',
|
|
268
264
|
},
|
|
269
265
|
labels: [],
|
|
270
266
|
createdAt: timestamp.toISOString(),
|
|
271
267
|
};
|
|
272
268
|
|
|
273
|
-
const scaffoldContext = {
|
|
274
|
-
packageDir,
|
|
275
|
-
contractJsonPath: contractPathAbsolute,
|
|
276
|
-
fromHash,
|
|
277
|
-
toHash: toStorageHash,
|
|
278
|
-
};
|
|
279
|
-
|
|
280
269
|
try {
|
|
281
|
-
|
|
270
|
+
const stack = createControlStack(config);
|
|
271
|
+
const familyInstance = config.family.create(stack);
|
|
272
|
+
const planner = migrations.createPlanner(familyInstance);
|
|
273
|
+
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
274
|
+
const plannerResult = planner.plan({
|
|
275
|
+
contract: toContractJson,
|
|
276
|
+
schema: fromSchema,
|
|
277
|
+
policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
|
|
278
|
+
fromHash,
|
|
279
|
+
fromContract,
|
|
280
|
+
frameworkComponents,
|
|
281
|
+
});
|
|
282
|
+
if (plannerResult.kind === 'failure') {
|
|
283
|
+
return notOk(
|
|
284
|
+
errorMigrationPlanningFailed({
|
|
285
|
+
conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
282
289
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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) {
|
|
290
|
+
// Accessing .operations triggers toOp() on each call. If any call
|
|
291
|
+
// is a DataTransformCall with an unfilled placeholder stub, toOp()
|
|
292
|
+
// throws PN-MIG-2001. We catch that here so the migration can still
|
|
293
|
+
// be scaffolded with `ops: []`; the user fills the placeholder, then
|
|
294
|
+
// re-runs `node migration.ts` to attest with the real ops.
|
|
295
|
+
let plannedOps: readonly MigrationPlanOperation[] = [];
|
|
296
|
+
let hasPlaceholders = false;
|
|
297
|
+
try {
|
|
298
|
+
plannedOps = plannerResult.plan.operations;
|
|
299
|
+
if (plannedOps.length === 0) {
|
|
302
300
|
return notOk(
|
|
303
301
|
errorMigrationPlanningFailed({
|
|
304
302
|
conflicts: [
|
|
@@ -312,75 +310,74 @@ async function executeMigrationPlanCommand(
|
|
|
312
310
|
}),
|
|
313
311
|
);
|
|
314
312
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
if (CliStructuredError.is(e) && e.domain === 'MIG' && e.code === '2001') {
|
|
315
|
+
hasPlaceholders = true;
|
|
316
|
+
} else {
|
|
317
|
+
throw e;
|
|
351
318
|
}
|
|
352
|
-
migrationTsContent = plannerResult.plan.renderTypeScript();
|
|
353
319
|
}
|
|
354
320
|
|
|
355
|
-
|
|
356
|
-
|
|
321
|
+
const migrationTsContent = plannerResult.plan.renderTypeScript();
|
|
322
|
+
|
|
323
|
+
// Always-attest: compute migrationId over (manifest, ops). When
|
|
324
|
+
// placeholders blocked lowering, ops is `[]` and the id hashes over
|
|
325
|
+
// the empty list — re-emitting after the user fills the placeholder
|
|
326
|
+
// produces a different id (over the real ops). This is intentional;
|
|
327
|
+
// there is no on-disk "draft" state.
|
|
328
|
+
const opsForWrite = hasPlaceholders ? [] : plannedOps;
|
|
329
|
+
const manifest: MigrationManifest = {
|
|
330
|
+
...baseManifest,
|
|
331
|
+
migrationId: computeMigrationId(baseManifest, opsForWrite),
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
await writeMigrationPackage(packageDir, manifest, opsForWrite);
|
|
335
|
+
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
336
|
+
await copyFilesWithRename(packageDir, [
|
|
337
|
+
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
338
|
+
{ sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
|
|
339
|
+
]);
|
|
340
|
+
if (fromContractSourceDir !== null) {
|
|
341
|
+
const sourceArtifacts = getEmittedArtifactPaths(
|
|
342
|
+
join(fromContractSourceDir, 'end-contract.json'),
|
|
343
|
+
);
|
|
344
|
+
await copyFilesWithRename(packageDir, [
|
|
345
|
+
{ sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
|
|
346
|
+
{ sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
357
349
|
await writeMigrationTs(packageDir, migrationTsContent);
|
|
358
350
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
351
|
+
if (hasPlaceholders) {
|
|
352
|
+
const result: MigrationPlanResult = {
|
|
353
|
+
ok: true,
|
|
354
|
+
noOp: false,
|
|
355
|
+
from: fromHash,
|
|
356
|
+
to: toStorageHash,
|
|
357
|
+
dir: relative(process.cwd(), packageDir),
|
|
358
|
+
operations: [],
|
|
359
|
+
pendingPlaceholders: true,
|
|
360
|
+
summary:
|
|
361
|
+
'Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
|
|
362
|
+
timings: { total: Date.now() - startTime },
|
|
363
|
+
};
|
|
364
|
+
return ok(result);
|
|
365
|
+
}
|
|
368
366
|
|
|
369
|
-
const sql = extractSqlDdl(
|
|
367
|
+
const sql = extractSqlDdl(plannedOps);
|
|
370
368
|
const result: MigrationPlanResult = {
|
|
371
369
|
ok: true,
|
|
372
370
|
noOp: false,
|
|
373
371
|
from: fromHash,
|
|
374
372
|
to: toStorageHash,
|
|
375
|
-
migrationId,
|
|
376
373
|
dir: relative(process.cwd(), packageDir),
|
|
377
|
-
operations:
|
|
374
|
+
operations: plannedOps.map((op) => ({
|
|
378
375
|
id: op.id,
|
|
379
376
|
label: op.label,
|
|
380
377
|
operationClass: op.operationClass,
|
|
381
378
|
})),
|
|
382
379
|
sql,
|
|
383
|
-
summary: `Planned ${
|
|
380
|
+
summary: `Planned ${plannedOps.length} operation(s)`,
|
|
384
381
|
timings: { total: Date.now() - startTime },
|
|
385
382
|
};
|
|
386
383
|
return ok(result);
|
|
@@ -442,6 +439,22 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
442
439
|
return lines.join('\n');
|
|
443
440
|
}
|
|
444
441
|
|
|
442
|
+
if (result.pendingPlaceholders) {
|
|
443
|
+
lines.push(`${yellow_('⚠')} ${result.summary}`);
|
|
444
|
+
lines.push('');
|
|
445
|
+
lines.push(dim_(`from: ${result.from}`));
|
|
446
|
+
lines.push(dim_(`to: ${result.to}`));
|
|
447
|
+
if (result.dir) {
|
|
448
|
+
lines.push(dim_(`dir: ${result.dir}`));
|
|
449
|
+
}
|
|
450
|
+
lines.push('');
|
|
451
|
+
lines.push(
|
|
452
|
+
'Open migration.ts and replace each `placeholder(...)` call with your actual query.',
|
|
453
|
+
);
|
|
454
|
+
lines.push(`Then run: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)}`);
|
|
455
|
+
return lines.join('\n');
|
|
456
|
+
}
|
|
457
|
+
|
|
445
458
|
lines.push(`${green_('✔')} ${result.summary}`);
|
|
446
459
|
lines.push('');
|
|
447
460
|
|
|
@@ -470,13 +483,15 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
470
483
|
|
|
471
484
|
lines.push(dim_(`from: ${result.from}`));
|
|
472
485
|
lines.push(dim_(`to: ${result.to}`));
|
|
473
|
-
if (result.migrationId) {
|
|
474
|
-
lines.push(dim_(`migrationId: ${result.migrationId}`));
|
|
475
|
-
}
|
|
476
486
|
if (result.dir) {
|
|
477
487
|
lines.push(dim_(`dir: ${result.dir}`));
|
|
478
488
|
}
|
|
479
489
|
|
|
490
|
+
lines.push('');
|
|
491
|
+
lines.push(
|
|
492
|
+
`Next: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)} to emit ops.json and attest migrationId before running ${green_('prisma-next migration apply')}.`,
|
|
493
|
+
);
|
|
494
|
+
|
|
480
495
|
if (result.sql && result.sql.length > 0) {
|
|
481
496
|
lines.push('');
|
|
482
497
|
lines.push(dim_('DDL preview'));
|
|
@@ -2,7 +2,7 @@ import type { MigrationPlanOperation } from '@prisma-next/framework-components/c
|
|
|
2
2
|
import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
|
|
3
3
|
import { readMigrationPackage, readMigrationsDir } from '@prisma-next/migration-tools/io';
|
|
4
4
|
import type { MigrationBundle } from '@prisma-next/migration-tools/types';
|
|
5
|
-
import {
|
|
5
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
6
6
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { relative, resolve } from 'pathe';
|
|
@@ -31,7 +31,7 @@ export interface MigrationShowResult {
|
|
|
31
31
|
readonly dirPath: string;
|
|
32
32
|
readonly from: string;
|
|
33
33
|
readonly to: string;
|
|
34
|
-
readonly migrationId: string
|
|
34
|
+
readonly migrationId: string;
|
|
35
35
|
readonly kind: string;
|
|
36
36
|
readonly createdAt: string;
|
|
37
37
|
readonly operations: readonly {
|
|
@@ -52,8 +52,7 @@ export function resolveByHashPrefix(
|
|
|
52
52
|
prefix: string,
|
|
53
53
|
): Result<MigrationBundle, CliStructuredError> {
|
|
54
54
|
const normalizedPrefix = prefix.startsWith('sha256:') ? prefix : `sha256:${prefix}`;
|
|
55
|
-
const
|
|
56
|
-
const matches = attested.filter((p) => p.manifest.migrationId!.startsWith(normalizedPrefix));
|
|
55
|
+
const matches = packages.filter((p) => p.manifest.migrationId.startsWith(normalizedPrefix));
|
|
57
56
|
|
|
58
57
|
if (matches.length === 1) {
|
|
59
58
|
return ok(matches[0]!);
|
|
@@ -62,7 +61,7 @@ export function resolveByHashPrefix(
|
|
|
62
61
|
if (matches.length === 0) {
|
|
63
62
|
return notOk(
|
|
64
63
|
errorRuntime('No migration found matching prefix', {
|
|
65
|
-
why: `No
|
|
64
|
+
why: `No migration has a migrationId starting with "${normalizedPrefix}"`,
|
|
66
65
|
fix: 'Run `prisma-next migration show` (no argument) to see the latest migration, or check the migrations directory for available packages.',
|
|
67
66
|
}),
|
|
68
67
|
);
|
|
@@ -132,16 +131,7 @@ async function executeMigrationShowCommand(
|
|
|
132
131
|
if (!resolved.ok) return resolved;
|
|
133
132
|
pkg = resolved.value;
|
|
134
133
|
} else {
|
|
135
|
-
const
|
|
136
|
-
if (attested.length === 0) {
|
|
137
|
-
return notOk(
|
|
138
|
-
errorRuntime('No attested migrations found', {
|
|
139
|
-
why: `All migrations in ${migrationsRelative} are drafts (migrationId: null)`,
|
|
140
|
-
fix: 'Run `prisma-next migration emit --dir <path>` to attest a draft migration.',
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
const graph = reconstructGraph(attested);
|
|
134
|
+
const graph = reconstructGraph(allPackages);
|
|
145
135
|
const latestMigration = findLatestMigration(graph);
|
|
146
136
|
if (!latestMigration) {
|
|
147
137
|
return notOk(
|
|
@@ -151,7 +141,7 @@ async function executeMigrationShowCommand(
|
|
|
151
141
|
}),
|
|
152
142
|
);
|
|
153
143
|
}
|
|
154
|
-
const leafPkg =
|
|
144
|
+
const leafPkg = allPackages.find(
|
|
155
145
|
(p) => p.manifest.migrationId === latestMigration.migrationId,
|
|
156
146
|
);
|
|
157
147
|
if (!leafPkg) {
|
|
@@ -8,8 +8,7 @@ import {
|
|
|
8
8
|
import type { Refs } from '@prisma-next/migration-tools/refs';
|
|
9
9
|
import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
|
|
10
10
|
import type {
|
|
11
|
-
|
|
12
|
-
DraftMigrationBundle,
|
|
11
|
+
MigrationBundle,
|
|
13
12
|
MigrationChainEntry,
|
|
14
13
|
MigrationGraph,
|
|
15
14
|
} from '@prisma-next/migration-tools/types';
|
|
@@ -62,7 +61,7 @@ export interface MigrationStatusEntry {
|
|
|
62
61
|
readonly dirName: string;
|
|
63
62
|
readonly from: string;
|
|
64
63
|
readonly to: string;
|
|
65
|
-
readonly migrationId: string
|
|
64
|
+
readonly migrationId: string;
|
|
66
65
|
readonly operationCount: number;
|
|
67
66
|
readonly operationSummary: string;
|
|
68
67
|
readonly hasDestructive: boolean;
|
|
@@ -87,7 +86,7 @@ export interface MigrationStatusResult {
|
|
|
87
86
|
readonly refName?: string;
|
|
88
87
|
readonly selectedPath: readonly {
|
|
89
88
|
readonly dirName: string;
|
|
90
|
-
readonly migrationId: string
|
|
89
|
+
readonly migrationId: string;
|
|
91
90
|
readonly from: string;
|
|
92
91
|
readonly to: string;
|
|
93
92
|
}[];
|
|
@@ -95,8 +94,7 @@ export interface MigrationStatusResult {
|
|
|
95
94
|
readonly summary: string;
|
|
96
95
|
readonly diagnostics: readonly StatusDiagnostic[];
|
|
97
96
|
readonly graph?: MigrationGraph;
|
|
98
|
-
readonly bundles?: readonly
|
|
99
|
-
readonly drafts?: readonly DraftMigrationBundle[];
|
|
97
|
+
readonly bundles?: readonly MigrationBundle[];
|
|
100
98
|
readonly edgeStatuses?: readonly EdgeStatus[];
|
|
101
99
|
readonly activeRefHash?: string;
|
|
102
100
|
readonly activeRefName?: string;
|
|
@@ -227,7 +225,7 @@ export function deriveEdgeStatuses(
|
|
|
227
225
|
*/
|
|
228
226
|
function buildMigrationEntries(
|
|
229
227
|
chain: readonly MigrationChainEntry[],
|
|
230
|
-
packages: readonly
|
|
228
|
+
packages: readonly MigrationBundle[],
|
|
231
229
|
mode: 'online' | 'offline',
|
|
232
230
|
markerHash: string | undefined,
|
|
233
231
|
edgeStatuses?: readonly EdgeStatus[],
|
|
@@ -436,11 +434,10 @@ async function executeMigrationStatusCommand(
|
|
|
436
434
|
});
|
|
437
435
|
}
|
|
438
436
|
|
|
439
|
-
let
|
|
440
|
-
let drafts: readonly DraftMigrationBundle[];
|
|
437
|
+
let bundles: readonly MigrationBundle[];
|
|
441
438
|
let graph: MigrationGraph;
|
|
442
439
|
try {
|
|
443
|
-
({
|
|
440
|
+
({ bundles, graph } = await loadAllBundles(migrationsDir));
|
|
444
441
|
} catch (error) {
|
|
445
442
|
if (MigrationToolsError.is(error)) {
|
|
446
443
|
return notOk(
|
|
@@ -454,18 +451,7 @@ async function executeMigrationStatusCommand(
|
|
|
454
451
|
);
|
|
455
452
|
}
|
|
456
453
|
|
|
457
|
-
if (
|
|
458
|
-
diagnostics.push({
|
|
459
|
-
code: 'MIGRATION.DRAFTS',
|
|
460
|
-
severity: 'warn',
|
|
461
|
-
message: `${drafts.length} draft migration(s) found: ${drafts.map((d) => d.dirName).join(', ')}`,
|
|
462
|
-
hints: [
|
|
463
|
-
"Run 'prisma-next migration emit --dir <path>' to attest draft migrations before applying",
|
|
464
|
-
],
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (attested.length === 0) {
|
|
454
|
+
if (bundles.length === 0) {
|
|
469
455
|
if (contractHash !== EMPTY_CONTRACT_HASH) {
|
|
470
456
|
diagnostics.push({
|
|
471
457
|
code: 'CONTRACT.AHEAD',
|
|
@@ -576,7 +562,7 @@ async function executeMigrationStatusCommand(
|
|
|
576
562
|
migrations: [],
|
|
577
563
|
targetHash: EMPTY_CONTRACT_HASH,
|
|
578
564
|
contractHash,
|
|
579
|
-
summary: `${
|
|
565
|
+
summary: `${bundles.length} migration(s) on disk`,
|
|
580
566
|
diagnostics,
|
|
581
567
|
markerHash,
|
|
582
568
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
@@ -616,12 +602,12 @@ async function executeMigrationStatusCommand(
|
|
|
616
602
|
migrations: [],
|
|
617
603
|
targetHash: EMPTY_CONTRACT_HASH,
|
|
618
604
|
contractHash,
|
|
619
|
-
summary: `${
|
|
605
|
+
summary: `${bundles.length} migration(s) on disk`,
|
|
620
606
|
diagnostics,
|
|
621
607
|
...ifDefined('markerHash', markerHash),
|
|
622
608
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
623
609
|
graph,
|
|
624
|
-
bundles
|
|
610
|
+
bundles,
|
|
625
611
|
diverged: true,
|
|
626
612
|
});
|
|
627
613
|
}
|
|
@@ -638,7 +624,7 @@ async function executeMigrationStatusCommand(
|
|
|
638
624
|
}
|
|
639
625
|
|
|
640
626
|
const edgeStatuses = deriveEdgeStatuses(graph, targetHash, contractHash, markerHash, mode);
|
|
641
|
-
const entries = buildMigrationEntries(chain,
|
|
627
|
+
const entries = buildMigrationEntries(chain, bundles, mode, markerHash, edgeStatuses);
|
|
642
628
|
|
|
643
629
|
const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
|
|
644
630
|
const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
|
|
@@ -646,7 +632,7 @@ async function executeMigrationStatusCommand(
|
|
|
646
632
|
let summary: string;
|
|
647
633
|
if (mode === 'online') {
|
|
648
634
|
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
649
|
-
summary = `${
|
|
635
|
+
summary = `${bundles.length} migration(s) on disk`;
|
|
650
636
|
} else if (activeRefHash && markerHash !== undefined) {
|
|
651
637
|
summary = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
|
|
652
638
|
} else if (pendingCount === 0) {
|
|
@@ -706,8 +692,7 @@ async function executeMigrationStatusCommand(
|
|
|
706
692
|
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
707
693
|
...ifDefined('pathDecision', pathDecision),
|
|
708
694
|
graph,
|
|
709
|
-
bundles
|
|
710
|
-
...(drafts.length > 0 ? { drafts } : {}),
|
|
695
|
+
bundles,
|
|
711
696
|
edgeStatuses,
|
|
712
697
|
...ifDefined('activeRefHash', activeRefHash),
|
|
713
698
|
...ifDefined('activeRefName', activeRefName),
|
|
@@ -768,11 +753,6 @@ export function createMigrationStatusCommand(): Command {
|
|
|
768
753
|
activeRefHash: statusResult.activeRefHash,
|
|
769
754
|
activeRefName: statusResult.activeRefName,
|
|
770
755
|
edgeStatuses: statusResult.edgeStatuses,
|
|
771
|
-
draftEdges: statusResult.drafts?.map((d) => ({
|
|
772
|
-
from: d.manifest.from,
|
|
773
|
-
to: d.manifest.to,
|
|
774
|
-
dirName: d.dirName,
|
|
775
|
-
})),
|
|
776
756
|
});
|
|
777
757
|
|
|
778
758
|
const graphToRender =
|
package/src/config-loader.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { dirname, resolve } from 'node:path';
|
|
2
1
|
import type { PrismaNextConfig } from '@prisma-next/config/config-types';
|
|
3
2
|
import { ConfigValidationError, validateConfig } from '@prisma-next/config/config-validation';
|
|
4
3
|
import {
|
|
@@ -6,7 +5,41 @@ import {
|
|
|
6
5
|
errorConfigValidation,
|
|
7
6
|
errorUnexpected,
|
|
8
7
|
} from '@prisma-next/errors/control';
|
|
8
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
9
9
|
import { loadConfig as loadConfigC12 } from 'c12';
|
|
10
|
+
import { dirname, resolve } from 'pathe';
|
|
11
|
+
import { finalizeConfig } from './config-path-validation';
|
|
12
|
+
|
|
13
|
+
async function loadValidatedConfig(configPath?: string): Promise<PrismaNextConfig> {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const resolvedConfigPath = configPath ? resolve(cwd, configPath) : undefined;
|
|
16
|
+
const configCwd = resolvedConfigPath ? dirname(resolvedConfigPath) : cwd;
|
|
17
|
+
|
|
18
|
+
const result = await loadConfigC12<PrismaNextConfig>({
|
|
19
|
+
name: 'prisma-next',
|
|
20
|
+
...ifDefined('configFile', resolvedConfigPath),
|
|
21
|
+
cwd: configCwd,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// When a specific config file was requested, verify it was actually loaded
|
|
25
|
+
// (c12 falls back to searching by name if the specified file doesn't exist)
|
|
26
|
+
if (resolvedConfigPath && result.configFile !== resolvedConfigPath) {
|
|
27
|
+
throw errorConfigFileNotFound(resolvedConfigPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if config is missing or empty (c12 may return empty object when file doesn't exist)
|
|
31
|
+
if (!result.config || Object.keys(result.config).length === 0) {
|
|
32
|
+
// Use c12's configFile if available, otherwise use explicit configPath, otherwise omit path
|
|
33
|
+
const displayPath = result.configFile || resolvedConfigPath || configPath;
|
|
34
|
+
throw errorConfigFileNotFound(displayPath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate config structure
|
|
38
|
+
validateConfig(result.config);
|
|
39
|
+
|
|
40
|
+
const loadedConfigDir = result.configFile ? dirname(result.configFile) : configCwd;
|
|
41
|
+
return finalizeConfig(result.config, loadedConfigDir);
|
|
42
|
+
}
|
|
10
43
|
|
|
11
44
|
/**
|
|
12
45
|
* Loads the Prisma Next config from a TypeScript file.
|
|
@@ -19,34 +52,7 @@ import { loadConfig as loadConfigC12 } from 'c12';
|
|
|
19
52
|
*/
|
|
20
53
|
export async function loadConfig(configPath?: string): Promise<PrismaNextConfig> {
|
|
21
54
|
try {
|
|
22
|
-
|
|
23
|
-
// Resolve config path to absolute path and set cwd to config directory when path is provided
|
|
24
|
-
const resolvedConfigPath = configPath ? resolve(cwd, configPath) : undefined;
|
|
25
|
-
const configCwd = resolvedConfigPath ? dirname(resolvedConfigPath) : cwd;
|
|
26
|
-
|
|
27
|
-
const result = await loadConfigC12<PrismaNextConfig>({
|
|
28
|
-
name: 'prisma-next',
|
|
29
|
-
...(resolvedConfigPath ? { configFile: resolvedConfigPath } : {}),
|
|
30
|
-
cwd: configCwd,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// When a specific config file was requested, verify it was actually loaded
|
|
34
|
-
// (c12 falls back to searching by name if the specified file doesn't exist)
|
|
35
|
-
if (resolvedConfigPath && result.configFile !== resolvedConfigPath) {
|
|
36
|
-
throw errorConfigFileNotFound(resolvedConfigPath);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Check if config is missing or empty (c12 may return empty object when file doesn't exist)
|
|
40
|
-
if (!result.config || Object.keys(result.config).length === 0) {
|
|
41
|
-
// Use c12's configFile if available, otherwise use explicit configPath, otherwise omit path
|
|
42
|
-
const displayPath = result.configFile || resolvedConfigPath || configPath;
|
|
43
|
-
throw errorConfigFileNotFound(displayPath);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Validate config structure
|
|
47
|
-
validateConfig(result.config);
|
|
48
|
-
|
|
49
|
-
return result.config;
|
|
55
|
+
return await loadValidatedConfig(configPath);
|
|
50
56
|
} catch (error) {
|
|
51
57
|
if (error instanceof ConfigValidationError) {
|
|
52
58
|
throw errorConfigValidation(error.field, {
|