@prisma-next/migration-tools 0.5.0-dev.3 → 0.5.0-dev.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-Bl3cKiM8.mjs +244 -0
- package/dist/errors-Bl3cKiM8.mjs.map +1 -0
- package/dist/exports/constants.mjs +1 -1
- package/dist/exports/{types.d.mts → errors.d.mts} +7 -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/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/{dag.d.mts → migration-graph.d.mts} +31 -10
- package/dist/exports/migration-graph.d.mts.map +1 -0
- package/dist/exports/{dag.mjs → migration-graph.mjs} +143 -63
- package/dist/exports/migration-graph.mjs.map +1 -0
- package/dist/exports/migration-ts.mjs +1 -1
- package/dist/exports/migration.d.mts +15 -14
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +68 -40
- 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-BmrTBQ0A.mjs +42 -0
- package/dist/invariants-BmrTBQ0A.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 +32 -16
- package/src/errors.ts +139 -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/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 +0 -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 +97 -56
- package/src/{dag.ts → migration-graph.ts} +156 -54
- 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.map +0 -1
- package/dist/exports/dag.mjs.map +0 -1
- 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/exports/attestation.ts +0 -2
- package/src/exports/types.ts +0 -10
- package/src/types.ts +0 -66
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refs.mjs","names":["raw: string","parsed: unknown"],"sources":["../../src/refs.ts"],"sourcesContent":["import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { dirname, join } from 'pathe';\nimport {\n errorInvalidRefName,\n errorInvalidRefs,\n errorInvalidRefValue,\n MigrationToolsError,\n} from './errors';\n\nexport type Refs = Readonly<Record<string, string>>;\n\nconst REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;\nconst REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;\n\nexport function validateRefName(name: string): boolean {\n if (name.length === 0) return false;\n if (name.includes('..')) return false;\n if (name.includes('//')) return false;\n if (name.startsWith('.')) return false;\n return REF_NAME_PATTERN.test(name);\n}\n\nexport function validateRefValue(value: string): boolean {\n return REF_VALUE_PATTERN.test(value);\n}\n\nconst RefsSchema = type('Record<string, string>').narrow((refs, ctx) => {\n for (const [key, value] of Object.entries(refs)) {\n if (!validateRefName(key)) return ctx.mustBe(`valid ref names (invalid: \"${key}\")`);\n if (!validateRefValue(value))\n return ctx.mustBe(`valid contract hashes (invalid value for \"${key}\": \"${value}\")`);\n }\n return true;\n});\n\nexport async function readRefs(refsPath: string): Promise<Refs> {\n let raw: string;\n try {\n raw = await readFile(refsPath, 'utf-8');\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n return {};\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefs(refsPath, 'Failed to parse as JSON');\n }\n\n const result = RefsSchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefs(refsPath, result.summary);\n }\n\n return result;\n}\n\nexport async function writeRefs(refsPath: string, refs: Refs): Promise<void> {\n for (const [key, value] of Object.entries(refs)) {\n if (!validateRefName(key)) {\n throw errorInvalidRefName(key);\n }\n if (!validateRefValue(value)) {\n throw errorInvalidRefValue(value);\n }\n }\n\n const sorted = Object.fromEntries(Object.entries(refs).sort(([a], [b]) => a.localeCompare(b)));\n\n const dir = dirname(refsPath);\n await mkdir(dir, { recursive: true });\n\n const tmpPath = join(dir, `.refs.json.${Date.now()}.tmp`);\n await writeFile(tmpPath, `${JSON.stringify(sorted, null, 2)}\\n`);\n await rename(tmpPath, refsPath);\n}\n\nexport function resolveRef(refs: Refs, name: string): string {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const hash = refs[name];\n if (hash === undefined) {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref named \"${name}\" exists in refs.json.`,\n fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: set the \"${name}\" key in migrations/refs.json.`,\n details: { refName: name, availableRefs: Object.keys(refs) },\n });\n }\n\n if (!validateRefValue(hash)) {\n throw errorInvalidRefValue(hash);\n }\n\n return hash;\n}\n"],"mappings":";;;;;;AAYA,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;AAE1B,SAAgB,gBAAgB,MAAuB;AACrD,KAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,KAAI,KAAK,SAAS,KAAK,CAAE,QAAO;AAChC,KAAI,KAAK,SAAS,KAAK,CAAE,QAAO;AAChC,KAAI,KAAK,WAAW,IAAI,CAAE,QAAO;AACjC,QAAO,iBAAiB,KAAK,KAAK;;AAGpC,SAAgB,iBAAiB,OAAwB;AACvD,QAAO,kBAAkB,KAAK,MAAM;;AAGtC,MAAM,aAAa,KAAK,yBAAyB,CAAC,QAAQ,MAAM,QAAQ;AACtE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;AAC/C,MAAI,CAAC,gBAAgB,IAAI,CAAE,QAAO,IAAI,OAAO,8BAA8B,IAAI,IAAI;AACnF,MAAI,CAAC,iBAAiB,MAAM,CAC1B,QAAO,IAAI,OAAO,6CAA6C,IAAI,MAAM,MAAM,IAAI;;AAEvF,QAAO;EACP;AAEF,eAAsB,SAAS,UAAiC;CAC9D,IAAIA;AACJ,KAAI;AACF,QAAM,MAAM,SAAS,UAAU,QAAQ;UAChC,OAAO;AACd,MAAI,iBAAiB,SAAU,MAA4B,SAAS,SAClE,QAAO,EAAE;AAEX,QAAM;;CAGR,IAAIC;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,QAAM,iBAAiB,UAAU,0BAA0B;;CAG7D,MAAM,SAAS,WAAW,OAAO;AACjC,KAAI,kBAAkB,KAAK,OACzB,OAAM,iBAAiB,UAAU,OAAO,QAAQ;AAGlD,QAAO;;AAGT,eAAsB,UAAU,UAAkB,MAA2B;AAC3E,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;AAC/C,MAAI,CAAC,gBAAgB,IAAI,CACvB,OAAM,oBAAoB,IAAI;AAEhC,MAAI,CAAC,iBAAiB,MAAM,CAC1B,OAAM,qBAAqB,MAAM;;CAIrC,MAAM,SAAS,OAAO,YAAY,OAAO,QAAQ,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;CAE9F,MAAM,MAAM,QAAQ,SAAS;AAC7B,OAAM,MAAM,KAAK,EAAE,WAAW,MAAM,CAAC;CAErC,MAAM,UAAU,KAAK,KAAK,cAAc,KAAK,KAAK,CAAC,MAAM;AACzD,OAAM,UAAU,SAAS,GAAG,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC,IAAI;AAChE,OAAM,OAAO,SAAS,SAAS;;AAGjC,SAAgB,WAAW,MAAY,MAAsB;AAC3D,KAAI,CAAC,gBAAgB,KAAK,CACxB,OAAM,oBAAoB,KAAK;CAGjC,MAAM,OAAO,KAAK;AAClB,KAAI,SAAS,OACX,OAAM,IAAI,oBAAoB,yBAAyB,gBAAgB,KAAK,IAAI;EAC9E,KAAK,iBAAiB,KAAK;EAC3B,KAAK,mBAAmB,OAAO,KAAK,KAAK,CAAC,KAAK,KAAK,IAAI,SAAS,gCAAgC,KAAK;EACtG,SAAS;GAAE,SAAS;GAAM,eAAe,OAAO,KAAK,KAAK;GAAE;EAC7D,CAAC;AAGJ,KAAI,CAAC,iBAAiB,KAAK,CACzB,OAAM,qBAAqB,KAAK;AAGlC,QAAO"}
|
|
1
|
+
{"version":3,"file":"refs.mjs","names":["raw: string","parsed: unknown","entries: string[]","refs: Record<string, RefEntry>"],"sources":["../../src/refs.ts"],"sourcesContent":["import { mkdir, readdir, readFile, rename, rmdir, unlink, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { dirname, join, relative } from 'pathe';\nimport {\n errorInvalidRefFile,\n errorInvalidRefName,\n errorInvalidRefValue,\n MigrationToolsError,\n} from './errors';\n\nexport interface RefEntry {\n readonly hash: string;\n readonly invariants: readonly string[];\n}\n\nexport type Refs = Readonly<Record<string, RefEntry>>;\n\nconst REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;\nconst REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;\n\nexport function validateRefName(name: string): boolean {\n if (name.length === 0) return false;\n if (name.includes('..')) return false;\n if (name.includes('//')) return false;\n if (name.startsWith('.')) return false;\n return REF_NAME_PATTERN.test(name);\n}\n\nexport function validateRefValue(value: string): boolean {\n return REF_VALUE_PATTERN.test(value);\n}\n\nconst RefEntrySchema = type({\n hash: 'string',\n invariants: 'string[]',\n}).narrow((entry, ctx) => {\n if (!validateRefValue(entry.hash))\n return ctx.mustBe(`a valid contract hash (got \"${entry.hash}\")`);\n return true;\n});\n\nfunction refFilePath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.json`);\n}\n\nfunction refNameFromPath(refsDir: string, filePath: string): string {\n const rel = relative(refsDir, filePath);\n return rel.replace(/\\.json$/, '');\n}\n\nexport async function readRef(refsDir: string, name: string): Promise<RefEntry> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const filePath = refFilePath(refsDir, name);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref file found at \"${filePath}\".`,\n fix: `Create the ref with: prisma-next migration ref set ${name} <hash>`,\n details: { refName: name, filePath },\n });\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function readRefs(refsDir: string): Promise<Refs> {\n let entries: string[];\n try {\n entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n return {};\n }\n throw error;\n }\n\n const jsonFiles = entries.filter((entry) => entry.endsWith('.json'));\n const refs: Record<string, RefEntry> = {};\n\n for (const jsonFile of jsonFiles) {\n const filePath = join(refsDir, jsonFile);\n const name = refNameFromPath(refsDir, filePath);\n\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (error) {\n // Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT) and\n // benign EISDIR if a directory happens to end in `.json`. Anything else\n // (EACCES, EIO, EMFILE, …) is a real failure and propagates so the CLI\n // surfaces it rather than silently dropping the ref.\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOENT' || code === 'EISDIR') {\n continue;\n }\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n refs[name] = result;\n }\n\n return refs;\n}\n\nexport async function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n if (!validateRefValue(entry.hash)) {\n throw errorInvalidRefValue(entry.hash);\n }\n\n const filePath = refFilePath(refsDir, name);\n const dir = dirname(filePath);\n await mkdir(dir, { recursive: true });\n\n const tmpPath = join(dir, `.${name.split('/').pop()}.json.${Date.now()}.tmp`);\n await writeFile(\n tmpPath,\n `${JSON.stringify({ hash: entry.hash, invariants: [...entry.invariants] }, null, 2)}\\n`,\n );\n await rename(tmpPath, filePath);\n}\n\nexport async function deleteRef(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const filePath = refFilePath(refsDir, name);\n try {\n await unlink(filePath);\n } catch (error) {\n if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref file found at \"${filePath}\".`,\n fix: 'Run `prisma-next migration ref list` to see available refs.',\n details: { refName: name, filePath },\n });\n }\n throw error;\n }\n\n // Clean empty parent directories up to refsDir. Stop walking on the expected\n // \"directory has siblings\" signal (ENOTEMPTY on Linux, EEXIST on some BSDs)\n // and on ENOENT (concurrent removal). Anything else (EACCES, EIO, …) is a\n // real failure and propagates.\n let dir = dirname(filePath);\n while (dir !== refsDir && dir.startsWith(refsDir)) {\n try {\n await rmdir(dir);\n dir = dirname(dir);\n } catch (error) {\n const code = error instanceof Error ? (error as { code?: string }).code : undefined;\n if (code === 'ENOTEMPTY' || code === 'EEXIST' || code === 'ENOENT') {\n break;\n }\n throw error;\n }\n }\n}\n\nexport function resolveRef(refs: Refs, name: string): RefEntry {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n // Object.hasOwn gate: plain-object `refs` would otherwise let\n // `refs['constructor']` return Object.prototype.constructor and bypass the\n // UNKNOWN_REF throw. validateRefName accepts `\"constructor\"` as a name shape.\n if (!Object.hasOwn(refs, name)) {\n throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref \"${name}\"`, {\n why: `No ref named \"${name}\" exists.`,\n fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: prisma-next migration ref set ${name} <hash>`,\n details: { refName: name, availableRefs: Object.keys(refs) },\n });\n }\n\n // biome-ignore lint/style/noNonNullAssertion: Object.hasOwn gate above guarantees this is defined\n return refs[name]!;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/graph.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* An entry in the migration graph. All on-disk migrations are attested,
|
|
4
|
+
* so `migrationHash` is always a string.
|
|
5
|
+
*/
|
|
6
|
+
interface MigrationEdge {
|
|
7
|
+
readonly from: string;
|
|
8
|
+
readonly to: string;
|
|
9
|
+
readonly migrationHash: string;
|
|
10
|
+
readonly dirName: string;
|
|
11
|
+
readonly createdAt: string;
|
|
12
|
+
readonly labels: readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Sorted, deduplicated list of `invariantId`s this edge provides.
|
|
15
|
+
* An empty array means the migration declares no routing-visible
|
|
16
|
+
* data transforms.
|
|
17
|
+
*/
|
|
18
|
+
readonly invariants: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
interface MigrationGraph {
|
|
21
|
+
readonly nodes: ReadonlySet<string>;
|
|
22
|
+
readonly forwardChain: ReadonlyMap<string, readonly MigrationEdge[]>;
|
|
23
|
+
readonly reverseChain: ReadonlyMap<string, readonly MigrationEdge[]>;
|
|
24
|
+
readonly migrationByHash: ReadonlyMap<string, MigrationEdge>;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { MigrationGraph as n, MigrationEdge as t };
|
|
28
|
+
//# sourceMappingURL=graph-BHPv-9Gl.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-BHPv-9Gl.d.mts","names":[],"sources":["../src/graph.ts"],"sourcesContent":[],"mappings":";;AAIA;AAeA;;AAEsD,UAjBrC,aAAA,CAiBqC;EAA7B,SAAA,IAAA,EAAA,MAAA;EAC6B,SAAA,EAAA,EAAA,MAAA;EAA7B,SAAA,aAAA,EAAA,MAAA;EACuB,SAAA,OAAA,EAAA,MAAA;EAApB,SAAA,SAAA,EAAA,MAAA;EAAW,SAAA,MAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;UAJtB,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;4BAC1B,oBAAoB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
//#region src/canonicalize-json.ts
|
|
4
|
+
function sortKeys(value) {
|
|
5
|
+
if (value === null || typeof value !== "object") return value;
|
|
6
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
7
|
+
const sorted = {};
|
|
8
|
+
for (const key of Object.keys(value).sort()) sorted[key] = sortKeys(value[key]);
|
|
9
|
+
return sorted;
|
|
10
|
+
}
|
|
11
|
+
function canonicalizeJson(value) {
|
|
12
|
+
return JSON.stringify(sortKeys(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/hash.ts
|
|
17
|
+
function sha256Hex(input) {
|
|
18
|
+
return createHash("sha256").update(input).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Content-addressed migration hash over (metadata envelope sans
|
|
22
|
+
* contracts/hints/signature, ops). See ADR 199 — Storage-only migration
|
|
23
|
+
* identity for the rationale: contracts are anchored separately by the
|
|
24
|
+
* storage-hash bookends inside the envelope; planner hints are advisory
|
|
25
|
+
* and must not affect identity.
|
|
26
|
+
*
|
|
27
|
+
* The integrity check is purely structural, not semantic. The function
|
|
28
|
+
* canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`
|
|
29
|
+
* and hashes the result. Target-specific operation payloads (`step.sql`,
|
|
30
|
+
* Mongo's pipeline AST, …) are hashed verbatim — no per-target
|
|
31
|
+
* normalization is required, because what's being verified is "do the
|
|
32
|
+
* on-disk bytes still produce their recorded hash", not "do two
|
|
33
|
+
* semantically-equivalent migrations hash the same". The latter is an
|
|
34
|
+
* emit-drift concern (ADR 192 step 2).
|
|
35
|
+
*
|
|
36
|
+
* The symmetry across write and read holds because `JSON.parse(
|
|
37
|
+
* JSON.stringify(x))` round-trips JSON-safe values losslessly and
|
|
38
|
+
* `sortKeys` is idempotent and deterministic — write-time and read-time
|
|
39
|
+
* canonicalization produce the same canonical bytes regardless of
|
|
40
|
+
* source-side key ordering or whitespace.
|
|
41
|
+
*
|
|
42
|
+
* The `migrationHash` field on the metadata is stripped before hashing
|
|
43
|
+
* so the function can be used both at write time (when no hash exists
|
|
44
|
+
* yet) and at verify time (rehashing an already-attested record).
|
|
45
|
+
*/
|
|
46
|
+
function computeMigrationHash(metadata, ops) {
|
|
47
|
+
const { migrationHash: _migrationHash, signature: _signature, fromContract: _fromContract, toContract: _toContract, hints: _hints, ...strippedMeta } = metadata;
|
|
48
|
+
return `sha256:${sha256Hex(canonicalizeJson([canonicalizeJson(strippedMeta), canonicalizeJson(ops)].map(sha256Hex)))}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Re-hash an in-memory migration package and compare against the stored
|
|
52
|
+
* `migrationHash`. See `computeMigrationHash` for the canonicalization rules.
|
|
53
|
+
*
|
|
54
|
+
* Returns `{ ok: true }` when the package is internally consistent, or
|
|
55
|
+
* `{ ok: false, reason: 'mismatch', storedHash, computedHash }` when it is
|
|
56
|
+
* not — typically a sign of FS corruption, partial writes, or a post-emit
|
|
57
|
+
* hand edit.
|
|
58
|
+
*/
|
|
59
|
+
function verifyMigrationHash(pkg) {
|
|
60
|
+
const computed = computeMigrationHash(pkg.metadata, pkg.ops);
|
|
61
|
+
if (pkg.metadata.migrationHash === computed) return {
|
|
62
|
+
ok: true,
|
|
63
|
+
storedHash: pkg.metadata.migrationHash,
|
|
64
|
+
computedHash: computed
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
reason: "mismatch",
|
|
69
|
+
storedHash: pkg.metadata.migrationHash,
|
|
70
|
+
computedHash: computed
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { verifyMigrationHash as n, computeMigrationHash as t };
|
|
76
|
+
//# sourceMappingURL=hash-BARZdVgW.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash-BARZdVgW.mjs","names":["sorted: Record<string, unknown>"],"sources":["../src/canonicalize-json.ts","../src/hash.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 type { MigrationMetadata } from './metadata';\nimport type { MigrationOps, MigrationPackage } from './package';\n\nexport interface VerifyResult {\n readonly ok: boolean;\n readonly reason?: 'mismatch';\n readonly storedHash: string;\n readonly computedHash: string;\n}\n\nfunction sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\n/**\n * Content-addressed migration hash over (metadata envelope sans\n * contracts/hints/signature, ops). See ADR 199 — Storage-only migration\n * identity 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 integrity check is purely structural, not semantic. The function\n * canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`\n * and hashes the result. Target-specific operation payloads (`step.sql`,\n * Mongo's pipeline AST, …) are hashed verbatim — no per-target\n * normalization is required, because what's being verified is \"do the\n * on-disk bytes still produce their recorded hash\", not \"do two\n * semantically-equivalent migrations hash the same\". The latter is an\n * emit-drift concern (ADR 192 step 2).\n *\n * The symmetry across write and read holds because `JSON.parse(\n * JSON.stringify(x))` round-trips JSON-safe values losslessly and\n * `sortKeys` is idempotent and deterministic — write-time and read-time\n * canonicalization produce the same canonical bytes regardless of\n * source-side key ordering or whitespace.\n *\n * The `migrationHash` field on the metadata is stripped before hashing\n * so the function can be used both at write time (when no hash exists\n * yet) and at verify time (rehashing an already-attested record).\n */\nexport function computeMigrationHash(\n metadata: Omit<MigrationMetadata, 'migrationHash'> & { readonly migrationHash?: string },\n ops: MigrationOps,\n): string {\n const {\n migrationHash: _migrationHash,\n signature: _signature,\n fromContract: _fromContract,\n toContract: _toContract,\n hints: _hints,\n ...strippedMeta\n } = metadata;\n\n const canonicalMetadata = canonicalizeJson(strippedMeta);\n const canonicalOps = canonicalizeJson(ops);\n\n const partHashes = [canonicalMetadata, canonicalOps].map(sha256Hex);\n const hash = sha256Hex(canonicalizeJson(partHashes));\n\n return `sha256:${hash}`;\n}\n\n/**\n * Re-hash an in-memory migration package and compare against the stored\n * `migrationHash`. See `computeMigrationHash` for the canonicalization rules.\n *\n * Returns `{ ok: true }` when the package is internally consistent, or\n * `{ ok: false, reason: 'mismatch', storedHash, computedHash }` when it is\n * not — typically a sign of FS corruption, partial writes, or a post-emit\n * hand edit.\n */\nexport function verifyMigrationHash(pkg: MigrationPackage): VerifyResult {\n const computed = computeMigrationHash(pkg.metadata, pkg.ops);\n\n if (pkg.metadata.migrationHash === computed) {\n return {\n ok: true,\n storedHash: pkg.metadata.migrationHash,\n computedHash: computed,\n };\n }\n\n return {\n ok: false,\n reason: 'mismatch',\n storedHash: pkg.metadata.migrationHash,\n computedHash: computed,\n };\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BzD,SAAgB,qBACd,UACA,KACQ;CACR,MAAM,EACJ,eAAe,gBACf,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;;;;;;;;;;;AActD,SAAgB,oBAAoB,KAAqC;CACvE,MAAM,WAAW,qBAAqB,IAAI,UAAU,IAAI,IAAI;AAE5D,KAAI,IAAI,SAAS,kBAAkB,SACjC,QAAO;EACL,IAAI;EACJ,YAAY,IAAI,SAAS;EACzB,cAAc;EACf;AAGH,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,YAAY,IAAI,SAAS;EACzB,cAAc;EACf"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { i as errorDuplicateInvariantInEdge, s as errorInvalidInvariantId } from "./errors-Bl3cKiM8.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/invariants.ts
|
|
4
|
+
/**
|
|
5
|
+
* Hygiene check for `invariantId`. Rejects empty values plus any
|
|
6
|
+
* whitespace or control character (including Unicode whitespace like
|
|
7
|
+
* NBSP and em space, which are visually identical to ASCII space and
|
|
8
|
+
* routinely sneak in via paste).
|
|
9
|
+
*/
|
|
10
|
+
function validateInvariantId(invariantId) {
|
|
11
|
+
if (invariantId.length === 0) return false;
|
|
12
|
+
return !/[\p{Cc}\p{White_Space}]/u.test(invariantId);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Walk a migration's operations and produce its `providedInvariants`
|
|
16
|
+
* aggregate: the sorted, deduplicated list of `invariantId`s declared
|
|
17
|
+
* by data-transform ops. Ops without `operationClass === 'data'` are
|
|
18
|
+
* skipped; data ops without an `invariantId` are skipped.
|
|
19
|
+
*
|
|
20
|
+
* Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and
|
|
21
|
+
* `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.
|
|
22
|
+
*/
|
|
23
|
+
function deriveProvidedInvariants(ops) {
|
|
24
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25
|
+
for (const op of ops) {
|
|
26
|
+
const invariantId = readInvariantId(op);
|
|
27
|
+
if (invariantId === void 0) continue;
|
|
28
|
+
if (!validateInvariantId(invariantId)) throw errorInvalidInvariantId(invariantId);
|
|
29
|
+
if (seen.has(invariantId)) throw errorDuplicateInvariantInEdge(invariantId);
|
|
30
|
+
seen.add(invariantId);
|
|
31
|
+
}
|
|
32
|
+
return [...seen].sort();
|
|
33
|
+
}
|
|
34
|
+
function readInvariantId(op) {
|
|
35
|
+
if (op.operationClass !== "data") return void 0;
|
|
36
|
+
const candidate = op.invariantId;
|
|
37
|
+
return typeof candidate === "string" ? candidate : void 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { validateInvariantId as n, deriveProvidedInvariants as t };
|
|
42
|
+
//# sourceMappingURL=invariants-BmrTBQ0A.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariants-BmrTBQ0A.mjs","names":[],"sources":["../src/invariants.ts"],"sourcesContent":["import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport { errorDuplicateInvariantInEdge, errorInvalidInvariantId } from './errors';\nimport type { MigrationOps } from './package';\n\n/**\n * Hygiene check for `invariantId`. Rejects empty values plus any\n * whitespace or control character (including Unicode whitespace like\n * NBSP and em space, which are visually identical to ASCII space and\n * routinely sneak in via paste).\n */\nexport function validateInvariantId(invariantId: string): boolean {\n if (invariantId.length === 0) return false;\n return !/[\\p{Cc}\\p{White_Space}]/u.test(invariantId);\n}\n\n/**\n * Walk a migration's operations and produce its `providedInvariants`\n * aggregate: the sorted, deduplicated list of `invariantId`s declared\n * by data-transform ops. Ops without `operationClass === 'data'` are\n * skipped; data ops without an `invariantId` are skipped.\n *\n * Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and\n * `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.\n */\nexport function deriveProvidedInvariants(ops: MigrationOps): readonly string[] {\n const seen = new Set<string>();\n for (const op of ops) {\n const invariantId = readInvariantId(op);\n if (invariantId === undefined) continue;\n if (!validateInvariantId(invariantId)) {\n throw errorInvalidInvariantId(invariantId);\n }\n if (seen.has(invariantId)) {\n throw errorDuplicateInvariantInEdge(invariantId);\n }\n seen.add(invariantId);\n }\n return [...seen].sort();\n}\n\nfunction readInvariantId(op: MigrationPlanOperation): string | undefined {\n if (op.operationClass !== 'data') return undefined;\n const candidate = (op as { invariantId?: unknown }).invariantId;\n return typeof candidate === 'string' ? candidate : undefined;\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,oBAAoB,aAA8B;AAChE,KAAI,YAAY,WAAW,EAAG,QAAO;AACrC,QAAO,CAAC,2BAA2B,KAAK,YAAY;;;;;;;;;;;AAYtD,SAAgB,yBAAyB,KAAsC;CAC7E,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,cAAc,gBAAgB,GAAG;AACvC,MAAI,gBAAgB,OAAW;AAC/B,MAAI,CAAC,oBAAoB,YAAY,CACnC,OAAM,wBAAwB,YAAY;AAE5C,MAAI,KAAK,IAAI,YAAY,CACvB,OAAM,8BAA8B,YAAY;AAElD,OAAK,IAAI,YAAY;;AAEvB,QAAO,CAAC,GAAG,KAAK,CAAC,MAAM;;AAGzB,SAAS,gBAAgB,IAAgD;AACvE,KAAI,GAAG,mBAAmB,OAAQ,QAAO;CACzC,MAAM,YAAa,GAAiC;AACpD,QAAO,OAAO,cAAc,WAAW,YAAY"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Contract } from "@prisma-next/contract/types";
|
|
2
|
+
|
|
3
|
+
//#region src/metadata.d.ts
|
|
4
|
+
interface MigrationHints {
|
|
5
|
+
readonly used: readonly string[];
|
|
6
|
+
readonly applied: readonly string[];
|
|
7
|
+
readonly plannerVersion: string;
|
|
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
|
+
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?: {
|
|
39
|
+
readonly author?: string;
|
|
40
|
+
readonly email?: string;
|
|
41
|
+
};
|
|
42
|
+
readonly signature?: {
|
|
43
|
+
readonly keyId: string;
|
|
44
|
+
readonly value: string;
|
|
45
|
+
} | null;
|
|
46
|
+
readonly createdAt: string;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { MigrationMetadata as n, MigrationHints as t };
|
|
50
|
+
//# sourceMappingURL=metadata-BP1cmU7Z.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-BP1cmU7Z.d.mts","names":[],"sources":["../src/metadata.ts"],"sourcesContent":[],"mappings":";;;UAEiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAqBd,SAAA,cAAiB,EAAA,MAAA;;;;;;;;;;;;;;;;;UAAjB,iBAAA;;;;yBAIQ;uBACF;kBACL"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
//#region src/op-schema.ts
|
|
4
|
+
const MigrationOpSchema = type({
|
|
5
|
+
id: "string",
|
|
6
|
+
label: "string",
|
|
7
|
+
operationClass: "'additive' | 'widening' | 'destructive' | 'data'",
|
|
8
|
+
"invariantId?": "string"
|
|
9
|
+
});
|
|
10
|
+
const MigrationOpsSchema = MigrationOpSchema.array();
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { MigrationOpsSchema as n, MigrationOpSchema as t };
|
|
14
|
+
//# sourceMappingURL=op-schema-DZKFua46.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"op-schema-DZKFua46.mjs","names":[],"sources":["../src/op-schema.ts"],"sourcesContent":["import { type } from 'arktype';\n\nexport const MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n 'invariantId?': 'string',\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nexport const MigrationOpsSchema = MigrationOpSchema.array();\n"],"mappings":";;;AAEA,MAAa,oBAAoB,KAAK;CACpC,IAAI;CACJ,OAAO;CACP,gBAAgB;CAChB,gBAAgB;CACjB,CAAC;AAGF,MAAa,qBAAqB,kBAAkB,OAAO"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { n as MigrationMetadata } from "./metadata-BP1cmU7Z.mjs";
|
|
2
|
+
import { MigrationPlanOperation } from "@prisma-next/framework-components/control";
|
|
3
|
+
|
|
4
|
+
//#region src/package.d.ts
|
|
5
|
+
type MigrationOps = readonly MigrationPlanOperation[];
|
|
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
|
+
interface MigrationPackage {
|
|
14
|
+
readonly dirName: string;
|
|
15
|
+
readonly dirPath: string;
|
|
16
|
+
readonly metadata: MigrationMetadata;
|
|
17
|
+
readonly ops: MigrationOps;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { MigrationPackage as n, MigrationOps as t };
|
|
21
|
+
//# sourceMappingURL=package-5HCCg0z-.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-5HCCg0z-.d.mts","names":[],"sources":["../src/package.ts"],"sourcesContent":[],"mappings":";;;;KAGY,YAAA,YAAwB;;AAApC;AASA;;;;;UAAiB,gBAAA;;;qBAGI;gBACL"}
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/migration-tools",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
|
-
"description": "On-disk migration persistence,
|
|
6
|
+
"description": "On-disk migration persistence, hash verification, and chain reconstruction for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"arktype": "^2.1.29",
|
|
9
9
|
"pathe": "^2.0.3",
|
|
10
10
|
"prettier": "^3.6.2",
|
|
11
|
-
"@prisma-next/contract": "0.5.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.5.0-dev.
|
|
13
|
-
"@prisma-next/utils": "0.5.0-dev.
|
|
11
|
+
"@prisma-next/contract": "0.5.0-dev.31",
|
|
12
|
+
"@prisma-next/framework-components": "0.5.0-dev.31",
|
|
13
|
+
"@prisma-next/utils": "0.5.0-dev.31"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"tsdown": "0.18.4",
|
|
17
17
|
"typescript": "5.9.3",
|
|
18
18
|
"vitest": "4.0.17",
|
|
19
|
-
"@prisma-next/
|
|
20
|
-
"@prisma-next/
|
|
19
|
+
"@prisma-next/tsdown": "0.0.0",
|
|
20
|
+
"@prisma-next/tsconfig": "0.0.0"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
@@ -27,21 +27,37 @@
|
|
|
27
27
|
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"exports": {
|
|
30
|
-
"./
|
|
31
|
-
"types": "./dist/exports/
|
|
32
|
-
"import": "./dist/exports/
|
|
30
|
+
"./metadata": {
|
|
31
|
+
"types": "./dist/exports/metadata.d.mts",
|
|
32
|
+
"import": "./dist/exports/metadata.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./package": {
|
|
35
|
+
"types": "./dist/exports/package.d.mts",
|
|
36
|
+
"import": "./dist/exports/package.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./graph": {
|
|
39
|
+
"types": "./dist/exports/graph.d.mts",
|
|
40
|
+
"import": "./dist/exports/graph.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./errors": {
|
|
43
|
+
"types": "./dist/exports/errors.d.mts",
|
|
44
|
+
"import": "./dist/exports/errors.mjs"
|
|
33
45
|
},
|
|
34
46
|
"./io": {
|
|
35
47
|
"types": "./dist/exports/io.d.mts",
|
|
36
48
|
"import": "./dist/exports/io.mjs"
|
|
37
49
|
},
|
|
38
|
-
"./
|
|
39
|
-
"types": "./dist/exports/
|
|
40
|
-
"import": "./dist/exports/
|
|
50
|
+
"./hash": {
|
|
51
|
+
"types": "./dist/exports/hash.d.mts",
|
|
52
|
+
"import": "./dist/exports/hash.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./invariants": {
|
|
55
|
+
"types": "./dist/exports/invariants.d.mts",
|
|
56
|
+
"import": "./dist/exports/invariants.mjs"
|
|
41
57
|
},
|
|
42
|
-
"./
|
|
43
|
-
"types": "./dist/exports/
|
|
44
|
-
"import": "./dist/exports/
|
|
58
|
+
"./migration-graph": {
|
|
59
|
+
"types": "./dist/exports/migration-graph.d.mts",
|
|
60
|
+
"import": "./dist/exports/migration-graph.mjs"
|
|
45
61
|
},
|
|
46
62
|
"./refs": {
|
|
47
63
|
"types": "./dist/exports/refs.d.mts",
|
package/src/errors.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
import { basename, dirname, relative } from 'pathe';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the canonical "re-emit this package" remediation hint.
|
|
5
|
+
*
|
|
6
|
+
* Every on-disk migration package ships its own `migration.ts` author-time
|
|
7
|
+
* file. Running it regenerates `migration.json` and `ops.json` with the
|
|
8
|
+
* correct hash + metadata, so it is the right primitive whenever a single
|
|
9
|
+
* package's on-disk artifacts are missing, malformed, or otherwise corrupt.
|
|
10
|
+
* Pointing users at `migration plan` would emit a *new* package rather than
|
|
11
|
+
* heal the broken one.
|
|
12
|
+
*/
|
|
13
|
+
function reemitHint(dir: string, fallback?: string): string {
|
|
14
|
+
const relativeDir = relative(process.cwd(), dir);
|
|
15
|
+
const reemit = `Re-emit the package by running \`node "${relativeDir}/migration.ts"\``;
|
|
16
|
+
return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
/**
|
|
2
20
|
* Structured error for migration tooling operations.
|
|
3
21
|
*
|
|
4
22
|
* Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
|
|
5
|
-
* the MIGRATION namespace. These are tooling-time errors (file I/O,
|
|
6
|
-
* migration history reconstruction), distinct from the runtime
|
|
7
|
-
* failures (PRECHECK_FAILED, POSTCHECK_FAILED,
|
|
23
|
+
* the MIGRATION namespace. These are tooling-time errors (file I/O, hash
|
|
24
|
+
* verification, migration history reconstruction), distinct from the runtime
|
|
25
|
+
* MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,
|
|
26
|
+
* etc.).
|
|
8
27
|
*
|
|
9
28
|
* Fields:
|
|
10
29
|
* - code: Stable machine-readable code (MIGRATION.SUBCODE)
|
|
@@ -55,7 +74,10 @@ export function errorDirectoryExists(dir: string): MigrationToolsError {
|
|
|
55
74
|
export function errorMissingFile(file: string, dir: string): MigrationToolsError {
|
|
56
75
|
return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {
|
|
57
76
|
why: `Expected "${file}" in "${dir}" but the file does not exist.`,
|
|
58
|
-
fix:
|
|
77
|
+
fix: reemitHint(
|
|
78
|
+
dir,
|
|
79
|
+
'or delete the directory if the migration is unwanted and the source TypeScript is gone.',
|
|
80
|
+
),
|
|
59
81
|
details: { file, dir },
|
|
60
82
|
});
|
|
61
83
|
}
|
|
@@ -63,19 +85,52 @@ export function errorMissingFile(file: string, dir: string): MigrationToolsError
|
|
|
63
85
|
export function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {
|
|
64
86
|
return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {
|
|
65
87
|
why: `Failed to parse "${filePath}": ${parseError}`,
|
|
66
|
-
fix:
|
|
88
|
+
fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
|
|
67
89
|
details: { filePath, parseError },
|
|
68
90
|
});
|
|
69
91
|
}
|
|
70
92
|
|
|
71
93
|
export function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {
|
|
72
94
|
return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {
|
|
73
|
-
why: `
|
|
74
|
-
fix:
|
|
95
|
+
why: `Migration manifest at "${filePath}" is invalid: ${reason}`,
|
|
96
|
+
fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
|
|
75
97
|
details: { filePath, reason },
|
|
76
98
|
});
|
|
77
99
|
}
|
|
78
100
|
|
|
101
|
+
export function errorInvalidOperationEntry(index: number, reason: string): MigrationToolsError {
|
|
102
|
+
return new MigrationToolsError(
|
|
103
|
+
'MIGRATION.INVALID_OPERATION_ENTRY',
|
|
104
|
+
'Migration operation entry is malformed',
|
|
105
|
+
{
|
|
106
|
+
why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,
|
|
107
|
+
fix: "Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').",
|
|
108
|
+
details: { index, reason },
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function errorStaleContractBookends(args: {
|
|
114
|
+
readonly side: 'from' | 'to';
|
|
115
|
+
readonly metaHash: string | null;
|
|
116
|
+
readonly contractHash: string;
|
|
117
|
+
}): MigrationToolsError {
|
|
118
|
+
const { side, metaHash, contractHash } = args;
|
|
119
|
+
// `meta.from` is `string | null` (null = baseline). Render `null` as a
|
|
120
|
+
// human-readable token in the diagnostic so the message stays clear when
|
|
121
|
+
// the mismatch is a baseline-vs-non-baseline disagreement.
|
|
122
|
+
const renderedMetaHash = metaHash === null ? 'null (baseline)' : `"${metaHash}"`;
|
|
123
|
+
return new MigrationToolsError(
|
|
124
|
+
'MIGRATION.STALE_CONTRACT_BOOKENDS',
|
|
125
|
+
'Migration manifest contract bookends disagree with describe()',
|
|
126
|
+
{
|
|
127
|
+
why: `migration.json stores ${side}Contract.storage.storageHash "${contractHash}", but describe() returned meta.${side} = ${renderedMetaHash}. The bookend is stale — most likely the migration's describe() was edited after the package was scaffolded by \`migration plan\`.`,
|
|
128
|
+
fix: 'Re-run `migration plan` to regenerate the package with fresh contract bookends, or restore the directory from version control.',
|
|
129
|
+
details: { side, metaHash, contractHash },
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
79
134
|
export function errorInvalidSlug(slug: string): MigrationToolsError {
|
|
80
135
|
return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {
|
|
81
136
|
why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
|
|
@@ -92,13 +147,17 @@ export function errorInvalidDestName(destName: string): MigrationToolsError {
|
|
|
92
147
|
});
|
|
93
148
|
}
|
|
94
149
|
|
|
95
|
-
export function errorSameSourceAndTarget(
|
|
150
|
+
export function errorSameSourceAndTarget(dir: string, hash: string): MigrationToolsError {
|
|
151
|
+
const dirName = basename(dir);
|
|
96
152
|
return new MigrationToolsError(
|
|
97
153
|
'MIGRATION.SAME_SOURCE_AND_TARGET',
|
|
98
154
|
'Migration has same source and target',
|
|
99
155
|
{
|
|
100
156
|
why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
|
|
101
|
-
fix:
|
|
157
|
+
fix: reemitHint(
|
|
158
|
+
dir,
|
|
159
|
+
'or delete the directory if the migration is unwanted and the source TypeScript is gone.',
|
|
160
|
+
),
|
|
102
161
|
details: { dirName, hash },
|
|
103
162
|
},
|
|
104
163
|
);
|
|
@@ -175,14 +234,79 @@ export function errorInvalidRefValue(value: string): MigrationToolsError {
|
|
|
175
234
|
});
|
|
176
235
|
}
|
|
177
236
|
|
|
178
|
-
export function
|
|
237
|
+
export function errorDuplicateMigrationHash(migrationHash: string): MigrationToolsError {
|
|
238
|
+
return new MigrationToolsError(
|
|
239
|
+
'MIGRATION.DUPLICATE_MIGRATION_HASH',
|
|
240
|
+
'Duplicate migrationHash in migration graph',
|
|
241
|
+
{
|
|
242
|
+
why: `Multiple migrations share migrationHash "${migrationHash}". Each migration must have a unique content-addressed identity.`,
|
|
243
|
+
fix: 'Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.',
|
|
244
|
+
details: { migrationHash },
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function errorInvalidInvariantId(invariantId: string): MigrationToolsError {
|
|
250
|
+
return new MigrationToolsError('MIGRATION.INVALID_INVARIANT_ID', 'Invalid invariantId', {
|
|
251
|
+
why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,
|
|
252
|
+
fix: 'Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. "backfill-user-phone", "users/backfill-phone", or "BackfillUserPhone".',
|
|
253
|
+
details: { invariantId },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function errorDuplicateInvariantInEdge(invariantId: string): MigrationToolsError {
|
|
179
258
|
return new MigrationToolsError(
|
|
180
|
-
'MIGRATION.
|
|
181
|
-
'Duplicate
|
|
259
|
+
'MIGRATION.DUPLICATE_INVARIANT_IN_EDGE',
|
|
260
|
+
'Duplicate invariantId on a single migration',
|
|
182
261
|
{
|
|
183
|
-
why: `
|
|
184
|
-
fix: '
|
|
185
|
-
details: {
|
|
262
|
+
why: `invariantId "${invariantId}" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,
|
|
263
|
+
fix: 'Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.',
|
|
264
|
+
details: { invariantId },
|
|
186
265
|
},
|
|
187
266
|
);
|
|
188
267
|
}
|
|
268
|
+
|
|
269
|
+
export function errorProvidedInvariantsMismatch(
|
|
270
|
+
filePath: string,
|
|
271
|
+
stored: readonly string[],
|
|
272
|
+
derived: readonly string[],
|
|
273
|
+
): MigrationToolsError {
|
|
274
|
+
const storedSet = new Set(stored);
|
|
275
|
+
const derivedSet = new Set(derived);
|
|
276
|
+
const missing = [...derivedSet].filter((id) => !storedSet.has(id));
|
|
277
|
+
const extra = [...storedSet].filter((id) => !derivedSet.has(id));
|
|
278
|
+
// When sets agree but arrays don't, the only difference is ordering — call
|
|
279
|
+
// it out so the reader doesn't stare at two visually-identical arrays.
|
|
280
|
+
// Canonical providedInvariants is sorted ascending; a manifest with the
|
|
281
|
+
// same ids in a different order is still a mismatch (the hash check would
|
|
282
|
+
// also fail), but the human-readable diagnostic is otherwise unhelpful.
|
|
283
|
+
const orderingOnly = missing.length === 0 && extra.length === 0;
|
|
284
|
+
const why = orderingOnly
|
|
285
|
+
? `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.`
|
|
286
|
+
: `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`;
|
|
287
|
+
return new MigrationToolsError(
|
|
288
|
+
'MIGRATION.PROVIDED_INVARIANTS_MISMATCH',
|
|
289
|
+
'providedInvariants on migration.json disagrees with ops.json',
|
|
290
|
+
{
|
|
291
|
+
why,
|
|
292
|
+
fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
|
|
293
|
+
details: { filePath, stored, derived, difference: { missing, extra } },
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function errorMigrationHashMismatch(
|
|
299
|
+
dir: string,
|
|
300
|
+
storedHash: string,
|
|
301
|
+
computedHash: string,
|
|
302
|
+
): MigrationToolsError {
|
|
303
|
+
// Render a cwd-relative path in the human-readable diagnostic so users
|
|
304
|
+
// running CLI commands from the project root see a familiar short path.
|
|
305
|
+
// Keep the absolute path in `details.dir` for machine consumers.
|
|
306
|
+
const relativeDir = relative(process.cwd(), dir);
|
|
307
|
+
return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {
|
|
308
|
+
why: `Stored migrationHash "${storedHash}" does not match the recomputed hash "${computedHash}" for "${relativeDir}". The migration.json or ops.json has been edited or partially written since emit.`,
|
|
309
|
+
fix: reemitHint(dir, 'or restore the directory from version control.'),
|
|
310
|
+
details: { dir, storedHash, computedHash },
|
|
311
|
+
});
|
|
312
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { errorInvalidJson, MigrationToolsError } from '../errors';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { MigrationEdge, MigrationGraph } from '../graph';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deriveProvidedInvariants, validateInvariantId } from '../invariants';
|
package/src/exports/io.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { MigrationHints, MigrationMetadata } from '../metadata';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { PathDecision } from '../
|
|
1
|
+
export type { PathDecision } from '../migration-graph';
|
|
2
2
|
export {
|
|
3
3
|
detectCycles,
|
|
4
4
|
detectOrphans,
|
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
findLeaf,
|
|
7
7
|
findPath,
|
|
8
8
|
findPathWithDecision,
|
|
9
|
+
findPathWithInvariants,
|
|
9
10
|
findReachableLeaves,
|
|
10
11
|
reconstructGraph,
|
|
11
|
-
} from '../
|
|
12
|
+
} from '../migration-graph';
|