@prisma-next/cli 0.4.0-dev.9 → 0.4.2
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/agent-skill-mongo.md +63 -31
- package/dist/agent-skill-postgres.md +1 -1
- package/dist/cli-errors-BFYgBH3L.d.mts +4 -0
- package/dist/cli-errors-Cd79vmTH.mjs +5 -0
- package/dist/cli.mjs +127 -25
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-CJxHfhze.mjs → client-CrsnY58k.mjs} +9 -8
- package/dist/{client-CJxHfhze.mjs.map → client-CrsnY58k.mjs.map} +1 -1
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +7 -7
- package/dist/commands/contract-infer.mjs +8 -8
- package/dist/commands/db-init.mjs +8 -8
- package/dist/commands/db-schema.mjs +8 -8
- package/dist/commands/db-sign.mjs +8 -8
- package/dist/commands/db-update.mjs +8 -8
- package/dist/commands/db-verify.mjs +9 -9
- 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 +37 -28
- 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 +48 -23
- 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 +94 -71
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-ref.d.mts +6 -4
- package/dist/commands/migration-ref.d.mts.map +1 -1
- package/dist/commands/migration-ref.mjs +29 -34
- package/dist/commands/migration-ref.mjs.map +1 -1
- 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 +11 -16
- 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 +7 -7
- package/dist/config-loader-C25b63rJ.mjs +90 -0
- package/dist/config-loader-C25b63rJ.mjs.map +1 -0
- package/dist/config-loader.d.mts.map +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/contract-emit-DxgyXrqV.mjs +6 -0
- package/dist/{contract-emit-gpJNLGs7.mjs → contract-emit-NJ01hiiv.mjs} +20 -16
- package/dist/contract-emit-NJ01hiiv.mjs.map +1 -0
- package/dist/{contract-emit-CKig_Lra.mjs → contract-emit-V5SSitUT.mjs} +25 -21
- package/dist/contract-emit-V5SSitUT.mjs.map +1 -0
- package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-CAOELa-H.mjs} +1 -1
- package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-CAOELa-H.mjs.map} +1 -1
- package/dist/{contract-infer-BDJgg7Xb.mjs → contract-infer-D9cC3rJm.mjs} +4 -4
- package/dist/{contract-infer-BDJgg7Xb.mjs.map → contract-infer-D9cC3rJm.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 +6 -6
- package/dist/exports/index.mjs +7 -7
- package/dist/exports/init-output.d.mts +39 -0
- package/dist/exports/init-output.d.mts.map +1 -0
- package/dist/exports/init-output.mjs +3 -0
- package/dist/{extract-operation-statements-DZUJNmL3.mjs → extract-operation-statements-DsFfxXVZ.mjs} +2 -2
- package/dist/{extract-operation-statements-DZUJNmL3.mjs.map → extract-operation-statements-DsFfxXVZ.mjs.map} +1 -1
- package/dist/{extract-sql-ddl-DDMX-9mz.mjs → extract-sql-ddl-D9UbZDyz.mjs} +1 -1
- package/dist/{extract-sql-ddl-DDMX-9mz.mjs.map → extract-sql-ddl-D9UbZDyz.mjs.map} +1 -1
- package/dist/{framework-components-Bsr1GaIj.mjs → framework-components-Cr--XBKy.mjs} +2 -2
- package/dist/{framework-components-Bsr1GaIj.mjs.map → framework-components-Cr--XBKy.mjs.map} +1 -1
- package/dist/init-m8x0UoPY.mjs +2062 -0
- package/dist/init-m8x0UoPY.mjs.map +1 -0
- package/dist/{inspect-live-schema-ChqrALmw.mjs → inspect-live-schema-yrHAvG71.mjs} +6 -6
- package/dist/{inspect-live-schema-ChqrALmw.mjs.map → inspect-live-schema-yrHAvG71.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-B3B09et6.mjs} +7 -7
- package/dist/{migration-command-scaffold-B0oH_hyB.mjs.map → migration-command-scaffold-B3B09et6.mjs.map} +1 -1
- package/dist/{migration-status-CPamfEPj.mjs → migration-status-DUMiH8_G.mjs} +25 -43
- package/dist/migration-status-DUMiH8_G.mjs.map +1 -0
- package/dist/{migrations-BIsjFjSV.mjs → migrations-Bo5WtTla.mjs} +4 -15
- package/dist/migrations-Bo5WtTla.mjs.map +1 -0
- package/dist/output-BpcQrnnq.mjs +103 -0
- package/dist/output-BpcQrnnq.mjs.map +1 -0
- package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DvQWB1nK.mjs} +1 -1
- package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DvQWB1nK.mjs.map} +1 -1
- package/dist/quick-reference-mongo.md +34 -13
- package/dist/quick-reference-postgres.md +11 -9
- package/dist/{result-handler-AFK4hxyX.mjs → result-handler-Ba3zWQsI.mjs} +26 -88
- package/dist/result-handler-Ba3zWQsI.mjs.map +1 -0
- package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-C3ZLwQxK.mjs} +76 -2
- package/dist/terminal-ui-C3ZLwQxK.mjs.map +1 -0
- package/dist/{validate-contract-deps-DBH6iTAD.mjs → validate-contract-deps-B_Cs29TL.mjs} +1 -1
- package/dist/{validate-contract-deps-DBH6iTAD.mjs.map → validate-contract-deps-B_Cs29TL.mjs.map} +1 -1
- package/dist/{verify-C56CuQc7.mjs → verify-Bkycc-Tf.mjs} +2 -2
- package/dist/{verify-C56CuQc7.mjs.map → verify-Bkycc-Tf.mjs.map} +1 -1
- package/package.json +24 -19
- package/src/cli.ts +1 -5
- package/src/commands/contract-emit.ts +9 -10
- package/src/commands/init/detect-pnpm-catalog.ts +141 -0
- package/src/commands/init/errors.ts +254 -0
- package/src/commands/init/exit-codes.ts +62 -0
- package/src/commands/init/hygiene-gitattributes.ts +97 -0
- package/src/commands/init/hygiene-gitignore.ts +48 -0
- package/src/commands/init/hygiene-package-scripts.ts +91 -0
- package/src/commands/init/index.ts +112 -7
- package/src/commands/init/init.ts +766 -144
- package/src/commands/init/inputs.ts +421 -0
- package/src/commands/init/output.ts +147 -0
- package/src/commands/init/probe-db.ts +308 -0
- package/src/commands/init/reinit-cleanup.ts +83 -0
- package/src/commands/init/templates/agent-skill-mongo.md +63 -31
- package/src/commands/init/templates/agent-skill-postgres.md +1 -1
- package/src/commands/init/templates/agent-skill.ts +25 -3
- package/src/commands/init/templates/code-templates.ts +125 -32
- package/src/commands/init/templates/env.ts +80 -0
- package/src/commands/init/templates/quick-reference-mongo.md +34 -13
- package/src/commands/init/templates/quick-reference-postgres.md +11 -9
- package/src/commands/init/templates/quick-reference.ts +42 -3
- package/src/commands/init/templates/tsconfig.ts +167 -5
- package/src/commands/migration-apply.ts +37 -26
- package/src/commands/migration-new.ts +39 -17
- package/src/commands/migration-plan.ts +119 -104
- package/src/commands/migration-ref.ts +32 -47
- package/src/commands/migration-show.ts +6 -16
- package/src/commands/migration-status.ts +30 -55
- 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/exports/init-output.ts +10 -0
- package/src/migration-cli.ts +254 -0
- package/src/utils/cli-errors.ts +1 -0
- package/src/utils/command-helpers.ts +18 -22
- 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/init-DZWvhEP0.mjs +0 -430
- package/dist/init-DZWvhEP0.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/dist/terminal-ui-C5k88MmW.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,7 +1,8 @@
|
|
|
1
|
+
import { verifyMigrationBundle } from '@prisma-next/migration-tools/attestation';
|
|
1
2
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
2
3
|
import { findPathWithDecision } from '@prisma-next/migration-tools/dag';
|
|
3
4
|
import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
|
|
4
|
-
import type {
|
|
5
|
+
import type { MigrationBundle } from '@prisma-next/migration-tools/types';
|
|
5
6
|
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
6
7
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
7
8
|
import { Command } from 'commander';
|
|
@@ -63,7 +64,7 @@ export interface MigrationApplyResult {
|
|
|
63
64
|
readonly refName?: string;
|
|
64
65
|
readonly selectedPath: readonly {
|
|
65
66
|
readonly dirName: string;
|
|
66
|
-
readonly migrationId: string
|
|
67
|
+
readonly migrationId: string;
|
|
67
68
|
readonly from: string;
|
|
68
69
|
readonly to: string;
|
|
69
70
|
}[];
|
|
@@ -94,7 +95,7 @@ function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType
|
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function packageToStep(pkg:
|
|
98
|
+
function packageToStep(pkg: MigrationBundle): MigrationApplyStep {
|
|
98
99
|
return {
|
|
99
100
|
dirName: pkg.dirName,
|
|
100
101
|
from: pkg.manifest.from,
|
|
@@ -111,7 +112,7 @@ async function executeMigrationApplyCommand(
|
|
|
111
112
|
startTime: number,
|
|
112
113
|
): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
|
|
113
114
|
const config = await loadConfig(options.config);
|
|
114
|
-
const { configPath, migrationsDir, migrationsRelative,
|
|
115
|
+
const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
|
|
115
116
|
options.config,
|
|
116
117
|
config,
|
|
117
118
|
);
|
|
@@ -148,8 +149,8 @@ async function executeMigrationApplyCommand(
|
|
|
148
149
|
if (options.ref) {
|
|
149
150
|
refName = options.ref;
|
|
150
151
|
try {
|
|
151
|
-
const refs = await readRefs(
|
|
152
|
-
destinationHash = resolveRef(refs, refName);
|
|
152
|
+
const refs = await readRefs(refsDir);
|
|
153
|
+
destinationHash = resolveRef(refs, refName).hash;
|
|
153
154
|
} catch (error) {
|
|
154
155
|
if (MigrationToolsError.is(error)) {
|
|
155
156
|
return notOk(mapMigrationToolsError(error));
|
|
@@ -198,12 +199,6 @@ async function executeMigrationApplyCommand(
|
|
|
198
199
|
let migrations: MigrationBundleSet;
|
|
199
200
|
try {
|
|
200
201
|
migrations = await loadAllBundles(migrationsDir);
|
|
201
|
-
if (migrations.drafts.length > 0 && !flags.quiet) {
|
|
202
|
-
ui.warn(
|
|
203
|
-
`${migrations.drafts.length} draft migration(s) found: ${migrations.drafts.map((d) => d.dirName).join(', ')}. ` +
|
|
204
|
-
"Run 'prisma-next migration emit --dir <path>' to attest before applying.",
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
202
|
} catch (error) {
|
|
208
203
|
if (MigrationToolsError.is(error)) {
|
|
209
204
|
return notOk(mapMigrationToolsError(error));
|
|
@@ -211,6 +206,27 @@ async function executeMigrationApplyCommand(
|
|
|
211
206
|
throw error;
|
|
212
207
|
}
|
|
213
208
|
|
|
209
|
+
// Defense in depth: re-hash every bundle and confirm the recorded
|
|
210
|
+
// `migrationId` matches the on-disk `(manifest, ops)`. Catches FS
|
|
211
|
+
// corruption, partial writes, and post-emit hand edits before we
|
|
212
|
+
// start touching the database.
|
|
213
|
+
for (const bundle of migrations.bundles) {
|
|
214
|
+
const verified = verifyMigrationBundle(bundle);
|
|
215
|
+
if (!verified.ok) {
|
|
216
|
+
return notOk(
|
|
217
|
+
errorRuntime(`Migration package is corrupt: ${bundle.dirName}`, {
|
|
218
|
+
why: `Stored migrationId "${verified.storedMigrationId}" does not match the recomputed hash "${verified.computedMigrationId}" for ${migrationsRelative}/${bundle.dirName}. The migration.json or ops.json has been edited or partially written since emit.`,
|
|
219
|
+
fix: `Re-emit the package by running \`node "${migrationsRelative}/${bundle.dirName}/migration.ts"\`, or restore the directory from version control.`,
|
|
220
|
+
meta: {
|
|
221
|
+
dirName: bundle.dirName,
|
|
222
|
+
storedMigrationId: verified.storedMigrationId,
|
|
223
|
+
computedMigrationId: verified.computedMigrationId,
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
214
230
|
const client = createControlClient({
|
|
215
231
|
family: config.family,
|
|
216
232
|
target: config.target,
|
|
@@ -223,12 +239,12 @@ async function executeMigrationApplyCommand(
|
|
|
223
239
|
await client.connect(dbConnection);
|
|
224
240
|
const marker = await client.readMarker();
|
|
225
241
|
|
|
226
|
-
// --- No
|
|
227
|
-
if (migrations.
|
|
242
|
+
// --- No migrations on disk ---
|
|
243
|
+
if (migrations.bundles.length === 0) {
|
|
228
244
|
if (marker?.storageHash) {
|
|
229
245
|
return notOk(
|
|
230
246
|
errorRuntime('Database has state but no migrations exist', {
|
|
231
|
-
why: `The database marker hash "${marker.storageHash}" exists but no
|
|
247
|
+
why: `The database marker hash "${marker.storageHash}" exists but no migrations were found in ${migrationsRelative}`,
|
|
232
248
|
fix: 'Ensure the migrations directory is correct. If the database was managed with `db init` or `db update`, run `prisma-next db sign` to update the marker.',
|
|
233
249
|
meta: { markerHash: marker.storageHash, migrationsDir: migrationsRelative },
|
|
234
250
|
}),
|
|
@@ -238,8 +254,8 @@ async function executeMigrationApplyCommand(
|
|
|
238
254
|
if (destinationHash !== EMPTY_CONTRACT_HASH) {
|
|
239
255
|
return notOk(
|
|
240
256
|
errorRuntime('Current contract has no planned migrations', {
|
|
241
|
-
why: `No
|
|
242
|
-
fix: 'Run `prisma-next migration plan` to create
|
|
257
|
+
why: `No migrations were found in ${migrationsRelative}, but current contract hash is "${destinationHash}"`,
|
|
258
|
+
fix: 'Run `prisma-next migration plan` to create a migration for the current contract.',
|
|
243
259
|
meta: { destinationHash, migrationsDir: migrationsRelative },
|
|
244
260
|
}),
|
|
245
261
|
);
|
|
@@ -251,7 +267,7 @@ async function executeMigrationApplyCommand(
|
|
|
251
267
|
migrationsTotal: 0,
|
|
252
268
|
markerHash: EMPTY_CONTRACT_HASH,
|
|
253
269
|
applied: [],
|
|
254
|
-
summary: 'No
|
|
270
|
+
summary: 'No migrations found',
|
|
255
271
|
timings: { total: Date.now() - startTime },
|
|
256
272
|
});
|
|
257
273
|
}
|
|
@@ -283,15 +299,10 @@ async function executeMigrationApplyCommand(
|
|
|
283
299
|
}
|
|
284
300
|
|
|
285
301
|
if (!migrations.graph.nodes.has(destinationHash)) {
|
|
286
|
-
const matchingDraft = migrations.drafts.find((d) => d.manifest.to === destinationHash);
|
|
287
302
|
return notOk(
|
|
288
303
|
errorRuntime('Current contract has no planned migration path', {
|
|
289
|
-
why:
|
|
290
|
-
|
|
291
|
-
: `Current contract hash "${destinationHash}" is not present in the migration history at ${migrationsRelative}`,
|
|
292
|
-
fix: matchingDraft
|
|
293
|
-
? `Run 'prisma-next migration emit --dir "${migrationsRelative}/${matchingDraft.dirName}"' to attest, then re-run apply.`
|
|
294
|
-
: 'Run `prisma-next migration plan` to create a migration for the current contract, then re-run apply.',
|
|
304
|
+
why: `Current contract hash "${destinationHash}" is not present in the migration history at ${migrationsRelative}`,
|
|
305
|
+
fix: 'Run `prisma-next migration plan` to create a migration for the current contract, then re-run apply.',
|
|
295
306
|
meta: { destinationHash, knownNodes: [...migrations.graph.nodes] },
|
|
296
307
|
}),
|
|
297
308
|
);
|
|
@@ -329,7 +340,7 @@ async function executeMigrationApplyCommand(
|
|
|
329
340
|
});
|
|
330
341
|
}
|
|
331
342
|
|
|
332
|
-
const bundleByDir = new Map(migrations.
|
|
343
|
+
const bundleByDir = new Map(migrations.bundles.map((b) => [b.dirName, b]));
|
|
333
344
|
const pendingMigrations: MigrationApplyStep[] = [];
|
|
334
345
|
for (const migration of pendingPath) {
|
|
335
346
|
const pkg = bundleByDir.get(migration.dirName);
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* `migration new` — scaffolds a migration package with a `migration.ts` file
|
|
3
3
|
* for manual authoring.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* the same path here: the planner's `emptyMigration(context)` returns a
|
|
5
|
+
* The planner's `emptyMigration(context)` returns a
|
|
7
6
|
* `MigrationPlanWithAuthoringSurface`, whose `renderTypeScript()` produces
|
|
8
7
|
* the target-appropriate empty stub. The CLI writes the returned source
|
|
9
8
|
* verbatim.
|
|
@@ -11,18 +10,20 @@
|
|
|
11
10
|
|
|
12
11
|
import { readFileSync } from 'node:fs';
|
|
13
12
|
import type { Contract } from '@prisma-next/contract/types';
|
|
13
|
+
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
14
14
|
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
15
|
+
import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
|
|
15
16
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
16
17
|
import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
+
copyFilesWithRename,
|
|
19
20
|
formatMigrationDirName,
|
|
20
21
|
readMigrationsDir,
|
|
21
22
|
writeMigrationPackage,
|
|
22
23
|
} from '@prisma-next/migration-tools/io';
|
|
23
24
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
24
25
|
import type { MigrationManifest } from '@prisma-next/migration-tools/types';
|
|
25
|
-
import {
|
|
26
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
26
27
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
27
28
|
import { Command } from 'commander';
|
|
28
29
|
import { join, relative, resolve } from 'pathe';
|
|
@@ -116,16 +117,16 @@ async function executeMigrationNewCommand(
|
|
|
116
117
|
|
|
117
118
|
let fromContract: Contract | null = null;
|
|
118
119
|
let fromHash: string = EMPTY_CONTRACT_HASH;
|
|
120
|
+
let fromContractSourceDir: string | null = null;
|
|
119
121
|
|
|
120
122
|
try {
|
|
121
123
|
const packages = await readMigrationsDir(migrationsDir);
|
|
122
|
-
const attested = packages.filter(isAttested);
|
|
123
124
|
|
|
124
|
-
if (
|
|
125
|
-
const graph = reconstructGraph(
|
|
125
|
+
if (packages.length > 0) {
|
|
126
|
+
const graph = reconstructGraph(packages);
|
|
126
127
|
|
|
127
128
|
if (options.from) {
|
|
128
|
-
const match =
|
|
129
|
+
const match = packages.find((p) => p.manifest.to.startsWith(options.from!));
|
|
129
130
|
if (!match) {
|
|
130
131
|
return notOk(
|
|
131
132
|
errorRuntime('Starting contract not found', {
|
|
@@ -136,15 +137,17 @@ async function executeMigrationNewCommand(
|
|
|
136
137
|
}
|
|
137
138
|
fromHash = match.manifest.to;
|
|
138
139
|
fromContract = match.manifest.toContract;
|
|
140
|
+
fromContractSourceDir = match.dirPath;
|
|
139
141
|
} else {
|
|
140
142
|
const latestMigration = findLatestMigration(graph);
|
|
141
143
|
if (latestMigration) {
|
|
142
144
|
fromHash = latestMigration.to;
|
|
143
|
-
const leafPkg =
|
|
145
|
+
const leafPkg = packages.find(
|
|
144
146
|
(p) => p.manifest.migrationId === latestMigration.migrationId,
|
|
145
147
|
);
|
|
146
148
|
if (leafPkg) {
|
|
147
149
|
fromContract = leafPkg.manifest.toContract;
|
|
150
|
+
fromContractSourceDir = leafPkg.dirPath;
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
}
|
|
@@ -176,10 +179,13 @@ async function executeMigrationNewCommand(
|
|
|
176
179
|
const dirName = formatMigrationDirName(timestamp, slug);
|
|
177
180
|
const packageDir = join(migrationsDir, dirName);
|
|
178
181
|
|
|
179
|
-
|
|
182
|
+
// `migration new` scaffolds an empty `migration.ts` for the user to
|
|
183
|
+
// fill, so we attest over `ops: []`. Re-running self-emit after the
|
|
184
|
+
// user adds operations will produce a different `migrationId` (over
|
|
185
|
+
// the real ops). This is intentional — there is no on-disk draft.
|
|
186
|
+
const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
|
|
180
187
|
from: fromHash,
|
|
181
188
|
to: toStorageHash,
|
|
182
|
-
migrationId: null,
|
|
183
189
|
kind: 'regular',
|
|
184
190
|
fromContract,
|
|
185
191
|
toContract: toContractJson,
|
|
@@ -187,11 +193,14 @@ async function executeMigrationNewCommand(
|
|
|
187
193
|
used: [],
|
|
188
194
|
applied: [],
|
|
189
195
|
plannerVersion: '1.0.0',
|
|
190
|
-
planningStrategy: 'manual',
|
|
191
196
|
},
|
|
192
197
|
labels: [],
|
|
193
198
|
createdAt: timestamp.toISOString(),
|
|
194
199
|
};
|
|
200
|
+
const manifest: MigrationManifest = {
|
|
201
|
+
...baseManifest,
|
|
202
|
+
migrationId: computeMigrationId(baseManifest, []),
|
|
203
|
+
};
|
|
195
204
|
|
|
196
205
|
const migrations = getTargetMigrations(config.target);
|
|
197
206
|
if (!migrations) {
|
|
@@ -210,14 +219,27 @@ async function executeMigrationNewCommand(
|
|
|
210
219
|
]);
|
|
211
220
|
|
|
212
221
|
await writeMigrationPackage(packageDir, manifest, []);
|
|
213
|
-
|
|
222
|
+
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
223
|
+
await copyFilesWithRename(packageDir, [
|
|
224
|
+
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
225
|
+
{ sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
|
|
226
|
+
]);
|
|
227
|
+
if (fromContractSourceDir !== null) {
|
|
228
|
+
const sourceArtifacts = getEmittedArtifactPaths(
|
|
229
|
+
join(fromContractSourceDir, 'end-contract.json'),
|
|
230
|
+
);
|
|
231
|
+
await copyFilesWithRename(packageDir, [
|
|
232
|
+
{ sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
|
|
233
|
+
{ sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
214
236
|
|
|
215
237
|
const stack = createControlStack(config);
|
|
216
238
|
const familyInstance = config.family.create(stack);
|
|
217
239
|
const planner = migrations.createPlanner(familyInstance);
|
|
218
240
|
const emptyPlan = planner.emptyMigration({
|
|
219
241
|
packageDir,
|
|
220
|
-
contractJsonPath: join(packageDir, 'contract.json'),
|
|
242
|
+
contractJsonPath: join(packageDir, 'end-contract.json'),
|
|
221
243
|
fromHash,
|
|
222
244
|
toHash: toStorageHash,
|
|
223
245
|
});
|
|
@@ -248,8 +270,8 @@ export function createMigrationNewCommand(): Command {
|
|
|
248
270
|
command,
|
|
249
271
|
'Scaffold a new migration for manual authoring',
|
|
250
272
|
'Creates a migration package with a migration.ts file for manual authoring.\n' +
|
|
251
|
-
'Write
|
|
252
|
-
'`migration
|
|
273
|
+
'Write the migration body in migration.ts, then run the file with Node\n' +
|
|
274
|
+
'(`node migration.ts`) to self-emit ops.json and attest the package.',
|
|
253
275
|
);
|
|
254
276
|
setCommandExamples(command, [
|
|
255
277
|
'prisma-next migration new --name split-name',
|
|
@@ -283,7 +305,7 @@ export function createMigrationNewCommand(): Command {
|
|
|
283
305
|
ui.output(` from: ${value.from}`);
|
|
284
306
|
ui.output(` to: ${value.to}`);
|
|
285
307
|
ui.output(
|
|
286
|
-
`\nEdit migration.ts, then run
|
|
308
|
+
`\nEdit migration.ts, then run it directly (\`node "${value.dir}/migration.ts"\`) to self-emit and attest.`,
|
|
287
309
|
);
|
|
288
310
|
}
|
|
289
311
|
});
|
|
@@ -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'));
|