@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,167 @@
|
|
|
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
|
+
/** Only objects and functions can carry own properties. */
|
|
60
|
+
function canCarryProperties(value) {
|
|
61
|
+
return ((typeof value === "object" && value !== null) || typeof value === "function");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* One own *data* property, or `undefined`.
|
|
65
|
+
*
|
|
66
|
+
* `"value" in descriptor` is what separates a data descriptor from an
|
|
67
|
+
* accessor one: an accessor descriptor has `get`/`set` and no `value`, so
|
|
68
|
+
* it falls through and the getter is never invoked. The `try` is for a
|
|
69
|
+
* Proxy whose `getOwnPropertyDescriptor` trap throws.
|
|
70
|
+
*/
|
|
71
|
+
function ownValue(target, key) {
|
|
72
|
+
if (!canCarryProperties(target)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
77
|
+
return descriptor !== undefined && "value" in descriptor
|
|
78
|
+
? descriptor.value
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function ownString(target, key) {
|
|
86
|
+
const value = ownValue(target, key);
|
|
87
|
+
return typeof value === "string" ? value : undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Own enumerable key names, or none. Guarded for a hostile `ownKeys` trap. */
|
|
90
|
+
function ownKeys(target) {
|
|
91
|
+
if (!canCarryProperties(target)) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return Object.keys(target);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A string list read element by element: a sparse array, a nested array or
|
|
103
|
+
* a list of objects yields only the strings it does contain, and anything
|
|
104
|
+
* that is not an array at all yields `undefined`.
|
|
105
|
+
*/
|
|
106
|
+
function ownStringList(target, key) {
|
|
107
|
+
const value = ownValue(target, key);
|
|
108
|
+
if (!Array.isArray(value)) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const strings = [];
|
|
112
|
+
for (const entry of value) {
|
|
113
|
+
if (typeof entry === "string") {
|
|
114
|
+
strings.push(entry);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return strings;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `meta.driverAdapterError.cause.constraint`, as the engines shape it.
|
|
121
|
+
*
|
|
122
|
+
* The chain is walked one accessor-free step at a time, so a `meta` that is
|
|
123
|
+
* a string, a `driverAdapterError` that is `null`, or a `cause` with a
|
|
124
|
+
* throwing trap all end as `undefined` rather than as a throw. A
|
|
125
|
+
* `constraint` that is present but matches no measured arm reports
|
|
126
|
+
* `"unreadable"` — a recorded unknown, which is not the same answer as "no
|
|
127
|
+
* constraint was reported".
|
|
128
|
+
*/
|
|
129
|
+
function readConstraint(meta) {
|
|
130
|
+
const cause = ownValue(ownValue(meta, "driverAdapterError"), "cause");
|
|
131
|
+
const constraint = ownValue(cause, "constraint");
|
|
132
|
+
if (!canCarryProperties(constraint)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const fields = ownStringList(constraint, "fields");
|
|
136
|
+
if (fields !== undefined) {
|
|
137
|
+
return { kind: "fields", fields };
|
|
138
|
+
}
|
|
139
|
+
const index = ownString(constraint, "index");
|
|
140
|
+
if (index !== undefined) {
|
|
141
|
+
return { kind: "index", index };
|
|
142
|
+
}
|
|
143
|
+
if (canCarryProperties(ownValue(constraint, "foreignKey"))) {
|
|
144
|
+
return { kind: "foreign-key" };
|
|
145
|
+
}
|
|
146
|
+
return { kind: "unreadable" };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Reads one hostile value into the facts a classification may use.
|
|
150
|
+
*
|
|
151
|
+
* Total: every input produces a `PrismaErrorFacts`. `readPrismaError(null)`
|
|
152
|
+
* and `readPrismaError(Object.create(null))` are as valid as a real
|
|
153
|
+
* `PrismaClientKnownRequestError` — they simply report nothing, which the
|
|
154
|
+
* table answers as `unclassified`.
|
|
155
|
+
*/
|
|
156
|
+
export function readPrismaError(error) {
|
|
157
|
+
const meta = ownValue(error, "meta");
|
|
158
|
+
return {
|
|
159
|
+
code: ownString(error, "code"),
|
|
160
|
+
metaKeys: ownKeys(meta),
|
|
161
|
+
modelName: ownString(meta, "modelName"),
|
|
162
|
+
relation: ownString(meta, "relation"),
|
|
163
|
+
relatedModel: ownString(meta, "model"),
|
|
164
|
+
constraint: readConstraint(meta),
|
|
165
|
+
legacyTarget: ownStringList(meta, "target"),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FrameworkError } from "@aventara/core";
|
|
2
|
+
import type { PrismaIdentifierAddress } from "../discovery/discovery-artifact.interface.js";
|
|
3
|
+
import type { PrismaCall } from "../translation/operation.translator.js";
|
|
4
|
+
/**
|
|
5
|
+
* The lost-row conflict: the read selected a row and the re-asserted write
|
|
6
|
+
* matched none, so **nothing was written** and a retry may succeed.
|
|
7
|
+
*
|
|
8
|
+
* `A2014 STALE_SELECTION`, and this is the only place that names it. The cause
|
|
9
|
+
* carries framework text and no issues: like `A2008` and `A2013` it pairs with
|
|
10
|
+
* **no** V-code, because a conflict is not a Framework Validation Layer failure
|
|
11
|
+
* and ADR 0008 decision 1 would give a conflict V-code no legal rollup (C-839).
|
|
12
|
+
*/
|
|
13
|
+
export declare function lostRowConflict(): FrameworkError;
|
|
14
|
+
/**
|
|
15
|
+
* Issues one of the emulation's two statements.
|
|
16
|
+
*
|
|
17
|
+
* `step` is passed because the two are classified **differently** and the
|
|
18
|
+
* discrimination is by call site: the read's failures go through the closed P-code
|
|
19
|
+
* table like any other call, while the write's `P2025` is this module's lost row
|
|
20
|
+
* and must not reach D-811's rule.
|
|
21
|
+
*/
|
|
22
|
+
export type FirstVariantIssuer = (call: PrismaCall, step: "read" | "write") => Promise<unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* How the emulation's two statements are issued, and against which client.
|
|
25
|
+
*
|
|
26
|
+
* **`inUnit` opens the unit AND yields the issuer scoped to it**, in that order,
|
|
27
|
+
* which is the only shape that cannot get D-807 wrong. An earlier draft of this
|
|
28
|
+
* module took the two separately — a `unit` that opened a transaction and an
|
|
29
|
+
* `issue` already bound to a client — and the standalone case then opened a
|
|
30
|
+
* transaction whose two statements ran on the **root** client, outside it: the
|
|
31
|
+
* emulation would have looked atomic and been nothing of the kind, and no
|
|
32
|
+
* assertion about the issued `where` would have noticed. Making the unit the thing
|
|
33
|
+
* that hands out the issuer removes the gap rather than documenting it.
|
|
34
|
+
*
|
|
35
|
+
* This module therefore never sees a Prisma client, a provider name or a
|
|
36
|
+
* transaction; it sees one issuer, already in the right unit.
|
|
37
|
+
*/
|
|
38
|
+
export type FirstVariantEmulation = {
|
|
39
|
+
/** The translated plan's two steps: the read, then the write template. */
|
|
40
|
+
readonly steps: readonly PrismaCall[];
|
|
41
|
+
/** The Resource's recorded Prisma addressing, for the write's identifier. */
|
|
42
|
+
readonly identifiers: readonly PrismaIdentifierAddress[];
|
|
43
|
+
/**
|
|
44
|
+
* Opens the one adapter-internal unit and runs `work` inside it.
|
|
45
|
+
*
|
|
46
|
+
* Standalone this opens an interactive transaction and the issuer it yields is
|
|
47
|
+
* scoped to it; inside a plan it runs `work` against the outer transaction
|
|
48
|
+
* client, so the emulated steps roll back **with** the plan rather than
|
|
49
|
+
* independently (§14.5).
|
|
50
|
+
*/
|
|
51
|
+
readonly inUnit: <T>(work: (issue: FirstVariantIssuer) => Promise<T>) => Promise<T>;
|
|
52
|
+
};
|
|
53
|
+
/** A plan that is not the two-step shape this module manufactures. */
|
|
54
|
+
export declare class PrismaEmulationError extends Error {
|
|
55
|
+
readonly name = "PrismaEmulationError";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Runs one first-style variant.
|
|
59
|
+
*
|
|
60
|
+
* @returns the affected row, shaped to the caller's `select`/`include` by the
|
|
61
|
+
* write's own projection, or `null` when the read matched nothing.
|
|
62
|
+
* @throws the lost-row conflict when the re-asserted write matches no row.
|
|
63
|
+
*/
|
|
64
|
+
export declare function runFirstVariantEmulation(emulation: FirstVariantEmulation): Promise<unknown>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { FrameworkError } from "@aventara/core";
|
|
2
|
+
import { readPrismaError } from "../errors/prisma-error.reader.js";
|
|
3
|
+
import { addressIdentifier } from "../translation/identifier.addressing.js";
|
|
4
|
+
/**
|
|
5
|
+
* `update.first` and `delete.first` — the two **framework-manufactured**
|
|
6
|
+
* variants, and the one adapter-internal unit that manufactures them (D-807,
|
|
7
|
+
* D-828, C-856; closes F-704).
|
|
8
|
+
*
|
|
9
|
+
* # What is manufactured, and why that word
|
|
10
|
+
*
|
|
11
|
+
* The shape Aventara advertises — *general filter · ordering decides "first" ·
|
|
12
|
+
* effect limited to one row · the affected row is returned, in one call* — is
|
|
13
|
+
* essentially absent across ORM design, because SQL splits the ingredients along
|
|
14
|
+
* a dialect fault line: `UPDATE … ORDER BY … LIMIT` is a MySQL/MariaDB extension
|
|
15
|
+
* while `RETURNING` is Postgres/SQLite/MSSQL, so whichever dialect grants the
|
|
16
|
+
* limit denies the returning **[SURVEY]**. Prisma 7.10.0's `Operation` union has
|
|
17
|
+
* no `updateFirst` and no `deleteFirst` **[PKG]**. So these are not a Prisma-
|
|
18
|
+
* shaped gap an adapter might omit; they are an operation the framework
|
|
19
|
+
* manufactures, and the honest framing — the one the plan, C-857 and the package
|
|
20
|
+
* README all use — is *framework-manufactured, non-atomic, with a defined
|
|
21
|
+
* conflict outcome*, **not** "dangerous".
|
|
22
|
+
*
|
|
23
|
+
* # Two statements, and the firstness lives in the first one
|
|
24
|
+
*
|
|
25
|
+
* 1. `findFirst({ where, orderBy, select: <identifier> })`. The `orderBy` the
|
|
26
|
+
* caller sent is the **selector for which row**, not a limit: ordering and
|
|
27
|
+
* single-row selection are natural on a read. No match → `null`, which core's
|
|
28
|
+
* outcome mapper turns into `A1001 NO_MATCH` (200, `data: null`) (C-826).
|
|
29
|
+
* 2. `update`/`delete` narrowed to the identifier the read returned **with the
|
|
30
|
+
* caller's original predicate re-asserted** — `{ <identifier>, AND: <original
|
|
31
|
+
* where> }` — carrying the caller's own projection, so the row the contract
|
|
32
|
+
* owes comes back from the write itself (§8.2, §8.4). `WhereUniqueInput`'s
|
|
33
|
+
* extended form admits exactly this, and `extendedWhereUnique` is GA **[RUN8]**.
|
|
34
|
+
*
|
|
35
|
+
* Prisma's `limit` on `updateMany`/`deleteMany` is **not** used. That is not a
|
|
36
|
+
* principle — D-827 rules the opposite, an argument the ORM offers is not hidden —
|
|
37
|
+
* it is that the `limit` cannot express this operation: neither `updateMany` nor
|
|
38
|
+
* `deleteMany` carries an `orderBy` in 7.10.0 **[RUN8]**, so `limit: 1` selects an
|
|
39
|
+
* **arbitrary** row and two identical calls can hit different rows. The identifier
|
|
40
|
+
* from the read is what makes the write deterministic.
|
|
41
|
+
*
|
|
42
|
+
* # What re-assertion closes, stated precisely
|
|
43
|
+
*
|
|
44
|
+
* It closes the **wrong-row race**: without it the write is by identifier alone
|
|
45
|
+
* and can mutate a row that no longer matches what the caller asked for; with it
|
|
46
|
+
* the write can only ever touch a row the read selected, and a row that moved out
|
|
47
|
+
* of the predicate is **detected** rather than mis-mutated.
|
|
48
|
+
*
|
|
49
|
+
* It does **not** close **stale firstness**. Between the read and the write
|
|
50
|
+
* another writer can insert or update a row that sorts *ahead* of the selected one
|
|
51
|
+
* and still matches the original predicate. Both steps are blind to it — the read
|
|
52
|
+
* never saw it, and the re-asserted write succeeds against the row it was given —
|
|
53
|
+
* so `update.first` can mutate a row that is no longer the first ordered match and
|
|
54
|
+
* report ordinary success. Nothing detects it and no re-assertion can, because the
|
|
55
|
+
* missing fact is about a row that was never read. Closing it needs either
|
|
56
|
+
* `Serializable` isolation, which core's `Adapter.transaction(work)` seam cannot
|
|
57
|
+
* express (RC-3), so it is **accepted as a documented property**: C-830 has Phase 8
|
|
58
|
+
* escalate no isolation, and `first-variant.emulator.spec.ts` carries a case that
|
|
59
|
+
* *demonstrates* the residual rather than asserting it away.
|
|
60
|
+
*
|
|
61
|
+
* # The lost row is discriminated BY CALL SITE, never by `meta`
|
|
62
|
+
*
|
|
63
|
+
* A write that matches nothing after a successful read is the **lost-row race**: a
|
|
64
|
+
* distinct semantic outcome the caller may retry. Under the chosen Route A it
|
|
65
|
+
* arrives as a thrown `P2025` — measured at the emulator's own call site, inside
|
|
66
|
+
* an interactive transaction, on both engines (2026-10-02):
|
|
67
|
+
*
|
|
68
|
+
* ```text
|
|
69
|
+
* tx.author.update({ where: { id: <read>, AND: [{ tenantId: "gone" }] }, data })
|
|
70
|
+
* -> P2025, meta = { modelName: "Author", operation: "an update" }
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* That meta is **identical** to the one D-811 forgives into a `null` for
|
|
74
|
+
* `update.unique`/`delete.unique`, so the two rules cannot be told apart by the
|
|
75
|
+
* error at all. They are told apart by **where the call was made**: a `P2025`
|
|
76
|
+
* caught by {@link runFirstVariantEmulation}'s write step is the lost row, and
|
|
77
|
+
* nothing else in this adapter forgives a `P2025` into a `null`. This is why the
|
|
78
|
+
* emulator does not consult `classifyPrismaError` for its write step and why
|
|
79
|
+
* D-811's rule is not shared.
|
|
80
|
+
*
|
|
81
|
+
* {@link lostRowConflict} is the one seam that names the outcome. It lives here
|
|
82
|
+
* rather than in `error.normalizer.ts` (ownership corrected 2026-09-23) because
|
|
83
|
+
* the normalizer owns the closed P-code table and this condition has **no provider
|
|
84
|
+
* error behind it at all** under Route B, and under Route A the provider error is
|
|
85
|
+
* only the messenger. It answers **`A2014 STALE_SELECTION`** (409) — its own
|
|
86
|
+
* discriminated code since the architect's assent of 2026-10-02 (RC-8, D-831),
|
|
87
|
+
* replacing the coarse `A2008` it shared with the constraint cases and with
|
|
88
|
+
* `A2013`'s concurrent write. The code is reached from here and from nowhere
|
|
89
|
+
* else: the closed P-code table has no row that can emit it, because the
|
|
90
|
+
* discriminant is the call site and not anything in the error.
|
|
91
|
+
*
|
|
92
|
+
* # One adapter-internal unit — F-704's answer
|
|
93
|
+
*
|
|
94
|
+
* Inside a plan the emulation reuses the **outer transaction client**; standalone
|
|
95
|
+
* it opens **its own interactive transaction** (D-807, C-830). The nested option
|
|
96
|
+
* is real — `$transaction` is not in the interactive-client deny list, which is
|
|
97
|
+
* exactly `["$connect","$disconnect","$on","$use","$extends"]`, and
|
|
98
|
+
* `Transaction.Options.newTxId` exists for nested interactive transactions
|
|
99
|
+
* **[PKG]** — and it is rejected on semantics, not on SQL: a nested savepoint
|
|
100
|
+
* would let an emulated variant roll back *independently* of the plan, which
|
|
101
|
+
* contradicts §14.5 ("if any operation fails, all transaction writes roll back")
|
|
102
|
+
* and core's single-sentinel model. The literal `SAVEPOINT` statement is not in
|
|
103
|
+
* the JS bundle, so the decision deliberately rests on that semantic argument
|
|
104
|
+
* rather than on an unverifiable claim about the SQL issued.
|
|
105
|
+
*
|
|
106
|
+
* No isolation level is escalated: RC-3's readings are unadopted, safety comes
|
|
107
|
+
* from re-assertion, and the spec asserts the transaction **boundary**, not a
|
|
108
|
+
* level.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* The framework-owned text of the lost-row outcome, which is `A2014`'s published
|
|
112
|
+
* meaning verbatim. It holds for **any** adapter that reaches this state,
|
|
113
|
+
* including one that reaches it in a single statement with optimistic
|
|
114
|
+
* concurrency: it names no provider, no emulation, no savepoint, no retry loop
|
|
115
|
+
* and no statement count. Carries no provider string (C-819).
|
|
116
|
+
*/
|
|
117
|
+
const LOST_ROW_MESSAGE = "The state this operation had selected changed before its effect could be applied, so nothing was written.";
|
|
118
|
+
/**
|
|
119
|
+
* The lost-row conflict: the read selected a row and the re-asserted write
|
|
120
|
+
* matched none, so **nothing was written** and a retry may succeed.
|
|
121
|
+
*
|
|
122
|
+
* `A2014 STALE_SELECTION`, and this is the only place that names it. The cause
|
|
123
|
+
* carries framework text and no issues: like `A2008` and `A2013` it pairs with
|
|
124
|
+
* **no** V-code, because a conflict is not a Framework Validation Layer failure
|
|
125
|
+
* and ADR 0008 decision 1 would give a conflict V-code no legal rollup (C-839).
|
|
126
|
+
*/
|
|
127
|
+
export function lostRowConflict() {
|
|
128
|
+
return new FrameworkError("A2014", { message: LOST_ROW_MESSAGE });
|
|
129
|
+
}
|
|
130
|
+
/** Prisma's code for "the records required for this operation were not found". */
|
|
131
|
+
const MISSING_RECORD_CODE = "P2025";
|
|
132
|
+
/** A plan that is not the two-step shape this module manufactures. */
|
|
133
|
+
export class PrismaEmulationError extends Error {
|
|
134
|
+
name = "PrismaEmulationError";
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Runs one first-style variant.
|
|
138
|
+
*
|
|
139
|
+
* @returns the affected row, shaped to the caller's `select`/`include` by the
|
|
140
|
+
* write's own projection, or `null` when the read matched nothing.
|
|
141
|
+
* @throws the lost-row conflict when the re-asserted write matches no row.
|
|
142
|
+
*/
|
|
143
|
+
export async function runFirstVariantEmulation(emulation) {
|
|
144
|
+
const [read, write] = emulation.steps;
|
|
145
|
+
if (read === undefined || write === undefined) {
|
|
146
|
+
throw new PrismaEmulationError("a first-style emulation is exactly two steps: the read that selects the row and the write that applies the effect");
|
|
147
|
+
}
|
|
148
|
+
return emulation.inUnit(async (issue) => {
|
|
149
|
+
const selected = await issue(read, "read");
|
|
150
|
+
if (selected === null || selected === undefined) {
|
|
151
|
+
// C-826: the adapter returns `null` and core assigns `A1001 NO_MATCH`.
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(selected)) {
|
|
155
|
+
throw new PrismaEmulationError("the first-style read answered a value that is not a row, so no identifier can be read from it");
|
|
156
|
+
}
|
|
157
|
+
const addressed = addressIdentifier({
|
|
158
|
+
addresses: emulation.identifiers,
|
|
159
|
+
values: selected,
|
|
160
|
+
// `"subset"`: the read selects the identifier today, and a future route
|
|
161
|
+
// may select the caller's projection beside it. Either way the criterion
|
|
162
|
+
// is complete, which is all the write needs.
|
|
163
|
+
match: "subset",
|
|
164
|
+
emit: (_member, value) => value,
|
|
165
|
+
});
|
|
166
|
+
if (addressed === undefined) {
|
|
167
|
+
throw new PrismaEmulationError(`the first-style read returned { ${Object.keys(selected).join(", ")} }, which completes no recorded identifier, so the write has nothing to target`);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
return await issue({
|
|
171
|
+
...write,
|
|
172
|
+
// The invariant, and the whole point of the step: the issued `where`
|
|
173
|
+
// carries BOTH the identifier and the caller's original predicate
|
|
174
|
+
// (C-856, D-829). The predicate arrives already under `AND` from the
|
|
175
|
+
// translator, so merging the identifier in cannot reshape it — and when
|
|
176
|
+
// the caller sent no `where` there is no `AND` to carry, because
|
|
177
|
+
// inventing an empty one would emit a token the request never did.
|
|
178
|
+
args: {
|
|
179
|
+
...write.args,
|
|
180
|
+
where: { ...addressed.where, ...writeWhere(write) },
|
|
181
|
+
},
|
|
182
|
+
}, "write");
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (readPrismaError(error).code === MISSING_RECORD_CODE) {
|
|
186
|
+
throw lostRowConflict();
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** The re-asserted predicate the translator placed on the write template. */
|
|
193
|
+
function writeWhere(write) {
|
|
194
|
+
const where = write.args.where;
|
|
195
|
+
if (where === undefined) {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
if (!isRecord(where)) {
|
|
199
|
+
throw new PrismaEmulationError("the first-style write template's `where` must be the re-asserted predicate");
|
|
200
|
+
}
|
|
201
|
+
return where;
|
|
202
|
+
}
|
|
203
|
+
function isRecord(value) {
|
|
204
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
205
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AdapterModel, AdapterOperationExecutor } from "@aventara/core";
|
|
2
|
+
import type { PrismaResourceTranslation } from "../discovery/discovery-artifact.interface.js";
|
|
3
|
+
import { type PrismaDelegateHost, type PrismaTransactionOptions } from "../prisma-client.interface.js";
|
|
4
|
+
/**
|
|
5
|
+
* The execution half of T-7: a translated {@link PrismaCall} → a provider call,
|
|
6
|
+
* and `Adapter.execute` made real.
|
|
7
|
+
*
|
|
8
|
+
* Translation is pure and lives in `translation/operation.translator.ts`; this
|
|
9
|
+
* module owns the three things that need a client and nothing else:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Reaching the delegate.** See {@link delegateKeyOf} — the one derivation in
|
|
12
|
+
* this package, and the one place it is made.
|
|
13
|
+
* 2. **Classifying a failure.** The closed P-code table in
|
|
14
|
+
* `errors/error.normalizer.ts` answers `absence` → `null` (D-811),
|
|
15
|
+
* `outcome` → a deliberate `FrameworkError`, or `unclassified` → propagate, so
|
|
16
|
+
* core's `AdapterBoundary` reports `A3001` with core's own text and sends the
|
|
17
|
+
* raw error to diagnostics. The adapter never guesses a code it did not measure.
|
|
18
|
+
* 3. **Shaping the answer.** `execution/result.shaper.ts` places every `$count`
|
|
19
|
+
* envelope, converts every returned value to the runtime form core's output
|
|
20
|
+
* validator accepts (C-834's inbound half) and reads the number a count variant
|
|
21
|
+
* owes; which of its three entry points runs is the *variant's* `result` row in
|
|
22
|
+
* the dispatch table, not a guess from the shape Prisma returned. The shaper is
|
|
23
|
+
* handed the model and the Resource as well as the plan, because the scalar a
|
|
24
|
+
* returned value belongs to is a fact of the model and not of the value.
|
|
25
|
+
*
|
|
26
|
+
* # One executor type, two scopes (D-807, C-804)
|
|
27
|
+
*
|
|
28
|
+
* `scope: "root"` is the adapter's own `execute`. `scope: "transaction"` is what
|
|
29
|
+
* `Adapter.transaction`'s callback hands core: the same code over a transaction
|
|
30
|
+
* client. The scope decides exactly one thing — whether a first-style emulation
|
|
31
|
+
* opens its own interactive transaction or reuses the one already open — and a
|
|
32
|
+
* transaction-scoped executor closes over the transaction client only, so the root
|
|
33
|
+
* client is unreachable from inside it.
|
|
34
|
+
*
|
|
35
|
+
* # C-834 is converted at its own seam, not at this one
|
|
36
|
+
*
|
|
37
|
+
* All three value crossings read ONE table, `translation/scalar.conversion.ts`:
|
|
38
|
+
* a write's data through `mutation.translator.ts`, a filter operand through
|
|
39
|
+
* `where.translator.ts`, and a returned row through `execution/result.shaper.ts`.
|
|
40
|
+
* None of them is converted here. Converting at this seam would have put the
|
|
41
|
+
* scalar table in a fourth place and hidden the gap from the module that owns it.
|
|
42
|
+
*
|
|
43
|
+
* **Core's output validation runs after this module, and strips.** For a
|
|
44
|
+
* projection-carrying variant `planReadOperation` sets `materialize: true` and
|
|
45
|
+
* `materializeRecord` ends in `Object.fromEntries(plan.requested…)` **[CORE]**, so
|
|
46
|
+
* a spare key the adapter returned would be removed before the validator saw it.
|
|
47
|
+
* That is why the emulator's own return value is asserted directly in
|
|
48
|
+
* `first-variant.emulator.spec.ts` as well as end-to-end here: an end-to-end
|
|
49
|
+
* assertion about extra keys cannot fail on that path.
|
|
50
|
+
*/
|
|
51
|
+
/** A call the executor cannot issue against the client it was given. */
|
|
52
|
+
export declare class PrismaExecutionError extends Error {
|
|
53
|
+
readonly name = "PrismaExecutionError";
|
|
54
|
+
}
|
|
55
|
+
export type PrismaExecutionScope = "root" | "transaction";
|
|
56
|
+
export type PrismaOperationExecutorDependencies = {
|
|
57
|
+
/**
|
|
58
|
+
* The client in force: the application's own client at `"root"` scope, the
|
|
59
|
+
* transaction client inside a plan.
|
|
60
|
+
*/
|
|
61
|
+
readonly client: PrismaDelegateHost;
|
|
62
|
+
readonly model: AdapterModel;
|
|
63
|
+
/** The generate-time translation table (Prisma addressing per Resource). */
|
|
64
|
+
readonly translation: Readonly<Record<string, PrismaResourceTranslation>>;
|
|
65
|
+
readonly scope: PrismaExecutionScope;
|
|
66
|
+
/** Adapter-owned, never Aventara public configuration (D-806). */
|
|
67
|
+
readonly transactionOptions?: PrismaTransactionOptions;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Prisma's delegate property for a model.
|
|
71
|
+
*
|
|
72
|
+
* **Derived, and that is a recorded residual rather than a comfortable choice.**
|
|
73
|
+
* The authoritative token is readable — `generated/<engine>/models/<M>.ts`, a 🟢
|
|
74
|
+
* file, writes it as the extension key: `GetSelect<{ … },
|
|
75
|
+
* ExtArgs["result"]["author"]>` for model `Author` — but `types.reader.ts` reads
|
|
76
|
+
* the 🟢 files for *capability* facts and does not record it, and the client
|
|
77
|
+
* property itself is declared only in `internal/class.ts`, which that reader
|
|
78
|
+
* refuses by design. So the rule applied here is Prisma's own: lower-case the
|
|
79
|
+
* first character and nothing else. `operation.executor.spec.ts` asserts the
|
|
80
|
+
* derivation against the real generated client for **every** model in the schema,
|
|
81
|
+
* so it is observed for this repository's six models; a model name whose Prisma
|
|
82
|
+
* property does not follow the rule would be caught there and not in production.
|
|
83
|
+
* The clean owner of the fact is the generate-time translation table, beside
|
|
84
|
+
* `identifierKeys`, and that is left as a pointer rather than taken here.
|
|
85
|
+
*/
|
|
86
|
+
export declare function delegateKeyOf(model: string): string;
|
|
87
|
+
export declare function createPrismaOperationExecutor<M extends AdapterModel>(dependencies: PrismaOperationExecutorDependencies): AdapterOperationExecutor<M>;
|