@prisma-next/migration-tools 0.4.1 → 0.4.3
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 +34 -22
- package/dist/{constants-BRi0X7B_.mjs → constants-BQEHsaEx.mjs} +1 -1
- package/dist/{constants-BRi0X7B_.mjs.map → constants-BQEHsaEx.mjs.map} +1 -1
- package/dist/errors-CfmjBeK0.mjs +272 -0
- package/dist/errors-CfmjBeK0.mjs.map +1 -0
- package/dist/exports/constants.mjs +1 -1
- package/dist/exports/errors.d.mts +63 -0
- package/dist/exports/errors.d.mts.map +1 -0
- package/dist/exports/errors.mjs +3 -0
- package/dist/exports/graph.d.mts +2 -0
- package/dist/exports/graph.mjs +1 -0
- package/dist/exports/hash.d.mts +52 -0
- package/dist/exports/hash.d.mts.map +1 -0
- package/dist/exports/hash.mjs +3 -0
- package/dist/exports/invariants.d.mts +24 -0
- package/dist/exports/invariants.d.mts.map +1 -0
- package/dist/exports/invariants.mjs +4 -0
- package/dist/exports/io.d.mts +7 -6
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +162 -2
- package/dist/exports/io.mjs.map +1 -0
- package/dist/exports/metadata.d.mts +2 -0
- package/dist/exports/metadata.mjs +1 -0
- package/dist/exports/migration-graph.d.mts +124 -0
- package/dist/exports/migration-graph.d.mts.map +1 -0
- package/dist/exports/migration-graph.mjs +526 -0
- package/dist/exports/migration-graph.mjs.map +1 -0
- package/dist/exports/migration-ts.d.mts +5 -1
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs +6 -2
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +51 -20
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +110 -99
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +2 -0
- package/dist/exports/package.mjs +1 -0
- package/dist/exports/refs.d.mts +11 -5
- package/dist/exports/refs.d.mts.map +1 -1
- package/dist/exports/refs.mjs +106 -30
- package/dist/exports/refs.mjs.map +1 -1
- package/dist/graph-BHPv-9Gl.d.mts +28 -0
- package/dist/graph-BHPv-9Gl.d.mts.map +1 -0
- package/dist/hash-BARZdVgW.mjs +76 -0
- package/dist/hash-BARZdVgW.mjs.map +1 -0
- package/dist/invariants-30VA65sB.mjs +42 -0
- package/dist/invariants-30VA65sB.mjs.map +1 -0
- package/dist/metadata-BP1cmU7Z.d.mts +50 -0
- package/dist/metadata-BP1cmU7Z.d.mts.map +1 -0
- package/dist/op-schema-DZKFua46.mjs +14 -0
- package/dist/op-schema-DZKFua46.mjs.map +1 -0
- package/dist/package-5HCCg0z-.d.mts +21 -0
- package/dist/package-5HCCg0z-.d.mts.map +1 -0
- package/package.json +30 -14
- package/src/errors.ts +210 -17
- package/src/exports/errors.ts +7 -0
- package/src/exports/graph.ts +1 -0
- package/src/exports/hash.ts +2 -0
- package/src/exports/invariants.ts +1 -0
- package/src/exports/io.ts +1 -1
- package/src/exports/metadata.ts +1 -0
- package/src/exports/{dag.ts → migration-graph.ts} +3 -2
- package/src/exports/migration.ts +7 -1
- package/src/exports/package.ts +1 -0
- package/src/exports/refs.ts +10 -2
- package/src/graph-ops.ts +57 -30
- package/src/graph.ts +25 -0
- package/src/hash.ts +91 -0
- package/src/invariants.ts +45 -0
- package/src/io.ts +57 -31
- package/src/metadata.ts +41 -0
- package/src/migration-base.ts +155 -124
- package/src/migration-graph.ts +676 -0
- package/src/migration-ts.ts +5 -1
- package/src/op-schema.ts +11 -0
- package/src/package.ts +18 -0
- package/src/refs.ts +148 -37
- package/dist/attestation-DtF8tEOM.mjs +0 -65
- package/dist/attestation-DtF8tEOM.mjs.map +0 -1
- package/dist/errors-BKbRGCJM.mjs +0 -160
- package/dist/errors-BKbRGCJM.mjs.map +0 -1
- package/dist/exports/attestation.d.mts +0 -37
- package/dist/exports/attestation.d.mts.map +0 -1
- package/dist/exports/attestation.mjs +0 -4
- package/dist/exports/dag.d.mts +0 -51
- package/dist/exports/dag.d.mts.map +0 -1
- package/dist/exports/dag.mjs +0 -386
- package/dist/exports/dag.mjs.map +0 -1
- package/dist/exports/types.d.mts +0 -35
- package/dist/exports/types.d.mts.map +0 -1
- package/dist/exports/types.mjs +0 -3
- package/dist/io-CCnYsUHU.mjs +0 -153
- package/dist/io-CCnYsUHU.mjs.map +0 -1
- package/dist/types-DyGXcWWp.d.mts +0 -71
- package/dist/types-DyGXcWWp.d.mts.map +0 -1
- package/src/attestation.ts +0 -81
- package/src/dag.ts +0 -426
- package/src/exports/attestation.ts +0 -2
- package/src/exports/types.ts +0 -10
- package/src/types.ts +0 -66
package/src/io.ts
CHANGED
|
@@ -7,9 +7,15 @@ import {
|
|
|
7
7
|
errorInvalidJson,
|
|
8
8
|
errorInvalidManifest,
|
|
9
9
|
errorInvalidSlug,
|
|
10
|
+
errorMigrationHashMismatch,
|
|
10
11
|
errorMissingFile,
|
|
12
|
+
errorProvidedInvariantsMismatch,
|
|
11
13
|
} from './errors';
|
|
12
|
-
import
|
|
14
|
+
import { verifyMigrationHash } from './hash';
|
|
15
|
+
import { deriveProvidedInvariants } from './invariants';
|
|
16
|
+
import type { MigrationMetadata } from './metadata';
|
|
17
|
+
import { MigrationOpsSchema } from './op-schema';
|
|
18
|
+
import type { MigrationOps, MigrationPackage } from './package';
|
|
13
19
|
|
|
14
20
|
const MANIFEST_FILE = 'migration.json';
|
|
15
21
|
const OPS_FILE = 'ops.json';
|
|
@@ -25,15 +31,16 @@ const MigrationHintsSchema = type({
|
|
|
25
31
|
plannerVersion: 'string',
|
|
26
32
|
});
|
|
27
33
|
|
|
28
|
-
const
|
|
29
|
-
|
|
34
|
+
const MigrationMetadataSchema = type({
|
|
35
|
+
'+': 'reject',
|
|
36
|
+
from: 'string > 0 | null',
|
|
30
37
|
to: 'string',
|
|
31
|
-
|
|
32
|
-
kind: "'regular' | 'baseline'",
|
|
38
|
+
migrationHash: 'string',
|
|
33
39
|
fromContract: 'object | null',
|
|
34
40
|
toContract: 'object',
|
|
35
41
|
hints: MigrationHintsSchema,
|
|
36
42
|
labels: 'string[]',
|
|
43
|
+
providedInvariants: 'string[]',
|
|
37
44
|
'authorship?': type({
|
|
38
45
|
'author?': 'string',
|
|
39
46
|
'email?': 'string',
|
|
@@ -45,18 +52,9 @@ const MigrationManifestSchema = type({
|
|
|
45
52
|
createdAt: 'string',
|
|
46
53
|
});
|
|
47
54
|
|
|
48
|
-
const MigrationOpSchema = type({
|
|
49
|
-
id: 'string',
|
|
50
|
-
label: 'string',
|
|
51
|
-
operationClass: "'additive' | 'widening' | 'destructive' | 'data'",
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.
|
|
55
|
-
const MigrationOpsSchema = MigrationOpSchema.array();
|
|
56
|
-
|
|
57
55
|
export async function writeMigrationPackage(
|
|
58
56
|
dir: string,
|
|
59
|
-
|
|
57
|
+
metadata: MigrationMetadata,
|
|
60
58
|
ops: MigrationOps,
|
|
61
59
|
): Promise<void> {
|
|
62
60
|
await mkdir(dirname(dir), { recursive: true });
|
|
@@ -70,7 +68,9 @@ export async function writeMigrationPackage(
|
|
|
70
68
|
throw error;
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(
|
|
71
|
+
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {
|
|
72
|
+
flag: 'wx',
|
|
73
|
+
});
|
|
74
74
|
await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -98,18 +98,18 @@ export async function copyFilesWithRename(
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export async function
|
|
101
|
+
export async function writeMigrationMetadata(
|
|
102
102
|
dir: string,
|
|
103
|
-
|
|
103
|
+
metadata: MigrationMetadata,
|
|
104
104
|
): Promise<void> {
|
|
105
|
-
await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(
|
|
105
|
+
await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
export async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {
|
|
109
109
|
await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
export async function readMigrationPackage(dir: string): Promise<
|
|
112
|
+
export async function readMigrationPackage(dir: string): Promise<MigrationPackage> {
|
|
113
113
|
const manifestPath = join(dir, MANIFEST_FILE);
|
|
114
114
|
const opsPath = join(dir, OPS_FILE);
|
|
115
115
|
|
|
@@ -133,9 +133,9 @@ export async function readMigrationPackage(dir: string): Promise<MigrationBundle
|
|
|
133
133
|
throw error;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
let
|
|
136
|
+
let metadata: MigrationMetadata;
|
|
137
137
|
try {
|
|
138
|
-
|
|
138
|
+
metadata = JSON.parse(manifestRaw);
|
|
139
139
|
} catch (e) {
|
|
140
140
|
throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
|
|
141
141
|
}
|
|
@@ -147,22 +147,48 @@ export async function readMigrationPackage(dir: string): Promise<MigrationBundle
|
|
|
147
147
|
throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
validateMetadata(metadata, manifestPath);
|
|
151
151
|
validateOps(ops, opsPath);
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
// Re-derive before the hash check so format/duplicate diagnostics
|
|
154
|
+
// fire with their dedicated codes rather than as a generic hash mismatch.
|
|
155
|
+
const derivedInvariants = deriveProvidedInvariants(ops);
|
|
156
|
+
if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) {
|
|
157
|
+
throw errorProvidedInvariantsMismatch(
|
|
158
|
+
manifestPath,
|
|
159
|
+
metadata.providedInvariants,
|
|
160
|
+
derivedInvariants,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pkg: MigrationPackage = {
|
|
154
165
|
dirName: basename(dir),
|
|
155
166
|
dirPath: dir,
|
|
156
|
-
|
|
167
|
+
metadata,
|
|
157
168
|
ops,
|
|
158
169
|
};
|
|
170
|
+
|
|
171
|
+
const verification = verifyMigrationHash(pkg);
|
|
172
|
+
if (!verification.ok) {
|
|
173
|
+
throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return pkg;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
180
|
+
if (a.length !== b.length) return false;
|
|
181
|
+
for (let i = 0; i < a.length; i++) {
|
|
182
|
+
if (a[i] !== b[i]) return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
159
185
|
}
|
|
160
186
|
|
|
161
|
-
function
|
|
162
|
-
|
|
187
|
+
function validateMetadata(
|
|
188
|
+
metadata: unknown,
|
|
163
189
|
filePath: string,
|
|
164
|
-
): asserts
|
|
165
|
-
const result =
|
|
190
|
+
): asserts metadata is MigrationMetadata {
|
|
191
|
+
const result = MigrationMetadataSchema(metadata);
|
|
166
192
|
if (result instanceof type.errors) {
|
|
167
193
|
throw errorInvalidManifest(filePath, result.summary);
|
|
168
194
|
}
|
|
@@ -177,7 +203,7 @@ function validateOps(ops: unknown, filePath: string): asserts ops is MigrationOp
|
|
|
177
203
|
|
|
178
204
|
export async function readMigrationsDir(
|
|
179
205
|
migrationsRoot: string,
|
|
180
|
-
): Promise<readonly
|
|
206
|
+
): Promise<readonly MigrationPackage[]> {
|
|
181
207
|
let entries: string[];
|
|
182
208
|
try {
|
|
183
209
|
entries = await readdir(migrationsRoot);
|
|
@@ -188,7 +214,7 @@ export async function readMigrationsDir(
|
|
|
188
214
|
throw error;
|
|
189
215
|
}
|
|
190
216
|
|
|
191
|
-
const packages:
|
|
217
|
+
const packages: MigrationPackage[] = [];
|
|
192
218
|
|
|
193
219
|
for (const entry of entries.sort()) {
|
|
194
220
|
const entryPath = join(migrationsRoot, entry);
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
|
|
3
|
+
export interface MigrationHints {
|
|
4
|
+
readonly used: readonly string[];
|
|
5
|
+
readonly applied: readonly string[];
|
|
6
|
+
readonly plannerVersion: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory migration metadata envelope. Every migration is content-addressed:
|
|
11
|
+
* the `migrationHash` is a hash over the metadata envelope plus the operations
|
|
12
|
+
* list, computed at write time. There is no draft state — a migration
|
|
13
|
+
* directory either exists with fully attested metadata or it does not.
|
|
14
|
+
*
|
|
15
|
+
* When the planner cannot lower an operation because of an unfilled
|
|
16
|
+
* `placeholder(...)` slot, the migration is still written with `migrationHash`
|
|
17
|
+
* hashed over `ops: []`. Re-running self-emit after the user fills the
|
|
18
|
+
* placeholder produces a *different* `migrationHash` (committed to the real
|
|
19
|
+
* ops); this is intentional.
|
|
20
|
+
*
|
|
21
|
+
* The on-disk JSON shape in `migration.json` matches this type field-for-field
|
|
22
|
+
* — `JSON.stringify(metadata, null, 2)` is the canonical writer output.
|
|
23
|
+
*/
|
|
24
|
+
export interface MigrationMetadata {
|
|
25
|
+
readonly migrationHash: string;
|
|
26
|
+
readonly from: string | null;
|
|
27
|
+
readonly to: string;
|
|
28
|
+
readonly fromContract: Contract | null;
|
|
29
|
+
readonly toContract: Contract;
|
|
30
|
+
readonly hints: MigrationHints;
|
|
31
|
+
readonly labels: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* Sorted, deduplicated list of `invariantId`s declared by the
|
|
34
|
+
* migration's data-transform ops. Always present; an empty array
|
|
35
|
+
* means the migration has no routing-visible data transforms.
|
|
36
|
+
*/
|
|
37
|
+
readonly providedInvariants: readonly string[];
|
|
38
|
+
readonly authorship?: { readonly author?: string; readonly email?: string };
|
|
39
|
+
readonly signature?: { readonly keyId: string; readonly value: string } | null;
|
|
40
|
+
readonly createdAt: string;
|
|
41
|
+
}
|
package/src/migration-base.ts
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import type { Contract } from '@prisma-next/contract/types';
|
|
4
4
|
import type {
|
|
5
|
+
ControlStack,
|
|
5
6
|
MigrationPlan,
|
|
6
7
|
MigrationPlanOperation,
|
|
7
8
|
} from '@prisma-next/framework-components/control';
|
|
8
9
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
9
10
|
import { type } from 'arktype';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
11
|
+
import { errorInvalidOperationEntry, errorStaleContractBookends } from './errors';
|
|
12
|
+
import { computeMigrationHash } from './hash';
|
|
13
|
+
import { deriveProvidedInvariants } from './invariants';
|
|
14
|
+
import type { MigrationHints, MigrationMetadata } from './metadata';
|
|
15
|
+
import { MigrationOpSchema } from './op-schema';
|
|
16
|
+
import type { MigrationOps } from './package';
|
|
13
17
|
|
|
14
18
|
export interface MigrationMeta {
|
|
15
|
-
readonly from: string;
|
|
19
|
+
readonly from: string | null;
|
|
16
20
|
readonly to: string;
|
|
17
|
-
readonly kind?: 'regular' | 'baseline';
|
|
18
21
|
readonly labels?: readonly string[];
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
// `from` rejects empty strings to mirror `MigrationMetadataSchema` in
|
|
25
|
+
// `./io.ts`. Without this match, an authored migration could `describe()` with
|
|
26
|
+
// `from: ''` and pass `buildMigrationArtifacts`'s validation, only to have
|
|
27
|
+
// `readMigrationPackage` reject the resulting `migration.json` later — the
|
|
28
|
+
// two validators must agree on the legal value space.
|
|
21
29
|
const MigrationMetaSchema = type({
|
|
22
|
-
from: 'string',
|
|
30
|
+
from: 'string > 0 | null',
|
|
23
31
|
to: 'string',
|
|
24
|
-
'kind?': "'regular' | 'baseline'",
|
|
25
32
|
'labels?': type('string').array(),
|
|
26
33
|
});
|
|
27
34
|
|
|
@@ -30,15 +37,33 @@ const MigrationMetaSchema = type({
|
|
|
30
37
|
*
|
|
31
38
|
* A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
|
|
32
39
|
* runner can consume it directly via `targetId`, `operations`, `origin`, and
|
|
33
|
-
* `destination`. The
|
|
40
|
+
* `destination`. The metadata-shaped inputs come from `describe()`, which
|
|
34
41
|
* every migration must implement — `migration.json` is required for a
|
|
35
42
|
* migration to be valid.
|
|
36
43
|
*/
|
|
37
|
-
export abstract class Migration<
|
|
38
|
-
|
|
44
|
+
export abstract class Migration<
|
|
45
|
+
TOperation extends MigrationPlanOperation = MigrationPlanOperation,
|
|
46
|
+
TFamilyId extends string = string,
|
|
47
|
+
TTargetId extends string = string,
|
|
48
|
+
> implements MigrationPlan
|
|
39
49
|
{
|
|
40
50
|
abstract readonly targetId: string;
|
|
41
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Assembled `ControlStack` injected by the orchestrator (`runMigration`).
|
|
54
|
+
*
|
|
55
|
+
* Subclasses (e.g. `PostgresMigration`) read the stack to materialize their
|
|
56
|
+
* adapter once per instance. Optional at the abstract level so unit tests can
|
|
57
|
+
* construct `Migration` instances purely for `operations` / `describe`
|
|
58
|
+
* assertions without needing a real stack; concrete subclasses that need the
|
|
59
|
+
* stack at runtime should narrow the parameter to required.
|
|
60
|
+
*/
|
|
61
|
+
protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;
|
|
62
|
+
|
|
63
|
+
constructor(stack?: ControlStack<TFamilyId, TTargetId>) {
|
|
64
|
+
this.stack = stack;
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
/**
|
|
43
68
|
* Ordered list of operations this migration performs.
|
|
44
69
|
*
|
|
@@ -56,124 +81,140 @@ export abstract class Migration<TOperation extends MigrationPlanOperation = Migr
|
|
|
56
81
|
|
|
57
82
|
get origin(): { readonly storageHash: string } | null {
|
|
58
83
|
const from = this.describe().from;
|
|
59
|
-
|
|
60
|
-
// initial baseline, or an in-process plan that was never persisted).
|
|
61
|
-
// Surface that as a null origin so runners treat the plan as
|
|
62
|
-
// origin-less rather than matching against an empty storage hash.
|
|
63
|
-
return from === '' ? null : { storageHash: from };
|
|
84
|
+
return from === null ? null : { storageHash: from };
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
get destination(): { readonly storageHash: string } {
|
|
67
88
|
return { storageHash: this.describe().to };
|
|
68
89
|
}
|
|
90
|
+
}
|
|
69
91
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const metaFilename = fileURLToPath(importMetaUrl);
|
|
87
|
-
const argv1 = process.argv[1];
|
|
88
|
-
if (!argv1) return;
|
|
89
|
-
|
|
90
|
-
let isEntrypoint: boolean;
|
|
91
|
-
try {
|
|
92
|
-
isEntrypoint = realpathSync(metaFilename) === realpathSync(argv1);
|
|
93
|
-
} catch {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (!isEntrypoint) return;
|
|
97
|
-
|
|
98
|
-
const args = process.argv.slice(2);
|
|
99
|
-
|
|
100
|
-
if (args.includes('--help')) {
|
|
101
|
-
printHelp();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const dryRun = args.includes('--dry-run');
|
|
106
|
-
const migrationDir = dirname(metaFilename);
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
serializeMigration(MigrationClass, migrationDir, dryRun);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
112
|
-
process.exitCode = 1;
|
|
113
|
-
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns true when `import.meta.url` resolves to the same file that was
|
|
94
|
+
* invoked as the node entrypoint (`process.argv[1]`). Used by
|
|
95
|
+
* `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when
|
|
96
|
+
* the migration module is being imported (e.g. by another script) rather
|
|
97
|
+
* than executed directly.
|
|
98
|
+
*/
|
|
99
|
+
export function isDirectEntrypoint(importMetaUrl: string): boolean {
|
|
100
|
+
const metaFilename = fileURLToPath(importMetaUrl);
|
|
101
|
+
const argv1 = process.argv[1];
|
|
102
|
+
if (!argv1) return false;
|
|
103
|
+
try {
|
|
104
|
+
return realpathSync(metaFilename) === realpathSync(argv1);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
110
|
+
/**
|
|
111
|
+
* In-memory artifacts produced from a `Migration` instance: the
|
|
112
|
+
* serialized `ops.json` body, the `migration.json` metadata object, and
|
|
113
|
+
* its serialized form. Returned by `buildMigrationArtifacts` so callers
|
|
114
|
+
* (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can
|
|
115
|
+
* decide how to persist them — write to disk, print in dry-run, ship
|
|
116
|
+
* over the wire — without coupling artifact construction to file I/O.
|
|
117
|
+
*
|
|
118
|
+
* `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical
|
|
119
|
+
* on-disk shape that the arktype loader-schema in `./io` validates.
|
|
120
|
+
*/
|
|
121
|
+
export interface MigrationArtifacts {
|
|
122
|
+
readonly opsJson: string;
|
|
123
|
+
readonly metadata: MigrationMetadata;
|
|
124
|
+
readonly metadataJson: string;
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
/**
|
|
131
|
-
* Build the attested
|
|
128
|
+
* Build the attested metadata from `describe()`-derived metadata, the
|
|
129
|
+
* operations list, and the previously-scaffolded metadata (if any).
|
|
132
130
|
*
|
|
133
|
-
* When a `migration.json` already exists
|
|
134
|
-
*
|
|
131
|
+
* When a `migration.json` already exists for this package (the common
|
|
132
|
+
* case: it was scaffolded by `migration plan`), preserve the contract
|
|
135
133
|
* bookends, hints, labels, and `createdAt` set there — those fields are
|
|
136
134
|
* owned by the CLI scaffolder, not the authored class. Only the
|
|
137
|
-
* `describe()`-derived fields (`from`, `to
|
|
138
|
-
* change as the author iterates. When no
|
|
135
|
+
* `describe()`-derived fields (`from`, `to`) and the operations
|
|
136
|
+
* change as the author iterates. When no metadata exists yet (a bare
|
|
139
137
|
* `migration.ts` run from scratch), synthesize a minimal but
|
|
140
|
-
* schema-conformant
|
|
138
|
+
* schema-conformant record so the resulting package can still be read,
|
|
141
139
|
* verified, and applied.
|
|
142
140
|
*
|
|
143
|
-
* The `
|
|
141
|
+
* The `migrationHash` is recomputed against the current metadata + ops so
|
|
144
142
|
* the on-disk artifacts are always fully attested.
|
|
145
143
|
*/
|
|
146
|
-
function
|
|
147
|
-
migrationDir: string,
|
|
144
|
+
function buildAttestedMetadata(
|
|
148
145
|
meta: MigrationMeta,
|
|
149
146
|
ops: MigrationOps,
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
existing: Partial<MigrationMetadata> | null,
|
|
148
|
+
): MigrationMetadata {
|
|
149
|
+
assertBookendsMatchMeta(meta, existing);
|
|
152
150
|
|
|
153
|
-
const
|
|
151
|
+
const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
|
|
154
152
|
from: meta.from,
|
|
155
153
|
to: meta.to,
|
|
156
|
-
kind: meta.kind ?? 'regular',
|
|
157
154
|
labels: meta.labels ?? existing?.labels ?? [],
|
|
155
|
+
providedInvariants: deriveProvidedInvariants(ops),
|
|
158
156
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
159
157
|
fromContract: existing?.fromContract ?? null,
|
|
160
|
-
// When no scaffolded
|
|
158
|
+
// When no scaffolded metadata exists we synthesize a minimal contract
|
|
161
159
|
// stub so the package is still readable end-to-end. The cast is
|
|
162
160
|
// intentional: only the storage bookend matters for hash computation
|
|
163
|
-
// (everything else is stripped by `
|
|
161
|
+
// (everything else is stripped by `computeMigrationHash`), and a real
|
|
164
162
|
// contract bookend would only be available after `migration plan`.
|
|
165
163
|
toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),
|
|
166
164
|
hints: normalizeHints(existing?.hints),
|
|
167
165
|
...ifDefined('authorship', existing?.authorship),
|
|
168
166
|
};
|
|
169
167
|
|
|
170
|
-
const
|
|
171
|
-
return { ...
|
|
168
|
+
const migrationHash = computeMigrationHash(baseMetadata, ops);
|
|
169
|
+
return { ...baseMetadata, migrationHash };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Verify each preserved contract bookend in `existing` agrees with the
|
|
174
|
+
* corresponding side of `describe()`'s output. A mismatch indicates the
|
|
175
|
+
* migration's `describe()` was edited after `migration plan` scaffolded
|
|
176
|
+
* the package, leaving a self-inconsistent manifest. Failing fast at
|
|
177
|
+
* write-time turns a silent foot-gun into an actionable diagnostic.
|
|
178
|
+
*
|
|
179
|
+
* Skipped when a side's `existing.<side>Contract` is null/absent (the
|
|
180
|
+
* synthesis path stays open for origin-less initial migrations and for
|
|
181
|
+
* bare `migration.ts` runs from scratch). When a bookend is *present*
|
|
182
|
+
* but its `storage.storageHash` is missing, that's treated as a
|
|
183
|
+
* mismatch — a malformed bookend is not equivalent to "no bookend".
|
|
184
|
+
*
|
|
185
|
+
* This check is paired with TML-2274, which removes `fromContract` /
|
|
186
|
+
* `toContract` from the manifest entirely; once that lands, this
|
|
187
|
+
* function and its error code are deleted.
|
|
188
|
+
*/
|
|
189
|
+
function assertBookendsMatchMeta(
|
|
190
|
+
meta: MigrationMeta,
|
|
191
|
+
existing: Partial<MigrationMetadata> | null,
|
|
192
|
+
): void {
|
|
193
|
+
if (existing?.fromContract != null) {
|
|
194
|
+
const contractHash = existing.fromContract.storage?.storageHash ?? '';
|
|
195
|
+
if (contractHash !== meta.from) {
|
|
196
|
+
throw errorStaleContractBookends({
|
|
197
|
+
side: 'from',
|
|
198
|
+
metaHash: meta.from,
|
|
199
|
+
contractHash,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (existing?.toContract != null) {
|
|
204
|
+
const contractHash = existing.toContract.storage?.storageHash ?? '';
|
|
205
|
+
if (contractHash !== meta.to) {
|
|
206
|
+
throw errorStaleContractBookends({
|
|
207
|
+
side: 'to',
|
|
208
|
+
metaHash: meta.to,
|
|
209
|
+
contractHash,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
172
213
|
}
|
|
173
214
|
|
|
174
215
|
/**
|
|
175
216
|
* Project `existing.hints` down to the known `MigrationHints` shape, dropping
|
|
176
|
-
* any legacy keys that may linger in
|
|
217
|
+
* any legacy keys that may linger in metadata scaffolded by older CLI
|
|
177
218
|
* versions (e.g. `planningStrategy`). Picking fields explicitly instead of
|
|
178
219
|
* spreading keeps refreshed `migration.json` files schema-clean regardless
|
|
179
220
|
* of what was on disk before.
|
|
@@ -186,34 +227,30 @@ function normalizeHints(existing: MigrationHints | undefined): MigrationHints {
|
|
|
186
227
|
};
|
|
187
228
|
}
|
|
188
229
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
function serializeMigration(
|
|
204
|
-
MigrationClass: new () => Migration,
|
|
205
|
-
migrationDir: string,
|
|
206
|
-
dryRun: boolean,
|
|
207
|
-
): void {
|
|
208
|
-
const instance = new MigrationClass();
|
|
209
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Pure conversion from a `Migration` instance (plus the previously
|
|
232
|
+
* scaffolded metadata, when one exists on disk) to the in-memory
|
|
233
|
+
* artifacts that downstream tooling persists. Owns metadata validation,
|
|
234
|
+
* metadata synthesis/preservation, hint normalization, and the
|
|
235
|
+
* content-addressed `migrationHash` computation, but performs no file I/O
|
|
236
|
+
* — callers handle reads (to source `existing`) and writes (to persist
|
|
237
|
+
* `opsJson` / `metadataJson`).
|
|
238
|
+
*/
|
|
239
|
+
export function buildMigrationArtifacts(
|
|
240
|
+
instance: Migration,
|
|
241
|
+
existing: Partial<MigrationMetadata> | null,
|
|
242
|
+
): MigrationArtifacts {
|
|
210
243
|
const ops = instance.operations;
|
|
211
|
-
|
|
212
244
|
if (!Array.isArray(ops)) {
|
|
213
245
|
throw new Error('operations must be an array');
|
|
214
246
|
}
|
|
215
247
|
|
|
216
|
-
|
|
248
|
+
for (let index = 0; index < ops.length; index++) {
|
|
249
|
+
const result = MigrationOpSchema(ops[index]);
|
|
250
|
+
if (result instanceof type.errors) {
|
|
251
|
+
throw errorInvalidOperationEntry(index, result.summary);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
217
254
|
|
|
218
255
|
const rawMeta: unknown = instance.describe();
|
|
219
256
|
const parsed = MigrationMetaSchema(rawMeta);
|
|
@@ -221,17 +258,11 @@ function serializeMigration(
|
|
|
221
258
|
throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
|
|
222
259
|
}
|
|
223
260
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
if (dryRun) {
|
|
227
|
-
process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
|
|
228
|
-
process.stdout.write('--- ops.json ---\n');
|
|
229
|
-
process.stdout.write(`${serializedOps}\n`);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
261
|
+
const metadata = buildAttestedMetadata(parsed, ops, existing);
|
|
232
262
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
263
|
+
return {
|
|
264
|
+
opsJson: JSON.stringify(ops, null, 2),
|
|
265
|
+
metadata,
|
|
266
|
+
metadataJson: JSON.stringify(metadata, null, 2),
|
|
267
|
+
};
|
|
237
268
|
}
|