@prisma-next/migration-tools 0.4.0-dev.9 → 0.4.2

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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/{attestation-DnebS4XZ.mjs → attestation-BnzTb0Qp.mjs} +24 -23
  3. package/dist/attestation-BnzTb0Qp.mjs.map +1 -0
  4. package/dist/{errors-C_XuSbX7.mjs → errors-BmiSgz1j.mjs} +14 -7
  5. package/dist/errors-BmiSgz1j.mjs.map +1 -0
  6. package/dist/exports/attestation.d.mts +20 -6
  7. package/dist/exports/attestation.d.mts.map +1 -1
  8. package/dist/exports/attestation.mjs +3 -3
  9. package/dist/exports/dag.d.mts +8 -6
  10. package/dist/exports/dag.d.mts.map +1 -1
  11. package/dist/exports/dag.mjs +181 -107
  12. package/dist/exports/dag.mjs.map +1 -1
  13. package/dist/exports/io.d.mts +16 -13
  14. package/dist/exports/io.d.mts.map +1 -1
  15. package/dist/exports/io.mjs +2 -2
  16. package/dist/exports/migration-ts.d.mts +15 -21
  17. package/dist/exports/migration-ts.d.mts.map +1 -1
  18. package/dist/exports/migration-ts.mjs +28 -36
  19. package/dist/exports/migration-ts.mjs.map +1 -1
  20. package/dist/exports/migration.d.mts +48 -18
  21. package/dist/exports/migration.d.mts.map +1 -1
  22. package/dist/exports/migration.mjs +75 -85
  23. package/dist/exports/migration.mjs.map +1 -1
  24. package/dist/exports/refs.d.mts +11 -5
  25. package/dist/exports/refs.d.mts.map +1 -1
  26. package/dist/exports/refs.mjs +106 -30
  27. package/dist/exports/refs.mjs.map +1 -1
  28. package/dist/exports/types.d.mts +2 -2
  29. package/dist/exports/types.mjs +2 -16
  30. package/dist/{io-Cun81AIZ.mjs → io-Cd6GLyjK.mjs} +18 -22
  31. package/dist/io-Cd6GLyjK.mjs.map +1 -0
  32. package/dist/types-DyGXcWWp.d.mts +71 -0
  33. package/dist/types-DyGXcWWp.d.mts.map +1 -0
  34. package/package.json +5 -4
  35. package/src/attestation.ts +34 -26
  36. package/src/dag.ts +140 -154
  37. package/src/errors.ts +8 -0
  38. package/src/exports/attestation.ts +2 -1
  39. package/src/exports/io.ts +1 -1
  40. package/src/exports/migration-ts.ts +1 -1
  41. package/src/exports/migration.ts +8 -1
  42. package/src/exports/refs.ts +10 -2
  43. package/src/exports/types.ts +2 -8
  44. package/src/graph-ops.ts +65 -0
  45. package/src/io.ts +23 -24
  46. package/src/migration-base.ts +99 -101
  47. package/src/migration-ts.ts +28 -50
  48. package/src/queue.ts +37 -0
  49. package/src/refs.ts +148 -37
  50. package/src/types.ts +15 -55
  51. package/dist/attestation-DnebS4XZ.mjs.map +0 -1
  52. package/dist/errors-C_XuSbX7.mjs.map +0 -1
  53. package/dist/exports/types.mjs.map +0 -1
  54. package/dist/io-Cun81AIZ.mjs.map +0 -1
  55. package/dist/types-D2uX4ql7.d.mts +0 -100
  56. package/dist/types-D2uX4ql7.d.mts.map +0 -1
@@ -1,7 +1,7 @@
1
- import { c as errorInvalidRefValue, l as errorInvalidRefs, s as errorInvalidRefName, t as MigrationToolsError } from "../errors-C_XuSbX7.mjs";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1
+ import { c as errorInvalidRefFile, l as errorInvalidRefName, t as MigrationToolsError, u as errorInvalidRefValue } from "../errors-BmiSgz1j.mjs";
2
+ import { mkdir, readFile, readdir, rename, rmdir, unlink, writeFile } from "node:fs/promises";
3
3
  import { type } from "arktype";
4
- import { dirname, join } from "pathe";
4
+ import { dirname, join, relative } from "pathe";
5
5
 
6
6
  //#region src/refs.ts
7
7
  const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
@@ -16,58 +16,134 @@ function validateRefName(name) {
16
16
  function validateRefValue(value) {
17
17
  return REF_VALUE_PATTERN.test(value);
18
18
  }
19
- const RefsSchema = type("Record<string, string>").narrow((refs, ctx) => {
20
- for (const [key, value] of Object.entries(refs)) {
21
- if (!validateRefName(key)) return ctx.mustBe(`valid ref names (invalid: "${key}")`);
22
- if (!validateRefValue(value)) return ctx.mustBe(`valid contract hashes (invalid value for "${key}": "${value}")`);
23
- }
19
+ const RefEntrySchema = type({
20
+ hash: "string",
21
+ invariants: "string[]"
22
+ }).narrow((entry, ctx) => {
23
+ if (!validateRefValue(entry.hash)) return ctx.mustBe(`a valid contract hash (got "${entry.hash}")`);
24
24
  return true;
25
25
  });
26
- async function readRefs(refsPath) {
26
+ function refFilePath(refsDir, name) {
27
+ return join(refsDir, `${name}.json`);
28
+ }
29
+ function refNameFromPath(refsDir, filePath) {
30
+ return relative(refsDir, filePath).replace(/\.json$/, "");
31
+ }
32
+ async function readRef(refsDir, name) {
33
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
34
+ const filePath = refFilePath(refsDir, name);
27
35
  let raw;
28
36
  try {
29
- raw = await readFile(refsPath, "utf-8");
37
+ raw = await readFile(filePath, "utf-8");
30
38
  } catch (error) {
31
- if (error instanceof Error && error.code === "ENOENT") return {};
39
+ if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
40
+ why: `No ref file found at "${filePath}".`,
41
+ fix: `Create the ref with: prisma-next migration ref set ${name} <hash>`,
42
+ details: {
43
+ refName: name,
44
+ filePath
45
+ }
46
+ });
32
47
  throw error;
33
48
  }
34
49
  let parsed;
35
50
  try {
36
51
  parsed = JSON.parse(raw);
37
52
  } catch {
38
- throw errorInvalidRefs(refsPath, "Failed to parse as JSON");
53
+ throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
39
54
  }
40
- const result = RefsSchema(parsed);
41
- if (result instanceof type.errors) throw errorInvalidRefs(refsPath, result.summary);
55
+ const result = RefEntrySchema(parsed);
56
+ if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
42
57
  return result;
43
58
  }
44
- async function writeRefs(refsPath, refs) {
45
- for (const [key, value] of Object.entries(refs)) {
46
- if (!validateRefName(key)) throw errorInvalidRefName(key);
47
- if (!validateRefValue(value)) throw errorInvalidRefValue(value);
59
+ async function readRefs(refsDir) {
60
+ let entries;
61
+ try {
62
+ entries = await readdir(refsDir, {
63
+ recursive: true,
64
+ encoding: "utf-8"
65
+ });
66
+ } catch (error) {
67
+ if (error instanceof Error && error.code === "ENOENT") return {};
68
+ throw error;
69
+ }
70
+ const jsonFiles = entries.filter((entry) => entry.endsWith(".json"));
71
+ const refs = {};
72
+ for (const jsonFile of jsonFiles) {
73
+ const filePath = join(refsDir, jsonFile);
74
+ const name = refNameFromPath(refsDir, filePath);
75
+ let raw;
76
+ try {
77
+ raw = await readFile(filePath, "utf-8");
78
+ } catch (error) {
79
+ const code = error instanceof Error ? error.code : void 0;
80
+ if (code === "ENOENT" || code === "EISDIR") continue;
81
+ throw error;
82
+ }
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(raw);
86
+ } catch {
87
+ throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
88
+ }
89
+ const result = RefEntrySchema(parsed);
90
+ if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
91
+ refs[name] = result;
48
92
  }
49
- const sorted = Object.fromEntries(Object.entries(refs).sort(([a], [b]) => a.localeCompare(b)));
50
- const dir = dirname(refsPath);
93
+ return refs;
94
+ }
95
+ async function writeRef(refsDir, name, entry) {
96
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
97
+ if (!validateRefValue(entry.hash)) throw errorInvalidRefValue(entry.hash);
98
+ const filePath = refFilePath(refsDir, name);
99
+ const dir = dirname(filePath);
51
100
  await mkdir(dir, { recursive: true });
52
- const tmpPath = join(dir, `.refs.json.${Date.now()}.tmp`);
53
- await writeFile(tmpPath, `${JSON.stringify(sorted, null, 2)}\n`);
54
- await rename(tmpPath, refsPath);
101
+ const tmpPath = join(dir, `.${name.split("/").pop()}.json.${Date.now()}.tmp`);
102
+ await writeFile(tmpPath, `${JSON.stringify({
103
+ hash: entry.hash,
104
+ invariants: [...entry.invariants]
105
+ }, null, 2)}\n`);
106
+ await rename(tmpPath, filePath);
107
+ }
108
+ async function deleteRef(refsDir, name) {
109
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
110
+ const filePath = refFilePath(refsDir, name);
111
+ try {
112
+ await unlink(filePath);
113
+ } catch (error) {
114
+ if (error instanceof Error && error.code === "ENOENT") throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
115
+ why: `No ref file found at "${filePath}".`,
116
+ fix: "Run `prisma-next migration ref list` to see available refs.",
117
+ details: {
118
+ refName: name,
119
+ filePath
120
+ }
121
+ });
122
+ throw error;
123
+ }
124
+ let dir = dirname(filePath);
125
+ while (dir !== refsDir && dir.startsWith(refsDir)) try {
126
+ await rmdir(dir);
127
+ dir = dirname(dir);
128
+ } catch (error) {
129
+ const code = error instanceof Error ? error.code : void 0;
130
+ if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT") break;
131
+ throw error;
132
+ }
55
133
  }
56
134
  function resolveRef(refs, name) {
57
135
  if (!validateRefName(name)) throw errorInvalidRefName(name);
58
- const hash = refs[name];
59
- if (hash === void 0) throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
60
- why: `No ref named "${name}" exists in refs.json.`,
61
- fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: set the "${name}" key in migrations/refs.json.`,
136
+ if (!Object.hasOwn(refs, name)) throw new MigrationToolsError("MIGRATION.UNKNOWN_REF", `Unknown ref "${name}"`, {
137
+ why: `No ref named "${name}" exists.`,
138
+ fix: `Available refs: ${Object.keys(refs).join(", ") || "(none)"}. Create a ref with: prisma-next migration ref set ${name} <hash>`,
62
139
  details: {
63
140
  refName: name,
64
141
  availableRefs: Object.keys(refs)
65
142
  }
66
143
  });
67
- if (!validateRefValue(hash)) throw errorInvalidRefValue(hash);
68
- return hash;
144
+ return refs[name];
69
145
  }
70
146
 
71
147
  //#endregion
72
- export { readRefs, resolveRef, validateRefName, validateRefValue, writeRefs };
148
+ export { deleteRef, readRef, readRefs, resolveRef, validateRefName, validateRefValue, writeRef };
73
149
  //# sourceMappingURL=refs.mjs.map
@@ -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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"}
@@ -1,4 +1,4 @@
1
- import { a as DraftMigrationManifest, c as MigrationHints, d as isAttested, f as isDraft, i as DraftMigrationBundle, l as MigrationManifest, n as AttestedMigrationManifest, o as MigrationChainEntry, r as BaseMigrationBundle, s as MigrationGraph, t as AttestedMigrationBundle, u as MigrationOps } from "../types-D2uX4ql7.mjs";
1
+ import { a as MigrationManifest, i as MigrationHints, n as MigrationChainEntry, o as MigrationOps, r as MigrationGraph, t as MigrationBundle } from "../types-DyGXcWWp.mjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
 
@@ -31,5 +31,5 @@ declare class MigrationToolsError extends Error {
31
31
  static is(error: unknown): error is MigrationToolsError;
32
32
  }
33
33
  //#endregion
34
- export { type AttestedMigrationBundle, type AttestedMigrationManifest, type BaseMigrationBundle, type BaseMigrationBundle as MigrationBundle, type BaseMigrationBundle as MigrationPackage, type DraftMigrationBundle, type DraftMigrationManifest, type MigrationChainEntry, type MigrationGraph, type MigrationHints, type MigrationManifest, type MigrationOps, MigrationToolsError, isAttested, isDraft };
34
+ export { type MigrationBundle, type MigrationBundle as MigrationPackage, type MigrationChainEntry, type MigrationGraph, type MigrationHints, type MigrationManifest, type MigrationOps, MigrationToolsError };
35
35
  //# sourceMappingURL=types.d.mts.map
@@ -1,17 +1,3 @@
1
- import { t as MigrationToolsError } from "../errors-C_XuSbX7.mjs";
1
+ import { t as MigrationToolsError } from "../errors-BmiSgz1j.mjs";
2
2
 
3
- //#region src/types.ts
4
- /**
5
- * Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.
6
- * Use with `.filter(isAttested)` to get a typed array of attested bundles.
7
- */
8
- function isAttested(bundle) {
9
- return typeof bundle.manifest.migrationId === "string";
10
- }
11
- function isDraft(bundle) {
12
- return bundle.manifest.migrationId === null;
13
- }
14
-
15
- //#endregion
16
- export { MigrationToolsError, isAttested, isDraft };
17
- //# sourceMappingURL=types.mjs.map
3
+ export { MigrationToolsError };
@@ -1,4 +1,4 @@
1
- import { a as errorInvalidJson, d as errorMissingFile, o as errorInvalidManifest, r as errorDirectoryExists, u as errorInvalidSlug } from "./errors-C_XuSbX7.mjs";
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
2
  import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
3
  import { type } from "arktype";
4
4
  import { basename, dirname, join } from "pathe";
@@ -13,15 +13,14 @@ function hasErrnoCode(error, code) {
13
13
  const MigrationManifestSchema = type({
14
14
  from: "string",
15
15
  to: "string",
16
- migrationId: "string | null",
16
+ migrationId: "string",
17
17
  kind: "'regular' | 'baseline'",
18
18
  fromContract: "object | null",
19
19
  toContract: "object",
20
20
  hints: type({
21
21
  used: "string[]",
22
22
  applied: "string[]",
23
- plannerVersion: "string",
24
- planningStrategy: "string"
23
+ plannerVersion: "string"
25
24
  }),
26
25
  labels: "string[]",
27
26
  "authorship?": type({
@@ -51,24 +50,21 @@ async function writeMigrationPackage(dir, manifest, ops) {
51
50
  await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
52
51
  }
53
52
  /**
54
- * Copy the destination contract artifacts (`contract.json` and the
55
- * colocated `contract.d.ts`) into the migration package directory so
56
- * authors of the scaffolded `migration.ts` can import the typed
57
- * contract relative to the migration directory
58
- * (`import type { Contract } from './contract'`).
53
+ * Copy a list of files into `destDir`, optionally renaming each one.
59
54
  *
60
- * A missing `.d.ts` is tolerated (only the `.json` is required) so the
61
- * helper stays usable in tests that hand-roll a bare `contract.json`.
62
- * A missing `contract.json` or any other I/O failure — throws.
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.*`).
63
62
  */
64
- async function copyContractToMigrationDir(packageDir, contractJsonPath) {
65
- await copyFile(contractJsonPath, join(packageDir, "contract.json"));
66
- const dtsPath = `${contractJsonPath.slice(0, -5)}.d.ts`;
67
- try {
68
- await copyFile(dtsPath, join(packageDir, "contract.d.ts"));
69
- } catch (error) {
70
- if (hasErrnoCode(error, "ENOENT")) return;
71
- throw error;
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));
72
68
  }
73
69
  }
74
70
  async function writeMigrationManifest(dir, manifest) {
@@ -153,5 +149,5 @@ function formatMigrationDirName(timestamp, slug) {
153
149
  }
154
150
 
155
151
  //#endregion
156
- export { writeMigrationManifest as a, readMigrationsDir as i, formatMigrationDirName as n, writeMigrationOps as o, readMigrationPackage as r, writeMigrationPackage as s, copyContractToMigrationDir as t };
157
- //# sourceMappingURL=io-Cun81AIZ.mjs.map
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
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,71 @@
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
@@ -0,0 +1 @@
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"}
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@prisma-next/migration-tools",
3
- "version": "0.4.0-dev.9",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "On-disk migration persistence, attestation, and chain reconstruction for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.1.29",
9
9
  "pathe": "^2.0.3",
10
- "@prisma-next/contract": "0.4.0-dev.9",
11
- "@prisma-next/framework-components": "0.4.0-dev.9",
12
- "@prisma-next/utils": "0.4.0-dev.9"
10
+ "prettier": "^3.6.2",
11
+ "@prisma-next/contract": "0.4.2",
12
+ "@prisma-next/framework-components": "0.4.2",
13
+ "@prisma-next/utils": "0.4.2"
13
14
  },
14
15
  "devDependencies": {
15
16
  "tsdown": "0.18.4",
@@ -1,11 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalizeJson } from './canonicalize-json';
3
- import { readMigrationPackage, writeMigrationManifest } from './io';
4
- import type { MigrationManifest, MigrationOps } from './types';
3
+ import { readMigrationPackage } from './io';
4
+ import type { MigrationBundle, MigrationManifest, MigrationOps } from './types';
5
5
 
6
6
  export interface VerifyResult {
7
7
  readonly ok: boolean;
8
- readonly reason?: 'draft' | 'mismatch';
8
+ readonly reason?: 'mismatch';
9
9
  readonly storedMigrationId?: string;
10
10
  readonly computedMigrationId?: string;
11
11
  }
@@ -20,8 +20,15 @@ function sha256Hex(input: string): string {
20
20
  * for the rationale: contracts are anchored separately by the
21
21
  * storage-hash bookends inside the envelope; planner hints are advisory
22
22
  * and must not affect identity.
23
+ *
24
+ * The `migrationId` field on the manifest is stripped before hashing so
25
+ * the function can be used both at write time (when no id exists yet)
26
+ * and at verify time (rehashing an already-attested manifest).
23
27
  */
24
- export function computeMigrationId(manifest: MigrationManifest, ops: MigrationOps): string {
28
+ export function computeMigrationId(
29
+ manifest: Omit<MigrationManifest, 'migrationId'> & { readonly migrationId?: string },
30
+ ops: MigrationOps,
31
+ ): string {
25
32
  const {
26
33
  migrationId: _migrationId,
27
34
  signature: _signature,
@@ -40,34 +47,35 @@ export function computeMigrationId(manifest: MigrationManifest, ops: MigrationOp
40
47
  return `sha256:${hash}`;
41
48
  }
42
49
 
43
- /** Compute and persist `migrationId` to `manifest.json`. */
44
- export async function attestMigration(dir: string): Promise<string> {
45
- const pkg = await readMigrationPackage(dir);
46
- const migrationId = computeMigrationId(pkg.manifest, pkg.ops);
47
-
48
- const updated = { ...pkg.manifest, migrationId };
49
- await writeMigrationManifest(dir, updated);
50
-
51
- return migrationId;
52
- }
53
-
54
- export async function verifyMigration(dir: string): Promise<VerifyResult> {
55
- const pkg = await readMigrationPackage(dir);
56
-
57
- if (pkg.manifest.migrationId === null) {
58
- return { ok: false, reason: 'draft' };
59
- }
60
-
61
- const computed = computeMigrationId(pkg.manifest, pkg.ops);
50
+ /**
51
+ * Re-hash an on-disk migration bundle and compare against the stored
52
+ * `migrationId`. Returns `{ ok: true }` when the package is internally
53
+ * consistent (manifest + ops still produce the recorded id), or
54
+ * `{ ok: false, reason: 'mismatch', stored, computed }` when they do
55
+ * not typically a sign of FS corruption, partial writes, or a
56
+ * post-emit hand edit.
57
+ */
58
+ export function verifyMigrationBundle(bundle: MigrationBundle): VerifyResult {
59
+ const computed = computeMigrationId(bundle.manifest, bundle.ops);
62
60
 
63
- if (pkg.manifest.migrationId === computed) {
64
- return { ok: true, storedMigrationId: pkg.manifest.migrationId, computedMigrationId: computed };
61
+ if (bundle.manifest.migrationId === computed) {
62
+ return {
63
+ ok: true,
64
+ storedMigrationId: bundle.manifest.migrationId,
65
+ computedMigrationId: computed,
66
+ };
65
67
  }
66
68
 
67
69
  return {
68
70
  ok: false,
69
71
  reason: 'mismatch',
70
- storedMigrationId: pkg.manifest.migrationId,
72
+ storedMigrationId: bundle.manifest.migrationId,
71
73
  computedMigrationId: computed,
72
74
  };
73
75
  }
76
+
77
+ /** Convenience wrapper: read the package from disk then verify it. */
78
+ export async function verifyMigration(dir: string): Promise<VerifyResult> {
79
+ const pkg = await readMigrationPackage(dir);
80
+ return verifyMigrationBundle(pkg);
81
+ }