@aventara/prisma7-adapter 0.1.0-pilot.0

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 (86) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +564 -0
  4. package/dist/discovery/artifact.loader.d.ts +26 -0
  5. package/dist/discovery/artifact.loader.js +124 -0
  6. package/dist/discovery/capability-surface.interface.d.ts +191 -0
  7. package/dist/discovery/capability-surface.interface.js +13 -0
  8. package/dist/discovery/capability.deriver.d.ts +92 -0
  9. package/dist/discovery/capability.deriver.js +350 -0
  10. package/dist/discovery/discovery-artifact.interface.d.ts +210 -0
  11. package/dist/discovery/discovery-artifact.interface.js +1 -0
  12. package/dist/discovery/enum.mapper.d.ts +59 -0
  13. package/dist/discovery/enum.mapper.js +32 -0
  14. package/dist/discovery/field.mapper.d.ts +244 -0
  15. package/dist/discovery/field.mapper.js +387 -0
  16. package/dist/discovery/identifier.mapper.d.ts +188 -0
  17. package/dist/discovery/identifier.mapper.js +132 -0
  18. package/dist/discovery/model.discoverer.d.ts +108 -0
  19. package/dist/discovery/model.discoverer.js +341 -0
  20. package/dist/discovery/operations.deriver.d.ts +75 -0
  21. package/dist/discovery/operations.deriver.js +92 -0
  22. package/dist/discovery/scalar.mapper.d.ts +81 -0
  23. package/dist/discovery/scalar.mapper.js +51 -0
  24. package/dist/discovery/schema-facts.interface.d.ts +174 -0
  25. package/dist/discovery/schema-facts.interface.js +11 -0
  26. package/dist/discovery/schema.reader.d.ts +162 -0
  27. package/dist/discovery/schema.reader.js +692 -0
  28. package/dist/discovery/types.reader.d.ts +24 -0
  29. package/dist/discovery/types.reader.js +617 -0
  30. package/dist/discovery/vocabulary.dictionary.d.ts +258 -0
  31. package/dist/discovery/vocabulary.dictionary.js +429 -0
  32. package/dist/errors/error.normalizer.d.ts +250 -0
  33. package/dist/errors/error.normalizer.js +135 -0
  34. package/dist/errors/prisma-error.reader.d.ts +144 -0
  35. package/dist/errors/prisma-error.reader.js +167 -0
  36. package/dist/execution/first-variant.emulator.d.ts +64 -0
  37. package/dist/execution/first-variant.emulator.js +205 -0
  38. package/dist/execution/operation.executor.d.ts +87 -0
  39. package/dist/execution/operation.executor.js +210 -0
  40. package/dist/execution/result.shaper.d.ts +103 -0
  41. package/dist/execution/result.shaper.js +141 -0
  42. package/dist/execution/transaction.executor.d.ts +147 -0
  43. package/dist/execution/transaction.executor.js +13 -0
  44. package/dist/generate/artifact.emitter.d.ts +50 -0
  45. package/dist/generate/artifact.emitter.js +105 -0
  46. package/dist/generate/declared-support.reader.d.ts +33 -0
  47. package/dist/generate/declared-support.reader.js +74 -0
  48. package/dist/generate/discovery.bin.d.ts +2 -0
  49. package/dist/generate/discovery.bin.js +15 -0
  50. package/dist/generate/discovery.cli.d.ts +24 -0
  51. package/dist/generate/discovery.cli.js +163 -0
  52. package/dist/generate/existence.gate.d.ts +73 -0
  53. package/dist/generate/existence.gate.js +91 -0
  54. package/dist/generate/index.d.ts +81 -0
  55. package/dist/generate/index.js +65 -0
  56. package/dist/generate/node-version.guard.d.ts +8 -0
  57. package/dist/generate/node-version.guard.js +59 -0
  58. package/dist/generate/prisma-cli.resolver.d.ts +48 -0
  59. package/dist/generate/prisma-cli.resolver.js +95 -0
  60. package/dist/generate/prisma.generate.d.ts +164 -0
  61. package/dist/generate/prisma.generate.js +278 -0
  62. package/dist/generate/reader-crosscheck.d.ts +156 -0
  63. package/dist/generate/reader-crosscheck.js +202 -0
  64. package/dist/index.d.ts +5 -0
  65. package/dist/index.js +1 -0
  66. package/dist/prisma-client.interface.d.ts +48 -0
  67. package/dist/prisma-client.interface.js +9 -0
  68. package/dist/prisma.adapter.d.ts +55 -0
  69. package/dist/prisma.adapter.js +105 -0
  70. package/dist/provider/provider-profile.d.ts +53 -0
  71. package/dist/provider/provider-profile.js +91 -0
  72. package/dist/translation/identifier.addressing.d.ts +80 -0
  73. package/dist/translation/identifier.addressing.js +30 -0
  74. package/dist/translation/mutation.translator.d.ts +115 -0
  75. package/dist/translation/mutation.translator.js +580 -0
  76. package/dist/translation/operation.translator.d.ts +273 -0
  77. package/dist/translation/operation.translator.js +377 -0
  78. package/dist/translation/order-pagination.translator.d.ts +86 -0
  79. package/dist/translation/order-pagination.translator.js +159 -0
  80. package/dist/translation/projection.translator.d.ts +115 -0
  81. package/dist/translation/projection.translator.js +151 -0
  82. package/dist/translation/scalar.conversion.d.ts +101 -0
  83. package/dist/translation/scalar.conversion.js +221 -0
  84. package/dist/translation/where.translator.d.ts +79 -0
  85. package/dist/translation/where.translator.js +254 -0
  86. package/package.json +102 -0
@@ -0,0 +1,278 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import ts from "typescript";
6
+ import { computeModelHash } from "../discovery/artifact.loader.js";
7
+ import { readPrismaSchema } from "../discovery/schema.reader.js";
8
+ import { readPrismaCapabilitySurface } from "../discovery/types.reader.js";
9
+ import { emitDiscoveryArtifactModule } from "./artifact.emitter.js";
10
+ import { readDeclaredSupport } from "./declared-support.reader.js";
11
+ import { applyExistenceGate } from "./existence.gate.js";
12
+ import { resolvePrismaCli } from "./prisma-cli.resolver.js";
13
+ import { crossCheckReaders, describeReaderFindings, } from "./reader-crosscheck.js";
14
+ /**
15
+ * Task 3 โ€” the adapter's **generate step**: a devDependency-only executable and
16
+ * the only module that runs `prisma` or spawns a process. Nothing under
17
+ * `execution/`, and nothing in `discovery/artifact.loader.ts`, imports it; the
18
+ * package's build `exclude` keeps `src/generate/**` out of `dist` entirely, so
19
+ * a deployed application never carries `typescript`, a PSL reader or a child
20
+ * process.
21
+ *
22
+ * **The order is fixed, and each step earns its place:**
23
+ *
24
+ * 1. **The pinned `prisma` major** (C-846). PSL's grammar and the generated
25
+ * client's shape are version-bound; a major this reader has never seen must
26
+ * stop the build rather than produce a wrong `AdapterModel` from a grammar
27
+ * nobody read.
28
+ * 2. **`@prisma/client/runtime/client` resolves** (ยง1.2 item 16). This one is
29
+ * first among equals, because its failure is *silent*: every generated file
30
+ * opens `// @ts-nocheck`, so with the runtime unresolvable a TypeScript
31
+ * program over the generated tree reports **zero** diagnostics while
32
+ * `FieldRef` degrades to its uninstantiated form and `JsonValue` stops being
33
+ * a union โ€” flipping a types-derived nullability from `true` to `false` with
34
+ * nothing anywhere to say so **[RUN8]**. The guard is therefore a module
35
+ * *resolution*, never a diagnostic count: a diagnostic count is exactly the
36
+ * instrument that reads clean in this failure.
37
+ * 3. **`prisma generate`**, run by this step itself. The two sources must
38
+ * describe one instant; a client generated by some earlier command may
39
+ * describe an older schema, and nothing downstream could tell.
40
+ * 4. **Both sources read in that same run** โ€” PSL for the semantic model, the
41
+ * ๐ŸŸข generated types for the capability surface and for existence.
42
+ * 5. **The structural checks, in that order.** The existence gate first (T-3.1
43
+ * invariant 3), because it decides *which* entities exist and a cross-check
44
+ * of an entity nobody admitted is noise; then the reader-versus-reader
45
+ * cross-check (`reader-crosscheck.ts`), which answers the reverse direction
46
+ * the gate hands it and every other fact both sources carry. Its findings
47
+ * stop the step; its two documented exemptions are reported and carried out
48
+ * on the result.
49
+ * 6. **Emit** one TypeScript module (ADR 0009).
50
+ *
51
+ * The plan's ยง9 sketch puts the runtime check *after* `prisma generate`; the
52
+ * order above is the brief's, and it is the better one โ€” module resolution
53
+ * walks `node_modules` upward from the client directory and does not need the
54
+ * directory populated, so nothing is gained by generating output first that the
55
+ * step is about to refuse to read.
56
+ *
57
+ * **The two readers fail differently, and the step keeps them different.** A
58
+ * PSL `unhandled` entry means *I located a token I do not model*: there is a
59
+ * construct, a spelling and a site to report, so they are aggregated and
60
+ * reported together. A types-reader failure means *I cannot read the surface at
61
+ * all*: there is nothing to record, so it throws. Inventing an `unhandled`
62
+ * vocabulary for the type surface would be a second parallel representation of
63
+ * "something went wrong" for no gain.
64
+ */
65
+ /** The `prisma` major this reader's grammar was written against (C-846). */
66
+ export const PINNED_PRISMA_MAJOR = 7;
67
+ export class PrismaGenerateStepError extends Error {
68
+ name = "PrismaGenerateStepError";
69
+ }
70
+ const run = promisify(execFile);
71
+ /**
72
+ * The Prisma CLI this step runs, chosen as D11 states (`prisma-cli.resolver.ts`):
73
+ * `@prisma/prisma7`'s `prisma7` when it is installed, else `prisma` when its
74
+ * version is in the adapter's declared range, else a refusal naming both. Looked
75
+ * up from this package โ€” where an optional peer is linked โ€” and then from the
76
+ * project the step runs in.
77
+ */
78
+ export function resolveInstalledPrismaCli() {
79
+ const resolution = resolvePrismaCli({
80
+ searchFrom: [import.meta.url, path.join(process.cwd(), "package.json")],
81
+ range: readDeclaredSupport().prisma,
82
+ });
83
+ if (resolution.kind === "refused") {
84
+ throw new PrismaGenerateStepError(resolution.reason);
85
+ }
86
+ return resolution.cli;
87
+ }
88
+ /**
89
+ * The version fence's input: the version of the Prisma CLI this step runs, which
90
+ * is the tool that produces the client and the grammar the PSL reader was
91
+ * written against. (`@prisma/client`'s version is a different fact, fenced at
92
+ * load time against the artifact โ€” see `discovery/artifact.loader.ts`.)
93
+ */
94
+ export async function readInstalledPrismaVersion() {
95
+ return resolveInstalledPrismaCli().version;
96
+ }
97
+ /**
98
+ * D4 โ€” the types reader and the runtime-resolution fence use TypeScript's
99
+ * classic compiler API. TypeScript 7 has none (plan B3: `createProgram` and
100
+ * `resolveModuleName` are absent; the package exports its version and nothing
101
+ * else), and before this check the step died on it with a `TypeError` stack.
102
+ * Asked of the API itself rather than of the version, because the API is what
103
+ * the step needs.
104
+ */
105
+ function assertClassicCompilerApi() {
106
+ // Three members, not `Partial<typeof ts>`: a mapped type over the whole
107
+ // compiler namespace costs thousands of instantiations to ask three questions.
108
+ const compiler = ts;
109
+ if (typeof compiler?.createProgram === "function" &&
110
+ typeof compiler.resolveModuleName === "function") {
111
+ return;
112
+ }
113
+ throw new PrismaGenerateStepError(`the installed \`typescript\` ${compiler?.version ?? "(no version)"} has no classic compiler API (\`createProgram\`), which this step reads the generated client's types with; install \`typescript\` in this adapter's declared range ${readDeclaredSupport().typescript}.`);
114
+ }
115
+ /**
116
+ * Whether `@prisma/client/runtime/client` resolves from inside the generated
117
+ * tree โ€” the single question ยง1.2 item 16 turns on.
118
+ *
119
+ * Asked as a module resolution, never as a diagnostic count, and asked from a
120
+ * file path *inside* `clientDirectory` because resolution walks `node_modules`
121
+ * upward from the importer. The directory need not exist yet: resolution is a
122
+ * path question, which is what lets this fence run before `prisma generate`.
123
+ */
124
+ export function clientRuntimeResolves(clientDirectory) {
125
+ const containing = path.join(path.resolve(clientDirectory), "models", "_.ts");
126
+ const resolved = ts.resolveModuleName("@prisma/client/runtime/client", containing, {
127
+ target: ts.ScriptTarget.ESNext,
128
+ module: ts.ModuleKind.ESNext,
129
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
130
+ }, ts.sys);
131
+ return resolved.resolvedModule !== undefined;
132
+ }
133
+ export function createPrismaGenerateToolchain() {
134
+ assertClassicCompilerApi();
135
+ // Resolved once per toolchain, so the version the fence reads and the CLI the
136
+ // step runs cannot be two different installs.
137
+ let cli;
138
+ const prismaCli = () => {
139
+ cli ??= resolveInstalledPrismaCli();
140
+ return cli;
141
+ };
142
+ return {
143
+ installedPrismaVersion: async () => prismaCli().version,
144
+ clientRuntimeResolves,
145
+ runPrismaGenerate: async (schema) => {
146
+ // The CLI is the one resolved beside THIS package (or the project), run
147
+ // by path with this `node`: invoking whatever `prisma` is on PATH, or at
148
+ // a workspace root, can be a different install โ€” or produce no output at
149
+ // all, which looks exactly like success.
150
+ await run(process.execPath, [
151
+ prismaCli().entry,
152
+ "generate",
153
+ "--schema",
154
+ schema,
155
+ ]);
156
+ },
157
+ readSchema: readPrismaSchema,
158
+ readSurface: (clientDirectory) => readPrismaCapabilitySurface(clientDirectory).surface,
159
+ writeArtifact: writeArtifactAtomically,
160
+ };
161
+ }
162
+ /**
163
+ * Writes the artifact so that no reader can ever open a partial file.
164
+ *
165
+ * Other packages' tests import the artifact while this package's `test` script
166
+ * regenerates it, and turbo runs the two in parallel. `writeFile` truncates in
167
+ * place, so a concurrent `import` could read an empty module โ€” measured: the
168
+ * client's pilot fixture failed with ``Cannot read properties of undefined
169
+ * (reading 'enums')``. Two rules close it:
170
+ *
171
+ * - **unchanged bytes are not written** โ€” generation is deterministic, so the
172
+ * common re-run touches nothing (and keeps the file's mtime for incremental
173
+ * tooling);
174
+ * - **changed bytes land by `rename`** from a sibling temporary file, which POSIX
175
+ * makes atomic within one directory: a reader sees the old file or the new one.
176
+ */
177
+ async function writeArtifactAtomically(file, source) {
178
+ const current = await readFile(file, "utf8").catch(() => undefined);
179
+ if (current === source) {
180
+ return;
181
+ }
182
+ await mkdir(path.dirname(file), { recursive: true });
183
+ const staged = `${file}.${process.pid}.${Date.now()}.tmp`;
184
+ try {
185
+ await writeFile(staged, source, "utf8");
186
+ await rename(staged, file);
187
+ }
188
+ catch (error) {
189
+ await rm(staged, { force: true });
190
+ throw error;
191
+ }
192
+ }
193
+ function majorOf(version) {
194
+ const major = /^(\d+)\./.exec(version)?.[1];
195
+ return major === undefined ? undefined : Number(major);
196
+ }
197
+ function assertPinnedMajor(version) {
198
+ const major = majorOf(version);
199
+ if (major === undefined) {
200
+ throw new PrismaGenerateStepError(`the installed \`prisma\` reports \`${version}\`, which is not a major.minor.patch version, so its major cannot be fenced against the pinned major ${PINNED_PRISMA_MAJOR} (C-846).`);
201
+ }
202
+ if (major !== PINNED_PRISMA_MAJOR) {
203
+ throw new PrismaGenerateStepError(`the installed \`prisma\` is major ${major} and this adapter's readers are written against pinned major ${PINNED_PRISMA_MAJOR} (C-846). PSL's grammar and the generated client's shape are version-bound, so a new major stops the build here rather than producing an AdapterModel from a grammar nobody has read.`);
204
+ }
205
+ }
206
+ function assertClientRuntimeResolves(toolchain, clientDirectory) {
207
+ if (toolchain.clientRuntimeResolves(clientDirectory)) {
208
+ return;
209
+ }
210
+ throw new PrismaGenerateStepError(`\`@prisma/client/runtime/client\` does not resolve from ${clientDirectory}, so nothing read from the generated types can be trusted. This failure is SILENT by construction: every generated file opens \`// @ts-nocheck\`, so a program over the tree reports zero diagnostics while \`FieldRef\` degrades to its uninstantiated form and \`JsonValue\` stops being a union โ€” flipping a types-derived nullability from \`true\` to \`false\` with no signal anywhere. Install \`@prisma/client\` where the generated output can resolve it, then regenerate.`);
211
+ }
212
+ function assertNothingUnhandled(facts) {
213
+ if (facts.unhandled.length === 0) {
214
+ return;
215
+ }
216
+ throw new PrismaGenerateStepError([
217
+ "the schema reader located constructs it does not model, and a silently dropped construct is how a grammar change becomes wrong output instead of a build stop:",
218
+ ...facts.unhandled.map((unknown) => ` - ${unknown.spelling} (${unknown.kind}) in \`${unknown.block ?? "<top level>"}\` at ${unknown.site.file}:${unknown.site.line}: ${unknown.text}`),
219
+ ].join("\n"));
220
+ }
221
+ export async function runPrismaGenerateStep(request) {
222
+ const toolchain = request.toolchain ?? createPrismaGenerateToolchain();
223
+ // 1. The version fence, before anything is run or read. The version is read
224
+ // ONCE and carried to the emit: a second read could answer differently, and
225
+ // an artifact stamped with a version the fence never saw is exactly the
226
+ // artifact the fence exists to prevent.
227
+ const prismaVersion = await toolchain.installedPrismaVersion();
228
+ assertPinnedMajor(prismaVersion);
229
+ // 2. The silent one.
230
+ assertClientRuntimeResolves(toolchain, request.clientDirectory);
231
+ // 3. This step generates the client it is about to read.
232
+ await toolchain.runPrismaGenerate(request.schema);
233
+ // 4. Both sources, in that same run.
234
+ const facts = await toolchain.readSchema(request.schema);
235
+ assertNothingUnhandled(facts);
236
+ // The types reader's failure mode is a throw with nothing to record, so it
237
+ // is re-raised as this step's own error with the cause preserved rather than
238
+ // aggregated into a vocabulary invented for it.
239
+ let surface;
240
+ try {
241
+ surface = toolchain.readSurface(request.clientDirectory);
242
+ }
243
+ catch (cause) {
244
+ throw new PrismaGenerateStepError(`the capability surface at ${request.clientDirectory} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
245
+ }
246
+ // 5. The structural checks. The gate decides existence; the cross-check then
247
+ // compares every fact both readers carry. A finding means one of the two
248
+ // READERS is wrong, never that the schema is โ€” so the step stops rather than
249
+ // emitting a model it cannot justify.
250
+ const { admitted, excluded } = applyExistenceGate(facts, surface);
251
+ const { findings, exemptions } = crossCheckReaders(facts, surface);
252
+ if (findings.length > 0) {
253
+ throw new PrismaGenerateStepError(describeReaderFindings(findings));
254
+ }
255
+ // 6. Emit.
256
+ const { model, translation, referentialActions } = request.composeModel({
257
+ facts,
258
+ surface,
259
+ admitted,
260
+ });
261
+ const source = await emitDiscoveryArtifactModule({
262
+ prismaVersion,
263
+ provider: request.provider,
264
+ model,
265
+ translation,
266
+ referentialActions,
267
+ });
268
+ await toolchain.writeArtifact(request.artifactFile, source);
269
+ const modelHash = await computeModelHash(model);
270
+ return {
271
+ artifactFile: request.artifactFile,
272
+ source,
273
+ modelHash,
274
+ admitted,
275
+ excluded,
276
+ exemptions,
277
+ };
278
+ }
@@ -0,0 +1,156 @@
1
+ import type { PrismaCapabilitySurface } from "../discovery/capability-surface.interface.js";
2
+ import type { PrismaIdentifierMapping } from "../discovery/identifier.mapper.js";
3
+ import type { PrismaSchemaFacts, PrismaSchemaModel } from "../discovery/schema-facts.interface.js";
4
+ /**
5
+ * Task 6 โ€” the **reader-versus-reader cross-check**: PSL's facts against the ๐ŸŸข
6
+ * generated types' facts, at the one instant the generate step reads both.
7
+ *
8
+ * # It is a self-test of the two readers, not a check on the developer
9
+ *
10
+ * That framing decides every case in this module. A finding here does not mean
11
+ * *your schema is wrong*; it means **one of this adapter's two readers is
12
+ * wrong**, or the client it read was generated from a different schema than the
13
+ * one PSL was read from. The remedy is never "edit the schema" โ€” it is
14
+ * regenerate, or fix a reader.
15
+ *
16
+ * It follows that the check can only see facts **both** sources carry. That is
17
+ * not a gap to be closed by trying harder; it is arithmetic. The honest limit is
18
+ * measured below.
19
+ *
20
+ * # What it cannot see, measured rather than described
21
+ *
22
+ * **Re-measured 2026-10-02 on BOTH Phase 8 engines (prisma 7.10.0), by
23
+ * `reader-crosscheck.spec.ts`'s "what the cross-check cannot see, by arithmetic"
24
+ * case.** Four *real semantic* schema edits, each applied to the schema text
25
+ * alone with the generated client left where it was, produce **zero findings
26
+ * between them โ€” eight measurements, eight zeros**:
27
+ *
28
+ * | edit | sqlite | postgres |
29
+ * | --------------------------------------------------- | ------ | -------- |
30
+ * | `@default(uuid())` โ†’ `@default(cuid())` | 0 | 0 |
31
+ * | `@default(now())` โ†’ `@default(dbgenerated("โ€ฆ"))` | 0 | 0 |
32
+ * | `@default(BRONZE)` โ†’ `@default(GOLD)` | 0 | 0 |
33
+ * | `@relation("Mentorship")` โ†’ `@relation("Coaching")` | 0 | 0 |
34
+ *
35
+ * Every one of them changes what the database does. None of them is visible to a
36
+ * cross-check, because none of them reaches the ๐ŸŸข surface at all: a `@default`
37
+ * argument's *content* and a relation's *name* are PSL-exclusive (the name
38
+ * appears nowhere on the generated surface, which is why `field.mapper.ts` pairs
39
+ * edges from PSL alone). A fifth class is the mirror image: which filter
40
+ * operators a `<Kind>Filter` exposes is ๐ŸŸข-exclusive, so a reader that
41
+ * under-reads one is equally invisible โ€” seeded as the sixth edit in
42
+ * `__fixtures__/diverged-surface.ts` and caught by nothing.
43
+ *
44
+ * **So this module is not a schema-drift detector and must not be trusted as
45
+ * one.** Staleness โ€” a developer who edits `schema.prisma` and does not re-run
46
+ * the generate step โ€” is prevented by the step *running `prisma generate`
47
+ * itself*, so that both sources describe one instant (T-3.2, the architect's
48
+ * 2026-09-27 ruling). What survives that ruling catches a different failure
49
+ * again: `modelHash` proves the artifact is internally intact and the
50
+ * `prismaVersion` major fence catches a client that moved underneath it. Neither
51
+ * notices a schema that moved, and neither does this.
52
+ *
53
+ * # Two exemptions, and both are explanations rather than allowances
54
+ *
55
+ * A freshly generated pair is not silent, and the two cases where it speaks are
56
+ * *understood*:
57
+ *
58
+ * 1. **`json-null-is-a-value`** โ€” the one disagreement a pristine pair produces
59
+ * on both engines. JSON null is a *value*, so `JsonFilterBase` writes `|
60
+ * null` operands (`array_starts_with`, `array_ends_with`, and
61
+ * `array_contains` on PostgreSQL) on a column PSL declares **required**. A
62
+ * plausible "the operands mention null, therefore the column is nullable"
63
+ * rule is right for every other scalar and wrong for every `Json` one, which
64
+ * is why nullability comes from PSL's `?` and nothing else (T-3.1 invariant
65
+ * 4) and why this one is exempted by PSL *type* rather than waved through.
66
+ * 2. **`nullable-participant-key`** โ€” `Author`'s `tenantId_handle` `AtLeast` key
67
+ * has no identifier behind it, because C-815 omits a tuple with a nullable
68
+ * participant (`handle String?`) while the generator still emits a key for
69
+ * it. C-814's rule is therefore a **subset relation with one closed
70
+ * explanation, never an equality**: equality is unsatisfiable by
71
+ * construction.
72
+ *
73
+ * Each exemption is **recorded with its reason**, not dropped. A spec can then
74
+ * assert both by name and by reason, so an unexplained third disagreement fails
75
+ * rather than hiding behind "the pair is clean".
76
+ *
77
+ * # What belongs to other modules, and is deliberately not restated here
78
+ *
79
+ * - **A PSL-declared entity the ๐ŸŸข types do not expose** is the EXISTENCE GATE's
80
+ * answer (`existence.gate.ts`, C-808): it decides admission and whether the
81
+ * absence is explained. This module answers only the reverse direction โ€” a
82
+ * type-exclusive resource or field โ€” which the gate's own header hands to it.
83
+ * - **A `FieldRef` token that disagrees with the written PSL type** is
84
+ * `field.mapper.ts`'s, where it *throws*: the two readers describing one field
85
+ * two ways is not a schema a Contract can be compiled from. Restating the rule
86
+ * here would be two owners for one fact.
87
+ * - **A create-optional marker with no PSL cause** is `field.mapper.ts`'s
88
+ * `crossCheckCreateOptional`, run by `model.discoverer.ts` where the field
89
+ * projections already exist. It needs the projected lifecycle, which this
90
+ * module does not compute.
91
+ */
92
+ /** A disagreement about a fact both sources carry: one of the readers is wrong. */
93
+ export type PrismaReaderFindingKind =
94
+ /** The ๐ŸŸข types expose a resource PSL does not declare. */
95
+ "resource-exclusive-to-types"
96
+ /** The ๐ŸŸข types expose a field PSL does not declare on that model. */
97
+ | "field-exclusive-to-types"
98
+ /** PSL's `?` and the operands the ๐ŸŸข filter admits disagree. */
99
+ | "nullability"
100
+ /** C-814's subset rule is broken in one of its two directions. */
101
+ | "identifier-key"
102
+ /** An enum is exposed and not declared, declared and not exposed, or its members differ. */
103
+ | "enum";
104
+ export type PrismaReaderFinding = {
105
+ readonly kind: PrismaReaderFindingKind;
106
+ /**
107
+ * What the finding is about: `Model`, `Model.field`, `Enum`, or
108
+ * `Model:whereUniqueKey`. The **colon** marks an addressing key rather than a
109
+ * field path, because a `@@unique(name:)` key may be spelled like a column and
110
+ * reading one as the other is how C-814's cross-check once refused a schema
111
+ * Prisma accepts.
112
+ */
113
+ readonly entity: string;
114
+ readonly message: string;
115
+ };
116
+ /** A disagreement that is understood, and therefore not a finding. */
117
+ export type PrismaReaderExemptionKind = "json-null-is-a-value" | "nullable-participant-key";
118
+ export type PrismaReaderExemption = {
119
+ readonly kind: PrismaReaderExemptionKind;
120
+ readonly entity: string;
121
+ /** Why the two sources may differ here. Recorded so a spec can assert the reason. */
122
+ readonly reason: string;
123
+ };
124
+ export type PrismaReaderCrossCheck = {
125
+ /** Fatal: the generate step refuses on any one of these. */
126
+ readonly findings: readonly PrismaReaderFinding[];
127
+ /** Reported, never fatal. */
128
+ readonly exemptions: readonly PrismaReaderExemption[];
129
+ };
130
+ /** One direction of C-814's subset rule, as a finding a spec can match on. */
131
+ export type PrismaIdentifierKeyFinding = {
132
+ readonly key: string;
133
+ readonly reason: string;
134
+ };
135
+ /**
136
+ * C-814's two directions, in one total function.
137
+ *
138
+ * Exported because it has **two callers that must check one rule**: the generate
139
+ * step, through {@link crossCheckReaders}, and `identifier.mapper.spec.ts`, whose
140
+ * own cases were written against a local copy while this file did not exist. The
141
+ * explanation is re-derived **from PSL**, never taken on the mapping's word โ€”
142
+ * otherwise a mapper that invented a nullable participant would clear its own
143
+ * exemption.
144
+ */
145
+ export declare function crossCheckIdentifierKeys(declared: PrismaSchemaModel, mapping: PrismaIdentifierMapping, uniqueKeys: readonly string[]): readonly PrismaIdentifierKeyFinding[];
146
+ /**
147
+ * The whole cross-check: a pure function of the two readers' output.
148
+ *
149
+ * The order is a property of the model and not of how the readers happened to
150
+ * build their records: resources in PSL declaration order, and within one
151
+ * resource the type-exclusive fields, then nullability in PSL field order, then
152
+ * the identifier keys; enums last.
153
+ */
154
+ export declare function crossCheckReaders(facts: PrismaSchemaFacts, surface: PrismaCapabilitySurface): PrismaReaderCrossCheck;
155
+ /** The sentence the generate step refuses with. Built here, so one wording exists. */
156
+ export declare function describeReaderFindings(findings: readonly PrismaReaderFinding[]): string;
@@ -0,0 +1,202 @@
1
+ import { mapPrismaEnum } from "../discovery/enum.mapper.js";
2
+ import { mapPrismaIdentifiers } from "../discovery/identifier.mapper.js";
3
+ const JSON_EXEMPTION_REASON = "`Json` admits JSON null as a VALUE, so the generated `JsonFilterBase` writes `| null` operands " +
4
+ "(`array_starts_with`, `array_ends_with`, and `array_contains` on PostgreSQL) on a column PSL " +
5
+ "declares required. Nullability therefore comes from PSL's `?` and nothing else (T-3.1 invariant 4).";
6
+ function nullableParticipantReason(key, nullableMembers) {
7
+ return (`C-815 omits the uniqueness criterion behind \`${key}\` because [${nullableMembers.join(", ")}] ` +
8
+ "is nullable and SQL uniqueness does not constrain NULLs, while the generator still emits an " +
9
+ "`AtLeast` key for it. C-814's rule is a subset relation with this one closed explanation, " +
10
+ "never an equality: equality is unsatisfiable by construction.");
11
+ }
12
+ /**
13
+ * C-814's two directions, in one total function.
14
+ *
15
+ * Exported because it has **two callers that must check one rule**: the generate
16
+ * step, through {@link crossCheckReaders}, and `identifier.mapper.spec.ts`, whose
17
+ * own cases were written against a local copy while this file did not exist. The
18
+ * explanation is re-derived **from PSL**, never taken on the mapping's word โ€”
19
+ * otherwise a mapper that invented a nullable participant would clear its own
20
+ * exemption.
21
+ */
22
+ export function crossCheckIdentifierKeys(declared, mapping, uniqueKeys) {
23
+ const findings = [];
24
+ const nullableInPsl = new Set(declared.fields.filter((f) => f.optional).map((f) => f.name));
25
+ const identifierKeys = mapping.identifiers.map((address) => address.key);
26
+ // (a) An identifier the types do not key means the PSL reader invented a
27
+ // constraint.
28
+ for (const key of identifierKeys) {
29
+ if (!uniqueKeys.includes(key)) {
30
+ findings.push({
31
+ key,
32
+ reason: "an identifier with no `AtLeast` key behind it",
33
+ });
34
+ }
35
+ }
36
+ // (b) A key with no identifier must be explained by C-815's nullable
37
+ // participant and by nothing else.
38
+ for (const key of uniqueKeys) {
39
+ if (identifierKeys.includes(key)) {
40
+ continue;
41
+ }
42
+ const omission = mapping.omitted.find((o) => o.key === key);
43
+ if (omission === undefined) {
44
+ findings.push({
45
+ key,
46
+ reason: "an `AtLeast` key with neither an identifier nor an omission",
47
+ });
48
+ continue;
49
+ }
50
+ if (omission.reason !== "nullable-participant") {
51
+ findings.push({
52
+ key,
53
+ reason: `omitted as ${omission.reason}, which C-815 does not license`,
54
+ });
55
+ continue;
56
+ }
57
+ const unproven = omission.nullableMembers.filter((member) => !nullableInPsl.has(member));
58
+ if (omission.nullableMembers.length === 0 || unproven.length > 0) {
59
+ findings.push({
60
+ key,
61
+ reason: `claimed nullable participants [${omission.nullableMembers.join(", ")}] are not optional in PSL`,
62
+ });
63
+ }
64
+ }
65
+ return findings;
66
+ }
67
+ /**
68
+ * The whole cross-check: a pure function of the two readers' output.
69
+ *
70
+ * The order is a property of the model and not of how the readers happened to
71
+ * build their records: resources in PSL declaration order, and within one
72
+ * resource the type-exclusive fields, then nullability in PSL field order, then
73
+ * the identifier keys; enums last.
74
+ */
75
+ export function crossCheckReaders(facts, surface) {
76
+ const findings = [];
77
+ const exemptions = [];
78
+ const declaredModels = new Map(facts.models.map((m) => [m.name, m]));
79
+ for (const name of Object.keys(surface.resources)) {
80
+ if (declaredModels.has(name)) {
81
+ continue;
82
+ }
83
+ findings.push({
84
+ kind: "resource-exclusive-to-types",
85
+ entity: name,
86
+ message: `the generated ๐ŸŸข types expose a model \`${name}\` that \`schema.prisma\` does not declare. The two readers describe one instant, so this is a stale generated client or a reader defect โ€” the schema cannot have said it.`,
87
+ });
88
+ }
89
+ for (const model of facts.models) {
90
+ const exposed = surface.resources[model.name];
91
+ if (exposed === undefined) {
92
+ // A PSL-declared model the types do not expose is the existence gate's
93
+ // answer, with its own explained/unexplained split. Not restated here.
94
+ continue;
95
+ }
96
+ const declaredFields = new Set(model.fields.map((f) => f.name));
97
+ for (const field of [
98
+ ...Object.keys(exposed.scalarFields),
99
+ ...Object.keys(exposed.relationFields),
100
+ ]) {
101
+ if (declaredFields.has(field)) {
102
+ continue;
103
+ }
104
+ findings.push({
105
+ kind: "field-exclusive-to-types",
106
+ entity: `${model.name}.${field}`,
107
+ message: `the generated ๐ŸŸข types expose \`${model.name}.${field}\` and \`schema.prisma\` declares no such member. A rename seen by only one of the two readers looks exactly like this.`,
108
+ });
109
+ }
110
+ for (const field of model.fields) {
111
+ const scalar = exposed.scalarFields[field.name];
112
+ if (scalar === undefined) {
113
+ // A relation edge's nullability is checked by `field.mapper.ts`, which
114
+ // has the payload's own `nullable` and throws on a disagreement.
115
+ continue;
116
+ }
117
+ // A scalar LIST is out of scope, and the exclusion is a scoping decision
118
+ // rather than a measurement: `StringNullableListFilter.equals` is
119
+ // `string[] | null` on a `String[]` column PSL declares required
120
+ // (`Post.tags` on PostgreSQL), because the `null` there is about the
121
+ // absence of the whole list and not about an element. The rule below is a
122
+ // per-column nullability signal only where the column holds one value.
123
+ if (field.list) {
124
+ continue;
125
+ }
126
+ const nullAdmitting = Object.entries(scalar.filterOperands)
127
+ .filter(([, written]) => /\bnull\b/.test(written))
128
+ .map(([key]) => key);
129
+ const typesSayNullable = nullAdmitting.length > 0;
130
+ if (typesSayNullable === field.optional) {
131
+ continue;
132
+ }
133
+ if (field.type === "Json" && typesSayNullable) {
134
+ exemptions.push({
135
+ kind: "json-null-is-a-value",
136
+ entity: `${model.name}.${field.name}`,
137
+ reason: JSON_EXEMPTION_REASON,
138
+ });
139
+ continue;
140
+ }
141
+ findings.push({
142
+ kind: "nullability",
143
+ entity: `${model.name}.${field.name}`,
144
+ message: `\`${model.name}.${field.name}\` is ${field.optional ? "optional" : "required"} in \`schema.prisma\` and the generated ๐ŸŸข filter ${typesSayNullable ? `admits \`null\` on [${nullAdmitting.join(", ")}]` : "admits `null` nowhere"}. PSL's \`?\` is the only source of nullability, so the model is built from it either way โ€” this says one of the two readers, or the client, is from a different schema.`,
145
+ });
146
+ }
147
+ const mapping = mapPrismaIdentifiers(model);
148
+ for (const finding of crossCheckIdentifierKeys(model, mapping, exposed.uniqueKeys)) {
149
+ findings.push({
150
+ kind: "identifier-key",
151
+ entity: `${model.name}:${finding.key}`,
152
+ message: `\`${model.name}\`'s \`WhereUniqueInput\` key \`${finding.key}\` is ${finding.reason} (C-814).`,
153
+ });
154
+ }
155
+ for (const omission of mapping.omitted) {
156
+ if (omission.reason !== "nullable-participant" ||
157
+ !exposed.uniqueKeys.includes(omission.key)) {
158
+ continue;
159
+ }
160
+ exemptions.push({
161
+ kind: "nullable-participant-key",
162
+ entity: `${model.name}:${omission.key}`,
163
+ reason: nullableParticipantReason(omission.key, omission.nullableMembers),
164
+ });
165
+ }
166
+ }
167
+ for (const name of Object.keys(surface.enums)) {
168
+ if (facts.enums.some((declared) => declared.name === name)) {
169
+ continue;
170
+ }
171
+ findings.push({
172
+ kind: "enum",
173
+ entity: name,
174
+ message: `the generated ๐ŸŸข \`enums.ts\` exposes \`${name}\` and \`schema.prisma\` declares no such enum.`,
175
+ });
176
+ }
177
+ for (const declared of facts.enums) {
178
+ // The member comparison is the ENUM MAPPER's own rule, invoked rather than
179
+ // restated: declaration order is semantic (ยง19.2) and enters the Contract
180
+ // hash, so "diverged" is the same judgement in both callers.
181
+ const mapped = mapPrismaEnum({
182
+ declared,
183
+ exposed: surface.enums[declared.name],
184
+ });
185
+ if (mapped.outcome === "enum") {
186
+ continue;
187
+ }
188
+ findings.push({
189
+ kind: "enum",
190
+ entity: declared.name,
191
+ message: mapped.diagnostic.message,
192
+ });
193
+ }
194
+ return { findings, exemptions };
195
+ }
196
+ /** The sentence the generate step refuses with. Built here, so one wording exists. */
197
+ export function describeReaderFindings(findings) {
198
+ return [
199
+ "the two discovery readers disagree about facts they both carry, which means one of THEM is wrong โ€” not the schema:",
200
+ ...findings.map((finding) => ` - [${finding.kind}] ${finding.entity}: ${finding.message}`),
201
+ ].join("\n");
202
+ }
@@ -0,0 +1,5 @@
1
+ export type { PrismaAdapterModel, PrismaDiscoveryArtifact, PrismaReferentialAction, PrismaReferentialActionTable, PrismaReferentialEdge, PrismaResourceReferentialActions, } from "./discovery/discovery-artifact.interface.js";
2
+ export type { PrismaAdapterOptions } from "./prisma.adapter.js";
3
+ export { createPrismaAdapter } from "./prisma.adapter.js";
4
+ export type { PrismaDelegate, PrismaDelegateHost, PrismaDelegateIndex, PrismaTransactionOptions, } from "./prisma-client.interface.js";
5
+ export type { PrismaDriver, PrismaIsolationLevel, PrismaProvider, PrismaProviderProfile, } from "./provider/provider-profile.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createPrismaAdapter } from "./prisma.adapter.js";