@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,250 @@
|
|
|
1
|
+
import { FrameworkError, type OperationErrorCode, type OperationIdentity } from "@aventara/core";
|
|
2
|
+
import { type PrismaConstraintSubject } from "./prisma-error.reader.js";
|
|
3
|
+
/**
|
|
4
|
+
* T-10 — error normalization: one closed table from `@prisma/client` 7.x
|
|
5
|
+
* P-codes to Aventara outcomes, and nothing else.
|
|
6
|
+
*
|
|
7
|
+
* ## Version bound (C-846)
|
|
8
|
+
*
|
|
9
|
+
* The table's keys are P-codes, which are **7.x's** error surface. Prisma 8
|
|
10
|
+
* replaced them with dotted `NAMESPACE.SUBCODE` codes, so a Prisma 8
|
|
11
|
+
* adapter re-maps this module rather than patching it.
|
|
12
|
+
* `error.normalizer.spec.ts` reads `@prisma/client`'s own `package.json`
|
|
13
|
+
* and fails if the installed major is not `7`, so the bound is enforced
|
|
14
|
+
* rather than described.
|
|
15
|
+
*
|
|
16
|
+
* ## Every code in the table was induced, not inherited
|
|
17
|
+
*
|
|
18
|
+
* Nothing here rests on documentation. Each rule was produced by throwing a
|
|
19
|
+
* real statement at a real engine on this repository's own schema, on
|
|
20
|
+
* `@prisma/adapter-better-sqlite3` and `@prisma/adapter-pg`, both 7.10.0.
|
|
21
|
+
* The measured payloads, inlined because the source measurement lives
|
|
22
|
+
* outside the repository:
|
|
23
|
+
*
|
|
24
|
+
* ```text
|
|
25
|
+
* P2002 author.create duplicate @unique email
|
|
26
|
+
* sqlite meta = { driverAdapterError: { name: "DriverAdapterError", cause: {
|
|
27
|
+
* originalCode: "SQLITE_CONSTRAINT_UNIQUE",
|
|
28
|
+
* originalMessage: "UNIQUE constraint failed: Author.email",
|
|
29
|
+
* kind: "UniqueConstraintViolation",
|
|
30
|
+
* constraint: { fields: ["email"] }, table: "Author" } },
|
|
31
|
+
* modelName: "Author" }
|
|
32
|
+
* pg meta = { driverAdapterError: { name: "DriverAdapterError", cause: {
|
|
33
|
+
* originalCode: "23505",
|
|
34
|
+
* originalMessage: "duplicate key value violates unique constraint \"Author_email_key\"",
|
|
35
|
+
* kind: "UniqueConstraintViolation",
|
|
36
|
+
* constraint: { index: "Author_email_key" }, table: "Author" } },
|
|
37
|
+
* modelName: "Author" }
|
|
38
|
+
*
|
|
39
|
+
* P2002 author.create violating @@unique([tenantId, handle])
|
|
40
|
+
* sqlite constraint: { fields: ["tenantId", "handle"] }
|
|
41
|
+
* pg constraint: { index: "Author_tenantId_handle_key" }
|
|
42
|
+
*
|
|
43
|
+
* P2003 post.create with an authorId that does not exist
|
|
44
|
+
* sqlite meta = { modelName: "Post", driverAdapterError: { cause: {
|
|
45
|
+
* originalCode: "SQLITE_CONSTRAINT_FOREIGNKEY",
|
|
46
|
+
* originalMessage: "FOREIGN KEY constraint failed",
|
|
47
|
+
* kind: "ForeignKeyConstraintViolation",
|
|
48
|
+
* constraint: { foreignKey: {} } } } }
|
|
49
|
+
* pg constraint: { index: "Post_authorId_fkey" }, originalCode "23503"
|
|
50
|
+
*
|
|
51
|
+
* P2025 author.update / author.delete on a missing unique selector
|
|
52
|
+
* both meta = { modelName: "Author", operation: "an update" }
|
|
53
|
+
* both meta = { modelName: "Author", operation: "a delete" }
|
|
54
|
+
*
|
|
55
|
+
* P2025 nested connect whose target does not exist (create, update, upsert)
|
|
56
|
+
* both meta = { modelName: "Post", model: "Author",
|
|
57
|
+
* relation: "AuthorToPost", relationType: "one-to-many",
|
|
58
|
+
* operation: "a nested connect",
|
|
59
|
+
* neededFor: "inline the relation on 'Post' record(s)" }
|
|
60
|
+
*
|
|
61
|
+
* P2018 author.update with posts.connect to a missing post
|
|
62
|
+
* sqlite meta = { modelName: "Author", expectedRows: 1,
|
|
63
|
+
* relation: "AuthorToPost", relationType: "one-to-many" }
|
|
64
|
+
*
|
|
65
|
+
* P2034 two SERIALIZABLE transactions, both reading then both writing
|
|
66
|
+
* pg meta = { modelName: "Author", driverAdapterError: { cause: {
|
|
67
|
+
* originalCode: "40001",
|
|
68
|
+
* originalMessage: "could not serialize access due to concurrent update",
|
|
69
|
+
* kind: "TransactionWriteConflict" } } }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* ## Two inherited claims that the measurement refutes
|
|
73
|
+
*
|
|
74
|
+
* 1. **`meta.errorIdentifier` does not exist.** D-811 and the error-code map
|
|
75
|
+
* both state that P2025's cases are told apart by
|
|
76
|
+
* `meta.errorIdentifier` (`MISSING_RECORD` / `MISSING_RELATED_RECORD` /
|
|
77
|
+
* `INCOMPLETE_CONNECT_INPUT`), and both marked it unverified at the meta
|
|
78
|
+
* level. It is **absent from every payload above**; the identifiers are
|
|
79
|
+
* internal rule names, not a `meta` key. What 7.10.0 actually emits is
|
|
80
|
+
* two different *key sets*, and the discriminant is the presence of
|
|
81
|
+
* `meta.relation` (with `meta.model`): present → the failure is about a
|
|
82
|
+
* relation reference; absent → the record the call selected was not
|
|
83
|
+
* there. That is structural, which is what ADR 0006 requires — the
|
|
84
|
+
* alternative, `meta.operation`, is English prose.
|
|
85
|
+
* 2. **`meta.target` is never emitted.** RC-9's open question is answered:
|
|
86
|
+
* the v7 docs are stale (see `PrismaErrorFacts.legacyTarget`).
|
|
87
|
+
*
|
|
88
|
+
* Also unobserved: the `{}` meta that `expectNonEmpty` was expected to
|
|
89
|
+
* carry. `findUniqueOrThrow` and `findFirstOrThrow` both report
|
|
90
|
+
* `{ modelName, operation: "a query" }`, so no empty-meta P2025 was
|
|
91
|
+
* produced by any statement tried here. Either arm classifies identically
|
|
92
|
+
* under the rule above (no `relation` → root record), so the rule is safe
|
|
93
|
+
* either way, but the shape itself stays unverified.
|
|
94
|
+
*
|
|
95
|
+
* ## RC-8 is decided, and this measurement is what decided it
|
|
96
|
+
*
|
|
97
|
+
* Three distinct situations used to answer the one `A2008 CONFLICT`. Coarse
|
|
98
|
+
* but correct, so nothing provisional ever reached the wire and the
|
|
99
|
+
* discriminant was purely additive (RC-8, D-831). The architect assented
|
|
100
|
+
* (2026-10-02) to the discrimination this measurement showed to be available:
|
|
101
|
+
* `A2008` now means the **constraint** case alone, a write conflict or
|
|
102
|
+
* deadlock answers **`A2013 CONCURRENT_MODIFICATION`**, and the lost-row race
|
|
103
|
+
* answers **`A2014 STALE_SELECTION`** from the emulator's own seam. The
|
|
104
|
+
* observed surface each decision rests on is this:
|
|
105
|
+
*
|
|
106
|
+
* | Situation | Code | `meta` keys observed | Structurally separable? |
|
|
107
|
+
* |---|---|---|---|
|
|
108
|
+
* | Uniqueness violation | `P2002` | `modelName`, `driverAdapterError` | **Yes** — distinct P-code; `cause.kind` is `"UniqueConstraintViolation"`; `cause.constraint` is `{fields}` (SQLite) or `{index}` (PostgreSQL) |
|
|
109
|
+
* | Write conflict / deadlock | `P2034` | `modelName`, `driverAdapterError` | **Yes** from a constraint violation — distinct P-code; `cause.kind` is `"TransactionWriteConflict"`; **no** `constraint` at all. **No** from each other: `cause.originalCode` is `40001` for a write conflict and `40P01` for a deadlock, and that field is provider-owned and never crosses the boundary |
|
|
110
|
+
* | Lost-row race | *none* | *none* | **Yes, trivially** — there is no provider error behind it: it is a zero-row count in a sequence the framework manufactured, and it is raised in `execution/first-variant.emulator.ts`, never here |
|
|
111
|
+
*
|
|
112
|
+
* So all three are separable **without reading any message text**, which is
|
|
113
|
+
* what ADR 0006 requires and what C-821 warns `P2028` cannot offer, and that
|
|
114
|
+
* is why the discrimination is implemented by P-code and by call site alone.
|
|
115
|
+
* The two provider-backed ones differ by P-code; `driverAdapterError.cause.kind`
|
|
116
|
+
* is a second, independent witness.
|
|
117
|
+
*
|
|
118
|
+
* **A write conflict and a deadlock share `A2013` deliberately.** Both arrive
|
|
119
|
+
* as `P2034` with identical `meta` keys, differing only in a provider-owned
|
|
120
|
+
* `originalCode` (`40001` versus `40P01`) that never crosses the boundary.
|
|
121
|
+
* They are not separable at this seam, so one outcome is the only honest
|
|
122
|
+
* answer; splitting them later would need a discriminant that does not exist. `describePrismaConflict` is the recorded
|
|
123
|
+
* surface, and `PrismaConstraintSubject` records that the two engines do not
|
|
124
|
+
* agree on *which* identifier they can name — a uniqueness discriminant that
|
|
125
|
+
* promised the caller the constraint's name would be unsatisfiable on SQLite.
|
|
126
|
+
*
|
|
127
|
+
* ## F-608 / D-816 — what was observed about non-`Error` throws
|
|
128
|
+
*
|
|
129
|
+
* **No Prisma path observed here threw a non-`Error`.** Every induced failure
|
|
130
|
+
* arrived as a `PrismaClientKnownRequestError`, which extends `Error`
|
|
131
|
+
* (`error instanceof Error` was asserted true for all of them), and the one
|
|
132
|
+
* non-Prisma failure the engines produced — `better-sqlite3` refusing a
|
|
133
|
+
* missing directory — was a `TypeError`. So the primitive-throw ambiguity
|
|
134
|
+
* F-608 describes did not arise for the only real adapter that exists, and
|
|
135
|
+
* the general fix (boxing primitive Adapter throws at core's seam) has no
|
|
136
|
+
* demonstrated urgency. That observation, not a guess, is what Task 14 (§14a)
|
|
137
|
+
* judges the row on. The adapter half is discharged here:
|
|
138
|
+
* `prismaOutcomeFailure` returns a `FrameworkError`, so every failure this
|
|
139
|
+
* module originates is an `Error` subclass, enforced by a spec.
|
|
140
|
+
*
|
|
141
|
+
* ## F-723 — why nothing leaks today
|
|
142
|
+
*
|
|
143
|
+
* A deliberate `FrameworkError`'s top-level `cause.message` is forwarded
|
|
144
|
+
* verbatim; that channel is still open and is Phase 13's. What keeps it from
|
|
145
|
+
* leaking is this module's own rule (C-819): the cause is framework text, and
|
|
146
|
+
* the reader never reads `message` at all, so there is no provider string in
|
|
147
|
+
* scope to forward. If that rule is ever relaxed, the leak is immediate —
|
|
148
|
+
* which is the input Phase 13 should receive.
|
|
149
|
+
*/
|
|
150
|
+
/** The three answers a Prisma failure can get. T-10, verbatim. */
|
|
151
|
+
export type PrismaErrorClassification = {
|
|
152
|
+
readonly kind: "absence";
|
|
153
|
+
} | {
|
|
154
|
+
readonly kind: "outcome";
|
|
155
|
+
readonly code: OperationErrorCode;
|
|
156
|
+
} | {
|
|
157
|
+
readonly kind: "unclassified";
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* When a code is a `null` result rather than a failure (D-811).
|
|
161
|
+
*
|
|
162
|
+
* `variants` is read literally from D-811: Prisma's `update`/`delete` throw
|
|
163
|
+
* P2025 when the selector matches nothing, and core's outcome mapper turns a
|
|
164
|
+
* returned `null` into `A2003 NOT_FOUND` for exactly those two variants, so
|
|
165
|
+
* the adapter hands back `null` instead of inventing a failure.
|
|
166
|
+
*
|
|
167
|
+
* `subject` is the measured `meta` shape that may be forgiven. The same
|
|
168
|
+
* P2025 reaches `update.unique` for a second reason — a nested `connect`
|
|
169
|
+
* whose target is missing — and that one is a caller error about the
|
|
170
|
+
* *arguments*, not an absent record. `"root-record"` means `meta` carries no
|
|
171
|
+
* relation reference; the single-member union says that is the one shape
|
|
172
|
+
* measured, and leaves room for a second if one is ever observed.
|
|
173
|
+
*/
|
|
174
|
+
type PrismaAbsenceRule = {
|
|
175
|
+
readonly variants: readonly `${string}.${string}`[];
|
|
176
|
+
readonly subject: "root-record";
|
|
177
|
+
};
|
|
178
|
+
type PrismaErrorRule = {
|
|
179
|
+
/**
|
|
180
|
+
* The outcome this code answers. Typed as core's own
|
|
181
|
+
* `OperationErrorCode`, so the adapter cannot widen the code space —
|
|
182
|
+
* a code the table does not contain has no rule at all (C-838).
|
|
183
|
+
*/
|
|
184
|
+
readonly outcome: OperationErrorCode;
|
|
185
|
+
readonly absence?: PrismaAbsenceRule;
|
|
186
|
+
};
|
|
187
|
+
/** One closed table, scoped to `@prisma/client` 7.x (C-846). */
|
|
188
|
+
export type PrismaErrorTable = Readonly<Record<string, PrismaErrorRule>>;
|
|
189
|
+
/**
|
|
190
|
+
* The installed-major this table is written against. The spec asserts the
|
|
191
|
+
* installed `@prisma/client` agrees.
|
|
192
|
+
*/
|
|
193
|
+
export declare const PRISMA_ERROR_TABLE_MAJOR = "7";
|
|
194
|
+
/**
|
|
195
|
+
* Every code here was induced above. Codes that 7.10.0 *can* raise but that
|
|
196
|
+
* no statement here produced — `P2000`, `P2011`, `P2014`, `P2017`, `P2020`,
|
|
197
|
+
* `P2023`, `P2010`, `P2028`, `P2029`, `P2030`, `P2036`, `P2037`, `P2039`,
|
|
198
|
+
* `P1008`, `P1017`, `P1018` — are **deliberately absent**: an unmeasured
|
|
199
|
+
* rule is a guess, and D-812's answer for anything unrecognised is to leave
|
|
200
|
+
* it to core's `AdapterBoundary`, which reports `A3001 ADAPTER_ERROR` with
|
|
201
|
+
* core's own text and sends the raw error to diagnostics. Adding a row
|
|
202
|
+
* later is additive and costs nothing; shipping a wrong one is not
|
|
203
|
+
* recoverable.
|
|
204
|
+
*/
|
|
205
|
+
export declare const PRISMA_ERROR_TABLE: PrismaErrorTable;
|
|
206
|
+
/**
|
|
207
|
+
* Classifies one failed Prisma call.
|
|
208
|
+
*
|
|
209
|
+
* `error` is `unknown` and stays `unknown`: the reader is the only thing
|
|
210
|
+
* that touches it, and the rules see nothing but measured facts. The
|
|
211
|
+
* operation is needed because D-811's answer depends on the variant the
|
|
212
|
+
* caller asked for, which is framework knowledge and not in the error.
|
|
213
|
+
*/
|
|
214
|
+
export declare function classifyPrismaError(error: unknown, operation: Pick<OperationIdentity, "family" | "variant">): PrismaErrorClassification;
|
|
215
|
+
/**
|
|
216
|
+
* The deliberate failure an `outcome` classification stands for.
|
|
217
|
+
*
|
|
218
|
+
* It is a `FrameworkError`, so every failure this adapter originates is an
|
|
219
|
+
* `Error` subclass — D-816's adapter half, which needs no core change.
|
|
220
|
+
*
|
|
221
|
+
* The cause carries framework text and nothing else: no issues, and that is
|
|
222
|
+
* now the **rule** rather than an absence (C-839, ADR 0008 decision 4). A
|
|
223
|
+
* conflict is not a Framework Validation Layer failure, ADR 0008 decision 1
|
|
224
|
+
* makes a V-code *determine* the A-code, and `validationCode` rolls up to only
|
|
225
|
+
* `A2001`/`A2002`/`A2004`/`A2007`/`A2009` — so a conflict V-code would have no
|
|
226
|
+
* legal rollup, which is why `A2008`, `A2013` and `A2014` pair with none and
|
|
227
|
+
* carry `cause.issues` absent. And no provider text, because a deliberate
|
|
228
|
+
* outcome's top-level `cause.message` is forwarded verbatim (C-819, F-723).
|
|
229
|
+
* Nothing read off the Prisma error reaches this value.
|
|
230
|
+
*/
|
|
231
|
+
export declare function prismaOutcomeFailure(code: OperationErrorCode): FrameworkError;
|
|
232
|
+
/**
|
|
233
|
+
* What RC-8 will discriminate on, recorded rather than re-measured.
|
|
234
|
+
*
|
|
235
|
+
* **Server-side only.** Every field is provider-owned, so this value never
|
|
236
|
+
* reaches a `cause` — which is why it is a separate function and not a field
|
|
237
|
+
* on `PrismaErrorClassification`: nothing can carry it onto the wire by
|
|
238
|
+
* accident. It is the measurement that answered §14c — whether the three
|
|
239
|
+
* situations once collapsed into `A2008` are *structurally* separable, which
|
|
240
|
+
* they are — and it remains the record of what a diagnostics sink could
|
|
241
|
+
* usefully be told.
|
|
242
|
+
*/
|
|
243
|
+
export type PrismaConflictSubject = {
|
|
244
|
+
readonly code: string;
|
|
245
|
+
readonly model: string | undefined;
|
|
246
|
+
readonly constraint: PrismaConstraintSubject | undefined;
|
|
247
|
+
readonly metaKeys: readonly string[];
|
|
248
|
+
};
|
|
249
|
+
export declare function describePrismaConflict(error: unknown): PrismaConflictSubject | undefined;
|
|
250
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { FrameworkError, } from "@aventara/core";
|
|
2
|
+
import { readPrismaError, } from "./prisma-error.reader.js";
|
|
3
|
+
/**
|
|
4
|
+
* The installed-major this table is written against. The spec asserts the
|
|
5
|
+
* installed `@prisma/client` agrees.
|
|
6
|
+
*/
|
|
7
|
+
export const PRISMA_ERROR_TABLE_MAJOR = "7";
|
|
8
|
+
/**
|
|
9
|
+
* Every code here was induced above. Codes that 7.10.0 *can* raise but that
|
|
10
|
+
* no statement here produced — `P2000`, `P2011`, `P2014`, `P2017`, `P2020`,
|
|
11
|
+
* `P2023`, `P2010`, `P2028`, `P2029`, `P2030`, `P2036`, `P2037`, `P2039`,
|
|
12
|
+
* `P1008`, `P1017`, `P1018` — are **deliberately absent**: an unmeasured
|
|
13
|
+
* rule is a guess, and D-812's answer for anything unrecognised is to leave
|
|
14
|
+
* it to core's `AdapterBoundary`, which reports `A3001 ADAPTER_ERROR` with
|
|
15
|
+
* core's own text and sends the raw error to diagnostics. Adding a row
|
|
16
|
+
* later is additive and costs nothing; shipping a wrong one is not
|
|
17
|
+
* recoverable.
|
|
18
|
+
*/
|
|
19
|
+
export const PRISMA_ERROR_TABLE = {
|
|
20
|
+
// Uniqueness conflict — caller-actionable state conflict, HTTP 409 (D-810, C-820).
|
|
21
|
+
P2002: { outcome: "A2008" },
|
|
22
|
+
// Foreign-key violation — an invalid reference is the same class of
|
|
23
|
+
// conflict, and D-810 maps it to the same code. These two are what
|
|
24
|
+
// `A2008` now means, and the whole of it: re-running either request
|
|
25
|
+
// unchanged fails the same way, which is what separates them from
|
|
26
|
+
// `A2013`/`A2014`.
|
|
27
|
+
P2003: { outcome: "A2008" },
|
|
28
|
+
// Write conflict or deadlock: another operation reached the same state
|
|
29
|
+
// first, so nothing this one asked for was applied. Both arrive as P2034
|
|
30
|
+
// and are not separable at this seam, so they share one outcome. Prisma
|
|
31
|
+
// performs no automatic retry and this adapter adds none (C-822, D-809,
|
|
32
|
+
// F-705): a retried plan would re-run developer callbacks whose side
|
|
33
|
+
// effects are not rollback-safe. A retry is the caller's to make.
|
|
34
|
+
P2034: { outcome: "A2013" },
|
|
35
|
+
// The required connected records were not found: measured only with a
|
|
36
|
+
// relation reference in `meta`, and always about the arguments.
|
|
37
|
+
P2018: { outcome: "A2004" },
|
|
38
|
+
// Depends on records that were required but not found. A caller error
|
|
39
|
+
// about the arguments by default; a `null` result on the two strict
|
|
40
|
+
// unique variants, and only when the missing record is the one the call
|
|
41
|
+
// selected rather than a relation target (D-811).
|
|
42
|
+
P2025: {
|
|
43
|
+
outcome: "A2004",
|
|
44
|
+
absence: {
|
|
45
|
+
variants: ["update.unique", "delete.unique"],
|
|
46
|
+
subject: "root-record",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/** Framework-owned text, one line per code it may emit. */
|
|
51
|
+
const OUTCOME_MESSAGES = {
|
|
52
|
+
// `A2008` now means the constraint case alone, and its text is
|
|
53
|
+
// UNCHANGED on purpose. §13.2's narrowed meaning — "would violate a
|
|
54
|
+
// uniqueness or reference constraint the Resource declares" — is
|
|
55
|
+
// documentation, NOT a legal wire message: C-819's measured sweeps ban
|
|
56
|
+
// `constraint`, `Unique` and `UNIQUE` from a provider-originated
|
|
57
|
+
// envelope, because those are the provider's own words for this very
|
|
58
|
+
// failure. Narrowing the code's meaning does not license naming the
|
|
59
|
+
// constraint to the caller, and this line is what keeps the two apart.
|
|
60
|
+
A2008: "The operation conflicts with the current state of the resource.",
|
|
61
|
+
A2013: "Another operation modified the same state concurrently, so this operation was not applied and nothing was written.",
|
|
62
|
+
A2004: "The operation's arguments referenced a record that does not exist.",
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* `"root-record"`: the failure is about the record the call selected, not
|
|
66
|
+
* about a relation target. Measured discriminant, not message text.
|
|
67
|
+
*/
|
|
68
|
+
function matchesAbsenceSubject(subject, facts) {
|
|
69
|
+
switch (subject) {
|
|
70
|
+
case "root-record":
|
|
71
|
+
return facts.relation === undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Classifies one failed Prisma call.
|
|
76
|
+
*
|
|
77
|
+
* `error` is `unknown` and stays `unknown`: the reader is the only thing
|
|
78
|
+
* that touches it, and the rules see nothing but measured facts. The
|
|
79
|
+
* operation is needed because D-811's answer depends on the variant the
|
|
80
|
+
* caller asked for, which is framework knowledge and not in the error.
|
|
81
|
+
*/
|
|
82
|
+
export function classifyPrismaError(error, operation) {
|
|
83
|
+
const facts = readPrismaError(error);
|
|
84
|
+
if (facts.code === undefined) {
|
|
85
|
+
return { kind: "unclassified" };
|
|
86
|
+
}
|
|
87
|
+
const rule = Object.hasOwn(PRISMA_ERROR_TABLE, facts.code)
|
|
88
|
+
? PRISMA_ERROR_TABLE[facts.code]
|
|
89
|
+
: undefined;
|
|
90
|
+
if (rule === undefined) {
|
|
91
|
+
return { kind: "unclassified" };
|
|
92
|
+
}
|
|
93
|
+
const { absence } = rule;
|
|
94
|
+
if (absence === undefined) {
|
|
95
|
+
return { kind: "outcome", code: rule.outcome };
|
|
96
|
+
}
|
|
97
|
+
const forgiven = absence.variants.includes(`${operation.family}.${operation.variant}`) &&
|
|
98
|
+
matchesAbsenceSubject(absence.subject, facts);
|
|
99
|
+
return forgiven
|
|
100
|
+
? { kind: "absence" }
|
|
101
|
+
: { kind: "outcome", code: rule.outcome };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The deliberate failure an `outcome` classification stands for.
|
|
105
|
+
*
|
|
106
|
+
* It is a `FrameworkError`, so every failure this adapter originates is an
|
|
107
|
+
* `Error` subclass — D-816's adapter half, which needs no core change.
|
|
108
|
+
*
|
|
109
|
+
* The cause carries framework text and nothing else: no issues, and that is
|
|
110
|
+
* now the **rule** rather than an absence (C-839, ADR 0008 decision 4). A
|
|
111
|
+
* conflict is not a Framework Validation Layer failure, ADR 0008 decision 1
|
|
112
|
+
* makes a V-code *determine* the A-code, and `validationCode` rolls up to only
|
|
113
|
+
* `A2001`/`A2002`/`A2004`/`A2007`/`A2009` — so a conflict V-code would have no
|
|
114
|
+
* legal rollup, which is why `A2008`, `A2013` and `A2014` pair with none and
|
|
115
|
+
* carry `cause.issues` absent. And no provider text, because a deliberate
|
|
116
|
+
* outcome's top-level `cause.message` is forwarded verbatim (C-819, F-723).
|
|
117
|
+
* Nothing read off the Prisma error reaches this value.
|
|
118
|
+
*/
|
|
119
|
+
export function prismaOutcomeFailure(code) {
|
|
120
|
+
return new FrameworkError(code, {
|
|
121
|
+
message: OUTCOME_MESSAGES[code] ?? "The operation could not be completed.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export function describePrismaConflict(error) {
|
|
125
|
+
const facts = readPrismaError(error);
|
|
126
|
+
if (facts.code === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
code: facts.code,
|
|
131
|
+
model: facts.modelName,
|
|
132
|
+
constraint: facts.constraint,
|
|
133
|
+
metaKeys: facts.metaKeys,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The total read of one hostile value (T-10's first half).
|
|
3
|
+
*
|
|
4
|
+
* Everything on the failure path arrives as `unknown`. `@prisma/client`
|
|
5
|
+
* 7.10.0 raises `PrismaClientKnownRequestError`, whose `meta` is typed
|
|
6
|
+
* `Record<string, unknown>` and is **not** a discriminated union, so a
|
|
7
|
+
* reader that trusts the type would be reading a shape nobody promised.
|
|
8
|
+
* Worse, the value need not come from Prisma at all: a developer callback,
|
|
9
|
+
* a driver, or a proxy can hand the adapter anything.
|
|
10
|
+
*
|
|
11
|
+
* So this module reads structurally and defensively, under two rules it
|
|
12
|
+
* never relaxes — the same totality discipline as core's diagnostics
|
|
13
|
+
* reporter, which also observes values it cannot vouch for:
|
|
14
|
+
*
|
|
15
|
+
* 1. **No accessor is ever invoked.** Every read goes through
|
|
16
|
+
* `Object.getOwnPropertyDescriptor` and takes `descriptor.value`. An
|
|
17
|
+
* accessor descriptor yields `undefined` and its getter is never called,
|
|
18
|
+
* so a booby-trapped property cannot run code, mutate state, or throw
|
|
19
|
+
* from inside the adapter's failure path.
|
|
20
|
+
* 2. **Nothing escapes.** Every reflective call is guarded, because a Proxy
|
|
21
|
+
* may throw from its `getOwnPropertyDescriptor` or `ownKeys` trap, and a
|
|
22
|
+
* reader that throws while classifying a failure would replace a legible
|
|
23
|
+
* outcome with an illegible one.
|
|
24
|
+
*
|
|
25
|
+
* Reading only *own* properties is deliberate rather than incidental:
|
|
26
|
+
* 7.10.0 carries `code` and `meta` as own data properties (observed own
|
|
27
|
+
* keys: `["code","meta","clientVersion","name"]`), so the own read is
|
|
28
|
+
* sufficient for every real error, and a `code` that exists only as an
|
|
29
|
+
* inherited accessor is precisely the case rule 1 refuses to touch.
|
|
30
|
+
*
|
|
31
|
+
* ## What this module does NOT read
|
|
32
|
+
*
|
|
33
|
+
* `message` is never read. At 7.10.0 a `PrismaClientKnownRequestError`
|
|
34
|
+
* message is not a sentence — it is a rendered call site. Measured
|
|
35
|
+
* verbatim on this repository's own schema:
|
|
36
|
+
*
|
|
37
|
+
* ```text
|
|
38
|
+
* Invalid `client.author.create()` invocation in
|
|
39
|
+
* /home/.../packages/prisma7-adapter/src/errors/__probe__.spec.ts:50:18
|
|
40
|
+
*
|
|
41
|
+
* 49 await capture("P2002 duplicate email", () =>
|
|
42
|
+
* → 50 client.author.create(
|
|
43
|
+
* Unique constraint failed on the fields: (`email`)
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* That single string carries an absolute file path, a source excerpt and
|
|
47
|
+
* the violated constraint. C-819 forbids all three from reaching `cause`,
|
|
48
|
+
* and a *deliberate* outcome's top-level `cause.message` is still forwarded
|
|
49
|
+
* verbatim (register row F-723, open). The cheapest guarantee is not to
|
|
50
|
+
* read the message at all, so the leak has no source; the raw error still
|
|
51
|
+
* reaches core's diagnostics sink, which is server-side and may carry it.
|
|
52
|
+
*
|
|
53
|
+
* ## Version bound (C-846)
|
|
54
|
+
*
|
|
55
|
+
* Everything below is scoped to `@prisma/client` **7.x**. Prisma 8 replaced
|
|
56
|
+
* P-codes with dotted `NAMESPACE.SUBCODE` codes, which makes a Prisma 8
|
|
57
|
+
* adapter a re-map of this module rather than a patch to it.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* The violated constraint as 7.10.0's driver adapters actually report it,
|
|
61
|
+
* at `meta.driverAdapterError.cause.constraint` (D-810).
|
|
62
|
+
*
|
|
63
|
+
* The arms are **provider-shaped, measured on both engines**, not guessed:
|
|
64
|
+
*
|
|
65
|
+
* | Engine | Unique (P2002) | Foreign key (P2003) |
|
|
66
|
+
* |-----------------------------------|-------------------------------|-------------------------|
|
|
67
|
+
* | `@prisma/adapter-better-sqlite3` | `{ fields: ["email"] }` | `{ foreignKey: {} }` |
|
|
68
|
+
* | `@prisma/adapter-pg` | `{ index: "Author_email_key" }` | `{ index: "Post_authorId_fkey" }` |
|
|
69
|
+
*
|
|
70
|
+
* It is therefore **not a string**, and the two engines do not agree on
|
|
71
|
+
* which identifier they can name — SQLite names columns, PostgreSQL names
|
|
72
|
+
* the index. Every arm's payload is provider-owned text and never crosses
|
|
73
|
+
* the boundary (C-819); the *shape* is kept because it is structurally
|
|
74
|
+
* separable, which is what RC-8 needs from Task 12.
|
|
75
|
+
*/
|
|
76
|
+
export type PrismaConstraintSubject = {
|
|
77
|
+
readonly kind: "fields";
|
|
78
|
+
readonly fields: readonly string[];
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: "index";
|
|
81
|
+
readonly index: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly kind: "foreign-key";
|
|
84
|
+
} | {
|
|
85
|
+
readonly kind: "unreadable";
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Everything the normalizer is allowed to know about one failed call.
|
|
89
|
+
*
|
|
90
|
+
* Narrow and flat on purpose: a classification rule reads these fields and
|
|
91
|
+
* nothing else, so no rule can reach back into the raw value and no rule
|
|
92
|
+
* can depend on a shape this module has not measured.
|
|
93
|
+
*/
|
|
94
|
+
export type PrismaErrorFacts = {
|
|
95
|
+
/**
|
|
96
|
+
* The P-code, when it is an own string property. A missing or non-string
|
|
97
|
+
* `code` leaves this `undefined`, which the table answers as
|
|
98
|
+
* `unclassified` — never as a guess (D-812).
|
|
99
|
+
*/
|
|
100
|
+
readonly code: string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* The own, enumerable key names of `meta`, in the order 7.10.0 wrote
|
|
103
|
+
* them. This is the *structural* signature of a failure, and the only
|
|
104
|
+
* discriminant ADR 0006 permits: it forbids inferring an outcome from
|
|
105
|
+
* error text, and `meta.operation` is English prose (`"an update"`,
|
|
106
|
+
* `"a delete"`, `"a query"`, `"a nested connect"`).
|
|
107
|
+
*/
|
|
108
|
+
readonly metaKeys: readonly string[];
|
|
109
|
+
/** `meta.modelName` — the Prisma model the failure occurred on. */
|
|
110
|
+
readonly modelName: string | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* `meta.relation` — present exactly when the failure is about a
|
|
113
|
+
* *relation reference* rather than about the record the call selected.
|
|
114
|
+
* This is the measured discriminant behind D-811 (see the normalizer).
|
|
115
|
+
*/
|
|
116
|
+
readonly relation: string | undefined;
|
|
117
|
+
/** `meta.model` — the model at the far end of `relation`. */
|
|
118
|
+
readonly relatedModel: string | undefined;
|
|
119
|
+
/** `meta.driverAdapterError.cause.constraint`, read structurally. */
|
|
120
|
+
readonly constraint: PrismaConstraintSubject | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* `meta.target` — **read so that it is measured, depended on by
|
|
123
|
+
* nothing** (C-820, RC-9).
|
|
124
|
+
*
|
|
125
|
+
* The v7 error-reference page still documents `{ target: ['email'] }`,
|
|
126
|
+
* and RC-9 records that whether 7.10.0 ever emits it could not be
|
|
127
|
+
* determined from the package. It was induced on both engines here and
|
|
128
|
+
* **`target` was absent from every payload** — P2002 (single and
|
|
129
|
+
* compound unique), P2003, P2025 (three shapes), P2018 and P2034, on
|
|
130
|
+
* SQLite and on PostgreSQL. The docs are stale; the installed package
|
|
131
|
+
* governs (spec §18.1). This field exists so the next reader sees that
|
|
132
|
+
* measured, and does not "fix" the code to match the documentation.
|
|
133
|
+
*/
|
|
134
|
+
readonly legacyTarget: readonly string[] | undefined;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Reads one hostile value into the facts a classification may use.
|
|
138
|
+
*
|
|
139
|
+
* Total: every input produces a `PrismaErrorFacts`. `readPrismaError(null)`
|
|
140
|
+
* and `readPrismaError(Object.create(null))` are as valid as a real
|
|
141
|
+
* `PrismaClientKnownRequestError` — they simply report nothing, which the
|
|
142
|
+
* table answers as `unclassified`.
|
|
143
|
+
*/
|
|
144
|
+
export declare function readPrismaError(error: unknown): PrismaErrorFacts;
|