@prisma-next/migration-tools 0.4.2 → 0.5.0-dev.10
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 +32 -20
- package/dist/{constants-BRi0X7B_.mjs → constants-WVGVMOdu.mjs} +1 -1
- package/dist/{constants-BRi0X7B_.mjs.map → constants-WVGVMOdu.mjs.map} +1 -1
- package/dist/{errors-BmiSgz1j.mjs → errors-CZ9JD4sd.mjs} +45 -16
- package/dist/errors-CZ9JD4sd.mjs.map +1 -0
- package/dist/exports/constants.mjs +1 -1
- package/dist/exports/dag.d.mts +4 -3
- package/dist/exports/dag.d.mts.map +1 -1
- package/dist/exports/dag.mjs +15 -15
- package/dist/exports/dag.mjs.map +1 -1
- package/dist/exports/{types.d.mts → errors.d.mts} +6 -8
- 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/io.d.mts +7 -6
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +156 -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-ts.mjs +1 -1
- package/dist/exports/migration.d.mts +13 -10
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +20 -21
- 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.mjs +2 -2
- package/dist/graph-HiqjZROg.d.mts +22 -0
- package/dist/graph-HiqjZROg.d.mts.map +1 -0
- package/dist/hash-BNWumjn7.mjs +76 -0
- package/dist/hash-BNWumjn7.mjs.map +1 -0
- package/dist/metadata-DDa5L-uD.d.mts +45 -0
- package/dist/metadata-DDa5L-uD.d.mts.map +1 -0
- package/dist/package-BJ5KAEcD.d.mts +21 -0
- package/dist/package-BJ5KAEcD.d.mts.map +1 -0
- package/package.json +23 -11
- package/src/dag.ts +19 -18
- package/src/errors.ts +57 -15
- package/src/exports/errors.ts +1 -0
- package/src/exports/graph.ts +1 -0
- package/src/exports/hash.ts +2 -0
- package/src/exports/io.ts +1 -1
- package/src/exports/metadata.ts +1 -0
- package/src/exports/package.ts +1 -0
- package/src/graph.ts +19 -0
- package/src/hash.ts +91 -0
- package/src/io.ts +32 -20
- package/src/metadata.ts +36 -0
- package/src/migration-base.ts +32 -28
- package/src/package.ts +18 -0
- package/dist/attestation-BnzTb0Qp.mjs +0 -65
- package/dist/attestation-BnzTb0Qp.mjs.map +0 -1
- package/dist/errors-BmiSgz1j.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/types.d.mts.map +0 -1
- package/dist/exports/types.mjs +0 -3
- package/dist/io-Cd6GLyjK.mjs +0 -153
- package/dist/io-Cd6GLyjK.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/exports/attestation.ts +0 -2
- package/src/exports/types.ts +0 -10
- package/src/types.ts +0 -66
package/src/migration-base.ts
CHANGED
|
@@ -8,8 +8,9 @@ import type {
|
|
|
8
8
|
} from '@prisma-next/framework-components/control';
|
|
9
9
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
10
10
|
import { type } from 'arktype';
|
|
11
|
-
import {
|
|
12
|
-
import type { MigrationHints,
|
|
11
|
+
import { computeMigrationHash } from './hash';
|
|
12
|
+
import type { MigrationHints, MigrationMetadata } from './metadata';
|
|
13
|
+
import type { MigrationOps } from './package';
|
|
13
14
|
|
|
14
15
|
export interface MigrationMeta {
|
|
15
16
|
readonly from: string;
|
|
@@ -30,7 +31,7 @@ const MigrationMetaSchema = type({
|
|
|
30
31
|
*
|
|
31
32
|
* A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
|
|
32
33
|
* runner can consume it directly via `targetId`, `operations`, `origin`, and
|
|
33
|
-
* `destination`. The
|
|
34
|
+
* `destination`. The metadata-shaped inputs come from `describe()`, which
|
|
34
35
|
* every migration must implement — `migration.json` is required for a
|
|
35
36
|
* migration to be valid.
|
|
36
37
|
*/
|
|
@@ -123,64 +124,67 @@ function printHelp(): void {
|
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
126
|
* In-memory artifacts produced from a `Migration` instance: the
|
|
126
|
-
* serialized `ops.json` body, the `migration.json`
|
|
127
|
+
* serialized `ops.json` body, the `migration.json` metadata object, and
|
|
127
128
|
* its serialized form. Returned by `buildMigrationArtifacts` so callers
|
|
128
129
|
* (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can
|
|
129
130
|
* decide how to persist them — write to disk, print in dry-run, ship
|
|
130
131
|
* over the wire — without coupling artifact construction to file I/O.
|
|
132
|
+
*
|
|
133
|
+
* `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical
|
|
134
|
+
* on-disk shape that the arktype loader-schema in `./io` validates.
|
|
131
135
|
*/
|
|
132
136
|
export interface MigrationArtifacts {
|
|
133
137
|
readonly opsJson: string;
|
|
134
|
-
readonly
|
|
135
|
-
readonly
|
|
138
|
+
readonly metadata: MigrationMetadata;
|
|
139
|
+
readonly metadataJson: string;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
/**
|
|
139
|
-
* Build the attested
|
|
140
|
-
* operations list, and the previously-scaffolded
|
|
143
|
+
* Build the attested metadata from `describe()`-derived metadata, the
|
|
144
|
+
* operations list, and the previously-scaffolded metadata (if any).
|
|
141
145
|
*
|
|
142
146
|
* When a `migration.json` already exists for this package (the common
|
|
143
147
|
* case: it was scaffolded by `migration plan`), preserve the contract
|
|
144
148
|
* bookends, hints, labels, and `createdAt` set there — those fields are
|
|
145
149
|
* owned by the CLI scaffolder, not the authored class. Only the
|
|
146
150
|
* `describe()`-derived fields (`from`, `to`, `kind`) and the operations
|
|
147
|
-
* change as the author iterates. When no
|
|
151
|
+
* change as the author iterates. When no metadata exists yet (a bare
|
|
148
152
|
* `migration.ts` run from scratch), synthesize a minimal but
|
|
149
|
-
* schema-conformant
|
|
153
|
+
* schema-conformant record so the resulting package can still be read,
|
|
150
154
|
* verified, and applied.
|
|
151
155
|
*
|
|
152
|
-
* The `
|
|
156
|
+
* The `migrationHash` is recomputed against the current metadata + ops so
|
|
153
157
|
* the on-disk artifacts are always fully attested.
|
|
154
158
|
*/
|
|
155
|
-
function
|
|
159
|
+
function buildAttestedMetadata(
|
|
156
160
|
meta: MigrationMeta,
|
|
157
161
|
ops: MigrationOps,
|
|
158
|
-
existing: Partial<
|
|
159
|
-
):
|
|
160
|
-
const
|
|
162
|
+
existing: Partial<MigrationMetadata> | null,
|
|
163
|
+
): MigrationMetadata {
|
|
164
|
+
const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
|
|
161
165
|
from: meta.from,
|
|
162
166
|
to: meta.to,
|
|
163
167
|
kind: meta.kind ?? 'regular',
|
|
164
168
|
labels: meta.labels ?? existing?.labels ?? [],
|
|
165
169
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
166
170
|
fromContract: existing?.fromContract ?? null,
|
|
167
|
-
// When no scaffolded
|
|
171
|
+
// When no scaffolded metadata exists we synthesize a minimal contract
|
|
168
172
|
// stub so the package is still readable end-to-end. The cast is
|
|
169
173
|
// intentional: only the storage bookend matters for hash computation
|
|
170
|
-
// (everything else is stripped by `
|
|
174
|
+
// (everything else is stripped by `computeMigrationHash`), and a real
|
|
171
175
|
// contract bookend would only be available after `migration plan`.
|
|
172
176
|
toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),
|
|
173
177
|
hints: normalizeHints(existing?.hints),
|
|
174
178
|
...ifDefined('authorship', existing?.authorship),
|
|
175
179
|
};
|
|
176
180
|
|
|
177
|
-
const
|
|
178
|
-
return { ...
|
|
181
|
+
const migrationHash = computeMigrationHash(baseMetadata, ops);
|
|
182
|
+
return { ...baseMetadata, migrationHash };
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
/**
|
|
182
186
|
* Project `existing.hints` down to the known `MigrationHints` shape, dropping
|
|
183
|
-
* any legacy keys that may linger in
|
|
187
|
+
* any legacy keys that may linger in metadata scaffolded by older CLI
|
|
184
188
|
* versions (e.g. `planningStrategy`). Picking fields explicitly instead of
|
|
185
189
|
* spreading keeps refreshed `migration.json` files schema-clean regardless
|
|
186
190
|
* of what was on disk before.
|
|
@@ -195,16 +199,16 @@ function normalizeHints(existing: MigrationHints | undefined): MigrationHints {
|
|
|
195
199
|
|
|
196
200
|
/**
|
|
197
201
|
* Pure conversion from a `Migration` instance (plus the previously
|
|
198
|
-
* scaffolded
|
|
202
|
+
* scaffolded metadata, when one exists on disk) to the in-memory
|
|
199
203
|
* artifacts that downstream tooling persists. Owns metadata validation,
|
|
200
|
-
*
|
|
201
|
-
* content-addressed `
|
|
204
|
+
* metadata synthesis/preservation, hint normalization, and the
|
|
205
|
+
* content-addressed `migrationHash` computation, but performs no file I/O
|
|
202
206
|
* — callers handle reads (to source `existing`) and writes (to persist
|
|
203
|
-
* `opsJson` / `
|
|
207
|
+
* `opsJson` / `metadataJson`).
|
|
204
208
|
*/
|
|
205
209
|
export function buildMigrationArtifacts(
|
|
206
210
|
instance: Migration,
|
|
207
|
-
existing: Partial<
|
|
211
|
+
existing: Partial<MigrationMetadata> | null,
|
|
208
212
|
): MigrationArtifacts {
|
|
209
213
|
const ops = instance.operations;
|
|
210
214
|
if (!Array.isArray(ops)) {
|
|
@@ -217,11 +221,11 @@ export function buildMigrationArtifacts(
|
|
|
217
221
|
throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
|
|
218
222
|
}
|
|
219
223
|
|
|
220
|
-
const
|
|
224
|
+
const metadata = buildAttestedMetadata(parsed, ops, existing);
|
|
221
225
|
|
|
222
226
|
return {
|
|
223
227
|
opsJson: JSON.stringify(ops, null, 2),
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
metadata,
|
|
229
|
+
metadataJson: JSON.stringify(metadata, null, 2),
|
|
226
230
|
};
|
|
227
231
|
}
|
package/src/package.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
|
|
2
|
+
import type { MigrationMetadata } from './metadata';
|
|
3
|
+
|
|
4
|
+
export type MigrationOps = readonly MigrationPlanOperation[];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An on-disk migration directory (a "package") with its parsed metadata and
|
|
8
|
+
* operations. Returned from `readMigrationPackage` / `readMigrationsDir` only
|
|
9
|
+
* after the loader has verified the package's integrity (hash recomputation
|
|
10
|
+
* against the stored `migrationHash`); holding a `MigrationPackage` value
|
|
11
|
+
* therefore implies the package is internally consistent.
|
|
12
|
+
*/
|
|
13
|
+
export interface MigrationPackage {
|
|
14
|
+
readonly dirName: string;
|
|
15
|
+
readonly dirPath: string;
|
|
16
|
+
readonly metadata: MigrationMetadata;
|
|
17
|
+
readonly ops: MigrationOps;
|
|
18
|
+
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { r as readMigrationPackage } from "./io-Cd6GLyjK.mjs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
//#region src/canonicalize-json.ts
|
|
5
|
-
function sortKeys(value) {
|
|
6
|
-
if (value === null || typeof value !== "object") return value;
|
|
7
|
-
if (Array.isArray(value)) return value.map(sortKeys);
|
|
8
|
-
const sorted = {};
|
|
9
|
-
for (const key of Object.keys(value).sort()) sorted[key] = sortKeys(value[key]);
|
|
10
|
-
return sorted;
|
|
11
|
-
}
|
|
12
|
-
function canonicalizeJson(value) {
|
|
13
|
-
return JSON.stringify(sortKeys(value));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region src/attestation.ts
|
|
18
|
-
function sha256Hex(input) {
|
|
19
|
-
return createHash("sha256").update(input).digest("hex");
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Content-addressed migration identity over (manifest envelope sans
|
|
23
|
-
* contracts/hints, ops). See ADR 199 "Storage-only migration identity"
|
|
24
|
-
* for the rationale: contracts are anchored separately by the
|
|
25
|
-
* storage-hash bookends inside the envelope; planner hints are advisory
|
|
26
|
-
* and must not affect identity.
|
|
27
|
-
*
|
|
28
|
-
* The `migrationId` field on the manifest is stripped before hashing so
|
|
29
|
-
* the function can be used both at write time (when no id exists yet)
|
|
30
|
-
* and at verify time (rehashing an already-attested manifest).
|
|
31
|
-
*/
|
|
32
|
-
function computeMigrationId(manifest, ops) {
|
|
33
|
-
const { migrationId: _migrationId, signature: _signature, fromContract: _fromContract, toContract: _toContract, hints: _hints, ...strippedMeta } = manifest;
|
|
34
|
-
return `sha256:${sha256Hex(canonicalizeJson([canonicalizeJson(strippedMeta), canonicalizeJson(ops)].map(sha256Hex)))}`;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Re-hash an on-disk migration bundle and compare against the stored
|
|
38
|
-
* `migrationId`. Returns `{ ok: true }` when the package is internally
|
|
39
|
-
* consistent (manifest + ops still produce the recorded id), or
|
|
40
|
-
* `{ ok: false, reason: 'mismatch', stored, computed }` when they do
|
|
41
|
-
* not — typically a sign of FS corruption, partial writes, or a
|
|
42
|
-
* post-emit hand edit.
|
|
43
|
-
*/
|
|
44
|
-
function verifyMigrationBundle(bundle) {
|
|
45
|
-
const computed = computeMigrationId(bundle.manifest, bundle.ops);
|
|
46
|
-
if (bundle.manifest.migrationId === computed) return {
|
|
47
|
-
ok: true,
|
|
48
|
-
storedMigrationId: bundle.manifest.migrationId,
|
|
49
|
-
computedMigrationId: computed
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
ok: false,
|
|
53
|
-
reason: "mismatch",
|
|
54
|
-
storedMigrationId: bundle.manifest.migrationId,
|
|
55
|
-
computedMigrationId: computed
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/** Convenience wrapper: read the package from disk then verify it. */
|
|
59
|
-
async function verifyMigration(dir) {
|
|
60
|
-
return verifyMigrationBundle(await readMigrationPackage(dir));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
//#endregion
|
|
64
|
-
export { verifyMigration as n, verifyMigrationBundle as r, computeMigrationId as t };
|
|
65
|
-
//# sourceMappingURL=attestation-BnzTb0Qp.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"attestation-BnzTb0Qp.mjs","names":["sorted: Record<string, unknown>"],"sources":["../src/canonicalize-json.ts","../src/attestation.ts"],"sourcesContent":["function sortKeys(value: unknown): unknown {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value).sort()) {\n sorted[key] = sortKeys((value as Record<string, unknown>)[key]);\n }\n return sorted;\n}\n\nexport function canonicalizeJson(value: unknown): string {\n return JSON.stringify(sortKeys(value));\n}\n","import { createHash } from 'node:crypto';\nimport { canonicalizeJson } from './canonicalize-json';\nimport { readMigrationPackage } from './io';\nimport type { MigrationBundle, MigrationManifest, MigrationOps } from './types';\n\nexport interface VerifyResult {\n readonly ok: boolean;\n readonly reason?: 'mismatch';\n readonly storedMigrationId?: string;\n readonly computedMigrationId?: string;\n}\n\nfunction sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\n/**\n * Content-addressed migration identity over (manifest envelope sans\n * contracts/hints, ops). See ADR 199 \"Storage-only migration identity\"\n * for the rationale: contracts are anchored separately by the\n * storage-hash bookends inside the envelope; planner hints are advisory\n * and must not affect identity.\n *\n * The `migrationId` field on the manifest is stripped before hashing so\n * the function can be used both at write time (when no id exists yet)\n * and at verify time (rehashing an already-attested manifest).\n */\nexport function computeMigrationId(\n manifest: Omit<MigrationManifest, 'migrationId'> & { readonly migrationId?: string },\n ops: MigrationOps,\n): string {\n const {\n migrationId: _migrationId,\n signature: _signature,\n fromContract: _fromContract,\n toContract: _toContract,\n hints: _hints,\n ...strippedMeta\n } = manifest;\n\n const canonicalManifest = canonicalizeJson(strippedMeta);\n const canonicalOps = canonicalizeJson(ops);\n\n const partHashes = [canonicalManifest, canonicalOps].map(sha256Hex);\n const hash = sha256Hex(canonicalizeJson(partHashes));\n\n return `sha256:${hash}`;\n}\n\n/**\n * Re-hash an on-disk migration bundle and compare against the stored\n * `migrationId`. Returns `{ ok: true }` when the package is internally\n * consistent (manifest + ops still produce the recorded id), or\n * `{ ok: false, reason: 'mismatch', stored, computed }` when they do\n * not — typically a sign of FS corruption, partial writes, or a\n * post-emit hand edit.\n */\nexport function verifyMigrationBundle(bundle: MigrationBundle): VerifyResult {\n const computed = computeMigrationId(bundle.manifest, bundle.ops);\n\n if (bundle.manifest.migrationId === computed) {\n return {\n ok: true,\n storedMigrationId: bundle.manifest.migrationId,\n computedMigrationId: computed,\n };\n }\n\n return {\n ok: false,\n reason: 'mismatch',\n storedMigrationId: bundle.manifest.migrationId,\n computedMigrationId: computed,\n };\n}\n\n/** Convenience wrapper: read the package from disk then verify it. */\nexport async function verifyMigration(dir: string): Promise<VerifyResult> {\n const pkg = await readMigrationPackage(dir);\n return verifyMigrationBundle(pkg);\n}\n"],"mappings":";;;;AAAA,SAAS,SAAS,OAAyB;AACzC,KAAI,UAAU,QAAQ,OAAO,UAAU,SACrC,QAAO;AAET,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,SAAS;CAE5B,MAAMA,SAAkC,EAAE;AAC1C,MAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAAC,MAAM,CACzC,QAAO,OAAO,SAAU,MAAkC,KAAK;AAEjE,QAAO;;AAGT,SAAgB,iBAAiB,OAAwB;AACvD,QAAO,KAAK,UAAU,SAAS,MAAM,CAAC;;;;;ACHxC,SAAS,UAAU,OAAuB;AACxC,QAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;;;;;;;;;;;;AAczD,SAAgB,mBACd,UACA,KACQ;CACR,MAAM,EACJ,aAAa,cACb,WAAW,YACX,cAAc,eACd,YAAY,aACZ,OAAO,QACP,GAAG,iBACD;AAQJ,QAAO,UAFM,UAAU,iBADJ,CAHO,iBAAiB,aAAa,EACnC,iBAAiB,IAAI,CAEU,CAAC,IAAI,UAAU,CAChB,CAAC;;;;;;;;;;AAatD,SAAgB,sBAAsB,QAAuC;CAC3E,MAAM,WAAW,mBAAmB,OAAO,UAAU,OAAO,IAAI;AAEhE,KAAI,OAAO,SAAS,gBAAgB,SAClC,QAAO;EACL,IAAI;EACJ,mBAAmB,OAAO,SAAS;EACnC,qBAAqB;EACtB;AAGH,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,mBAAmB,OAAO,SAAS;EACnC,qBAAqB;EACtB;;;AAIH,eAAsB,gBAAgB,KAAoC;AAExE,QAAO,sBADK,MAAM,qBAAqB,IAAI,CACV"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors-BmiSgz1j.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,\n * migration history reconstruction), distinct from the runtime MIGRATION.* codes for apply-time\n * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: 'Ensure the migration directory contains both migration.json and ops.json. If the directory is corrupt, delete it and re-run migration plan.',\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: 'Fix the JSON syntax error, or delete the migration directory and re-run migration plan.',\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the manifest has all required fields (from, to, kind, toContract). If corrupt, delete and re-plan.',\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorInvalidDestName(destName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {\n why: `The destination name \"${destName}\" must be a single path segment (no \"..\" or directory separators).`,\n fix: 'Use a simple file name such as \"contract.json\" for each destination in the copy list.',\n details: { destName },\n });\n}\n\nexport function errorSameSourceAndTarget(dirName: string, hash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.SAME_SOURCE_AND_TARGET',\n 'Migration has same source and target',\n {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\". A migration must transition between two different contract states.`,\n fix: 'Delete the invalid migration directory and re-run migration plan.',\n details: { dirName, hash },\n },\n );\n}\n\nexport function errorAmbiguousTarget(\n branchTips: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n tip: string;\n edges: readonly { dirName: string; from: string; to: string }[];\n }[];\n },\n): MigrationToolsError {\n const divergenceInfo = context\n ? `\\nDivergence point: ${context.divergencePoint}\\nBranches:\\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {\n why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,\n fix: 'Use `migration ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.',\n details: {\n branchTips,\n ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),\n },\n });\n}\n\nexport function errorNoInitialMigration(nodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_INITIAL_MIGRATION', 'No initial migration found', {\n why: `No migration starts from the empty contract state (known hashes: ${nodes.join(', ')}). At least one migration must originate from the empty state.`,\n fix: 'Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.',\n details: { nodes },\n });\n}\n\nexport function errorInvalidRefs(refsPath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REFS', 'Invalid refs.json', {\n why: `refs.json at \"${refsPath}\" is invalid: ${reason}`,\n fix: 'Ensure refs.json is a flat object mapping valid ref names to contract hash strings.',\n details: { path: refsPath, reason },\n });\n}\n\nexport function errorInvalidRefFile(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_FILE', 'Invalid ref file', {\n why: `Ref file at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.',\n details: { path: filePath, reason },\n });\n}\n\nexport function errorInvalidRefName(refName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_NAME', 'Invalid ref name', {\n why: `Ref name \"${refName}\" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no \".\" or \"..\" segments).`,\n fix: `Use a valid ref name (e.g., \"staging\", \"envs/production\").`,\n details: { refName },\n });\n}\n\nexport function errorNoTarget(reachableHashes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_TARGET', 'No migration target could be resolved', {\n why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(', ')}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,\n fix: 'Use --from <hash> to specify the planning origin explicitly.',\n details: { reachableHashes },\n });\n}\n\nexport function errorInvalidRefValue(value: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_VALUE', 'Invalid ref value', {\n why: `Ref value \"${value}\" is not a valid contract hash. Values must be in the format \"sha256:<64 hex chars>\" or \"sha256:empty\".`,\n fix: 'Use a valid storage hash from `prisma-next contract emit` output or an existing migration.',\n details: { value },\n });\n}\n\nexport function errorDuplicateMigrationId(migrationId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_ID',\n 'Duplicate migrationId in migration graph',\n {\n why: `Multiple migrations share migrationId \"${migrationId}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.',\n details: { migrationId },\n },\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { a as MigrationManifest, o as MigrationOps, t as MigrationBundle } from "../types-DyGXcWWp.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/attestation.d.ts
|
|
4
|
-
interface VerifyResult {
|
|
5
|
-
readonly ok: boolean;
|
|
6
|
-
readonly reason?: 'mismatch';
|
|
7
|
-
readonly storedMigrationId?: string;
|
|
8
|
-
readonly computedMigrationId?: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Content-addressed migration identity over (manifest envelope sans
|
|
12
|
-
* contracts/hints, ops). See ADR 199 "Storage-only migration identity"
|
|
13
|
-
* for the rationale: contracts are anchored separately by the
|
|
14
|
-
* storage-hash bookends inside the envelope; planner hints are advisory
|
|
15
|
-
* and must not affect identity.
|
|
16
|
-
*
|
|
17
|
-
* The `migrationId` field on the manifest is stripped before hashing so
|
|
18
|
-
* the function can be used both at write time (when no id exists yet)
|
|
19
|
-
* and at verify time (rehashing an already-attested manifest).
|
|
20
|
-
*/
|
|
21
|
-
declare function computeMigrationId(manifest: Omit<MigrationManifest, 'migrationId'> & {
|
|
22
|
-
readonly migrationId?: string;
|
|
23
|
-
}, ops: MigrationOps): string;
|
|
24
|
-
/**
|
|
25
|
-
* Re-hash an on-disk migration bundle and compare against the stored
|
|
26
|
-
* `migrationId`. Returns `{ ok: true }` when the package is internally
|
|
27
|
-
* consistent (manifest + ops still produce the recorded id), or
|
|
28
|
-
* `{ ok: false, reason: 'mismatch', stored, computed }` when they do
|
|
29
|
-
* not — typically a sign of FS corruption, partial writes, or a
|
|
30
|
-
* post-emit hand edit.
|
|
31
|
-
*/
|
|
32
|
-
declare function verifyMigrationBundle(bundle: MigrationBundle): VerifyResult;
|
|
33
|
-
/** Convenience wrapper: read the package from disk then verify it. */
|
|
34
|
-
declare function verifyMigration(dir: string): Promise<VerifyResult>;
|
|
35
|
-
//#endregion
|
|
36
|
-
export { type VerifyResult, computeMigrationId, verifyMigration, verifyMigrationBundle };
|
|
37
|
-
//# sourceMappingURL=attestation.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.d.mts","names":[],"sources":["../../src/attestation.ts"],"sourcesContent":[],"mappings":";;;UAKiB,YAAA;;EAAA,SAAA,MAAY,CAAA,EAAA,UAAA;EAsBb,SAAA,iBAAkB,CAAA,EAAA,MAAA;EACjB,SAAA,mBAAA,CAAA,EAAA,MAAA;;;;AA6BjB;AAoBA;;;;;;;;iBAlDgB,kBAAA,WACJ,KAAK;;QACV;;;;;;;;;iBA4BS,qBAAA,SAA8B,kBAAkB;;iBAoB1C,eAAA,eAA8B,QAAQ"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/errors.ts"],"sourcesContent":[],"mappings":";;;;;;;AAeA;;;;;;;;;;;;cAAa,mBAAA,SAA4B,KAAA;;;;;oBAKrB;;;;uBAQK;;sCAWa"}
|
package/dist/exports/types.mjs
DELETED
package/dist/io-Cd6GLyjK.mjs
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { a as errorInvalidDestName, d as errorInvalidSlug, f as errorMissingFile, o as errorInvalidJson, r as errorDirectoryExists, s as errorInvalidManifest } from "./errors-BmiSgz1j.mjs";
|
|
2
|
-
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { type } from "arktype";
|
|
4
|
-
import { basename, dirname, join } from "pathe";
|
|
5
|
-
|
|
6
|
-
//#region src/io.ts
|
|
7
|
-
const MANIFEST_FILE = "migration.json";
|
|
8
|
-
const OPS_FILE = "ops.json";
|
|
9
|
-
const MAX_SLUG_LENGTH = 64;
|
|
10
|
-
function hasErrnoCode(error, code) {
|
|
11
|
-
return error instanceof Error && error.code === code;
|
|
12
|
-
}
|
|
13
|
-
const MigrationManifestSchema = type({
|
|
14
|
-
from: "string",
|
|
15
|
-
to: "string",
|
|
16
|
-
migrationId: "string",
|
|
17
|
-
kind: "'regular' | 'baseline'",
|
|
18
|
-
fromContract: "object | null",
|
|
19
|
-
toContract: "object",
|
|
20
|
-
hints: type({
|
|
21
|
-
used: "string[]",
|
|
22
|
-
applied: "string[]",
|
|
23
|
-
plannerVersion: "string"
|
|
24
|
-
}),
|
|
25
|
-
labels: "string[]",
|
|
26
|
-
"authorship?": type({
|
|
27
|
-
"author?": "string",
|
|
28
|
-
"email?": "string"
|
|
29
|
-
}),
|
|
30
|
-
"signature?": type({
|
|
31
|
-
keyId: "string",
|
|
32
|
-
value: "string"
|
|
33
|
-
}).or("null"),
|
|
34
|
-
createdAt: "string"
|
|
35
|
-
});
|
|
36
|
-
const MigrationOpsSchema = type({
|
|
37
|
-
id: "string",
|
|
38
|
-
label: "string",
|
|
39
|
-
operationClass: "'additive' | 'widening' | 'destructive' | 'data'"
|
|
40
|
-
}).array();
|
|
41
|
-
async function writeMigrationPackage(dir, manifest, ops) {
|
|
42
|
-
await mkdir(dirname(dir), { recursive: true });
|
|
43
|
-
try {
|
|
44
|
-
await mkdir(dir);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if (hasErrnoCode(error, "EEXIST")) throw errorDirectoryExists(dir);
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: "wx" });
|
|
50
|
-
await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Copy a list of files into `destDir`, optionally renaming each one.
|
|
54
|
-
*
|
|
55
|
-
* The destination directory is created (with `recursive: true`) if it
|
|
56
|
-
* does not already exist. Each source path is copied byte-for-byte into
|
|
57
|
-
* `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
|
|
58
|
-
* intentionally generic: callers own the list of files (e.g. a contract
|
|
59
|
-
* emitter's emitted output) and the naming convention (e.g. renaming
|
|
60
|
-
* the destination contract to `end-contract.*` and the source contract
|
|
61
|
-
* to `start-contract.*`).
|
|
62
|
-
*/
|
|
63
|
-
async function copyFilesWithRename(destDir, files) {
|
|
64
|
-
await mkdir(destDir, { recursive: true });
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
if (basename(file.destName) !== file.destName) throw errorInvalidDestName(file.destName);
|
|
67
|
-
await copyFile(file.sourcePath, join(destDir, file.destName));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async function writeMigrationManifest(dir, manifest) {
|
|
71
|
-
await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
72
|
-
}
|
|
73
|
-
async function writeMigrationOps(dir, ops) {
|
|
74
|
-
await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
|
|
75
|
-
}
|
|
76
|
-
async function readMigrationPackage(dir) {
|
|
77
|
-
const manifestPath = join(dir, MANIFEST_FILE);
|
|
78
|
-
const opsPath = join(dir, OPS_FILE);
|
|
79
|
-
let manifestRaw;
|
|
80
|
-
try {
|
|
81
|
-
manifestRaw = await readFile(manifestPath, "utf-8");
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(MANIFEST_FILE, dir);
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
let opsRaw;
|
|
87
|
-
try {
|
|
88
|
-
opsRaw = await readFile(opsPath, "utf-8");
|
|
89
|
-
} catch (error) {
|
|
90
|
-
if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(OPS_FILE, dir);
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
let manifest;
|
|
94
|
-
try {
|
|
95
|
-
manifest = JSON.parse(manifestRaw);
|
|
96
|
-
} catch (e) {
|
|
97
|
-
throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
|
|
98
|
-
}
|
|
99
|
-
let ops;
|
|
100
|
-
try {
|
|
101
|
-
ops = JSON.parse(opsRaw);
|
|
102
|
-
} catch (e) {
|
|
103
|
-
throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
|
|
104
|
-
}
|
|
105
|
-
validateManifest(manifest, manifestPath);
|
|
106
|
-
validateOps(ops, opsPath);
|
|
107
|
-
return {
|
|
108
|
-
dirName: basename(dir),
|
|
109
|
-
dirPath: dir,
|
|
110
|
-
manifest,
|
|
111
|
-
ops
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
function validateManifest(manifest, filePath) {
|
|
115
|
-
const result = MigrationManifestSchema(manifest);
|
|
116
|
-
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
|
|
117
|
-
}
|
|
118
|
-
function validateOps(ops, filePath) {
|
|
119
|
-
const result = MigrationOpsSchema(ops);
|
|
120
|
-
if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
|
|
121
|
-
}
|
|
122
|
-
async function readMigrationsDir(migrationsRoot) {
|
|
123
|
-
let entries;
|
|
124
|
-
try {
|
|
125
|
-
entries = await readdir(migrationsRoot);
|
|
126
|
-
} catch (error) {
|
|
127
|
-
if (hasErrnoCode(error, "ENOENT")) return [];
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
const packages = [];
|
|
131
|
-
for (const entry of entries.sort()) {
|
|
132
|
-
const entryPath = join(migrationsRoot, entry);
|
|
133
|
-
if (!(await stat(entryPath)).isDirectory()) continue;
|
|
134
|
-
const manifestPath = join(entryPath, MANIFEST_FILE);
|
|
135
|
-
try {
|
|
136
|
-
await stat(manifestPath);
|
|
137
|
-
} catch {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
packages.push(await readMigrationPackage(entryPath));
|
|
141
|
-
}
|
|
142
|
-
return packages;
|
|
143
|
-
}
|
|
144
|
-
function formatMigrationDirName(timestamp, slug) {
|
|
145
|
-
const sanitized = slug.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
146
|
-
if (sanitized.length === 0) throw errorInvalidSlug(slug);
|
|
147
|
-
const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);
|
|
148
|
-
return `${timestamp.getUTCFullYear()}${String(timestamp.getUTCMonth() + 1).padStart(2, "0")}${String(timestamp.getUTCDate()).padStart(2, "0")}T${String(timestamp.getUTCHours()).padStart(2, "0")}${String(timestamp.getUTCMinutes()).padStart(2, "0")}_${truncated}`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
//#endregion
|
|
152
|
-
export { writeMigrationManifest as a, readMigrationsDir as i, formatMigrationDirName as n, writeMigrationOps as o, readMigrationPackage as r, writeMigrationPackage as s, copyFilesWithRename as t };
|
|
153
|
-
//# sourceMappingURL=io-Cd6GLyjK.mjs.map
|
package/dist/io-Cd6GLyjK.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"io-Cd6GLyjK.mjs","names":["manifestRaw: string","opsRaw: string","manifest: MigrationManifest","ops: MigrationOps","entries: string[]","packages: MigrationBundle[]"],"sources":["../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMissingFile,\n} from './errors';\nimport type { MigrationBundle, MigrationManifest, MigrationOps } from './types';\n\nconst MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n});\n\nconst MigrationManifestSchema = type({\n from: 'string',\n to: 'string',\n migrationId: 'string',\n kind: \"'regular' | 'baseline'\",\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nconst MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nconst MigrationOpsSchema = MigrationOpSchema.array();\n\nexport async function writeMigrationPackage(\n dir: string,\n manifest: MigrationManifest,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: 'wx' });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationManifest(\n dir: string,\n manifest: MigrationManifest,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<MigrationBundle> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, dir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, dir);\n }\n throw error;\n }\n\n let manifest: MigrationManifest;\n try {\n manifest = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateManifest(manifest, manifestPath);\n validateOps(ops, opsPath);\n\n return {\n dirName: basename(dir),\n dirPath: dir,\n manifest,\n ops,\n };\n}\n\nfunction validateManifest(\n manifest: unknown,\n filePath: string,\n): asserts manifest is MigrationManifest {\n const result = MigrationManifestSchema(manifest);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly MigrationBundle[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: MigrationBundle[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { Contract } from "@prisma-next/contract/types";
|
|
2
|
-
import { MigrationPlanOperation } from "@prisma-next/framework-components/control";
|
|
3
|
-
|
|
4
|
-
//#region src/types.d.ts
|
|
5
|
-
interface MigrationHints {
|
|
6
|
-
readonly used: readonly string[];
|
|
7
|
-
readonly applied: readonly string[];
|
|
8
|
-
readonly plannerVersion: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* On-disk migration manifest. Every migration is content-addressed: the
|
|
12
|
-
* `migrationId` is a hash over the manifest envelope plus the operations
|
|
13
|
-
* list, computed at write time. There is no draft state — a migration
|
|
14
|
-
* directory either exists with a fully attested manifest or it does not.
|
|
15
|
-
*
|
|
16
|
-
* When the planner cannot lower an operation because of an unfilled
|
|
17
|
-
* `placeholder(...)` slot, the migration is still written with
|
|
18
|
-
* `migrationId` hashed over `ops: []`. Re-running self-emit after the
|
|
19
|
-
* user fills the placeholder produces a *different* `migrationId`
|
|
20
|
-
* (committed to the real ops); this is intentional.
|
|
21
|
-
*/
|
|
22
|
-
interface MigrationManifest {
|
|
23
|
-
readonly migrationId: string;
|
|
24
|
-
readonly from: string;
|
|
25
|
-
readonly to: string;
|
|
26
|
-
readonly kind: 'regular' | 'baseline';
|
|
27
|
-
readonly fromContract: Contract | null;
|
|
28
|
-
readonly toContract: Contract;
|
|
29
|
-
readonly hints: MigrationHints;
|
|
30
|
-
readonly labels: readonly string[];
|
|
31
|
-
readonly authorship?: {
|
|
32
|
-
readonly author?: string;
|
|
33
|
-
readonly email?: string;
|
|
34
|
-
};
|
|
35
|
-
readonly signature?: {
|
|
36
|
-
readonly keyId: string;
|
|
37
|
-
readonly value: string;
|
|
38
|
-
} | null;
|
|
39
|
-
readonly createdAt: string;
|
|
40
|
-
}
|
|
41
|
-
type MigrationOps = readonly MigrationPlanOperation[];
|
|
42
|
-
/**
|
|
43
|
-
* An on-disk migration directory containing a manifest and operations.
|
|
44
|
-
*/
|
|
45
|
-
interface MigrationBundle {
|
|
46
|
-
readonly dirName: string;
|
|
47
|
-
readonly dirPath: string;
|
|
48
|
-
readonly manifest: MigrationManifest;
|
|
49
|
-
readonly ops: MigrationOps;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* An entry in the migration graph. All on-disk migrations are attested,
|
|
53
|
-
* so `migrationId` is always a string.
|
|
54
|
-
*/
|
|
55
|
-
interface MigrationChainEntry {
|
|
56
|
-
readonly from: string;
|
|
57
|
-
readonly to: string;
|
|
58
|
-
readonly migrationId: string;
|
|
59
|
-
readonly dirName: string;
|
|
60
|
-
readonly createdAt: string;
|
|
61
|
-
readonly labels: readonly string[];
|
|
62
|
-
}
|
|
63
|
-
interface MigrationGraph {
|
|
64
|
-
readonly nodes: ReadonlySet<string>;
|
|
65
|
-
readonly forwardChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
|
|
66
|
-
readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
|
|
67
|
-
readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;
|
|
68
|
-
}
|
|
69
|
-
//#endregion
|
|
70
|
-
export { MigrationManifest as a, MigrationHints as i, MigrationChainEntry as n, MigrationOps as o, MigrationGraph as r, MigrationBundle as t };
|
|
71
|
-
//# sourceMappingURL=types-DyGXcWWp.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-DyGXcWWp.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;UAGiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAkBd,SAAA,cAAiB,EAAA,MAAA;;;;;AAclC;AAKA;AAWA;AASA;;;;;;AAI8C,UA3C7B,iBAAA,CA2C6B;EAApB,SAAA,WAAA,EAAA,MAAA;EAAW,SAAA,IAAA,EAAA,MAAA;;;yBAtCZ;uBACF;kBACL;;;;;;;;;;;;KAON,YAAA,YAAwB;;;;UAKnB,eAAA;;;qBAGI;gBACL;;;;;;UAOC,mBAAA;;;;;;;;UASA,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;0BAC5B,oBAAoB"}
|