@prisma-next/migration-tools 0.11.0-dev.9 → 0.12.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +4 -4
  2. package/dist/{errors-CoEN114u.mjs → errors-vFROOhCR.mjs} +34 -21
  3. package/dist/{errors-CoEN114u.mjs.map → errors-vFROOhCR.mjs.map} +1 -1
  4. package/dist/exports/aggregate.d.mts +328 -204
  5. package/dist/exports/aggregate.d.mts.map +1 -1
  6. package/dist/exports/aggregate.mjs +480 -243
  7. package/dist/exports/aggregate.mjs.map +1 -1
  8. package/dist/exports/errors.d.mts +2 -2
  9. package/dist/exports/errors.d.mts.map +1 -1
  10. package/dist/exports/errors.mjs +1 -1
  11. package/dist/exports/graph.d.mts +1 -1
  12. package/dist/exports/hash.d.mts +8 -9
  13. package/dist/exports/hash.d.mts.map +1 -1
  14. package/dist/exports/hash.mjs +1 -1
  15. package/dist/exports/invariants.d.mts +1 -1
  16. package/dist/exports/invariants.d.mts.map +1 -1
  17. package/dist/exports/invariants.mjs +1 -1
  18. package/dist/exports/io.d.mts +2 -83
  19. package/dist/exports/io.mjs +1 -1
  20. package/dist/exports/metadata.d.mts +2 -2
  21. package/dist/exports/migration-graph.d.mts +2 -2
  22. package/dist/exports/migration-graph.d.mts.map +1 -1
  23. package/dist/exports/migration-graph.mjs +2 -15
  24. package/dist/exports/migration-ts.d.mts.map +1 -1
  25. package/dist/exports/migration-ts.mjs.map +1 -1
  26. package/dist/exports/migration.d.mts +5 -6
  27. package/dist/exports/migration.d.mts.map +1 -1
  28. package/dist/exports/migration.mjs +14 -32
  29. package/dist/exports/migration.mjs.map +1 -1
  30. package/dist/exports/package.d.mts +1 -1
  31. package/dist/exports/ref-resolution.d.mts +2 -2
  32. package/dist/exports/ref-resolution.d.mts.map +1 -1
  33. package/dist/exports/ref-resolution.mjs +1 -1
  34. package/dist/exports/ref-resolution.mjs.map +1 -1
  35. package/dist/exports/refs.d.mts +2 -2
  36. package/dist/exports/refs.d.mts.map +1 -1
  37. package/dist/exports/refs.mjs +3 -137
  38. package/dist/exports/spaces.d.mts +31 -132
  39. package/dist/exports/spaces.d.mts.map +1 -1
  40. package/dist/exports/spaces.mjs +13 -9
  41. package/dist/exports/spaces.mjs.map +1 -1
  42. package/dist/{graph-BrLXqoUc.d.mts → graph-3dLMZp5l.d.mts} +1 -2
  43. package/dist/graph-3dLMZp5l.d.mts.map +1 -0
  44. package/dist/graph-membership-BV23F1IV.mjs +15 -0
  45. package/dist/graph-membership-BV23F1IV.mjs.map +1 -0
  46. package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
  47. package/dist/hash--Y7vCpN3.mjs.map +1 -0
  48. package/dist/{invariants-lbJddL-S.mjs → invariants-C23nXy1c.mjs} +2 -2
  49. package/dist/{invariants-lbJddL-S.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
  50. package/dist/{io-gHmDrSjQ.mjs → io-BGlPOt9b.mjs} +100 -13
  51. package/dist/io-BGlPOt9b.mjs.map +1 -0
  52. package/dist/io-BH4G3F-i.d.mts +124 -0
  53. package/dist/io-BH4G3F-i.d.mts.map +1 -0
  54. package/dist/metadata-Bp9X04gM.d.mts +2 -0
  55. package/dist/{migration-graph-C2iNX8dk.mjs → migration-graph-BMAqSfv9.mjs} +6 -26
  56. package/dist/migration-graph-BMAqSfv9.mjs.map +1 -0
  57. package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-CWEM2SLR.d.mts} +6 -6
  58. package/dist/migration-graph-CWEM2SLR.d.mts.map +1 -0
  59. package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
  60. package/dist/{package-DZj8YvD0.d.mts → package-Ca-J_z_0.d.mts} +1 -1
  61. package/dist/package-Ca-J_z_0.d.mts.map +1 -0
  62. package/dist/{read-contract-space-contract-Cglige7P.mjs → read-contract-space-contract-TbeXuJXL.mjs} +32 -5
  63. package/dist/read-contract-space-contract-TbeXuJXL.mjs.map +1 -0
  64. package/dist/{refs-HhOkD8BT.mjs → refs-C-_WUrPw.mjs} +96 -3
  65. package/dist/refs-C-_WUrPw.mjs.map +1 -0
  66. package/dist/refs-C7wuYFqZ.d.mts +42 -0
  67. package/dist/refs-C7wuYFqZ.d.mts.map +1 -0
  68. package/dist/snapshot-Bazwo13S.mjs +137 -0
  69. package/dist/snapshot-Bazwo13S.mjs.map +1 -0
  70. package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
  71. package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
  72. package/package.json +18 -9
  73. package/src/aggregate/aggregate.ts +266 -0
  74. package/src/aggregate/check-integrity.ts +243 -0
  75. package/src/aggregate/loader.ts +161 -334
  76. package/src/aggregate/planner-types.ts +14 -14
  77. package/src/aggregate/planner.ts +20 -23
  78. package/src/aggregate/project-schema-to-space.ts +3 -8
  79. package/src/aggregate/strategies/graph-walk.ts +15 -10
  80. package/src/aggregate/strategies/synth.ts +4 -4
  81. package/src/aggregate/types.ts +81 -62
  82. package/src/aggregate/verifier.ts +23 -23
  83. package/src/assert-descriptor-self-consistency.ts +6 -0
  84. package/src/compute-extension-space-apply-path.ts +1 -1
  85. package/src/emit-contract-space-artefacts.ts +4 -3
  86. package/src/errors.ts +43 -2
  87. package/src/exports/aggregate.ts +29 -19
  88. package/src/exports/io.ts +2 -0
  89. package/src/exports/metadata.ts +1 -1
  90. package/src/exports/refs.ts +3 -0
  91. package/src/exports/spaces.ts +3 -0
  92. package/src/graph.ts +0 -1
  93. package/src/hash.ts +7 -8
  94. package/src/integrity-violation.ts +114 -0
  95. package/src/io.ts +139 -14
  96. package/src/metadata.ts +1 -1
  97. package/src/migration-base.ts +10 -30
  98. package/src/migration-graph.ts +7 -35
  99. package/src/read-contract-space-head-ref.ts +5 -2
  100. package/src/refs/snapshot.ts +3 -1
  101. package/src/refs.ts +121 -0
  102. package/src/space-layout.ts +30 -0
  103. package/dist/exports/io.d.mts.map +0 -1
  104. package/dist/exports/migration-graph.mjs.map +0 -1
  105. package/dist/exports/refs.mjs.map +0 -1
  106. package/dist/graph-BrLXqoUc.d.mts.map +0 -1
  107. package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
  108. package/dist/io-gHmDrSjQ.mjs.map +0 -1
  109. package/dist/metadata-BFX0xdz8.d.mts +0 -2
  110. package/dist/migration-graph-C2iNX8dk.mjs.map +0 -1
  111. package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
  112. package/dist/package-DZj8YvD0.d.mts.map +0 -1
  113. package/dist/read-contract-space-contract-Cglige7P.mjs.map +0 -1
  114. package/dist/refs-CDaNerhT.d.mts +0 -16
  115. package/dist/refs-CDaNerhT.d.mts.map +0 -1
  116. package/dist/refs-HhOkD8BT.mjs.map +0 -1
  117. package/src/aggregate/extract-storage-element-names.ts +0 -75
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refs-C-_WUrPw.mjs","names":[],"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\n/**\n * The system head ref lives at `refs/head.json`. It is read (and its\n * corruption judged) through `readContractSpaceHeadRef`, not as a\n * user-authored ref, so {@link readRefsTolerant} excludes it.\n */\nexport const HEAD_REF_NAME = 'head';\n\n/**\n * A single ref file that exists on disk but cannot be turned into a\n * {@link RefEntry} (unparseable JSON or schema-invalid content). The ref\n * is omitted from the result; the problem is surfaced for the integrity\n * layer to report as `refUnreadable` rather than aborting the load.\n */\nexport interface RefLoadProblem {\n readonly refName: string;\n readonly detail: string;\n}\n\nexport interface TolerantRefsResult {\n readonly refs: Refs;\n readonly problems: readonly RefLoadProblem[];\n}\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 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(\n (entry) => entry.endsWith('.json') && !entry.endsWith('.contract.json'),\n );\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\n/**\n * Read a space's user-authored refs without ever throwing on disk\n * content. A ref whose JSON is unparseable or whose shape fails\n * {@link RefEntrySchema} is omitted from `refs` and reported as a\n * {@link RefLoadProblem}; the remaining well-formed refs are still\n * returned. A missing `refs/` directory yields no refs and no problems.\n *\n * `refs/head.json` is deliberately skipped here: the system head ref is\n * read through `readContractSpaceHeadRef` (which validates head-ref\n * shape, distinct from the strict user-ref hash grammar), so it is judged\n * there and never doubles as a user ref. Genuine I/O faults (EACCES, EIO,\n * …) still propagate — only parse / schema problems are made tolerant.\n */\nexport async function readRefsTolerant(refsDir: string): Promise<TolerantRefsResult> {\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 { refs: {}, problems: [] };\n }\n throw error;\n }\n\n const jsonFiles = entries.filter(\n (entry) =>\n entry.endsWith('.json') &&\n !entry.endsWith('.contract.json') &&\n entry !== `${HEAD_REF_NAME}.json`,\n );\n const refs: Record<string, RefEntry> = {};\n const problems: RefLoadProblem[] = [];\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)\n // and benign EISDIR if a directory happens to end in `.json`.\n // Anything else (EACCES, EIO, …) is a real failure and propagates.\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 (e) {\n problems.push({ refName: name, detail: e instanceof Error ? e.message : String(e) });\n continue;\n }\n\n const result = RefEntrySchema(parsed);\n if (result instanceof type.errors) {\n problems.push({ refName: name, detail: result.summary });\n continue;\n }\n\n refs[name] = result;\n }\n\n return { refs, problems };\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 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\n/**\n * Index user-authored refs by the contract hash each ref points at.\n * Each bucket is sorted lex-asc for deterministic output.\n */\nexport function refsByContractHash(refs: Refs): ReadonlyMap<string, readonly string[]> {\n const byHash = new Map<string, string[]>();\n for (const [name, entry] of Object.entries(refs)) {\n const bucket = byHash.get(entry.hash);\n if (bucket) bucket.push(name);\n else byHash.set(entry.hash, [name]);\n }\n for (const bucket of byHash.values()) {\n bucket.sort();\n }\n return byHash;\n}\n\n/**\n * Read `migrations/<space>/refs/*.json` and index by destination hash.\n * Returns an empty map when the refs directory does not exist.\n */\nexport async function resolveRefsByContractHash(\n refsDir: string,\n): Promise<ReadonlyMap<string, readonly string[]>> {\n return refsByContractHash(await readRefs(refsDir));\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 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,42 @@
1
+ //#region src/refs.d.ts
2
+ interface RefEntry {
3
+ readonly hash: string;
4
+ readonly invariants: readonly string[];
5
+ }
6
+ type Refs = Readonly<Record<string, RefEntry>>;
7
+ /**
8
+ * The system head ref lives at `refs/head.json`. It is read (and its
9
+ * corruption judged) through `readContractSpaceHeadRef`, not as a
10
+ * user-authored ref, so {@link readRefsTolerant} excludes it.
11
+ */
12
+ declare const HEAD_REF_NAME = "head";
13
+ /**
14
+ * A single ref file that exists on disk but cannot be turned into a
15
+ * {@link RefEntry} (unparseable JSON or schema-invalid content). The ref
16
+ * is omitted from the result; the problem is surfaced for the integrity
17
+ * layer to report as `refUnreadable` rather than aborting the load.
18
+ */
19
+ interface RefLoadProblem {
20
+ readonly refName: string;
21
+ readonly detail: string;
22
+ }
23
+ declare function validateRefName(name: string): boolean;
24
+ declare function validateRefValue(value: string): boolean;
25
+ declare function readRef(refsDir: string, name: string): Promise<RefEntry>;
26
+ declare function readRefs(refsDir: string): Promise<Refs>;
27
+ declare function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void>;
28
+ declare function deleteRef(refsDir: string, name: string): Promise<void>;
29
+ /**
30
+ * Index user-authored refs by the contract hash each ref points at.
31
+ * Each bucket is sorted lex-asc for deterministic output.
32
+ */
33
+ declare function refsByContractHash(refs: Refs): ReadonlyMap<string, readonly string[]>;
34
+ /**
35
+ * Read `migrations/<space>/refs/*.json` and index by destination hash.
36
+ * Returns an empty map when the refs directory does not exist.
37
+ */
38
+ declare function resolveRefsByContractHash(refsDir: string): Promise<ReadonlyMap<string, readonly string[]>>;
39
+ declare function resolveRef(refs: Refs, name: string): RefEntry;
40
+ //#endregion
41
+ export { deleteRef as a, refsByContractHash as c, validateRefName as d, validateRefValue as f, Refs as i, resolveRef as l, RefEntry as n, readRef as o, writeRef as p, RefLoadProblem as r, readRefs as s, HEAD_REF_NAME as t, resolveRefsByContractHash as u };
42
+ //# sourceMappingURL=refs-C7wuYFqZ.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refs-C7wuYFqZ.d.mts","names":[],"sources":["../src/refs.ts"],"mappings":";UAUiB,QAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;AAAA,KAGT,IAAA,GAAO,QAAA,CAAS,MAAA,SAAe,QAAA;AAHtB;AAGrB;;;;AAHqB,cAUR,aAAA;;;;;;;UAQI,cAAA;EAAA,SACN,OAAA;EAAA,SACA,MAAM;AAAA;AAAA,iBAWD,eAAA,CAAgB,IAAY;AAAA,iBAQ5B,gBAAA,CAAiB,KAAa;AAAA,iBAsBxB,OAAA,CAAQ,OAAA,UAAiB,IAAA,WAAe,OAAO,CAAC,QAAA;AAAA,iBAmChD,QAAA,CAAS,OAAA,WAAkB,OAAO,CAAC,IAAA;AAAA,iBA4HnC,QAAA,CAAS,OAAA,UAAiB,IAAA,UAAc,KAAA,EAAO,QAAA,GAAW,OAAO;AAAA,iBAoBjE,SAAA,CAAU,OAAA,UAAiB,IAAA,WAAe,OAAO;;;;AAnLO;iBA6N9D,kBAAA,CAAmB,IAAA,EAAM,IAAA,GAAO,WAAW;;;;;iBAiBrC,yBAAA,CACpB,OAAA,WACC,OAAO,CAAC,WAAA;AAAA,iBAIK,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,IAAA,WAAe,QAAQ"}
@@ -0,0 +1,137 @@
1
+ import { g as errorInvalidRefName, h as errorInvalidRefFile, t as MigrationToolsError } from "./errors-vFROOhCR.mjs";
2
+ import { d as writeRef, l as validateRefName, n as deleteRef } from "./refs-C-_WUrPw.mjs";
3
+ import { basename, dirname, join } from "pathe";
4
+ import { access, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
5
+ import { type } from "arktype";
6
+ import { randomBytes } from "node:crypto";
7
+ import { canonicalizeJson } from "@prisma-next/framework-components/utils";
8
+ //#region src/refs/snapshot.ts
9
+ const ContractIrSchema = type({
10
+ targetFamily: "string",
11
+ target: "string",
12
+ profileHash: "string",
13
+ storage: type({ storageHash: "string" }),
14
+ domain: type({ namespaces: "object" })
15
+ });
16
+ function hasErrnoCode(error, code) {
17
+ return error instanceof Error && error.code === code;
18
+ }
19
+ function snapshotJsonPath(refsDir, name) {
20
+ return join(refsDir, `${name}.contract.json`);
21
+ }
22
+ function snapshotDtsPath(refsDir, name) {
23
+ return join(refsDir, `${name}.contract.d.ts`);
24
+ }
25
+ function tmpPathFor(finalPath) {
26
+ return join(dirname(finalPath), `.${basename(finalPath)}.${Date.now()}-${randomBytes(4).toString("hex")}.tmp`);
27
+ }
28
+ async function atomicWriteFile(finalPath, content) {
29
+ await mkdir(dirname(finalPath), { recursive: true });
30
+ const tmpPath = tmpPathFor(finalPath);
31
+ await writeFile(tmpPath, content);
32
+ await rename(tmpPath, finalPath);
33
+ }
34
+ async function unlinkIfExists(filePath) {
35
+ try {
36
+ await unlink(filePath);
37
+ } catch (error) {
38
+ if (hasErrnoCode(error, "ENOENT")) return;
39
+ throw error;
40
+ }
41
+ }
42
+ function parseContractSnapshotJson(filePath, raw) {
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(raw);
46
+ } catch {
47
+ throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
48
+ }
49
+ const result = ContractIrSchema(parsed);
50
+ if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
51
+ return result;
52
+ }
53
+ async function writeRefSnapshot(refsDir, name, snapshot) {
54
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
55
+ const jsonPath = snapshotJsonPath(refsDir, name);
56
+ const dtsPath = snapshotDtsPath(refsDir, name);
57
+ const jsonContent = `${canonicalizeJson(snapshot.contract)}\n`;
58
+ const dtsContent = snapshot.contractDts.endsWith("\n") ? snapshot.contractDts : `${snapshot.contractDts}\n`;
59
+ try {
60
+ await atomicWriteFile(jsonPath, jsonContent);
61
+ } catch (error) {
62
+ await unlinkIfExists(jsonPath);
63
+ throw error;
64
+ }
65
+ try {
66
+ await atomicWriteFile(dtsPath, dtsContent);
67
+ } catch (error) {
68
+ await unlinkIfExists(jsonPath);
69
+ await unlinkIfExists(dtsPath);
70
+ throw error;
71
+ }
72
+ }
73
+ async function readRefSnapshot(refsDir, name) {
74
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
75
+ const jsonPath = snapshotJsonPath(refsDir, name);
76
+ const dtsPath = snapshotDtsPath(refsDir, name);
77
+ let raw;
78
+ try {
79
+ raw = await readFile(jsonPath, "utf-8");
80
+ } catch (error) {
81
+ if (hasErrnoCode(error, "ENOENT")) return null;
82
+ throw error;
83
+ }
84
+ const contract = parseContractSnapshotJson(jsonPath, raw);
85
+ let contractDts;
86
+ try {
87
+ contractDts = await readFile(dtsPath, "utf-8");
88
+ } catch (error) {
89
+ if (hasErrnoCode(error, "ENOENT")) throw errorInvalidRefFile(dtsPath, "Missing paired contract.d.ts snapshot file");
90
+ throw error;
91
+ }
92
+ return {
93
+ contract,
94
+ contractDts
95
+ };
96
+ }
97
+ async function deleteRefSnapshot(refsDir, name) {
98
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
99
+ await unlinkIfExists(snapshotJsonPath(refsDir, name));
100
+ await unlinkIfExists(snapshotDtsPath(refsDir, name));
101
+ }
102
+ async function writeRefPaired(refsDir, name, entry, snapshot) {
103
+ await writeRefSnapshot(refsDir, name, snapshot);
104
+ try {
105
+ await writeRef(refsDir, name, entry);
106
+ } catch (writeError) {
107
+ try {
108
+ await deleteRefSnapshot(refsDir, name);
109
+ } catch {}
110
+ throw writeError;
111
+ }
112
+ }
113
+ function isUnknownRefError(error) {
114
+ return MigrationToolsError.is(error) && error.code === "MIGRATION.UNKNOWN_REF";
115
+ }
116
+ async function snapshotFilesExist(refsDir, name) {
117
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
118
+ const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];
119
+ return (await Promise.allSettled(paths.map((filePath) => access(filePath)))).some((result) => result.status === "fulfilled");
120
+ }
121
+ async function deleteRefPaired(refsDir, name) {
122
+ if (await snapshotFilesExist(refsDir, name)) {
123
+ try {
124
+ await deleteRef(refsDir, name);
125
+ } catch (error) {
126
+ if (!isUnknownRefError(error)) throw error;
127
+ }
128
+ await deleteRefSnapshot(refsDir, name);
129
+ return;
130
+ }
131
+ await deleteRef(refsDir, name);
132
+ await deleteRefSnapshot(refsDir, name);
133
+ }
134
+ //#endregion
135
+ export { writeRefSnapshot as a, writeRefPaired as i, deleteRefSnapshot as n, readRefSnapshot as r, deleteRefPaired as t };
136
+
137
+ //# sourceMappingURL=snapshot-Bazwo13S.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-Bazwo13S.mjs","names":[],"sources":["../src/refs/snapshot.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport { errorInvalidRefFile, errorInvalidRefName, MigrationToolsError } from '../errors';\nimport { deleteRef, type RefEntry, validateRefName, writeRef } from '../refs';\n\nexport interface ContractIR {\n readonly contract: unknown;\n readonly contractDts: string;\n}\n\nconst ContractIrSchema = type({\n targetFamily: 'string',\n target: 'string',\n profileHash: 'string',\n storage: type({\n storageHash: 'string',\n }),\n domain: type({\n namespaces: 'object',\n }),\n});\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nfunction snapshotJsonPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.json`);\n}\n\nfunction snapshotDtsPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.d.ts`);\n}\n\nfunction tmpPathFor(finalPath: string): string {\n const dir = dirname(finalPath);\n const fileName = basename(finalPath);\n return join(dir, `.${fileName}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`);\n}\n\nasync function atomicWriteFile(finalPath: string, content: string): Promise<void> {\n const dir = dirname(finalPath);\n await mkdir(dir, { recursive: true });\n const tmpPath = tmpPathFor(finalPath);\n await writeFile(tmpPath, content);\n await rename(tmpPath, finalPath);\n}\n\nasync function unlinkIfExists(filePath: string): Promise<void> {\n try {\n await unlink(filePath);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return;\n throw error;\n }\n}\n\nfunction parseContractSnapshotJson(filePath: string, raw: string): unknown {\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 = ContractIrSchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function writeRefSnapshot(\n refsDir: string,\n name: string,\n snapshot: ContractIR,\n): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n const jsonContent = `${canonicalizeJson(snapshot.contract)}\\n`;\n const dtsContent = snapshot.contractDts.endsWith('\\n')\n ? snapshot.contractDts\n : `${snapshot.contractDts}\\n`;\n\n try {\n await atomicWriteFile(jsonPath, jsonContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n throw error;\n }\n\n try {\n await atomicWriteFile(dtsPath, dtsContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n await unlinkIfExists(dtsPath);\n throw error;\n }\n}\n\nexport async function readRefSnapshot(refsDir: string, name: string): Promise<ContractIR | null> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n\n let raw: string;\n try {\n raw = await readFile(jsonPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return null;\n }\n throw error;\n }\n\n const contract = parseContractSnapshotJson(jsonPath, raw);\n\n let contractDts: string;\n try {\n contractDts = await readFile(dtsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorInvalidRefFile(dtsPath, 'Missing paired contract.d.ts snapshot file');\n }\n throw error;\n }\n\n return { contract, contractDts };\n}\n\nexport async function deleteRefSnapshot(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n await unlinkIfExists(snapshotJsonPath(refsDir, name));\n await unlinkIfExists(snapshotDtsPath(refsDir, name));\n}\n\nexport async function writeRefPaired(\n refsDir: string,\n name: string,\n entry: RefEntry,\n snapshot: ContractIR,\n): Promise<void> {\n await writeRefSnapshot(refsDir, name, snapshot);\n try {\n await writeRef(refsDir, name, entry);\n } catch (writeError) {\n try {\n await deleteRefSnapshot(refsDir, name);\n } catch {\n // Rollback failure is secondary; preserve the original writeRef error.\n }\n throw writeError;\n }\n}\n\nfunction isUnknownRefError(error: unknown): boolean {\n return MigrationToolsError.is(error) && error.code === 'MIGRATION.UNKNOWN_REF';\n}\n\nasync function snapshotFilesExist(refsDir: string, name: string): Promise<boolean> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];\n const checks = await Promise.allSettled(paths.map((filePath) => access(filePath)));\n return checks.some((result) => result.status === 'fulfilled');\n}\n\nexport async function deleteRefPaired(refsDir: string, name: string): Promise<void> {\n if (await snapshotFilesExist(refsDir, name)) {\n try {\n await deleteRef(refsDir, name);\n } catch (error) {\n if (!isUnknownRefError(error)) {\n throw error;\n }\n }\n await deleteRefSnapshot(refsDir, name);\n return;\n }\n\n await deleteRef(refsDir, name);\n await deleteRefSnapshot(refsDir, name);\n}\n"],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ //#region src/verify-contract-spaces.d.ts
2
+ /**
3
+ * List the per-space subdirectories under
4
+ * `<projectRoot>/migrations/`. Returns space-id directory names (sorted
5
+ * alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
6
+ * does **not** contain a `migration.json` manifest. The manifest is the
7
+ * structural marker of a user-authored migration directory (see
8
+ * `readMigrationsDir` in `./io`); directory names themselves belong to
9
+ * the user and are not part of the contract.
10
+ *
11
+ * Returns `[]` if the migrations directory does not exist (greenfield
12
+ * project).
13
+ *
14
+ * Reads only the user's repo. **No descriptor import.** The caller
15
+ * (verifier) feeds the result into {@link verifyContractSpaces} alongside
16
+ * the loaded-space set and the marker rows.
17
+ */
18
+ declare function listContractSpaceDirectories(projectMigrationsDir: string): Promise<readonly string[]>;
19
+ /**
20
+ * On-disk head value (`(hash, invariants)`) for one contract space.
21
+ * The verifier compares this against the marker row for the same space
22
+ * to detect drift between the user-emitted artefacts and the live DB
23
+ * marker.
24
+ */
25
+ interface ContractSpaceHeadRecord {
26
+ readonly hash: string;
27
+ readonly invariants: readonly string[];
28
+ }
29
+ /**
30
+ * Marker row read from `prisma_contract.marker` (one per `space`).
31
+ * Caller resolves these via the family runtime's marker reader before
32
+ * invoking {@link verifyContractSpaces}.
33
+ */
34
+ interface SpaceMarkerRecord {
35
+ readonly hash: string;
36
+ readonly invariants: readonly string[];
37
+ }
38
+ interface VerifyContractSpacesInputs {
39
+ /**
40
+ * Set of contract spaces the project declares: `'app'` plus each
41
+ * extension space in `extensionPacks`. The caller's discovery path
42
+ * never reads the extension descriptor module — it walks the
43
+ * `extensionPacks` configuration in `prisma-next.config.ts` for the
44
+ * space ids.
45
+ */
46
+ readonly loadedSpaces: ReadonlySet<string>;
47
+ /**
48
+ * Per-space subdirectories observed under
49
+ * `<projectRoot>/migrations/`. Resolved via
50
+ * {@link listContractSpaceDirectories}.
51
+ */
52
+ readonly spaceDirsOnDisk: readonly string[];
53
+ /**
54
+ * Head ref per space, keyed by space id. Caller reads
55
+ * `<projectRoot>/migrations/<space-id>/contract.json` and
56
+ * `<projectRoot>/migrations/<space-id>/refs/head.json` to construct
57
+ * this map. Spaces with no contract-space dir on disk simply omit a
58
+ * map entry.
59
+ */
60
+ readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;
61
+ /**
62
+ * Marker rows keyed by `space`. Caller reads them from the
63
+ * `prisma_contract.marker` table.
64
+ */
65
+ readonly markerRowsBySpace: ReadonlyMap<string, SpaceMarkerRecord>;
66
+ }
67
+ type SpaceVerifierViolation = {
68
+ readonly kind: 'declaredButUnmigrated';
69
+ readonly spaceId: string;
70
+ readonly remediation: string;
71
+ } | {
72
+ readonly kind: 'orphanMarker';
73
+ readonly spaceId: string;
74
+ readonly remediation: string;
75
+ } | {
76
+ readonly kind: 'orphanSpaceDir';
77
+ readonly spaceId: string;
78
+ readonly remediation: string;
79
+ } | {
80
+ readonly kind: 'hashMismatch';
81
+ readonly spaceId: string;
82
+ readonly priorHeadHash: string;
83
+ readonly markerHash: string;
84
+ readonly remediation: string;
85
+ } | {
86
+ readonly kind: 'invariantsMismatch';
87
+ readonly spaceId: string;
88
+ readonly onDiskInvariants: readonly string[];
89
+ readonly markerInvariants: readonly string[];
90
+ readonly remediation: string;
91
+ };
92
+ type VerifyContractSpacesResult = {
93
+ readonly ok: true;
94
+ } | {
95
+ readonly ok: false;
96
+ readonly violations: readonly SpaceVerifierViolation[];
97
+ };
98
+ /**
99
+ * Pure structural verifier for the per-space mechanism. Aggregates the
100
+ * three orphan / missing checks plus per-space hash and invariant
101
+ * comparison.
102
+ *
103
+ * Algorithm:
104
+ *
105
+ * - For every extension space declared in `loadedSpaces` (`'app'`
106
+ * excluded — the per-space verifier is scoped to extension members;
107
+ * the app is verified through the aggregate path):
108
+ * - If no contract-space dir on disk → `declaredButUnmigrated`.
109
+ * - Else if `markerRowsBySpace` lacks an entry → no violation here;
110
+ * the live-DB compare done outside this helper is where the
111
+ * absence shows up.
112
+ * - Else compare marker hash / invariants vs. on-disk head hash /
113
+ * invariants → `hashMismatch` / `invariantsMismatch` on drift.
114
+ * - For every contract-space dir on disk that is not in `loadedSpaces` →
115
+ * `orphanSpaceDir`.
116
+ * - For every marker row whose `space` is not in `loadedSpaces` →
117
+ * `orphanMarker`. The app-space marker is always loaded (`'app'` is
118
+ * in `loadedSpaces` by definition).
119
+ *
120
+ * Output is deterministic: violations are sorted first by `kind`
121
+ * (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
122
+ * `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
123
+ * passing equivalent inputs see byte-identical violation lists.
124
+ *
125
+ * Synchronous, pure, no I/O. **Does not import the extension descriptor**
126
+ * (the inputs are pre-resolved by the caller); the verifier reads only
127
+ * the user repo, not `node_modules`.
128
+ */
129
+ declare function verifyContractSpaces(inputs: VerifyContractSpacesInputs): VerifyContractSpacesResult;
130
+ //#endregion
131
+ export { VerifyContractSpacesResult as a, VerifyContractSpacesInputs as i, SpaceMarkerRecord as n, listContractSpaceDirectories as o, SpaceVerifierViolation as r, verifyContractSpaces as s, ContractSpaceHeadRecord as t };
132
+ //# sourceMappingURL=verify-contract-spaces-BdysZdQk.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-contract-spaces-BdysZdQk.d.mts","names":[],"sources":["../src/verify-contract-spaces.ts"],"mappings":";;AAyBA;;;;AAEU;AAyCV;;;;AAEqB;AAQrB;;;;AAEqB;iBAvDC,4BAAA,CACpB,oBAAA,WACC,OAAO;;;;;;;UAyCO,uBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;;;;;;UAQJ,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;AAAA,UAGJ,0BAAA;EAiCL;;;;;;;EAAA,SAzBD,YAAA,EAAc,WAAA;EAiCV;;;;;EAAA,SA1BJ,eAAA;EAoCI;;;;;;;EAAA,SA3BJ,eAAA,EAAiB,WAAA,SAAoB,uBAAA;EAqCjC;;AAAW;AAG1B;EAHe,SA/BJ,iBAAA,EAAmB,WAAA,SAAoB,iBAAA;AAAA;AAAA,KAGtC,sBAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,aAAA;EAAA,SACA,UAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,gBAAA;EAAA,SACA,gBAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGH,0BAAA;EAAA,SACG,EAAA;AAAA;EAAA,SACA,EAAA;EAAA,SAAoB,UAAA,WAAqB,sBAAsB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC9D,oBAAA,CACd,MAAA,EAAQ,0BAAA,GACP,0BAA0B"}
package/package.json CHANGED
@@ -1,32 +1,38 @@
1
1
  {
2
2
  "name": "@prisma-next/migration-tools",
3
- "version": "0.11.0-dev.9",
3
+ "version": "0.12.0-dev.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "On-disk migration persistence, hash verification, and chain reconstruction for Prisma Next",
8
8
  "dependencies": {
9
- "@prisma-next/contract": "0.11.0-dev.9",
10
- "@prisma-next/framework-components": "0.11.0-dev.9",
11
- "@prisma-next/utils": "0.11.0-dev.9",
9
+ "@prisma-next/contract": "0.12.0-dev.1",
10
+ "@prisma-next/framework-components": "0.12.0-dev.1",
11
+ "@prisma-next/utils": "0.12.0-dev.1",
12
12
  "arktype": "^2.2.0",
13
13
  "pathe": "^2.0.3",
14
14
  "prettier": "^3.8.3"
15
15
  },
16
16
  "devDependencies": {
17
- "@prisma-next/tsconfig": "0.11.0-dev.9",
18
- "@prisma-next/tsdown": "0.11.0-dev.9",
17
+ "@prisma-next/test-utils": "0.12.0-dev.1",
18
+ "@prisma-next/tsconfig": "0.12.0-dev.1",
19
+ "@prisma-next/tsdown": "0.12.0-dev.1",
19
20
  "tsdown": "0.22.0",
20
21
  "typescript": "5.9.3",
21
22
  "vitest": "4.1.6"
22
23
  },
24
+ "peerDependencies": {
25
+ "typescript": ">=5.9"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "typescript": {
29
+ "optional": true
30
+ }
31
+ },
23
32
  "files": [
24
33
  "dist",
25
34
  "src"
26
35
  ],
27
- "engines": {
28
- "node": ">=20"
29
- },
30
36
  "exports": {
31
37
  "./metadata": {
32
38
  "types": "./dist/exports/metadata.d.mts",
@@ -90,6 +96,9 @@
90
96
  },
91
97
  "./package.json": "./package.json"
92
98
  },
99
+ "engines": {
100
+ "node": ">=24"
101
+ },
93
102
  "repository": {
94
103
  "type": "git",
95
104
  "url": "https://github.com/prisma/prisma-next.git",