@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ContractSourceProvider,
|
|
3
|
+
normalizeContractConfig,
|
|
4
|
+
type PrismaNextConfig,
|
|
5
|
+
} from '@prisma-next/config/config-types';
|
|
6
|
+
import { ConfigValidationError } from '@prisma-next/config/config-validation';
|
|
7
|
+
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
8
|
+
import { resolve } from 'pathe';
|
|
9
|
+
|
|
10
|
+
function throwValidation(field: string, why: string): never {
|
|
11
|
+
throw new ConfigValidationError(field, why);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function finalizeContractSource(
|
|
15
|
+
source: ContractSourceProvider,
|
|
16
|
+
configDir: string,
|
|
17
|
+
): ContractSourceProvider {
|
|
18
|
+
const resolvedInputs = source.inputs?.map((input) => resolve(configDir, input));
|
|
19
|
+
if (resolvedInputs === undefined) {
|
|
20
|
+
return source;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...source,
|
|
25
|
+
inputs: resolvedInputs,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateNoOutputsAreInputs(
|
|
30
|
+
inputs: readonly string[] | undefined,
|
|
31
|
+
output: string | undefined,
|
|
32
|
+
): void {
|
|
33
|
+
if (inputs === undefined || output === undefined) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let emittedArtifactPaths: ReturnType<typeof getEmittedArtifactPaths>;
|
|
38
|
+
try {
|
|
39
|
+
emittedArtifactPaths = getEmittedArtifactPaths(output);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throwValidation('contract.output', error instanceof Error ? error.message : String(error));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const emittedPaths = new Set([emittedArtifactPaths.jsonPath, emittedArtifactPaths.dtsPath]);
|
|
45
|
+
|
|
46
|
+
for (const input of inputs) {
|
|
47
|
+
if (emittedPaths.has(input)) {
|
|
48
|
+
throwValidation(
|
|
49
|
+
'contract.source.inputs[]',
|
|
50
|
+
'Config.contract.source.inputs must not include emitted artifact paths derived from contract.output',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function finalizeConfig(config: PrismaNextConfig, configDir: string): PrismaNextConfig {
|
|
57
|
+
if (!config.contract) {
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const contract = normalizeContractConfig(config.contract);
|
|
62
|
+
const source = finalizeContractSource(contract.source, configDir);
|
|
63
|
+
const output = resolve(configDir, contract.output);
|
|
64
|
+
|
|
65
|
+
validateNoOutputsAreInputs(source.inputs, output);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
...config,
|
|
69
|
+
contract: {
|
|
70
|
+
...contract,
|
|
71
|
+
source,
|
|
72
|
+
output,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -496,8 +496,9 @@ class ControlClientImpl implements ControlClient {
|
|
|
496
496
|
authoringContributions: stack.authoringContributions,
|
|
497
497
|
codecLookup: stack.codecLookup,
|
|
498
498
|
controlMutationDefaults: stack.controlMutationDefaults,
|
|
499
|
+
resolvedInputs: contractConfig.source.inputs ?? [],
|
|
499
500
|
};
|
|
500
|
-
const providerResult = await contractConfig.
|
|
501
|
+
const providerResult = await contractConfig.source.load(sourceContext);
|
|
501
502
|
if (!providerResult.ok) {
|
|
502
503
|
onProgress?.({
|
|
503
504
|
action: 'emit',
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
-
import { emit } from '@prisma-next/emitter';
|
|
3
|
+
import { emit, getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
5
5
|
import { abortable } from '@prisma-next/utils/abortable';
|
|
6
6
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
7
|
-
import { dirname
|
|
7
|
+
import { dirname } from 'pathe';
|
|
8
8
|
import { loadConfig } from '../../config-loader';
|
|
9
9
|
import { errorContractConfigMissing, errorRuntime } from '../../utils/cli-errors';
|
|
10
10
|
import { assertFrameworkComponentsCompatible } from '../../utils/framework-components';
|
|
@@ -72,27 +72,23 @@ export async function executeContractEmit(
|
|
|
72
72
|
why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
// Validate source exists and is callable
|
|
77
|
+
if (typeof contractConfig.source?.load !== 'function') {
|
|
76
78
|
throw errorContractConfigMissing({
|
|
77
|
-
why: 'Contract config
|
|
79
|
+
why: 'Contract config must include a valid source provider object',
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
let outputPaths: ReturnType<typeof getEmittedArtifactPaths>;
|
|
84
|
+
try {
|
|
85
|
+
outputPaths = getEmittedArtifactPaths(contractConfig.output);
|
|
86
|
+
} catch (error) {
|
|
83
87
|
throw errorContractConfigMissing({
|
|
84
|
-
why:
|
|
88
|
+
why: error instanceof Error ? error.message : String(error),
|
|
85
89
|
});
|
|
86
90
|
}
|
|
87
|
-
|
|
88
|
-
// Normalize configPath and resolve artifact paths relative to config file directory
|
|
89
|
-
const normalizedConfigPath = resolve(configPath);
|
|
90
|
-
const configDir = dirname(normalizedConfigPath);
|
|
91
|
-
const outputJsonPath = isAbsolute(contractConfig.output)
|
|
92
|
-
? contractConfig.output
|
|
93
|
-
: join(configDir, contractConfig.output);
|
|
94
|
-
// Colocate .d.ts with .json (contract.json → contract.d.ts)
|
|
95
|
-
const outputDtsPath = `${outputJsonPath.slice(0, -5)}.d.ts`;
|
|
91
|
+
const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = outputPaths;
|
|
96
92
|
|
|
97
93
|
const stack = createControlStack(config);
|
|
98
94
|
|
|
@@ -102,39 +98,40 @@ export async function executeContractEmit(
|
|
|
102
98
|
authoringContributions: stack.authoringContributions,
|
|
103
99
|
codecLookup: stack.codecLookup,
|
|
104
100
|
controlMutationDefaults: stack.controlMutationDefaults,
|
|
101
|
+
resolvedInputs: contractConfig.source.inputs ?? [],
|
|
105
102
|
};
|
|
106
103
|
|
|
107
|
-
let providerResult: Awaited<ReturnType<typeof contractConfig.source>>;
|
|
104
|
+
let providerResult: Awaited<ReturnType<typeof contractConfig.source.load>>;
|
|
108
105
|
try {
|
|
109
|
-
providerResult = await unlessAborted(contractConfig.source(sourceContext));
|
|
106
|
+
providerResult = await unlessAborted(contractConfig.source.load(sourceContext));
|
|
110
107
|
} catch (error) {
|
|
111
108
|
if (signal.aborted || isAbortError(error)) {
|
|
112
109
|
throw error;
|
|
113
110
|
}
|
|
114
111
|
throw errorRuntime('Failed to resolve contract source', {
|
|
115
112
|
why: error instanceof Error ? error.message : String(error),
|
|
116
|
-
fix: 'Ensure contract.source resolves to ok(Contract) or returns structured diagnostics.',
|
|
113
|
+
fix: 'Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.',
|
|
117
114
|
});
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
if (!isRecord(providerResult) || typeof providerResult.ok !== 'boolean') {
|
|
121
118
|
throw errorRuntime('Failed to resolve contract source', {
|
|
122
119
|
why: 'Contract source provider returned malformed result shape.',
|
|
123
|
-
fix: 'Ensure contract.source resolves to ok(Contract) or notOk({ summary, diagnostics }).',
|
|
120
|
+
fix: 'Ensure contract.source.load resolves to ok(Contract) or notOk({ summary, diagnostics }).',
|
|
124
121
|
});
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
if (providerResult.ok && !('value' in providerResult)) {
|
|
128
125
|
throw errorRuntime('Failed to resolve contract source', {
|
|
129
126
|
why: 'Contract source provider returned malformed success result: missing value.',
|
|
130
|
-
fix: 'Ensure contract.source success payload is ok(Contract).',
|
|
127
|
+
fix: 'Ensure contract.source.load success payload is ok(Contract).',
|
|
131
128
|
});
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
if (!providerResult.ok && !isProviderFailureLike(providerResult.failure)) {
|
|
135
132
|
throw errorRuntime('Failed to resolve contract source', {
|
|
136
133
|
why: 'Contract source provider returned malformed failure result: expected summary and diagnostics.',
|
|
137
|
-
fix: 'Ensure contract.source failure payload is notOk({ summary, diagnostics, meta? }).',
|
|
134
|
+
fix: 'Ensure contract.source.load failure payload is notOk({ summary, diagnostics, meta? }).',
|
|
138
135
|
});
|
|
139
136
|
}
|
|
140
137
|
|
|
@@ -160,7 +157,11 @@ export async function executeContractEmit(
|
|
|
160
157
|
const enrichedIR = enrichContract(providerResult.value as Contract, frameworkComponents);
|
|
161
158
|
|
|
162
159
|
familyInstance.validateContract(enrichedIR);
|
|
163
|
-
const emitResult = await unlessAborted(
|
|
160
|
+
const emitResult = await unlessAborted(
|
|
161
|
+
emit(enrichedIR, stack, config.family.emission, {
|
|
162
|
+
outputJsonPath: outputJsonPath,
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
164
165
|
|
|
165
166
|
// Create directory if needed and write files (both colocated)
|
|
166
167
|
await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
|
package/src/control-api/types.ts
CHANGED
|
@@ -248,7 +248,7 @@ export interface EmitContractConfig {
|
|
|
248
248
|
/**
|
|
249
249
|
* Contract source provider.
|
|
250
250
|
*/
|
|
251
|
-
readonly
|
|
251
|
+
readonly source: ContractSourceProvider;
|
|
252
252
|
/**
|
|
253
253
|
* Output path for contract.json.
|
|
254
254
|
* The .d.ts types file will be colocated (e.g., contract.json → contract.d.ts).
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The migration-file CLI interface: the actor invoked when the author runs
|
|
3
|
+
* `node migration.ts` directly.
|
|
4
|
+
*
|
|
5
|
+
* Naming: this is *not* a "migration runner" in the apply-time sense. The
|
|
6
|
+
* apply-time runner is the thing `prisma-next migration apply` uses to
|
|
7
|
+
* execute migration JSON ops against a database. `MigrationCLI` is the
|
|
8
|
+
* tiny CLI surface owned by an authored `migration.ts` file: parse the
|
|
9
|
+
* file's argv, load the project's `prisma-next.config.ts`, assemble a
|
|
10
|
+
* `ControlStack`, instantiate the migration class, and serialize.
|
|
11
|
+
*
|
|
12
|
+
* The user authors a migration class, then calls
|
|
13
|
+
* `MigrationCLI.run(import.meta.url, MigrationClass)` at module scope
|
|
14
|
+
* after the class definition. When the file is invoked as a node
|
|
15
|
+
* entrypoint (`node migration.ts`), the CLI:
|
|
16
|
+
*
|
|
17
|
+
* 1. Detects whether the file is the direct entrypoint (no-op when imported).
|
|
18
|
+
* 2. Parses CLI args (`--help`, `--dry-run`, `--config <path>`).
|
|
19
|
+
* 3. Loads the project's `prisma-next.config.ts` via the same `loadConfig`
|
|
20
|
+
* the CLI commands use, walking up from the migration file's directory.
|
|
21
|
+
* 4. Probe-instantiates the migration class without a stack so it can read
|
|
22
|
+
* `targetId` and verify it matches `config.target.targetId`
|
|
23
|
+
* (`PN-MIG-2006` on mismatch) before any stack-driven adapter
|
|
24
|
+
* construction runs.
|
|
25
|
+
* 5. Assembles a `ControlStack` from the loaded config descriptors and
|
|
26
|
+
* constructs the migration with that stack.
|
|
27
|
+
* 6. Reads any previously-scaffolded `migration.json`, then calls
|
|
28
|
+
* `buildMigrationArtifacts` from `@prisma-next/migration-tools` to
|
|
29
|
+
* produce in-memory `ops.json` + `migration.json` content. Persists
|
|
30
|
+
* the result to disk (or prints in dry-run mode).
|
|
31
|
+
*
|
|
32
|
+
* File I/O lives here, in `@prisma-next/cli`: this is the only place
|
|
33
|
+
* that legitimately combines config loading, stack assembly, and
|
|
34
|
+
* on-disk persistence. `@prisma-next/migration-tools` owns the pure
|
|
35
|
+
* conversion from a `Migration` instance to artifact strings; `Migration`
|
|
36
|
+
* stays a pure abstract class.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
import { CliStructuredError, errorMigrationCliInvalidConfigArg } from '@prisma-next/errors/control';
|
|
42
|
+
import { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';
|
|
43
|
+
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
44
|
+
import {
|
|
45
|
+
buildMigrationArtifacts,
|
|
46
|
+
isDirectEntrypoint,
|
|
47
|
+
type Migration,
|
|
48
|
+
printMigrationHelp,
|
|
49
|
+
} from '@prisma-next/migration-tools/migration';
|
|
50
|
+
import type { MigrationManifest } from '@prisma-next/migration-tools/types';
|
|
51
|
+
import { dirname, join } from 'pathe';
|
|
52
|
+
import { loadConfig } from './config-loader';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Constructor shape accepted by `MigrationCLI.run`. `Migration` subclasses
|
|
56
|
+
* accept an optional `ControlStack` in their constructor (each subclass
|
|
57
|
+
* narrows the stack to its own family/target generics); the CLI always
|
|
58
|
+
* passes one assembled from the loaded config. We use a rest-args `any[]`
|
|
59
|
+
* constructor signature so that subclass constructors with narrower
|
|
60
|
+
* parameter types remain assignable - constructor type compatibility in
|
|
61
|
+
* TS is contravariant in the parameter, and a wider `unknown` parameter
|
|
62
|
+
* on the alias side would reject any narrower subclass signature.
|
|
63
|
+
*
|
|
64
|
+
* The CLI only ever passes one argument (`new MigrationClass(stack)`);
|
|
65
|
+
* the rest-arity is purely a type-compatibility concession for subclass
|
|
66
|
+
* constructors that declare narrower parameter types, not an extension
|
|
67
|
+
* point for additional construction arguments.
|
|
68
|
+
*/
|
|
69
|
+
// biome-ignore lint/suspicious/noExplicitAny: see JSDoc - rest args with any are the idiomatic TS pattern for accepting arbitrary subclass constructor signatures
|
|
70
|
+
export type MigrationConstructor = new (...args: any[]) => Migration;
|
|
71
|
+
|
|
72
|
+
interface ParsedArgs {
|
|
73
|
+
readonly help: boolean;
|
|
74
|
+
readonly dryRun: boolean;
|
|
75
|
+
readonly configPath: string | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse the subset of `process.argv` that `MigrationCLI.run` cares about.
|
|
80
|
+
* Recognised flags: `--help`, `--dry-run`, `--config <path>` /
|
|
81
|
+
* `--config=<path>`. Unknown flags are ignored to keep the surface
|
|
82
|
+
* forgiving for ad-hoc tooling that wraps a migration file.
|
|
83
|
+
*
|
|
84
|
+
* Throws `errorMigrationCliInvalidConfigArg` (`PN-CLI-4012`) when
|
|
85
|
+
* `--config` is missing its path argument or is followed by another flag
|
|
86
|
+
* (e.g. `--config --dry-run`); silently consuming the next flag would
|
|
87
|
+
* either drop dry-run handling or serialize against the wrong project.
|
|
88
|
+
*
|
|
89
|
+
* NOTE: this hand-rolled parser is a known wart, tracked separately by
|
|
90
|
+
* TML-2318 ("Migration CLI: replace handrolled arg parser with shared
|
|
91
|
+
* CLI library"). Until that lands the surface is intentionally tiny.
|
|
92
|
+
*/
|
|
93
|
+
function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
94
|
+
let help = false;
|
|
95
|
+
let dryRun = false;
|
|
96
|
+
let configPath: string | undefined;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
const arg = argv[i]!;
|
|
100
|
+
if (arg === '--help' || arg === '-h') {
|
|
101
|
+
help = true;
|
|
102
|
+
} else if (arg === '--dry-run') {
|
|
103
|
+
dryRun = true;
|
|
104
|
+
} else if (arg === '--config') {
|
|
105
|
+
const next = argv[i + 1];
|
|
106
|
+
if (next === undefined) {
|
|
107
|
+
throw errorMigrationCliInvalidConfigArg();
|
|
108
|
+
}
|
|
109
|
+
if (next.startsWith('-')) {
|
|
110
|
+
throw errorMigrationCliInvalidConfigArg({ nextToken: next });
|
|
111
|
+
}
|
|
112
|
+
configPath = next;
|
|
113
|
+
i++;
|
|
114
|
+
} else if (arg.startsWith('--config=')) {
|
|
115
|
+
configPath = arg.slice('--config='.length);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { help, dryRun, configPath };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The CLI surface invoked by an authored `migration.ts` file. Exposed as
|
|
124
|
+
* a class with a static `run` method (rather than a free function) to
|
|
125
|
+
* give the concept a stable identity in the ubiquitous language: this is
|
|
126
|
+
* the "migration-file CLI", distinct from the apply-time runner that
|
|
127
|
+
* executes migration JSON ops.
|
|
128
|
+
*
|
|
129
|
+
* Currently a single static method. Future surface (e.g. a programmatic
|
|
130
|
+
* `MigrationCLI.serializeOnly(...)` for tests, or extra subcommands) can
|
|
131
|
+
* land here without changing the import shape used by every authored
|
|
132
|
+
* migration.
|
|
133
|
+
*/
|
|
134
|
+
// biome-ignore lint/complexity/noStaticOnlyClass: see JSDoc - intentional class facade for the migration-file CLI surface; future methods will share state derived from argv/config.
|
|
135
|
+
export class MigrationCLI {
|
|
136
|
+
/**
|
|
137
|
+
* Orchestrates a class-flow `migration.ts` script run. Awaitable:
|
|
138
|
+
* callers may `await MigrationCLI.run(...)` to surface async failures
|
|
139
|
+
* from config loading, but the typical usage pattern (top-level call
|
|
140
|
+
* after the class definition) does not require awaiting because
|
|
141
|
+
* node's module evaluation keeps the promise alive until completion.
|
|
142
|
+
*
|
|
143
|
+
* Any throwable inside this function must surface through the internal
|
|
144
|
+
* try/catch — script callers do not await, so an unhandled rejection
|
|
145
|
+
* would silently exit 0. Treat the try/catch as load-bearing for the
|
|
146
|
+
* no-await usage pattern.
|
|
147
|
+
*/
|
|
148
|
+
static async run(importMetaUrl: string, MigrationClass: MigrationConstructor): Promise<void> {
|
|
149
|
+
if (!importMetaUrl) return;
|
|
150
|
+
if (!isDirectEntrypoint(importMetaUrl)) return;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const args = parseArgs(process.argv.slice(2));
|
|
154
|
+
|
|
155
|
+
if (args.help) {
|
|
156
|
+
printMigrationHelp();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const migrationFile = fileURLToPath(importMetaUrl);
|
|
161
|
+
const migrationDir = dirname(migrationFile);
|
|
162
|
+
|
|
163
|
+
const config = await loadConfig(args.configPath);
|
|
164
|
+
|
|
165
|
+
// Probe-instantiate without a stack so we can read `targetId` before
|
|
166
|
+
// any target-specific constructor side effects (e.g.
|
|
167
|
+
// `PostgresMigration`'s `stack.adapter.create(stack)`) run. Concrete
|
|
168
|
+
// subclasses are required to accept the no-arg form; the abstract
|
|
169
|
+
// `Migration` constructor declares `stack?` and target subclasses
|
|
170
|
+
// (Postgres, Mongo) propagate that optionality. This makes the
|
|
171
|
+
// target-mismatch guard fail fast with `PN-MIG-2006` before any
|
|
172
|
+
// stack-driven adapter construction begins, even if the wrong-target
|
|
173
|
+
// adapter's `create` would otherwise succeed and silently misshapen
|
|
174
|
+
// the stored adapter cast.
|
|
175
|
+
const probe = new MigrationClass();
|
|
176
|
+
|
|
177
|
+
if (probe.targetId !== config.target.targetId) {
|
|
178
|
+
throw errorMigrationTargetMismatch({
|
|
179
|
+
migrationTargetId: probe.targetId,
|
|
180
|
+
configTargetId: config.target.targetId,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const stack = createControlStack(config);
|
|
185
|
+
const instance = new MigrationClass(stack);
|
|
186
|
+
|
|
187
|
+
serializeMigrationToDisk(instance, migrationDir, args.dryRun);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (CliStructuredError.is(err)) {
|
|
190
|
+
process.stderr.write(`${err.message}: ${err.why}\n`);
|
|
191
|
+
} else {
|
|
192
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
193
|
+
}
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Read a previously-scaffolded `migration.json` from disk, returning
|
|
201
|
+
* `null` when the file is missing or unparseable. The CLI feeds this into
|
|
202
|
+
* `buildMigrationArtifacts` so the pure builder can preserve fields owned
|
|
203
|
+
* by `migration plan` (contract bookends, hints, labels, `createdAt`)
|
|
204
|
+
* across re-emits.
|
|
205
|
+
*/
|
|
206
|
+
function readExistingManifest(manifestPath: string): Partial<MigrationManifest> | null {
|
|
207
|
+
let raw: string;
|
|
208
|
+
try {
|
|
209
|
+
raw = readFileSync(manifestPath, 'utf-8');
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(raw) as Partial<MigrationManifest>;
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Persist a migration instance's artifacts to `migrationDir`. In
|
|
222
|
+
* `dryRun` mode the artifacts are printed to stdout (with the same
|
|
223
|
+
* `--- migration.json --- / --- ops.json ---` framing the legacy
|
|
224
|
+
* `serializeMigration` helper used) and no files are written. Otherwise
|
|
225
|
+
* `ops.json` and `migration.json` are written next to `migration.ts` and
|
|
226
|
+
* a confirmation line is printed.
|
|
227
|
+
*
|
|
228
|
+
* File I/O lives in the CLI rather than `@prisma-next/migration-tools`
|
|
229
|
+
* so the migration-tools package stays focused on the pure
|
|
230
|
+
* `Migration` → in-memory artifact conversion. The CLI is the only
|
|
231
|
+
* legitimate site for combining config loading, stack assembly, and
|
|
232
|
+
* filesystem persistence.
|
|
233
|
+
*/
|
|
234
|
+
function serializeMigrationToDisk(
|
|
235
|
+
instance: Migration,
|
|
236
|
+
migrationDir: string,
|
|
237
|
+
dryRun: boolean,
|
|
238
|
+
): void {
|
|
239
|
+
const manifestPath = join(migrationDir, 'migration.json');
|
|
240
|
+
const existing = readExistingManifest(manifestPath);
|
|
241
|
+
const { opsJson, manifestJson } = buildMigrationArtifacts(instance, existing);
|
|
242
|
+
|
|
243
|
+
if (dryRun) {
|
|
244
|
+
process.stdout.write(`--- migration.json ---\n${manifestJson}\n`);
|
|
245
|
+
process.stdout.write('--- ops.json ---\n');
|
|
246
|
+
process.stdout.write(`${opsJson}\n`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
writeFileSync(join(migrationDir, 'ops.json'), opsJson);
|
|
251
|
+
writeFileSync(manifestPath, manifestJson);
|
|
252
|
+
|
|
253
|
+
process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\n`);
|
|
254
|
+
}
|
package/src/utils/cli-errors.ts
CHANGED
|
@@ -4,12 +4,7 @@ import { hasMigrations } from '@prisma-next/framework-components/control';
|
|
|
4
4
|
import type { PathDecision } from '@prisma-next/migration-tools/dag';
|
|
5
5
|
import { reconstructGraph } from '@prisma-next/migration-tools/dag';
|
|
6
6
|
import { readMigrationsDir } from '@prisma-next/migration-tools/io';
|
|
7
|
-
import type {
|
|
8
|
-
AttestedMigrationBundle,
|
|
9
|
-
DraftMigrationBundle,
|
|
10
|
-
MigrationGraph,
|
|
11
|
-
} from '@prisma-next/migration-tools/types';
|
|
12
|
-
import { isAttested, isDraft } from '@prisma-next/migration-tools/types';
|
|
7
|
+
import type { MigrationBundle, MigrationGraph } from '@prisma-next/migration-tools/types';
|
|
13
8
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
14
9
|
import type { Command } from 'commander';
|
|
15
10
|
import { relative, resolve } from 'pathe';
|
|
@@ -150,32 +145,33 @@ export function getTargetMigrations(target: ControlTargetDescriptor<string, stri
|
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
/**
|
|
153
|
-
* Reads the migrations directory
|
|
154
|
-
*
|
|
155
|
-
*
|
|
148
|
+
* Reads the migrations directory and builds the migration graph from all
|
|
149
|
+
* bundles. Throws on I/O or graph errors — callers handle error mapping.
|
|
150
|
+
*
|
|
151
|
+
* Every on-disk bundle is content-addressed (`migrationId` is always a
|
|
152
|
+
* string); there is no draft state to filter out.
|
|
156
153
|
*/
|
|
157
154
|
export async function loadMigrationBundles(migrationsDir: string): Promise<{
|
|
158
|
-
bundles: readonly
|
|
155
|
+
bundles: readonly MigrationBundle[];
|
|
159
156
|
graph: MigrationGraph;
|
|
160
157
|
}> {
|
|
161
|
-
const
|
|
162
|
-
const bundles = allBundles.filter(isAttested);
|
|
158
|
+
const bundles = await readMigrationsDir(migrationsDir);
|
|
163
159
|
const graph = reconstructGraph(bundles);
|
|
164
160
|
return { bundles, graph };
|
|
165
161
|
}
|
|
166
162
|
|
|
167
163
|
export interface MigrationBundleSet {
|
|
168
|
-
readonly
|
|
169
|
-
readonly drafts: readonly DraftMigrationBundle[];
|
|
164
|
+
readonly bundles: readonly MigrationBundle[];
|
|
170
165
|
readonly graph: MigrationGraph;
|
|
171
166
|
}
|
|
172
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Alias of `loadMigrationBundles` retained for naming-clarity in commands
|
|
170
|
+
* that previously needed both attested and draft splits. With the
|
|
171
|
+
* collapse of the draft state, both helpers do the same thing.
|
|
172
|
+
*/
|
|
173
173
|
export async function loadAllBundles(migrationsDir: string): Promise<MigrationBundleSet> {
|
|
174
|
-
|
|
175
|
-
const attested = all.filter(isAttested);
|
|
176
|
-
const drafts = all.filter(isDraft);
|
|
177
|
-
const graph = reconstructGraph(attested);
|
|
178
|
-
return { attested, drafts, graph };
|
|
174
|
+
return loadMigrationBundles(migrationsDir);
|
|
179
175
|
}
|
|
180
176
|
|
|
181
177
|
/**
|
|
@@ -39,12 +39,6 @@ export interface EdgeStatus {
|
|
|
39
39
|
readonly status: EdgeStatusKind;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export interface DraftEdge {
|
|
43
|
-
readonly from: string;
|
|
44
|
-
readonly to: string;
|
|
45
|
-
readonly dirName: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
42
|
export interface MigrationGraphInput {
|
|
49
43
|
readonly graph: MigrationGraph;
|
|
50
44
|
readonly mode: 'online' | 'offline';
|
|
@@ -58,8 +52,6 @@ export interface MigrationGraphInput {
|
|
|
58
52
|
* icons (✓/⧗) are baked into edge labels. Undefined in offline mode.
|
|
59
53
|
*/
|
|
60
54
|
readonly edgeStatuses?: readonly EdgeStatus[] | undefined;
|
|
61
|
-
/** Draft migrations to render as dashed edges. */
|
|
62
|
-
readonly draftEdges?: readonly DraftEdge[] | undefined;
|
|
63
55
|
}
|
|
64
56
|
|
|
65
57
|
export interface MigrationRenderInput {
|
|
@@ -204,7 +196,9 @@ export function migrationGraphToRenderInput(input: MigrationGraphInput): Migrati
|
|
|
204
196
|
spineTargetHash = lastEdge?.to ?? EMPTY_CONTRACT_HASH;
|
|
205
197
|
}
|
|
206
198
|
|
|
207
|
-
// Contract not in
|
|
199
|
+
// Contract not in the migration graph — connect from spine target with a
|
|
200
|
+
// dashed edge so the user can see the gap (contract has changed but no
|
|
201
|
+
// migration has been planned yet).
|
|
208
202
|
if (contractHash !== EMPTY_CONTRACT_HASH && !graph.nodes.has(contractHash)) {
|
|
209
203
|
const contractMarkers: NodeMarker[] = [];
|
|
210
204
|
if (mode === 'online' && markerHash === contractHash) {
|
|
@@ -216,13 +210,10 @@ export function migrationGraphToRenderInput(input: MigrationGraphInput): Migrati
|
|
|
216
210
|
markers: contractMarkers,
|
|
217
211
|
});
|
|
218
212
|
|
|
219
|
-
|
|
220
|
-
const fromHash = matchingDraft?.from ?? spineTargetHash;
|
|
221
|
-
if (graph.nodes.has(fromHash) || fromHash === spineTargetHash) {
|
|
213
|
+
if (graph.nodes.has(spineTargetHash) || spineTargetHash === EMPTY_CONTRACT_HASH) {
|
|
222
214
|
edgeList.push({
|
|
223
|
-
from: toShortId(
|
|
215
|
+
from: toShortId(spineTargetHash),
|
|
224
216
|
to: shortHash(contractHash),
|
|
225
|
-
...ifDefined('label', matchingDraft ? `${matchingDraft.dirName} [draft]` : undefined),
|
|
226
217
|
style: 'dashed',
|
|
227
218
|
});
|
|
228
219
|
}
|
|
@@ -137,7 +137,6 @@ function getCommandDocsUrl(commandPath: string): string | undefined {
|
|
|
137
137
|
'migration apply': 'https://pris.ly/migration-apply',
|
|
138
138
|
'migration show': 'https://pris.ly/migration-show',
|
|
139
139
|
'migration status': 'https://pris.ly/migration-status',
|
|
140
|
-
'migration emit': 'https://pris.ly/migration-emit',
|
|
141
140
|
};
|
|
142
141
|
return docsMap[commandPath];
|
|
143
142
|
}
|
|
@@ -136,10 +136,6 @@ export interface MigrationApplyCommandOutputResult {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
export interface MigrationEmitCommandOutputResult {
|
|
140
|
-
readonly migrationId: string;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
139
|
export function formatMigrationApplyCommandOutput(
|
|
144
140
|
result: MigrationApplyCommandOutputResult,
|
|
145
141
|
flags: GlobalFlags,
|
|
@@ -182,31 +178,12 @@ export function formatMigrationApplyCommandOutput(
|
|
|
182
178
|
return lines.join('\n');
|
|
183
179
|
}
|
|
184
180
|
|
|
185
|
-
export function formatMigrationEmitCommandOutput(
|
|
186
|
-
result: MigrationEmitCommandOutputResult,
|
|
187
|
-
flags: GlobalFlags,
|
|
188
|
-
): string {
|
|
189
|
-
if (flags.quiet) {
|
|
190
|
-
return '';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const lines: string[] = [];
|
|
194
|
-
const useColor = flags.color !== false;
|
|
195
|
-
const formatGreen = createColorFormatter(useColor, green);
|
|
196
|
-
const formatDimText = (text: string) => formatDim(useColor, text);
|
|
197
|
-
|
|
198
|
-
lines.push(`${formatGreen('✔')} Emitted ops.json and attested migration`);
|
|
199
|
-
lines.push(formatDimText(` migrationId: ${result.migrationId}`));
|
|
200
|
-
|
|
201
|
-
return lines.join('\n');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
181
|
interface MigrationShowResult {
|
|
205
182
|
readonly dirName: string;
|
|
206
183
|
readonly dirPath: string;
|
|
207
184
|
readonly from: string;
|
|
208
185
|
readonly to: string;
|
|
209
|
-
readonly migrationId: string
|
|
186
|
+
readonly migrationId: string;
|
|
210
187
|
readonly kind: string;
|
|
211
188
|
readonly createdAt: string;
|
|
212
189
|
readonly operations: readonly {
|
|
@@ -234,11 +211,7 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
|
|
|
234
211
|
lines.push(`${formatDimText(` kind: ${result.kind}`)}`);
|
|
235
212
|
lines.push(`${formatDimText(` from: ${result.from}`)}`);
|
|
236
213
|
lines.push(`${formatDimText(` to: ${result.to}`)}`);
|
|
237
|
-
|
|
238
|
-
lines.push(`${formatDimText(` migrationId: ${result.migrationId}`)}`);
|
|
239
|
-
} else {
|
|
240
|
-
lines.push(`${formatYellow(' migrationId: (draft — not yet attested)')}`);
|
|
241
|
-
}
|
|
214
|
+
lines.push(`${formatDimText(` migrationId: ${result.migrationId}`)}`);
|
|
242
215
|
lines.push(`${formatDimText(` created: ${result.createdAt}`)}`);
|
|
243
216
|
|
|
244
217
|
lines.push('');
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { CliStructuredError, errorConfigValidation as errorConfigValidation$1, errorContractConfigMissing as errorContractConfigMissing$1, errorContractValidationFailed, errorDatabaseConnectionRequired, errorDriverRequired, errorFileNotFound, errorMigrationPlanningFailed, errorTargetMigrationNotSupported, errorUnexpected as errorUnexpected$1 } from "@prisma-next/errors/control";
|
|
2
|
-
import { ERROR_CODE_DESTRUCTIVE_CHANGES, errorDestructiveChanges, errorHashMismatch, errorMarkerMissing, errorRunnerFailed, errorRuntime as errorRuntime$1, errorTargetMismatch } from "@prisma-next/errors/execution";
|
|
3
|
-
import { errorMigrationFileMissing } from "@prisma-next/errors/migration";
|
|
4
|
-
|
|
5
|
-
export { errorTargetMismatch as _, errorContractValidationFailed as a, errorDriverRequired as c, errorMarkerMissing as d, errorMigrationFileMissing as f, errorTargetMigrationNotSupported as g, errorRuntime$1 as h, errorContractConfigMissing$1 as i, errorFileNotFound as l, errorRunnerFailed as m, ERROR_CODE_DESTRUCTIVE_CHANGES as n, errorDatabaseConnectionRequired as o, errorMigrationPlanningFailed as p, errorConfigValidation$1 as r, errorDestructiveChanges as s, CliStructuredError as t, errorHashMismatch as u, errorUnexpected$1 as v };
|