@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,273 @@
|
|
|
1
|
+
import type { AdapterModel, AdapterOperation } from "@aventara/core";
|
|
2
|
+
import type { PrismaResourceTranslation } from "../discovery/discovery-artifact.interface.js";
|
|
3
|
+
import { type PrismaProjectionPlan } from "./projection.translator.js";
|
|
4
|
+
/**
|
|
5
|
+
* T-7 — **one validated Aventara operation → one provider call description**,
|
|
6
|
+
* and the module that makes the whole sixteen-variant grammar assertable as pure
|
|
7
|
+
* data with no database in sight.
|
|
8
|
+
*
|
|
9
|
+
* Splitting translation from execution is the plan's single largest testability
|
|
10
|
+
* decision (T-7): everything below is a pure function of the discovered model,
|
|
11
|
+
* the generate-time translation table and the arguments core validated. The
|
|
12
|
+
* database is needed only for the *other* half, `execution/operation.executor.ts`.
|
|
13
|
+
*
|
|
14
|
+
* # The dispatch table is the module
|
|
15
|
+
*
|
|
16
|
+
* {@link PRISMA_VARIANT_DISPATCH} is total over core's own sixteen
|
|
17
|
+
* `family.variant` keys by `satisfies`, so a seventeenth standard operation added
|
|
18
|
+
* to core is a compile error here rather than a variant that quietly reaches no
|
|
19
|
+
* provider call. Reading it answers, in one place, every question of the form
|
|
20
|
+
* "what does Aventara's X actually issue".
|
|
21
|
+
*
|
|
22
|
+
* # The negative half of the table is the load-bearing half
|
|
23
|
+
*
|
|
24
|
+
* {@link PrismaAction} deliberately **excludes** `findUniqueOrThrow` and
|
|
25
|
+
* `findFirstOrThrow` (RC-2: core's outcome mapper owns the absence decision, and
|
|
26
|
+
* an `OrThrow` call would move it into the adapter), `groupBy` and `aggregate`
|
|
27
|
+
* (spec §18.5), and **every raw action** — the `findRaw`/`aggregateRaw` model
|
|
28
|
+
* actions and the client's own raw methods — which C-853 forbids outright and
|
|
29
|
+
* permanently. Aventara is not an ORM and reaches the database only through the
|
|
30
|
+
* adapter's ORM; a raw statement would forfeit the ORM's own injection
|
|
31
|
+
* protection and oblige Aventara to build its own. Because the exclusion lives in
|
|
32
|
+
* the *type*, a raw call is not representable in a {@link PrismaExecutionPlan} at
|
|
33
|
+
* all: the rule is enforced by the compiler, not by a reviewer's memory.
|
|
34
|
+
* `operation.translator.spec.ts` asserts the same thing from the value side, as
|
|
35
|
+
* the belt to that type's braces.
|
|
36
|
+
*
|
|
37
|
+
* # `delete.many` is a hole in the table, and the hole is the decision
|
|
38
|
+
*
|
|
39
|
+
* 7.10.0 has no `deleteManyAndReturn` **[PKG]**, and the architect ruled the
|
|
40
|
+
* variant **omitted rather than emulated** (C-829, D-826). `operations.deriver.ts`
|
|
41
|
+
* therefore never advertises it, so core rejects the request with `A2002
|
|
42
|
+
* UNKNOWN_OPERATION` before any adapter call. The table still carries the key —
|
|
43
|
+
* it is total over core's grammar — with an `"unavailable"` entry, so the
|
|
44
|
+
* omission is a fact a reader can find rather than an absence they have to infer.
|
|
45
|
+
*
|
|
46
|
+
* # First-style variants, and the write route
|
|
47
|
+
*
|
|
48
|
+
* `update.first` and `delete.first` are **framework-manufactured** (D-828): a
|
|
49
|
+
* cross-ORM survey found the four-part shape — general filter, ordering decides
|
|
50
|
+
* "first", effect limited to one row, the affected row is returned, all in one
|
|
51
|
+
* call — essentially absent across ORM design, and Prisma 7.10.0 has no
|
|
52
|
+
* `updateFirst`/`deleteFirst` **[PKG]**. So the plan is an ordinary read then
|
|
53
|
+
* write, and its two steps are {@link PrismaExecutionPlan}'s `"emulated"` arm.
|
|
54
|
+
*
|
|
55
|
+
* **The write route is Route A — select-first + unique mutation — and Task 10
|
|
56
|
+
* measured it rather than inheriting it.** T-7.1.1 recorded two routes and §14e
|
|
57
|
+
* reserved the choice; §1.0 forbids an implementation task from blocking on an
|
|
58
|
+
* owner decision, so Task 10 ran both against the real 7.10.0 client on **both**
|
|
59
|
+
* engines. Every row came out for Route A, and two of them came out *against*
|
|
60
|
+
* Route B:
|
|
61
|
+
*
|
|
62
|
+
* ```text
|
|
63
|
+
* measured 2026-10-02, @prisma/client 7.10.0, SQLite + PostgreSQL 18, identical
|
|
64
|
+
*
|
|
65
|
+
* A update({ where: { id, AND: […] }, data, select }) -> returned the row
|
|
66
|
+
* A …with a non-matching AND -> threw P2025, row untouched
|
|
67
|
+
* A …select reaching a to-many relation AND _count -> returned both
|
|
68
|
+
* A delete({ where: { id, AND: […] }, select }) -> returned the deleted row
|
|
69
|
+
* A …with a non-matching AND -> threw P2025, row still present
|
|
70
|
+
* A findFirst + update inside $transaction, non-matching AND -> threw P2025 at the emulator's own call site
|
|
71
|
+
* B updateManyAndReturn({ where: { id, AND: […] }, … }) -> returned [row]
|
|
72
|
+
* B …with a non-matching AND -> resolved []
|
|
73
|
+
* B …select: { posts: { … } } -> REJECTED: "Unknown field `posts` for select
|
|
74
|
+
* statement on model `UpdateManyAuthorAndReturnOutputType`"
|
|
75
|
+
* B …select: { _count: { … } } -> REJECTED: same, for `_count`
|
|
76
|
+
* B deleteMany({ where: { id, AND: […] } }) -> { count: 1 } — carries no row at all
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* So Route A is the mechanism spec §8.3's own worked example names, it satisfies
|
|
80
|
+
* §8.2's return contract for **every** projection core can plan, and it needs no
|
|
81
|
+
* pre-write projection read and no identifier strip. Route B cannot serve a
|
|
82
|
+
* caller projection that reaches a to-many relation or `$count` at all, and its
|
|
83
|
+
* `delete.first` must read before it writes. The route is **one entry** in the
|
|
84
|
+
* table below — {@link FIRST_VARIANT_WRITE_ROUTE} — which is the seam D-801/§9
|
|
85
|
+
* put here precisely so §14e's ratification (or reversal) is one edit.
|
|
86
|
+
*
|
|
87
|
+
* # The write step of an emulated plan is a template, and says so
|
|
88
|
+
*
|
|
89
|
+
* A `"single"` plan's call is issuable as written. An `"emulated"` plan's write
|
|
90
|
+
* step is **not**, by construction: its `where` carries the caller's re-asserted
|
|
91
|
+
* predicate under `AND` and the identifier is added by
|
|
92
|
+
* `execution/first-variant.emulator.ts` from the row the read returned. That is
|
|
93
|
+
* what "emulated" means — the second statement is not knowable until the first
|
|
94
|
+
* has run — and it is why the emulator, not this module, issues it.
|
|
95
|
+
*
|
|
96
|
+
* # Nothing is emitted that the framework did not send (C-855, D-830)
|
|
97
|
+
*
|
|
98
|
+
* Every key below is added only when the corresponding Aventara argument is
|
|
99
|
+
* **present**. An absent argument and an `undefined` one are different arguments
|
|
100
|
+
* to a provider, so `"take" in args` is never made true by accident. An argument
|
|
101
|
+
* this module does not recognise raises rather than being dropped: an argument
|
|
102
|
+
* that typechecks and is silently ignored is worse than an absent one, because
|
|
103
|
+
* the absent one fails loudly at the caller (D-830's MikroORM evidence).
|
|
104
|
+
*/
|
|
105
|
+
/** The `ModelAction` subset this adapter uses. See the header for the exclusions. */
|
|
106
|
+
export type PrismaAction = "findUnique" | "findFirst" | "findMany" | "count" | "create" | "createMany" | "createManyAndReturn" | "update" | "updateMany" | "updateManyAndReturn" | "delete" | "deleteMany" | "upsert";
|
|
107
|
+
/** One provider call description, as plain data. */
|
|
108
|
+
export type PrismaCall = {
|
|
109
|
+
/**
|
|
110
|
+
* The Prisma **model** name. Discovery's existence gate keys a Resource on the
|
|
111
|
+
* PSL model name, so the Resource key *is* the model name; the delegate
|
|
112
|
+
* property that reaches it is the executor's concern, not translation's.
|
|
113
|
+
*/
|
|
114
|
+
readonly model: string;
|
|
115
|
+
readonly action: PrismaAction;
|
|
116
|
+
readonly args: Readonly<Record<string, unknown>>;
|
|
117
|
+
};
|
|
118
|
+
/** A variant is either one call, or an emulated sequence (§8.3, D-807). */
|
|
119
|
+
export type PrismaExecutionPlan = {
|
|
120
|
+
readonly kind: "single";
|
|
121
|
+
readonly call: PrismaCall;
|
|
122
|
+
} | {
|
|
123
|
+
readonly kind: "emulated";
|
|
124
|
+
readonly steps: readonly PrismaCall[];
|
|
125
|
+
readonly atomicity: "outer-tx" | "own-tx";
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* What the variant owes its caller, and therefore which shaper runs (T-8).
|
|
129
|
+
*
|
|
130
|
+
* It is a property of the *variant*, not of the provider answer, so it belongs
|
|
131
|
+
* in the dispatch table beside the action rather than in a second table the
|
|
132
|
+
* executor would have to keep in step.
|
|
133
|
+
*/
|
|
134
|
+
export type PrismaResultKind = "row" | "rows" | "count";
|
|
135
|
+
export type PrismaVariantDispatch = {
|
|
136
|
+
readonly kind: "single";
|
|
137
|
+
readonly action: PrismaAction;
|
|
138
|
+
readonly result: PrismaResultKind;
|
|
139
|
+
} | {
|
|
140
|
+
readonly kind: "emulated";
|
|
141
|
+
readonly read: PrismaAction;
|
|
142
|
+
readonly write: PrismaAction;
|
|
143
|
+
readonly result: PrismaResultKind;
|
|
144
|
+
} | {
|
|
145
|
+
readonly kind: "unavailable";
|
|
146
|
+
readonly reason: string;
|
|
147
|
+
};
|
|
148
|
+
/** The sixteen-variant dispatch table (T-7, §18.4), total over core's grammar. */
|
|
149
|
+
export declare const PRISMA_VARIANT_DISPATCH: {
|
|
150
|
+
readonly "find.first": {
|
|
151
|
+
readonly kind: "single";
|
|
152
|
+
readonly action: "findFirst";
|
|
153
|
+
readonly result: "row";
|
|
154
|
+
};
|
|
155
|
+
readonly "find.unique": {
|
|
156
|
+
readonly kind: "single";
|
|
157
|
+
readonly action: "findUnique";
|
|
158
|
+
readonly result: "row";
|
|
159
|
+
};
|
|
160
|
+
readonly "find.many": {
|
|
161
|
+
readonly kind: "single";
|
|
162
|
+
readonly action: "findMany";
|
|
163
|
+
readonly result: "rows";
|
|
164
|
+
};
|
|
165
|
+
readonly "find.count": {
|
|
166
|
+
readonly kind: "single";
|
|
167
|
+
readonly action: "count";
|
|
168
|
+
readonly result: "count";
|
|
169
|
+
};
|
|
170
|
+
readonly "create.one": {
|
|
171
|
+
readonly kind: "single";
|
|
172
|
+
readonly action: "create";
|
|
173
|
+
readonly result: "row";
|
|
174
|
+
};
|
|
175
|
+
readonly "create.many": {
|
|
176
|
+
readonly kind: "single";
|
|
177
|
+
readonly action: "createManyAndReturn";
|
|
178
|
+
readonly result: "rows";
|
|
179
|
+
};
|
|
180
|
+
readonly "create.count": {
|
|
181
|
+
readonly kind: "single";
|
|
182
|
+
readonly action: "createMany";
|
|
183
|
+
readonly result: "count";
|
|
184
|
+
};
|
|
185
|
+
readonly "update.first": {
|
|
186
|
+
readonly kind: "emulated";
|
|
187
|
+
readonly read: "findFirst";
|
|
188
|
+
readonly write: "update";
|
|
189
|
+
readonly result: "row";
|
|
190
|
+
};
|
|
191
|
+
readonly "update.unique": {
|
|
192
|
+
readonly kind: "single";
|
|
193
|
+
readonly action: "update";
|
|
194
|
+
readonly result: "row";
|
|
195
|
+
};
|
|
196
|
+
readonly "update.many": {
|
|
197
|
+
readonly kind: "single";
|
|
198
|
+
readonly action: "updateManyAndReturn";
|
|
199
|
+
readonly result: "rows";
|
|
200
|
+
};
|
|
201
|
+
readonly "update.count": {
|
|
202
|
+
readonly kind: "single";
|
|
203
|
+
readonly action: "updateMany";
|
|
204
|
+
readonly result: "count";
|
|
205
|
+
};
|
|
206
|
+
readonly "delete.first": {
|
|
207
|
+
readonly kind: "emulated";
|
|
208
|
+
readonly read: "findFirst";
|
|
209
|
+
readonly write: "delete";
|
|
210
|
+
readonly result: "row";
|
|
211
|
+
};
|
|
212
|
+
readonly "delete.unique": {
|
|
213
|
+
readonly kind: "single";
|
|
214
|
+
readonly action: "delete";
|
|
215
|
+
readonly result: "row";
|
|
216
|
+
};
|
|
217
|
+
readonly "delete.many": {
|
|
218
|
+
readonly kind: "unavailable";
|
|
219
|
+
readonly reason: "@prisma/client 7.10.0 has no deleteManyAndReturn, so delete.many is omitted rather than emulated (C-829, D-826); delete.count deletes a filtered set";
|
|
220
|
+
};
|
|
221
|
+
readonly "delete.count": {
|
|
222
|
+
readonly kind: "single";
|
|
223
|
+
readonly action: "deleteMany";
|
|
224
|
+
readonly result: "count";
|
|
225
|
+
};
|
|
226
|
+
readonly "upsert.unique": {
|
|
227
|
+
readonly kind: "single";
|
|
228
|
+
readonly action: "upsert";
|
|
229
|
+
readonly result: "row";
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
/** An operation the adapter will not translate. */
|
|
233
|
+
export declare class PrismaOperationTranslationError extends Error {
|
|
234
|
+
readonly name = "PrismaOperationTranslationError";
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The translation of one operation: the plan, plus the projection plan the result
|
|
238
|
+
* shaper needs.
|
|
239
|
+
*
|
|
240
|
+
* T-7 names only `PrismaExecutionPlan`, and that is not enough on its own:
|
|
241
|
+
* `AdapterOperation` carries no read plan **[CORE]**, so Task 8's
|
|
242
|
+
* {@link PrismaProjectionPlan} — the reconstruction of where every `$count`
|
|
243
|
+
* envelope sits — is produced *here*, while translating the projection, and
|
|
244
|
+
* cannot be recovered from the emitted Prisma arguments afterwards (Prisma's
|
|
245
|
+
* `_count` is a sibling key, not a position). Returning it beside the plan is
|
|
246
|
+
* what keeps `result.shaper.ts` free of a second, drifting reconstruction.
|
|
247
|
+
*/
|
|
248
|
+
export type PrismaOperationTranslation = {
|
|
249
|
+
readonly plan: PrismaExecutionPlan;
|
|
250
|
+
readonly projection: PrismaProjectionPlan;
|
|
251
|
+
/**
|
|
252
|
+
* The caller's `where`, translated, or `undefined` when they sent none.
|
|
253
|
+
*
|
|
254
|
+
* Recorded for the emulator, which re-asserts it on the write (C-856, D-829).
|
|
255
|
+
* It is the same value the read step carries, by identity, so the two halves
|
|
256
|
+
* of the manufacture cannot assert different predicates.
|
|
257
|
+
*/
|
|
258
|
+
readonly predicate: Readonly<Record<string, unknown>> | undefined;
|
|
259
|
+
};
|
|
260
|
+
export type PrismaOperationTranslationInput = {
|
|
261
|
+
readonly model: AdapterModel;
|
|
262
|
+
/** The generate-time translation table: Prisma addressing per Resource. */
|
|
263
|
+
readonly translation: Readonly<Record<string, PrismaResourceTranslation>>;
|
|
264
|
+
readonly operation: AdapterOperation;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Translates one validated Aventara operation.
|
|
268
|
+
*
|
|
269
|
+
* @throws {PrismaOperationTranslationError} for a variant the adapter advertises
|
|
270
|
+
* no call for, an argument it does not recognise, or a unique selector that
|
|
271
|
+
* addresses no row — never a silently narrowed or widened call.
|
|
272
|
+
*/
|
|
273
|
+
export declare function translateOperation(input: PrismaOperationTranslationInput): PrismaOperationTranslation;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { addressIdentifier } from "./identifier.addressing.js";
|
|
2
|
+
import { translateMutationData } from "./mutation.translator.js";
|
|
3
|
+
import { translateOrderBy, translatePagination, } from "./order-pagination.translator.js";
|
|
4
|
+
import { translateProjection, } from "./projection.translator.js";
|
|
5
|
+
import { translateWhere } from "./where.translator.js";
|
|
6
|
+
/**
|
|
7
|
+
* The chosen first-variant write route, in one place (§14e, T-7.1.1).
|
|
8
|
+
*
|
|
9
|
+
* Route A: the read selects the identifier, and the write is Prisma's own
|
|
10
|
+
* `update`/`delete` narrowed to that identifier with the caller's predicate
|
|
11
|
+
* re-asserted. Both are projection-carrying — their result maps through
|
|
12
|
+
* `GetFindResult`, the same helper `findFirst` uses **[PKG,
|
|
13
|
+
* `runtime/client.d.ts:1352`, `:1356`, `:1144`]** — and Task 10 measured both on
|
|
14
|
+
* both engines (see the header). Switching to Route B is an edit to these two
|
|
15
|
+
* cells and to `first-variant.emulator.ts`'s lost-row detection, and nothing else.
|
|
16
|
+
*/
|
|
17
|
+
const FIRST_VARIANT_WRITE_ROUTE = {
|
|
18
|
+
update: "update",
|
|
19
|
+
delete: "delete",
|
|
20
|
+
};
|
|
21
|
+
/** The sixteen-variant dispatch table (T-7, §18.4), total over core's grammar. */
|
|
22
|
+
export const PRISMA_VARIANT_DISPATCH = {
|
|
23
|
+
"find.first": { kind: "single", action: "findFirst", result: "row" },
|
|
24
|
+
// RC-2: `findUnique` and a returned `null`, never `findUniqueOrThrow` — core's
|
|
25
|
+
// `OUTCOME_TABLE` owns the absence decision and maps the `null` to `A2003`.
|
|
26
|
+
"find.unique": { kind: "single", action: "findUnique", result: "row" },
|
|
27
|
+
"find.many": { kind: "single", action: "findMany", result: "rows" },
|
|
28
|
+
"find.count": { kind: "single", action: "count", result: "count" },
|
|
29
|
+
"create.one": { kind: "single", action: "create", result: "row" },
|
|
30
|
+
// C-817: the row-returning bulk write is what `create.many` owes its caller;
|
|
31
|
+
// `createMany` answers `{ count }` and is `create.count`'s call instead.
|
|
32
|
+
"create.many": {
|
|
33
|
+
kind: "single",
|
|
34
|
+
action: "createManyAndReturn",
|
|
35
|
+
result: "rows",
|
|
36
|
+
},
|
|
37
|
+
"create.count": { kind: "single", action: "createMany", result: "count" },
|
|
38
|
+
"update.first": {
|
|
39
|
+
kind: "emulated",
|
|
40
|
+
read: "findFirst",
|
|
41
|
+
write: FIRST_VARIANT_WRITE_ROUTE.update,
|
|
42
|
+
result: "row",
|
|
43
|
+
},
|
|
44
|
+
"update.unique": { kind: "single", action: "update", result: "row" },
|
|
45
|
+
"update.many": {
|
|
46
|
+
kind: "single",
|
|
47
|
+
action: "updateManyAndReturn",
|
|
48
|
+
result: "rows",
|
|
49
|
+
},
|
|
50
|
+
"update.count": { kind: "single", action: "updateMany", result: "count" },
|
|
51
|
+
"delete.first": {
|
|
52
|
+
kind: "emulated",
|
|
53
|
+
read: "findFirst",
|
|
54
|
+
write: FIRST_VARIANT_WRITE_ROUTE.delete,
|
|
55
|
+
result: "row",
|
|
56
|
+
},
|
|
57
|
+
"delete.unique": { kind: "single", action: "delete", result: "row" },
|
|
58
|
+
"delete.many": {
|
|
59
|
+
kind: "unavailable",
|
|
60
|
+
reason: "@prisma/client 7.10.0 has no deleteManyAndReturn, so delete.many is omitted rather than emulated (C-829, D-826); delete.count deletes a filtered set",
|
|
61
|
+
},
|
|
62
|
+
"delete.count": { kind: "single", action: "deleteMany", result: "count" },
|
|
63
|
+
"upsert.unique": { kind: "single", action: "upsert", result: "row" },
|
|
64
|
+
};
|
|
65
|
+
/** An operation the adapter will not translate. */
|
|
66
|
+
export class PrismaOperationTranslationError extends Error {
|
|
67
|
+
name = "PrismaOperationTranslationError";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Translates one validated Aventara operation.
|
|
71
|
+
*
|
|
72
|
+
* @throws {PrismaOperationTranslationError} for a variant the adapter advertises
|
|
73
|
+
* no call for, an argument it does not recognise, or a unique selector that
|
|
74
|
+
* addresses no row — never a silently narrowed or widened call.
|
|
75
|
+
*/
|
|
76
|
+
export function translateOperation(input) {
|
|
77
|
+
const key = `${input.operation.family}.${input.operation.variant}`;
|
|
78
|
+
const dispatch = Object.hasOwn(PRISMA_VARIANT_DISPATCH, key)
|
|
79
|
+
? PRISMA_VARIANT_DISPATCH[key]
|
|
80
|
+
: undefined;
|
|
81
|
+
if (dispatch === undefined) {
|
|
82
|
+
throw new PrismaOperationTranslationError(`"${key}" is not a standard Aventara operation`);
|
|
83
|
+
}
|
|
84
|
+
if (dispatch.kind === "unavailable") {
|
|
85
|
+
throw new PrismaOperationTranslationError(`"${key}" is not available on the Prisma adapter: ${dispatch.reason}`);
|
|
86
|
+
}
|
|
87
|
+
const context = {
|
|
88
|
+
model: input.model,
|
|
89
|
+
translation: input.translation,
|
|
90
|
+
operation: input.operation,
|
|
91
|
+
resource: input.operation.resource,
|
|
92
|
+
args: input.operation.arguments,
|
|
93
|
+
};
|
|
94
|
+
const projection = translateProjection(context.args, {
|
|
95
|
+
translateRelationArguments: relationArgumentTranslator(context),
|
|
96
|
+
});
|
|
97
|
+
const predicate = context.args.where === undefined || isUniqueSelectorVariant(key)
|
|
98
|
+
? undefined
|
|
99
|
+
: translateWhere({
|
|
100
|
+
model: context.model,
|
|
101
|
+
resource: context.resource,
|
|
102
|
+
where: context.args.where,
|
|
103
|
+
});
|
|
104
|
+
if (dispatch.kind === "single") {
|
|
105
|
+
return {
|
|
106
|
+
plan: {
|
|
107
|
+
kind: "single",
|
|
108
|
+
call: {
|
|
109
|
+
model: context.resource,
|
|
110
|
+
action: dispatch.action,
|
|
111
|
+
args: singleCallArguments(context, key, predicate, projection.args),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
projection: projection.plan,
|
|
115
|
+
predicate,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
plan: {
|
|
120
|
+
kind: "emulated",
|
|
121
|
+
steps: [
|
|
122
|
+
{
|
|
123
|
+
model: context.resource,
|
|
124
|
+
action: dispatch.read,
|
|
125
|
+
args: {
|
|
126
|
+
...(predicate === undefined ? {} : { where: predicate }),
|
|
127
|
+
...orderByArgument(context),
|
|
128
|
+
// The read selects the identifier and nothing else: under Route A
|
|
129
|
+
// the WRITE carries the caller's projection, so reading it twice
|
|
130
|
+
// would be a statement the contract does not need.
|
|
131
|
+
select: identifierSelect(context),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
model: context.resource,
|
|
136
|
+
action: dispatch.write,
|
|
137
|
+
args: {
|
|
138
|
+
// A template: the identifier is added by the emulator from the row
|
|
139
|
+
// the read returned. `AND` carries the re-assertion (C-856, D-829).
|
|
140
|
+
...(predicate === undefined ? {} : { where: { AND: predicate } }),
|
|
141
|
+
...(context.operation.family === "update"
|
|
142
|
+
? { data: mutationData(context, "update", "data") }
|
|
143
|
+
: {}),
|
|
144
|
+
...projection.args,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
atomicity: "own-tx",
|
|
149
|
+
},
|
|
150
|
+
projection: projection.plan,
|
|
151
|
+
predicate,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The four variants whose `where` is core's FLAT unique selector rather than a
|
|
156
|
+
* general predicate, read from core's own rule: `hasGeneralRootWhere` is false
|
|
157
|
+
* for exactly `find.unique`, `update.unique`, `delete.unique` and `upsert.unique`
|
|
158
|
+
* **[CORE, operations/operation-validator.ts]**.
|
|
159
|
+
*/
|
|
160
|
+
function isUniqueSelectorVariant(key) {
|
|
161
|
+
return (key === "find.unique" ||
|
|
162
|
+
key === "update.unique" ||
|
|
163
|
+
key === "delete.unique" ||
|
|
164
|
+
key === "upsert.unique");
|
|
165
|
+
}
|
|
166
|
+
function singleCallArguments(context, key, predicate, projectionArgs) {
|
|
167
|
+
const unique = isUniqueSelectorVariant(key);
|
|
168
|
+
const where = unique
|
|
169
|
+
? { where: uniqueWhere(context) }
|
|
170
|
+
: predicate === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { where: predicate };
|
|
173
|
+
switch (key) {
|
|
174
|
+
case "find.first":
|
|
175
|
+
return { ...where, ...orderByArgument(context), ...projectionArgs };
|
|
176
|
+
case "find.unique":
|
|
177
|
+
return { ...where, ...projectionArgs };
|
|
178
|
+
case "find.many":
|
|
179
|
+
return {
|
|
180
|
+
...where,
|
|
181
|
+
...orderByArgument(context),
|
|
182
|
+
...paginationArguments(context),
|
|
183
|
+
...projectionArgs,
|
|
184
|
+
};
|
|
185
|
+
case "find.count":
|
|
186
|
+
case "delete.count":
|
|
187
|
+
return where;
|
|
188
|
+
case "create.one":
|
|
189
|
+
return {
|
|
190
|
+
data: mutationData(context, "create", "data"),
|
|
191
|
+
...projectionArgs,
|
|
192
|
+
};
|
|
193
|
+
case "create.many":
|
|
194
|
+
return {
|
|
195
|
+
data: bulkMutationData(context, key, "create"),
|
|
196
|
+
...projectionArgs,
|
|
197
|
+
};
|
|
198
|
+
case "create.count":
|
|
199
|
+
return { data: bulkMutationData(context, key, "create") };
|
|
200
|
+
case "update.unique":
|
|
201
|
+
return {
|
|
202
|
+
...where,
|
|
203
|
+
data: mutationData(context, "update", "data"),
|
|
204
|
+
...projectionArgs,
|
|
205
|
+
};
|
|
206
|
+
case "update.many":
|
|
207
|
+
return {
|
|
208
|
+
...where,
|
|
209
|
+
data: mutationData(context, "update", "data"),
|
|
210
|
+
...projectionArgs,
|
|
211
|
+
};
|
|
212
|
+
case "update.count":
|
|
213
|
+
return { ...where, data: mutationData(context, "update", "data") };
|
|
214
|
+
case "delete.unique":
|
|
215
|
+
return { ...where, ...projectionArgs };
|
|
216
|
+
case "upsert.unique":
|
|
217
|
+
return {
|
|
218
|
+
...where,
|
|
219
|
+
create: mutationData(context, "create", "create"),
|
|
220
|
+
update: mutationData(context, "update", "update"),
|
|
221
|
+
...projectionArgs,
|
|
222
|
+
};
|
|
223
|
+
default:
|
|
224
|
+
throw new PrismaOperationTranslationError(`"${key}" has a dispatch entry and no argument shape, which is a translator defect`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Core's flat unique selector → `<M>WhereUniqueInput`, at the top level. */
|
|
228
|
+
function uniqueWhere(context) {
|
|
229
|
+
const selector = context.args.where;
|
|
230
|
+
if (!isRecord(selector)) {
|
|
231
|
+
throw new PrismaOperationTranslationError("where must be a unique identifier object for a unique variant");
|
|
232
|
+
}
|
|
233
|
+
const addressed = addressIdentifier({
|
|
234
|
+
addresses: identifiersOf(context, context.resource),
|
|
235
|
+
values: selector,
|
|
236
|
+
match: "exact",
|
|
237
|
+
// A unique selector's values are scalars core has already normalized. The
|
|
238
|
+
// one conversion C-834 names — a core `Decimal`, which Prisma refuses — is
|
|
239
|
+
// NOT applied here, and that is a recorded gap rather than an omission: see
|
|
240
|
+
// `operation.executor.ts`'s header.
|
|
241
|
+
emit: (_member, value) => value,
|
|
242
|
+
});
|
|
243
|
+
if (addressed === undefined) {
|
|
244
|
+
throw new PrismaOperationTranslationError(`where: { ${Object.keys(selector).join(", ")} } is no identifier of Resource "${context.resource}", so it addresses no row`);
|
|
245
|
+
}
|
|
246
|
+
return addressed.where;
|
|
247
|
+
}
|
|
248
|
+
/** The identifier the first-style read selects: the first recorded criterion. */
|
|
249
|
+
function identifierSelect(context) {
|
|
250
|
+
const [address] = identifiersOf(context, context.resource);
|
|
251
|
+
if (address === undefined) {
|
|
252
|
+
throw new PrismaOperationTranslationError(`Resource "${context.resource}" has no identifier, so a first-style variant has nothing to write by`);
|
|
253
|
+
}
|
|
254
|
+
return Object.fromEntries(address.members.map((member) => [member, true]));
|
|
255
|
+
}
|
|
256
|
+
function identifiersOf(context, resource) {
|
|
257
|
+
const entry = context.translation[resource];
|
|
258
|
+
if (entry === undefined) {
|
|
259
|
+
throw new PrismaOperationTranslationError(`Resource "${resource}" has no Prisma addressing in the translation table`);
|
|
260
|
+
}
|
|
261
|
+
return entry.identifierKeys;
|
|
262
|
+
}
|
|
263
|
+
function orderByArgument(context) {
|
|
264
|
+
if (context.args.orderBy === undefined) {
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
orderBy: translateOrderBy({
|
|
269
|
+
model: context.model,
|
|
270
|
+
resource: context.resource,
|
|
271
|
+
orderBy: context.args.orderBy,
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function paginationArguments(context) {
|
|
276
|
+
return translatePagination({
|
|
277
|
+
...(typeof context.args.limit === "number"
|
|
278
|
+
? { limit: context.args.limit }
|
|
279
|
+
: {}),
|
|
280
|
+
...(typeof context.args.offset === "number"
|
|
281
|
+
? { offset: context.args.offset }
|
|
282
|
+
: {}),
|
|
283
|
+
...(isRecord(context.args.cursor) ? { cursor: context.args.cursor } : {}),
|
|
284
|
+
identifiers: identifiersOf(context, context.resource),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function mutationData(context, stage, argument) {
|
|
288
|
+
return translateMutationData({
|
|
289
|
+
model: context.model,
|
|
290
|
+
resource: context.resource,
|
|
291
|
+
operation: context.operation,
|
|
292
|
+
stage,
|
|
293
|
+
data: context.args[argument],
|
|
294
|
+
}, mutationDependencies(context));
|
|
295
|
+
}
|
|
296
|
+
/** `create.many`/`create.count` carry an ARRAY of records **[CORE]**. */
|
|
297
|
+
function bulkMutationData(context, key, stage) {
|
|
298
|
+
const data = context.args.data;
|
|
299
|
+
if (!Array.isArray(data)) {
|
|
300
|
+
throw new PrismaOperationTranslationError(`"${key}" carries an array of records in \`data\``);
|
|
301
|
+
}
|
|
302
|
+
const dependencies = mutationDependencies(context);
|
|
303
|
+
return data.map((entry) => translateMutationData({
|
|
304
|
+
model: context.model,
|
|
305
|
+
resource: context.resource,
|
|
306
|
+
operation: context.operation,
|
|
307
|
+
stage,
|
|
308
|
+
data: entry,
|
|
309
|
+
}, dependencies));
|
|
310
|
+
}
|
|
311
|
+
function mutationDependencies(context) {
|
|
312
|
+
return {
|
|
313
|
+
translateWhere: (resource, where) => translateWhere({ model: context.model, resource, where }),
|
|
314
|
+
identifiers: Object.fromEntries(Object.entries(context.translation).map(([resource, entry]) => [
|
|
315
|
+
resource,
|
|
316
|
+
entry.identifierKeys,
|
|
317
|
+
])),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Task 7's vocabulary, as the projection translator's one dependency.
|
|
322
|
+
*
|
|
323
|
+
* The nested arguments are translated against the **target** Resource, which the
|
|
324
|
+
* projection path names: `projection.translator.ts` owns where a relation sits
|
|
325
|
+
* and this closure owns what its arguments mean, which is the seam that keeps the
|
|
326
|
+
* `_count` filter the *same* translated `where` the relation read uses.
|
|
327
|
+
*/
|
|
328
|
+
function relationArgumentTranslator(context) {
|
|
329
|
+
return (args, { path }) => {
|
|
330
|
+
const target = resolveRelationTarget(context, path);
|
|
331
|
+
const emitted = {};
|
|
332
|
+
for (const key of Object.keys(args)) {
|
|
333
|
+
if (key !== "where" &&
|
|
334
|
+
key !== "orderBy" &&
|
|
335
|
+
key !== "limit" &&
|
|
336
|
+
key !== "offset" &&
|
|
337
|
+
key !== "cursor") {
|
|
338
|
+
throw new PrismaOperationTranslationError(`select.${path.join(".")}.${key} is not an Aventara relation argument, so the adapter will not emit it`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (args.where !== undefined) {
|
|
342
|
+
emitted.where = translateWhere({
|
|
343
|
+
model: context.model,
|
|
344
|
+
resource: target,
|
|
345
|
+
where: args.where,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (args.orderBy !== undefined) {
|
|
349
|
+
emitted.orderBy = translateOrderBy({
|
|
350
|
+
model: context.model,
|
|
351
|
+
resource: target,
|
|
352
|
+
orderBy: args.orderBy,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
Object.assign(emitted, translatePagination({
|
|
356
|
+
...(typeof args.limit === "number" ? { limit: args.limit } : {}),
|
|
357
|
+
...(typeof args.offset === "number" ? { offset: args.offset } : {}),
|
|
358
|
+
...(isRecord(args.cursor) ? { cursor: args.cursor } : {}),
|
|
359
|
+
identifiers: identifiersOf(context, target),
|
|
360
|
+
}));
|
|
361
|
+
return emitted;
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function resolveRelationTarget(context, path) {
|
|
365
|
+
let resource = context.resource;
|
|
366
|
+
for (const segment of path) {
|
|
367
|
+
const field = context.model.resources[resource]?.fields[segment];
|
|
368
|
+
if (field === undefined || field.kind !== "relation") {
|
|
369
|
+
throw new PrismaOperationTranslationError(`select.${path.join(".")}: "${segment}" is not a relation of Resource "${resource}"`);
|
|
370
|
+
}
|
|
371
|
+
resource = field.target;
|
|
372
|
+
}
|
|
373
|
+
return resource;
|
|
374
|
+
}
|
|
375
|
+
function isRecord(value) {
|
|
376
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
377
|
+
}
|