@prisma-next/cli 0.5.0-dev.3 → 0.5.0-dev.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -21
- package/dist/agent-skill-mongo.md +63 -31
- package/dist/agent-skill-postgres.md +1 -1
- package/dist/cli-errors-By1iVE3z.mjs +34 -0
- package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
- package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
- package/dist/cli.mjs +131 -15
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-TG7rbCWT.mjs → client-keSCAgjW.mjs} +43 -19
- package/dist/client-keSCAgjW.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +7 -2
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +8 -2
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +11 -9
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +8 -5
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +8 -7
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.mjs +10 -9
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +10 -9
- package/dist/commands/db-verify.mjs.map +1 -1
- package/dist/commands/migration-apply.d.mts +2 -2
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +15 -38
- 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 +24 -30
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +14 -5
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +45 -47
- 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 +31 -40
- package/dist/commands/migration-ref.mjs.map +1 -1
- package/dist/commands/migration-show.d.mts +13 -7
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +28 -29
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +5 -4
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +7 -2
- package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
- package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
- package/dist/config-loader.mjs +1 -1
- package/dist/contract-emit-DS5NzZh2.mjs +6 -0
- package/dist/contract-emit-DWtGQYCD.mjs +150 -0
- package/dist/contract-emit-DWtGQYCD.mjs.map +1 -0
- package/dist/contract-emit-RZBWzkop.mjs +329 -0
- package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
- package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
- package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
- package/dist/{contract-infer-BP3DrGgz.mjs → contract-infer-GztVCOCJ.mjs} +11 -19
- package/dist/contract-infer-GztVCOCJ.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +78 -21
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +7 -5
- package/dist/exports/index.mjs +8 -3
- package/dist/exports/index.mjs.map +1 -1
- 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/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
- package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
- package/dist/init-DAbQMxIR.mjs +2062 -0
- package/dist/init-DAbQMxIR.mjs.map +1 -0
- package/dist/{inspect-live-schema-DWzf4Q_m.mjs → inspect-live-schema-BaR9ISwa.mjs} +9 -9
- package/dist/inspect-live-schema-BaR9ISwa.mjs.map +1 -0
- package/dist/migration-cli.d.mts +41 -11
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +308 -84
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-CLMD302g.mjs → migration-command-scaffold-D1dWuEWQ.mjs} +7 -7
- package/dist/{migration-command-scaffold-CLMD302g.mjs.map → migration-command-scaffold-D1dWuEWQ.mjs.map} +1 -1
- package/dist/{migration-status-B0HLF7So.mjs → migration-status-CP5k8O5i.mjs} +21 -35
- package/dist/migration-status-CP5k8O5i.mjs.map +1 -0
- package/dist/{migrations-B0dOQlk0.mjs → migrations-MEoKMiV5.mjs} +42 -21
- package/dist/migrations-MEoKMiV5.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-DgRGldpT.mjs} +1 -1
- package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
- package/dist/quick-reference-mongo.md +34 -13
- package/dist/quick-reference-postgres.md +11 -9
- package/dist/{result-handler-CIyu0Pdt.mjs → result-handler-BmVh8AeV.mjs} +12 -93
- package/dist/result-handler-BmVh8AeV.mjs.map +1 -0
- package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
- package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
- package/dist/{verify-BxiVp50b.mjs → verify-BT9tgCOH.mjs} +2 -2
- package/dist/{verify-BxiVp50b.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
- package/package.json +21 -15
- package/src/cli.ts +32 -6
- package/src/commands/contract-emit.ts +67 -163
- package/src/commands/contract-infer.ts +7 -20
- package/src/commands/db-init.ts +1 -0
- package/src/commands/db-update.ts +1 -1
- 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/inspect-live-schema.ts +10 -5
- package/src/commands/migration-apply.ts +16 -51
- package/src/commands/migration-new.ts +26 -32
- package/src/commands/migration-plan.ts +80 -55
- package/src/commands/migration-ref.ts +40 -54
- package/src/commands/migration-show.ts +53 -36
- package/src/commands/migration-status.ts +33 -50
- package/src/config-path-validation.ts +0 -1
- package/src/control-api/client.ts +21 -0
- package/src/control-api/operations/contract-emit.ts +198 -115
- package/src/control-api/operations/db-init.ts +8 -5
- package/src/control-api/operations/db-update.ts +8 -5
- package/src/control-api/operations/migration-apply.ts +29 -9
- package/src/control-api/types.ts +61 -7
- package/src/exports/control-api.ts +2 -1
- package/src/exports/init-output.ts +10 -0
- package/src/migration-cli.ts +445 -122
- package/src/utils/cli-errors.ts +49 -2
- package/src/utils/command-helpers.ts +13 -26
- package/src/utils/emit-queue.ts +26 -0
- package/src/utils/formatters/graph-migration-mapper.ts +2 -2
- package/src/utils/formatters/migrations.ts +62 -26
- package/src/utils/publish-contract-artifact-pair.ts +134 -0
- package/dist/cli-errors-DHq6GQGu.mjs +0 -5
- package/dist/client-TG7rbCWT.mjs.map +0 -1
- package/dist/config-loader-_W4T21X1.mjs.map +0 -1
- package/dist/contract-emit-CNYyzJwF.mjs +0 -195
- package/dist/contract-emit-CNYyzJwF.mjs.map +0 -1
- package/dist/contract-emit-CQfj7xJn.mjs +0 -122
- package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
- package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
- package/dist/contract-infer-BP3DrGgz.mjs.map +0 -1
- package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
- package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
- package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
- package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
- package/dist/init-CQfo_4Ro.mjs +0 -430
- package/dist/init-CQfo_4Ro.mjs.map +0 -1
- package/dist/inspect-live-schema-DWzf4Q_m.mjs.map +0 -1
- package/dist/migration-status-B0HLF7So.mjs.map +0 -1
- package/dist/migrations-B0dOQlk0.mjs.map +0 -1
- package/dist/result-handler-CIyu0Pdt.mjs.map +0 -1
- package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
- package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
- package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
- package/src/control-api/operations/extract-operation-statements.ts +0 -14
- package/src/control-api/operations/extract-sql-ddl.ts +0 -47
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CoreSchemaView } from '@prisma-next/framework-components/control';
|
|
2
|
-
import {
|
|
2
|
+
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
3
3
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
4
4
|
import { relative, resolve } from 'pathe';
|
|
5
5
|
import { loadConfig } from '../config-loader';
|
|
@@ -33,6 +33,12 @@ export interface InspectLiveSchemaResult {
|
|
|
33
33
|
readonly config: LoadedCliConfig;
|
|
34
34
|
readonly schema: unknown;
|
|
35
35
|
readonly schemaView: CoreSchemaView | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* PSL AST inferred from the introspected schema, when the configured family
|
|
38
|
+
* implements `PslContractInferCapable`. `undefined` for families that do not
|
|
39
|
+
* support inference (e.g. Mongo today).
|
|
40
|
+
*/
|
|
41
|
+
readonly pslContractAst: PslDocumentAst | undefined;
|
|
36
42
|
readonly target: {
|
|
37
43
|
readonly familyId: string;
|
|
38
44
|
readonly id: string;
|
|
@@ -122,14 +128,12 @@ export async function inspectLiveSchema(
|
|
|
122
128
|
const onProgress = createProgressAdapter({ ui, flags });
|
|
123
129
|
|
|
124
130
|
try {
|
|
125
|
-
const
|
|
131
|
+
const schema = await client.introspect({
|
|
126
132
|
connection: dbConnection,
|
|
127
133
|
onProgress,
|
|
128
134
|
});
|
|
129
|
-
// TODO(TML-2251): Remove SQL-specific branching — SQL should use the same family-agnostic path as Mongo.
|
|
130
|
-
const schema =
|
|
131
|
-
config.family.familyId === 'sql' ? validatePrintableSqlSchemaIR(schemaIR) : schemaIR;
|
|
132
135
|
const schemaView = client.toSchemaView(schema);
|
|
136
|
+
const pslContractAst = client.inferPslContract(schema);
|
|
133
137
|
|
|
134
138
|
const dbUrl = typeof dbConnection === 'string' ? maskConnectionUrl(dbConnection) : undefined;
|
|
135
139
|
|
|
@@ -137,6 +141,7 @@ export async function inspectLiveSchema(
|
|
|
137
141
|
config,
|
|
138
142
|
schema,
|
|
139
143
|
schemaView,
|
|
144
|
+
pslContractAst,
|
|
140
145
|
target: {
|
|
141
146
|
familyId: config.family.familyId,
|
|
142
147
|
id: config.target.targetId,
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { verifyMigrationBundle } from '@prisma-next/migration-tools/attestation';
|
|
2
1
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
3
|
-
import {
|
|
2
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
3
|
+
import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
|
|
4
|
+
import type { MigrationPackage } from '@prisma-next/migration-tools/package';
|
|
4
5
|
import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
|
|
5
|
-
import type { MigrationBundle } from '@prisma-next/migration-tools/types';
|
|
6
|
-
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
7
6
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
8
7
|
import { Command } from 'commander';
|
|
9
8
|
|
|
@@ -18,11 +17,11 @@ import {
|
|
|
18
17
|
errorRuntime,
|
|
19
18
|
errorTargetMigrationNotSupported,
|
|
20
19
|
errorUnexpected,
|
|
20
|
+
mapMigrationToolsError,
|
|
21
21
|
} from '../utils/cli-errors';
|
|
22
22
|
import {
|
|
23
23
|
addGlobalOptions,
|
|
24
|
-
|
|
25
|
-
type MigrationBundleSet,
|
|
24
|
+
loadMigrationPackages,
|
|
26
25
|
maskConnectionUrl,
|
|
27
26
|
readContractEnvelope,
|
|
28
27
|
resolveMigrationPaths,
|
|
@@ -51,7 +50,7 @@ export interface MigrationApplyResult {
|
|
|
51
50
|
readonly markerHash: string;
|
|
52
51
|
readonly applied: readonly {
|
|
53
52
|
readonly dirName: string;
|
|
54
|
-
readonly from: string;
|
|
53
|
+
readonly from: string | null;
|
|
55
54
|
readonly to: string;
|
|
56
55
|
readonly operationsExecuted: number;
|
|
57
56
|
}[];
|
|
@@ -64,7 +63,7 @@ export interface MigrationApplyResult {
|
|
|
64
63
|
readonly refName?: string;
|
|
65
64
|
readonly selectedPath: readonly {
|
|
66
65
|
readonly dirName: string;
|
|
67
|
-
readonly
|
|
66
|
+
readonly migrationHash: string;
|
|
68
67
|
readonly from: string;
|
|
69
68
|
readonly to: string;
|
|
70
69
|
}[];
|
|
@@ -74,19 +73,6 @@ export interface MigrationApplyResult {
|
|
|
74
73
|
};
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function mapMigrationToolsError(error: unknown): CliStructuredErrorType {
|
|
78
|
-
if (MigrationToolsError.is(error)) {
|
|
79
|
-
return errorRuntime(error.message, {
|
|
80
|
-
why: error.why,
|
|
81
|
-
fix: error.fix,
|
|
82
|
-
meta: { code: error.code, ...(error.details ?? {}) },
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
return errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
86
|
-
why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
76
|
function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
|
|
91
77
|
return errorRuntime(failure.summary, {
|
|
92
78
|
why: failure.why ?? 'Migration runner failed',
|
|
@@ -95,12 +81,12 @@ function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType
|
|
|
95
81
|
});
|
|
96
82
|
}
|
|
97
83
|
|
|
98
|
-
function packageToStep(pkg:
|
|
84
|
+
function packageToStep(pkg: MigrationPackage): MigrationApplyStep {
|
|
99
85
|
return {
|
|
100
86
|
dirName: pkg.dirName,
|
|
101
|
-
from: pkg.
|
|
102
|
-
to: pkg.
|
|
103
|
-
toContract: pkg.
|
|
87
|
+
from: pkg.metadata.from,
|
|
88
|
+
to: pkg.metadata.to,
|
|
89
|
+
toContract: pkg.metadata.toContract,
|
|
104
90
|
operations: pkg.ops,
|
|
105
91
|
};
|
|
106
92
|
}
|
|
@@ -112,7 +98,7 @@ async function executeMigrationApplyCommand(
|
|
|
112
98
|
startTime: number,
|
|
113
99
|
): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
|
|
114
100
|
const config = await loadConfig(options.config);
|
|
115
|
-
const { configPath, migrationsDir, migrationsRelative,
|
|
101
|
+
const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
|
|
116
102
|
options.config,
|
|
117
103
|
config,
|
|
118
104
|
);
|
|
@@ -149,8 +135,8 @@ async function executeMigrationApplyCommand(
|
|
|
149
135
|
if (options.ref) {
|
|
150
136
|
refName = options.ref;
|
|
151
137
|
try {
|
|
152
|
-
const refs = await readRefs(
|
|
153
|
-
destinationHash = resolveRef(refs, refName);
|
|
138
|
+
const refs = await readRefs(refsDir);
|
|
139
|
+
destinationHash = resolveRef(refs, refName).hash;
|
|
154
140
|
} catch (error) {
|
|
155
141
|
if (MigrationToolsError.is(error)) {
|
|
156
142
|
return notOk(mapMigrationToolsError(error));
|
|
@@ -196,9 +182,9 @@ async function executeMigrationApplyCommand(
|
|
|
196
182
|
}
|
|
197
183
|
|
|
198
184
|
// Read migrations and build migration chain model (offline — no DB needed)
|
|
199
|
-
let migrations:
|
|
185
|
+
let migrations: Awaited<ReturnType<typeof loadMigrationPackages>>;
|
|
200
186
|
try {
|
|
201
|
-
migrations = await
|
|
187
|
+
migrations = await loadMigrationPackages(migrationsDir);
|
|
202
188
|
} catch (error) {
|
|
203
189
|
if (MigrationToolsError.is(error)) {
|
|
204
190
|
return notOk(mapMigrationToolsError(error));
|
|
@@ -206,27 +192,6 @@ async function executeMigrationApplyCommand(
|
|
|
206
192
|
throw error;
|
|
207
193
|
}
|
|
208
194
|
|
|
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
|
-
|
|
230
195
|
const client = createControlClient({
|
|
231
196
|
family: config.family,
|
|
232
197
|
target: config.target,
|
|
@@ -12,31 +12,35 @@ import { readFileSync } from 'node:fs';
|
|
|
12
12
|
import type { Contract } from '@prisma-next/contract/types';
|
|
13
13
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
14
14
|
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
|
|
15
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
16
|
+
import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
18
17
|
import {
|
|
19
18
|
copyFilesWithRename,
|
|
20
19
|
formatMigrationDirName,
|
|
21
20
|
readMigrationsDir,
|
|
22
21
|
writeMigrationPackage,
|
|
23
22
|
} from '@prisma-next/migration-tools/io';
|
|
23
|
+
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
24
|
+
import {
|
|
25
|
+
findLatestMigration,
|
|
26
|
+
reconstructGraph,
|
|
27
|
+
} from '@prisma-next/migration-tools/migration-graph';
|
|
24
28
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
25
|
-
import type { MigrationManifest } from '@prisma-next/migration-tools/types';
|
|
26
|
-
import { MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
27
29
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
28
30
|
import { Command } from 'commander';
|
|
29
|
-
import { join, relative
|
|
31
|
+
import { join, relative } from 'pathe';
|
|
30
32
|
import { loadConfig } from '../config-loader';
|
|
31
33
|
import {
|
|
32
34
|
CliStructuredError,
|
|
33
35
|
errorRuntime,
|
|
34
36
|
errorTargetMigrationNotSupported,
|
|
35
37
|
errorUnexpected,
|
|
38
|
+
mapMigrationToolsError,
|
|
36
39
|
} from '../utils/cli-errors';
|
|
37
40
|
import {
|
|
38
41
|
addGlobalOptions,
|
|
39
42
|
getTargetMigrations,
|
|
43
|
+
resolveContractPath,
|
|
40
44
|
resolveMigrationPaths,
|
|
41
45
|
setCommandDescriptions,
|
|
42
46
|
setCommandExamples,
|
|
@@ -57,7 +61,7 @@ interface MigrationNewOptions extends CommonCommandOptions {
|
|
|
57
61
|
interface MigrationNewResult {
|
|
58
62
|
readonly ok: true;
|
|
59
63
|
readonly dir: string;
|
|
60
|
-
readonly from: string;
|
|
64
|
+
readonly from: string | null;
|
|
61
65
|
readonly to: string;
|
|
62
66
|
readonly summary: string;
|
|
63
67
|
}
|
|
@@ -68,11 +72,7 @@ async function executeMigrationNewCommand(
|
|
|
68
72
|
const config = await loadConfig(options.config);
|
|
69
73
|
const { migrationsDir, migrationsRelative } = resolveMigrationPaths(options.config, config);
|
|
70
74
|
|
|
71
|
-
const
|
|
72
|
-
const contractPathAbsolute = resolve(
|
|
73
|
-
options.config ? resolve(options.config, '..') : process.cwd(),
|
|
74
|
-
contractPath,
|
|
75
|
-
);
|
|
75
|
+
const contractPathAbsolute = resolveContractPath(config);
|
|
76
76
|
|
|
77
77
|
let contractJsonContent: string;
|
|
78
78
|
try {
|
|
@@ -116,7 +116,7 @@ async function executeMigrationNewCommand(
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
let fromContract: Contract | null = null;
|
|
119
|
-
let fromHash: string =
|
|
119
|
+
let fromHash: string | null = null;
|
|
120
120
|
let fromContractSourceDir: string | null = null;
|
|
121
121
|
|
|
122
122
|
try {
|
|
@@ -126,7 +126,7 @@ async function executeMigrationNewCommand(
|
|
|
126
126
|
const graph = reconstructGraph(packages);
|
|
127
127
|
|
|
128
128
|
if (options.from) {
|
|
129
|
-
const match = packages.find((p) => p.
|
|
129
|
+
const match = packages.find((p) => p.metadata.to.startsWith(options.from!));
|
|
130
130
|
if (!match) {
|
|
131
131
|
return notOk(
|
|
132
132
|
errorRuntime('Starting contract not found', {
|
|
@@ -135,18 +135,18 @@ async function executeMigrationNewCommand(
|
|
|
135
135
|
}),
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
|
-
fromHash = match.
|
|
139
|
-
fromContract = match.
|
|
138
|
+
fromHash = match.metadata.to;
|
|
139
|
+
fromContract = match.metadata.toContract;
|
|
140
140
|
fromContractSourceDir = match.dirPath;
|
|
141
141
|
} else {
|
|
142
142
|
const latestMigration = findLatestMigration(graph);
|
|
143
143
|
if (latestMigration) {
|
|
144
144
|
fromHash = latestMigration.to;
|
|
145
145
|
const leafPkg = packages.find(
|
|
146
|
-
(p) => p.
|
|
146
|
+
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
147
147
|
);
|
|
148
148
|
if (leafPkg) {
|
|
149
|
-
fromContract = leafPkg.
|
|
149
|
+
fromContract = leafPkg.metadata.toContract;
|
|
150
150
|
fromContractSourceDir = leafPkg.dirPath;
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -154,13 +154,7 @@ async function executeMigrationNewCommand(
|
|
|
154
154
|
}
|
|
155
155
|
} catch (error) {
|
|
156
156
|
if (MigrationToolsError.is(error)) {
|
|
157
|
-
return notOk(
|
|
158
|
-
errorRuntime(error.message, {
|
|
159
|
-
why: error.why,
|
|
160
|
-
fix: error.fix,
|
|
161
|
-
meta: { code: error.code },
|
|
162
|
-
}),
|
|
163
|
-
);
|
|
157
|
+
return notOk(mapMigrationToolsError(error));
|
|
164
158
|
}
|
|
165
159
|
throw error;
|
|
166
160
|
}
|
|
@@ -181,12 +175,11 @@ async function executeMigrationNewCommand(
|
|
|
181
175
|
|
|
182
176
|
// `migration new` scaffolds an empty `migration.ts` for the user to
|
|
183
177
|
// fill, so we attest over `ops: []`. Re-running self-emit after the
|
|
184
|
-
// user adds operations will produce a different `
|
|
178
|
+
// user adds operations will produce a different `migrationHash` (over
|
|
185
179
|
// the real ops). This is intentional — there is no on-disk draft.
|
|
186
|
-
const
|
|
180
|
+
const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
|
|
187
181
|
from: fromHash,
|
|
188
182
|
to: toStorageHash,
|
|
189
|
-
kind: 'regular',
|
|
190
183
|
fromContract,
|
|
191
184
|
toContract: toContractJson,
|
|
192
185
|
hints: {
|
|
@@ -195,11 +188,12 @@ async function executeMigrationNewCommand(
|
|
|
195
188
|
plannerVersion: '1.0.0',
|
|
196
189
|
},
|
|
197
190
|
labels: [],
|
|
191
|
+
providedInvariants: [],
|
|
198
192
|
createdAt: timestamp.toISOString(),
|
|
199
193
|
};
|
|
200
|
-
const
|
|
201
|
-
...
|
|
202
|
-
|
|
194
|
+
const metadata: MigrationMetadata = {
|
|
195
|
+
...baseMetadata,
|
|
196
|
+
migrationHash: computeMigrationHash(baseMetadata, []),
|
|
203
197
|
};
|
|
204
198
|
|
|
205
199
|
const migrations = getTargetMigrations(config.target);
|
|
@@ -218,7 +212,7 @@ async function executeMigrationNewCommand(
|
|
|
218
212
|
...(config.extensionPacks ?? []),
|
|
219
213
|
]);
|
|
220
214
|
|
|
221
|
-
await writeMigrationPackage(packageDir,
|
|
215
|
+
await writeMigrationPackage(packageDir, metadata, []);
|
|
222
216
|
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
223
217
|
await copyFilesWithRename(packageDir, [
|
|
224
218
|
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
@@ -3,23 +3,25 @@ import type { Contract } from '@prisma-next/contract/types';
|
|
|
3
3
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import {
|
|
5
5
|
createControlStack,
|
|
6
|
+
hasOperationPreview,
|
|
6
7
|
type MigrationPlanOperation,
|
|
8
|
+
type OperationPreview,
|
|
7
9
|
} from '@prisma-next/framework-components/control';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
11
|
+
import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
|
|
12
|
+
import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
|
|
11
13
|
import {
|
|
12
14
|
copyFilesWithRename,
|
|
13
15
|
formatMigrationDirName,
|
|
14
16
|
writeMigrationPackage,
|
|
15
17
|
} from '@prisma-next/migration-tools/io';
|
|
18
|
+
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
19
|
+
import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
|
|
16
20
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
17
|
-
import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
|
|
18
21
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
19
22
|
import { Command } from 'commander';
|
|
20
23
|
import { join, relative } from 'pathe';
|
|
21
24
|
import { loadConfig } from '../config-loader';
|
|
22
|
-
import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
|
|
23
25
|
import {
|
|
24
26
|
type CliErrorConflict,
|
|
25
27
|
CliStructuredError,
|
|
@@ -29,11 +31,12 @@ import {
|
|
|
29
31
|
errorRuntime,
|
|
30
32
|
errorTargetMigrationNotSupported,
|
|
31
33
|
errorUnexpected,
|
|
34
|
+
mapMigrationToolsError,
|
|
32
35
|
} from '../utils/cli-errors';
|
|
33
36
|
import {
|
|
34
37
|
addGlobalOptions,
|
|
35
38
|
getTargetMigrations,
|
|
36
|
-
|
|
39
|
+
loadMigrationPackages,
|
|
37
40
|
resolveContractPath,
|
|
38
41
|
resolveMigrationPaths,
|
|
39
42
|
setCommandDescriptions,
|
|
@@ -55,7 +58,7 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
55
58
|
export interface MigrationPlanResult {
|
|
56
59
|
readonly ok: boolean;
|
|
57
60
|
readonly noOp: boolean;
|
|
58
|
-
readonly from: string;
|
|
61
|
+
readonly from: string | null;
|
|
59
62
|
readonly to: string;
|
|
60
63
|
readonly dir?: string;
|
|
61
64
|
readonly operations: readonly {
|
|
@@ -63,7 +66,12 @@ export interface MigrationPlanResult {
|
|
|
63
66
|
readonly label: string;
|
|
64
67
|
readonly operationClass: string;
|
|
65
68
|
}[];
|
|
66
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Family-agnostic textual preview of the migration plan operations.
|
|
71
|
+
* Replaces the previous `sql?: readonly string[]` field; consumers should
|
|
72
|
+
* read `result.preview?.statements`.
|
|
73
|
+
*/
|
|
74
|
+
readonly preview?: OperationPreview;
|
|
67
75
|
readonly summary: string;
|
|
68
76
|
/**
|
|
69
77
|
* When true, `migration.ts` was written but contains unfilled
|
|
@@ -76,22 +84,6 @@ export interface MigrationPlanResult {
|
|
|
76
84
|
};
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
function mapMigrationToolsError(error: unknown): CliStructuredError {
|
|
80
|
-
if (CliStructuredError.is(error)) {
|
|
81
|
-
return error;
|
|
82
|
-
}
|
|
83
|
-
if (MigrationToolsError.is(error)) {
|
|
84
|
-
return errorRuntime(error.message, {
|
|
85
|
-
why: error.why,
|
|
86
|
-
fix: error.fix,
|
|
87
|
-
meta: { code: error.code, ...(error.details ?? {}) },
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
91
|
-
why: `Unexpected error during migration plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
87
|
async function executeMigrationPlanCommand(
|
|
96
88
|
options: MigrationPlanOptions,
|
|
97
89
|
flags: GlobalFlags,
|
|
@@ -173,11 +165,11 @@ async function executeMigrationPlanCommand(
|
|
|
173
165
|
|
|
174
166
|
// Read existing migrations and determine "from" contract
|
|
175
167
|
let fromContract: Contract | null = null;
|
|
176
|
-
let fromHash: string =
|
|
168
|
+
let fromHash: string | null = null;
|
|
177
169
|
let fromContractSourceDir: string | null = null;
|
|
178
170
|
|
|
179
171
|
try {
|
|
180
|
-
const { bundles, graph } = await
|
|
172
|
+
const { bundles, graph } = await loadMigrationPackages(migrationsDir);
|
|
181
173
|
|
|
182
174
|
if (options.from) {
|
|
183
175
|
const resolved = resolveBundleByPrefix(bundles, options.from);
|
|
@@ -195,16 +187,18 @@ async function executeMigrationPlanCommand(
|
|
|
195
187
|
}),
|
|
196
188
|
);
|
|
197
189
|
}
|
|
198
|
-
fromHash = resolved.value.
|
|
199
|
-
fromContract = resolved.value.
|
|
190
|
+
fromHash = resolved.value.metadata.to;
|
|
191
|
+
fromContract = resolved.value.metadata.toContract;
|
|
200
192
|
fromContractSourceDir = resolved.value.dirPath;
|
|
201
193
|
} else {
|
|
202
194
|
const latestMigration = findLatestMigration(graph);
|
|
203
195
|
if (latestMigration) {
|
|
204
196
|
fromHash = latestMigration.to;
|
|
205
|
-
const leafPkg = bundles.find(
|
|
197
|
+
const leafPkg = bundles.find(
|
|
198
|
+
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
199
|
+
);
|
|
206
200
|
if (leafPkg) {
|
|
207
|
-
fromContract = leafPkg.
|
|
201
|
+
fromContract = leafPkg.metadata.toContract;
|
|
208
202
|
fromContractSourceDir = leafPkg.dirPath;
|
|
209
203
|
}
|
|
210
204
|
}
|
|
@@ -213,7 +207,16 @@ async function executeMigrationPlanCommand(
|
|
|
213
207
|
if (MigrationToolsError.is(error)) {
|
|
214
208
|
return notOk(mapMigrationToolsError(error));
|
|
215
209
|
}
|
|
216
|
-
|
|
210
|
+
// Wrap unexpected (non-MigrationToolsError) failures from the migration
|
|
211
|
+
// load phase in a structured CLI envelope. Letting them throw would
|
|
212
|
+
// bypass `handleResult()` and crash the command — see CLI structured-
|
|
213
|
+
// errors guideline (CliStructuredError + Result pattern).
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
return notOk(
|
|
216
|
+
errorUnexpected(message, {
|
|
217
|
+
why: `Unexpected error while loading migrations: ${message}`,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
// Check for no-op (same hash means no changes)
|
|
@@ -251,10 +254,9 @@ async function executeMigrationPlanCommand(
|
|
|
251
254
|
const dirName = formatMigrationDirName(timestamp, slug);
|
|
252
255
|
const packageDir = join(migrationsDir, dirName);
|
|
253
256
|
|
|
254
|
-
const
|
|
257
|
+
const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
|
|
255
258
|
from: fromHash,
|
|
256
259
|
to: toStorageHash,
|
|
257
|
-
kind: 'regular',
|
|
258
260
|
fromContract,
|
|
259
261
|
toContract: toContractJson,
|
|
260
262
|
hints: {
|
|
@@ -320,18 +322,22 @@ async function executeMigrationPlanCommand(
|
|
|
320
322
|
|
|
321
323
|
const migrationTsContent = plannerResult.plan.renderTypeScript();
|
|
322
324
|
|
|
323
|
-
// Always-attest: compute
|
|
324
|
-
// placeholders blocked lowering, ops is `[]` and the
|
|
325
|
-
// the empty list — re-emitting after the user fills the placeholder
|
|
326
|
-
// produces a different
|
|
325
|
+
// Always-attest: compute migrationHash over (metadata, ops). When
|
|
326
|
+
// placeholders blocked lowering, ops is `[]` and the hash is computed
|
|
327
|
+
// over the empty list — re-emitting after the user fills the placeholder
|
|
328
|
+
// produces a different hash (over the real ops). This is intentional;
|
|
327
329
|
// there is no on-disk "draft" state.
|
|
328
330
|
const opsForWrite = hasPlaceholders ? [] : plannedOps;
|
|
329
|
-
const
|
|
330
|
-
...
|
|
331
|
-
|
|
331
|
+
const metadataWithInvariants: Omit<MigrationMetadata, 'migrationHash'> = {
|
|
332
|
+
...baseMetadata,
|
|
333
|
+
providedInvariants: deriveProvidedInvariants(opsForWrite),
|
|
334
|
+
};
|
|
335
|
+
const metadata: MigrationMetadata = {
|
|
336
|
+
...metadataWithInvariants,
|
|
337
|
+
migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
|
|
332
338
|
};
|
|
333
339
|
|
|
334
|
-
await writeMigrationPackage(packageDir,
|
|
340
|
+
await writeMigrationPackage(packageDir, metadata, opsForWrite);
|
|
335
341
|
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
336
342
|
await copyFilesWithRename(packageDir, [
|
|
337
343
|
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
@@ -364,7 +370,9 @@ async function executeMigrationPlanCommand(
|
|
|
364
370
|
return ok(result);
|
|
365
371
|
}
|
|
366
372
|
|
|
367
|
-
const
|
|
373
|
+
const preview = hasOperationPreview(familyInstance)
|
|
374
|
+
? familyInstance.toOperationPreview(plannedOps)
|
|
375
|
+
: undefined;
|
|
368
376
|
const result: MigrationPlanResult = {
|
|
369
377
|
ok: true,
|
|
370
378
|
noOp: false,
|
|
@@ -376,13 +384,24 @@ async function executeMigrationPlanCommand(
|
|
|
376
384
|
label: op.label,
|
|
377
385
|
operationClass: op.operationClass,
|
|
378
386
|
})),
|
|
379
|
-
|
|
387
|
+
...(preview !== undefined ? { preview } : {}),
|
|
380
388
|
summary: `Planned ${plannedOps.length} operation(s)`,
|
|
381
389
|
timings: { total: Date.now() - startTime },
|
|
382
390
|
};
|
|
383
391
|
return ok(result);
|
|
384
392
|
} catch (error) {
|
|
385
|
-
|
|
393
|
+
if (CliStructuredError.is(error)) {
|
|
394
|
+
return notOk(error);
|
|
395
|
+
}
|
|
396
|
+
if (MigrationToolsError.is(error)) {
|
|
397
|
+
return notOk(mapMigrationToolsError(error));
|
|
398
|
+
}
|
|
399
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
+
return notOk(
|
|
401
|
+
errorUnexpected(message, {
|
|
402
|
+
why: `Unexpected error during migration plan: ${message}`,
|
|
403
|
+
}),
|
|
404
|
+
);
|
|
386
405
|
}
|
|
387
406
|
}
|
|
388
407
|
|
|
@@ -489,17 +508,20 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
|
|
|
489
508
|
|
|
490
509
|
lines.push('');
|
|
491
510
|
lines.push(
|
|
492
|
-
`Next: ${green_(
|
|
511
|
+
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
|
|
493
512
|
);
|
|
494
513
|
|
|
495
|
-
if (result.
|
|
514
|
+
if (result.preview && result.preview.statements.length > 0) {
|
|
515
|
+
// The non-empty length is already guaranteed by the surrounding check, so
|
|
516
|
+
// a plain `every` here is equivalent to the helper in formatters/migrations.ts.
|
|
517
|
+
const allSql = result.preview.statements.every((s) => s.language === 'sql');
|
|
496
518
|
lines.push('');
|
|
497
|
-
lines.push(dim_('DDL preview'));
|
|
519
|
+
lines.push(dim_(allSql ? 'DDL preview' : 'Operation preview'));
|
|
498
520
|
lines.push('');
|
|
499
|
-
for (const statement of result.
|
|
500
|
-
const trimmed = statement.trim();
|
|
521
|
+
for (const statement of result.preview.statements) {
|
|
522
|
+
const trimmed = statement.text.trim();
|
|
501
523
|
if (!trimmed) continue;
|
|
502
|
-
const line = trimmed.endsWith(';') ? trimmed :
|
|
524
|
+
const line = statement.language === 'sql' && !trimmed.endsWith(';') ? `${trimmed};` : trimmed;
|
|
503
525
|
lines.push(line);
|
|
504
526
|
}
|
|
505
527
|
}
|
|
@@ -517,24 +539,27 @@ export type PrefixResolutionFailure =
|
|
|
517
539
|
| { reason: 'not-found' };
|
|
518
540
|
|
|
519
541
|
/**
|
|
520
|
-
* Resolve a migration
|
|
542
|
+
* Resolve a migration package by **target contract hash** (`metadata.to`)
|
|
543
|
+
* using exact match or prefix match.
|
|
521
544
|
*
|
|
545
|
+
* Note: matches `metadata.to` (the contract hash this migration produces),
|
|
546
|
+
* not `metadata.migrationHash` (the package's content-addressed identity).
|
|
522
547
|
* Tries exact match first, then prefix match (auto-prepending `sha256:` when
|
|
523
|
-
* the needle omits the scheme). Returns the matched
|
|
548
|
+
* the needle omits the scheme). Returns the matched package on success, or a
|
|
524
549
|
* discriminated failure indicating whether the prefix was ambiguous or simply
|
|
525
550
|
* not found.
|
|
526
551
|
*
|
|
527
552
|
* @internal Exported for testing only.
|
|
528
553
|
*/
|
|
529
|
-
export function resolveBundleByPrefix<T extends {
|
|
554
|
+
export function resolveBundleByPrefix<T extends { metadata: { to: string } }>(
|
|
530
555
|
bundles: readonly T[],
|
|
531
556
|
needle: string,
|
|
532
557
|
): Result<T, PrefixResolutionFailure> {
|
|
533
|
-
const exact = bundles.find((p) => p.
|
|
558
|
+
const exact = bundles.find((p) => p.metadata.to === needle);
|
|
534
559
|
if (exact) return ok(exact);
|
|
535
560
|
|
|
536
561
|
const prefixWithScheme = needle.startsWith('sha256:') ? needle : `sha256:${needle}`;
|
|
537
|
-
const candidates = bundles.filter((p) => p.
|
|
562
|
+
const candidates = bundles.filter((p) => p.metadata.to.startsWith(prefixWithScheme));
|
|
538
563
|
|
|
539
564
|
if (candidates.length === 1) return ok(candidates[0]!);
|
|
540
565
|
if (candidates.length > 1) return notOk({ reason: 'ambiguous', count: candidates.length });
|