@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.
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +564 -0
- package/dist/discovery/artifact.loader.d.ts +26 -0
- package/dist/discovery/artifact.loader.js +124 -0
- package/dist/discovery/capability-surface.interface.d.ts +191 -0
- package/dist/discovery/capability-surface.interface.js +13 -0
- package/dist/discovery/capability.deriver.d.ts +92 -0
- package/dist/discovery/capability.deriver.js +350 -0
- package/dist/discovery/discovery-artifact.interface.d.ts +210 -0
- package/dist/discovery/discovery-artifact.interface.js +1 -0
- package/dist/discovery/enum.mapper.d.ts +59 -0
- package/dist/discovery/enum.mapper.js +32 -0
- package/dist/discovery/field.mapper.d.ts +244 -0
- package/dist/discovery/field.mapper.js +387 -0
- package/dist/discovery/identifier.mapper.d.ts +188 -0
- package/dist/discovery/identifier.mapper.js +132 -0
- package/dist/discovery/model.discoverer.d.ts +108 -0
- package/dist/discovery/model.discoverer.js +341 -0
- package/dist/discovery/operations.deriver.d.ts +75 -0
- package/dist/discovery/operations.deriver.js +92 -0
- package/dist/discovery/scalar.mapper.d.ts +81 -0
- package/dist/discovery/scalar.mapper.js +51 -0
- package/dist/discovery/schema-facts.interface.d.ts +174 -0
- package/dist/discovery/schema-facts.interface.js +11 -0
- package/dist/discovery/schema.reader.d.ts +162 -0
- package/dist/discovery/schema.reader.js +692 -0
- package/dist/discovery/types.reader.d.ts +24 -0
- package/dist/discovery/types.reader.js +617 -0
- package/dist/discovery/vocabulary.dictionary.d.ts +258 -0
- package/dist/discovery/vocabulary.dictionary.js +429 -0
- package/dist/errors/error.normalizer.d.ts +250 -0
- package/dist/errors/error.normalizer.js +135 -0
- package/dist/errors/prisma-error.reader.d.ts +144 -0
- package/dist/errors/prisma-error.reader.js +167 -0
- package/dist/execution/first-variant.emulator.d.ts +64 -0
- package/dist/execution/first-variant.emulator.js +205 -0
- package/dist/execution/operation.executor.d.ts +87 -0
- package/dist/execution/operation.executor.js +210 -0
- package/dist/execution/result.shaper.d.ts +103 -0
- package/dist/execution/result.shaper.js +141 -0
- package/dist/execution/transaction.executor.d.ts +147 -0
- package/dist/execution/transaction.executor.js +13 -0
- package/dist/generate/artifact.emitter.d.ts +50 -0
- package/dist/generate/artifact.emitter.js +105 -0
- package/dist/generate/declared-support.reader.d.ts +33 -0
- package/dist/generate/declared-support.reader.js +74 -0
- package/dist/generate/discovery.bin.d.ts +2 -0
- package/dist/generate/discovery.bin.js +15 -0
- package/dist/generate/discovery.cli.d.ts +24 -0
- package/dist/generate/discovery.cli.js +163 -0
- package/dist/generate/existence.gate.d.ts +73 -0
- package/dist/generate/existence.gate.js +91 -0
- package/dist/generate/index.d.ts +81 -0
- package/dist/generate/index.js +65 -0
- package/dist/generate/node-version.guard.d.ts +8 -0
- package/dist/generate/node-version.guard.js +59 -0
- package/dist/generate/prisma-cli.resolver.d.ts +48 -0
- package/dist/generate/prisma-cli.resolver.js +95 -0
- package/dist/generate/prisma.generate.d.ts +164 -0
- package/dist/generate/prisma.generate.js +278 -0
- package/dist/generate/reader-crosscheck.d.ts +156 -0
- package/dist/generate/reader-crosscheck.js +202 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/prisma-client.interface.d.ts +48 -0
- package/dist/prisma-client.interface.js +9 -0
- package/dist/prisma.adapter.d.ts +55 -0
- package/dist/prisma.adapter.js +105 -0
- package/dist/provider/provider-profile.d.ts +53 -0
- package/dist/provider/provider-profile.js +91 -0
- package/dist/translation/identifier.addressing.d.ts +80 -0
- package/dist/translation/identifier.addressing.js +30 -0
- package/dist/translation/mutation.translator.d.ts +115 -0
- package/dist/translation/mutation.translator.js +580 -0
- package/dist/translation/operation.translator.d.ts +273 -0
- package/dist/translation/operation.translator.js +377 -0
- package/dist/translation/order-pagination.translator.d.ts +86 -0
- package/dist/translation/order-pagination.translator.js +159 -0
- package/dist/translation/projection.translator.d.ts +115 -0
- package/dist/translation/projection.translator.js +151 -0
- package/dist/translation/scalar.conversion.d.ts +101 -0
- package/dist/translation/scalar.conversion.js +221 -0
- package/dist/translation/where.translator.d.ts +79 -0
- package/dist/translation/where.translator.js +254 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|