@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,221 @@
|
|
|
1
|
+
import { Decimal, } from "@aventara/core";
|
|
2
|
+
/**
|
|
3
|
+
* C-834 — the scalar value conversion, in **one** table and **both**
|
|
4
|
+
* directions.
|
|
5
|
+
*
|
|
6
|
+
* # Why this is a module and not three private helpers
|
|
7
|
+
*
|
|
8
|
+
* Three consumers cross the same boundary with the same values: a write's `data`
|
|
9
|
+
* (`mutation.translator.ts`), a filter operand (`where.translator.ts`) and a row
|
|
10
|
+
* coming back (`execution/result.shaper.ts`). The emission table was born inside
|
|
11
|
+
* the mutation translator, which is where the first of the three needed it; a
|
|
12
|
+
* copy in each consumer would be three places to answer "what does a `decimal`
|
|
13
|
+
* look like on the wire", and the second copy to drift would be the one nobody
|
|
14
|
+
* reads. {@link SCALAR_BOUNDARY_CROSSINGS} is therefore the single owner, and
|
|
15
|
+
* Task 10's `identifier.addressing.ts` is the precedent — extracted for exactly
|
|
16
|
+
* this reason.
|
|
17
|
+
*
|
|
18
|
+
* The table is TOTAL over core's `BuiltInScalar` by a mapped type, so a tenth
|
|
19
|
+
* scalar added to the protocol is a compile error here rather than a value that
|
|
20
|
+
* silently crosses unconverted in one direction only.
|
|
21
|
+
*
|
|
22
|
+
* # `decimal` is the one real conversion, and that is measured
|
|
23
|
+
*
|
|
24
|
+
* Eight of the nine scalars pass through in both directions, re-measured on both
|
|
25
|
+
* driver adapters at `@prisma/client` 7.10.0 (2026-10-02): a `bigint` primitive,
|
|
26
|
+
* a `Date` and a `Uint8Array` are what the generated input types accept and what
|
|
27
|
+
* a read returns (`Buffer.isBuffer(v) === false`, `v instanceof Uint8Array ===
|
|
28
|
+
* true`), `Json` is written and returned as a plain object (its prototype IS
|
|
29
|
+
* `Object.prototype`, which is what core's own `isPlainRecord` output check
|
|
30
|
+
* requires **[CORE, runtime/scalar-runtime.ts]**), and an enum member is its own
|
|
31
|
+
* name on both sides. Those are the same forms core's runtime scope requires
|
|
32
|
+
* (`normalizeScalarAtom`'s `"application"` branch), so a pass-through is a
|
|
33
|
+
* decision, not an omission.
|
|
34
|
+
*
|
|
35
|
+
* `decimal` is the exception §6.2 names: *the public `Decimal` MUST NOT be
|
|
36
|
+
* `Prisma.Decimal`*, and it is not — core's `Decimal` is a class with one
|
|
37
|
+
* private `#value` and a `toString`, while Prisma's is its bundled decimal.js,
|
|
38
|
+
* whose constructor is named `Decimal2`. Neither accepts the other, so the
|
|
39
|
+
* crossing is a real conversion both ways: out, the base-10 string; in, core's
|
|
40
|
+
* own `Decimal`.
|
|
41
|
+
*
|
|
42
|
+
* # Outbound: three different failures, none of them a type error
|
|
43
|
+
*
|
|
44
|
+
* Passing core's `Decimal` through unconverted fails *differently* depending on
|
|
45
|
+
* where it lands, and none of the three is a plain rejection (measured on both
|
|
46
|
+
* engines, 2026-10-02):
|
|
47
|
+
*
|
|
48
|
+
* - as a **write value**: refused at the wire — *"Invalid value for argument
|
|
49
|
+
* `toString`: We could not serialize [object Function] value."*;
|
|
50
|
+
* - as a **comparison operand** (`gt`, `equals`, …): read as a **FieldRef**, not
|
|
51
|
+
* as a value — *"PrismaClientValidationError: Argument `_ref` is missing."*;
|
|
52
|
+
* - as a **bare equality operand**: *accepted*, and silently wrong. Core's
|
|
53
|
+
* `Decimal` keeps its digits in a private field, so `Object.keys` is empty and
|
|
54
|
+
* Prisma reads the object as an **empty `DecimalFilter`** — `findMany` returned
|
|
55
|
+
* **2 of 2** rows, byte-identical to passing no predicate at all, where the
|
|
56
|
+
* string `"1"` returned 1. A misinterpretation that validates is the failure
|
|
57
|
+
* this module exists to make impossible.
|
|
58
|
+
*
|
|
59
|
+
* # Inbound: the value core's output validator will accept
|
|
60
|
+
*
|
|
61
|
+
* Core validates adapter output in its `"runtime"` scope, which requires the
|
|
62
|
+
* *runtime* form of every scalar — `value instanceof Decimal` for a decimal
|
|
63
|
+
* **[CORE, runtime/output-validator.ts → normalizeScalarFieldValue]**. A
|
|
64
|
+
* `Decimal2` is not one, so an unconverted read fails core's own validation and
|
|
65
|
+
* the caller is told `A3000`. Converting here is therefore not a courtesy: it is
|
|
66
|
+
* the adapter's half of §6.2.
|
|
67
|
+
*
|
|
68
|
+
* # The scalar identity comes from the model, never from the value
|
|
69
|
+
*
|
|
70
|
+
* Both directions are keyed by the field's discovered scalar, never by the
|
|
71
|
+
* runtime shape of the value. A `Json` column holding `{ "x": 1 }` and a
|
|
72
|
+
* `Decimal` reaching Prisma are both "an object", and a `Json` column holding
|
|
73
|
+
* the string `"12.5"` is indistinguishable from a decimal string: shape-sniffing
|
|
74
|
+
* would convert one of those sooner or later, and silently. The discovered model
|
|
75
|
+
* already knows, so it is asked.
|
|
76
|
+
*/
|
|
77
|
+
/** A value this adapter will not convert rather than misrepresent. */
|
|
78
|
+
export class PrismaScalarConversionError extends Error {
|
|
79
|
+
name = "PrismaScalarConversionError";
|
|
80
|
+
}
|
|
81
|
+
function identity(value) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
/** A scalar whose Prisma representation IS its Aventara runtime form. */
|
|
85
|
+
const PASS_THROUGH = {
|
|
86
|
+
outbound: identity,
|
|
87
|
+
inbound: identity,
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* What each Aventara scalar becomes on the way to Prisma and on the way back.
|
|
91
|
+
*
|
|
92
|
+
* Total over `BuiltInScalar`: a scalar added to the protocol cannot reach a
|
|
93
|
+
* provider until both of its directions are decided here.
|
|
94
|
+
*/
|
|
95
|
+
const SCALAR_BOUNDARY_CROSSINGS = {
|
|
96
|
+
string: PASS_THROUGH,
|
|
97
|
+
boolean: PASS_THROUGH,
|
|
98
|
+
int: PASS_THROUGH,
|
|
99
|
+
float: PASS_THROUGH,
|
|
100
|
+
bigint: PASS_THROUGH,
|
|
101
|
+
decimal: { outbound: emitDecimal, inbound: receiveDecimal },
|
|
102
|
+
datetime: PASS_THROUGH,
|
|
103
|
+
bytes: PASS_THROUGH,
|
|
104
|
+
json: PASS_THROUGH,
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Core's `Decimal` → the base-10 string every Prisma decimal input accepts
|
|
108
|
+
* (`DecimalFieldUpdateOperationsInput` takes `runtime.Decimal | DecimalJsLike |
|
|
109
|
+
* number | string`, and `DecimalFilter`'s operands the same).
|
|
110
|
+
*
|
|
111
|
+
* A value that is not core's `Decimal` is refused rather than coerced: core
|
|
112
|
+
* normalizes every decimal to its own `Decimal` before the adapter is called
|
|
113
|
+
* (C-845), so a string or a number arriving here means the request bypassed
|
|
114
|
+
* validation, and guessing at it would be the adapter inventing precision.
|
|
115
|
+
*/
|
|
116
|
+
function emitDecimal(value, path) {
|
|
117
|
+
if (value === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (value instanceof Decimal) {
|
|
121
|
+
return value.toString();
|
|
122
|
+
}
|
|
123
|
+
throw new PrismaScalarConversionError(`${path}: a decimal value reaches the adapter as an Aventara Decimal (core normalizes before the adapter, C-834); the adapter will not guess at a ${describeValue(value)}`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Prisma's decimal → core's `Decimal`, which is the only decimal form core's
|
|
127
|
+
* output validator accepts in its runtime scope.
|
|
128
|
+
*
|
|
129
|
+
* The text is read through `String(value)`, which is exactly what RC-10
|
|
130
|
+
* measured as lossless for 7.10.0's `Decimal2` on both driver adapters
|
|
131
|
+
* (`String(v) === "12345.6789"`). A text core's own `Decimal` constructor
|
|
132
|
+
* rejects is reported as a conversion failure naming the form observed, rather
|
|
133
|
+
* than passed on to fail later as core's generic `A3000`.
|
|
134
|
+
*/
|
|
135
|
+
function receiveDecimal(value, path) {
|
|
136
|
+
if (value === null) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (value instanceof Decimal) {
|
|
140
|
+
// Already core's: a fake adapter, or a value this adapter round-tripped.
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
if (typeof value !== "object" &&
|
|
144
|
+
typeof value !== "string" &&
|
|
145
|
+
typeof value !== "number") {
|
|
146
|
+
throw new PrismaScalarConversionError(`${path}: the provider returned ${describeValue(value)} for a decimal column, which the adapter will not read as a decimal`);
|
|
147
|
+
}
|
|
148
|
+
const text = String(value);
|
|
149
|
+
try {
|
|
150
|
+
return new Decimal(text);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new PrismaScalarConversionError(`${path}: the provider returned ${describeValue(value)} for a decimal column, whose text "${text}" is not a decimal core accepts`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* One value out of Aventara and into a Prisma argument.
|
|
158
|
+
*
|
|
159
|
+
* @throws {PrismaScalarConversionError} for a value the adapter will not
|
|
160
|
+
* represent rather than let a provider misread.
|
|
161
|
+
*/
|
|
162
|
+
export function emitScalarValue(model, field, value, path) {
|
|
163
|
+
return convert("outbound", model, field, value, path);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* One value out of a Prisma result and into the runtime form core's output
|
|
167
|
+
* validator accepts.
|
|
168
|
+
*
|
|
169
|
+
* @throws {PrismaScalarConversionError} for a provider value the adapter cannot
|
|
170
|
+
* honestly read as that scalar.
|
|
171
|
+
*/
|
|
172
|
+
export function receiveScalarValue(model, field, value, path) {
|
|
173
|
+
return convert("inbound", model, field, value, path);
|
|
174
|
+
}
|
|
175
|
+
function convert(direction, model, field, value, path) {
|
|
176
|
+
if (!("scalar" in field.type)) {
|
|
177
|
+
// An enum member is its own name on both sides.
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
const scalar = field.type.scalar;
|
|
181
|
+
if (model.scalars[scalar] === undefined) {
|
|
182
|
+
throw new PrismaScalarConversionError(`${path}: scalar "${scalar}" is not in the discovered registry`);
|
|
183
|
+
}
|
|
184
|
+
if (!isBuiltInScalar(scalar)) {
|
|
185
|
+
throw new PrismaScalarConversionError(`${path}: scalar "${scalar}" has no ${direction} conversion in this adapter`);
|
|
186
|
+
}
|
|
187
|
+
return convertAtoms(SCALAR_BOUNDARY_CROSSINGS[scalar][direction], value, path);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A scalar list's members convert one by one, and the array is rebuilt rather
|
|
191
|
+
* than written into: a translator that mutated its argument would corrupt the
|
|
192
|
+
* second attempt of a retried operation, and a shaper that mutated a provider
|
|
193
|
+
* row would corrupt whatever else holds it.
|
|
194
|
+
*/
|
|
195
|
+
function convertAtoms(crossing, value, path) {
|
|
196
|
+
if (!Array.isArray(value)) {
|
|
197
|
+
return crossing(value, path);
|
|
198
|
+
}
|
|
199
|
+
let changed = false;
|
|
200
|
+
const converted = value.map((member, index) => {
|
|
201
|
+
const result = crossing(member, `${path}[${index}]`);
|
|
202
|
+
changed ||= result !== member;
|
|
203
|
+
return result;
|
|
204
|
+
});
|
|
205
|
+
// The same array, by identity, when no member needed converting: the shaper's
|
|
206
|
+
// copy-on-write depends on being able to tell.
|
|
207
|
+
return changed ? converted : value;
|
|
208
|
+
}
|
|
209
|
+
/** Whether a discovered scalar key is one this adapter has a crossing for. */
|
|
210
|
+
function isBuiltInScalar(key) {
|
|
211
|
+
return Object.hasOwn(SCALAR_BOUNDARY_CROSSINGS, key);
|
|
212
|
+
}
|
|
213
|
+
/** Names what arrived, without quoting a value that may be large or secret. */
|
|
214
|
+
function describeValue(value) {
|
|
215
|
+
if (value === null) {
|
|
216
|
+
return "null";
|
|
217
|
+
}
|
|
218
|
+
return typeof value === "object"
|
|
219
|
+
? `${value.constructor?.name ?? "object"} instance`
|
|
220
|
+
: typeof value;
|
|
221
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AdapterModel } from "@aventara/core";
|
|
2
|
+
/**
|
|
3
|
+
* T-7 — `where` translation: one validated Aventara predicate in, one Prisma
|
|
4
|
+
* `<M>WhereInput` out.
|
|
5
|
+
*
|
|
6
|
+
* # Pure, and that is the property to protect
|
|
7
|
+
*
|
|
8
|
+
* No client, no database, no I/O, no provider name. The only inputs are the
|
|
9
|
+
* discovered model and plain data, which is what makes the whole filter grammar
|
|
10
|
+
* exhaustively testable without an engine (T-7's "single largest testability
|
|
11
|
+
* decision").
|
|
12
|
+
*
|
|
13
|
+
* # One direction, and no Prisma token the framework did not ask for
|
|
14
|
+
*
|
|
15
|
+
* Every emitted key comes from one of the two tables below, both of which are
|
|
16
|
+
* TOTAL over core's own capability unions — a member added to
|
|
17
|
+
* `ScalarFilterOperator` or `RelationFilterCapability` with no row here is a
|
|
18
|
+
* compile error, not a silently dropped predicate. That is C-855's emission half:
|
|
19
|
+
* *"capability derivation advertises an argument only when a named translator
|
|
20
|
+
* step emits it"*, and this module is that step for the filter vocabulary.
|
|
21
|
+
*
|
|
22
|
+
* The inverse matters more. A missing key loses a predicate and shows up as a
|
|
23
|
+
* wrong row count; an INVENTED key silently changes what the query means. So
|
|
24
|
+
* nothing is ever added to the output that the request did not carry: no `equals`
|
|
25
|
+
* wrapper around a bare value, no `is`/`isNot` wrapper around a to-one predicate
|
|
26
|
+
* (§18.5), no `mode` nobody asked for.
|
|
27
|
+
*
|
|
28
|
+
* # Values cross the boundary too (C-834)
|
|
29
|
+
*
|
|
30
|
+
* A predicate is a key AND an operand, and the operand is a value of the field's
|
|
31
|
+
* scalar type. Those values go out through `scalar.conversion.ts`, the one table
|
|
32
|
+
* that knows how each scalar crosses — the same answers `mutation.translator.ts`
|
|
33
|
+
* uses for a write, because it is the same boundary. {@link SCALAR_FILTER_EMISSIONS}
|
|
34
|
+
* records, per operator, *whether* its operand is such a value: `in` carries a
|
|
35
|
+
* list of them, `isEmpty` carries a boolean, `mode` carries a Prisma keyword, and
|
|
36
|
+
* `path`/`string*`/`array*` carry a position inside a JSON document rather than a
|
|
37
|
+
* value of the column. Converting indiscriminately would corrupt the second kind;
|
|
38
|
+
* converting none of them is what let a core `Decimal` reach Prisma, where it was
|
|
39
|
+
* not rejected but **misread** (measured on both engines, 2026-10-02):
|
|
40
|
+
*
|
|
41
|
+
* - `{ sDecimal: { gt: new Decimal("1") } }` → *"PrismaClientValidationError:
|
|
42
|
+
* Argument `_ref` is missing."* — Prisma read the object as a **FieldRef**;
|
|
43
|
+
* - `{ sDecimal: new Decimal("1") }` → **no error and 2 of 2 rows**, the same as
|
|
44
|
+
* no predicate at all, because core's `Decimal` keeps its digits in a private
|
|
45
|
+
* field and `Object.keys` is therefore empty: Prisma read an **empty
|
|
46
|
+
* `DecimalFilter`**. The string `"1"` returned 1 row.
|
|
47
|
+
*
|
|
48
|
+
* The second is why the bare-value branch below follows core's own discriminant —
|
|
49
|
+
* `isPlainRecord`, not "is an object" **[CORE, runtime/scalar-runtime.ts
|
|
50
|
+
* `normalizeScalarFilter`]**. A class instance is a VALUE; reading one as an
|
|
51
|
+
* operator record produced `{}`, and an empty filter constrains nothing.
|
|
52
|
+
*
|
|
53
|
+
* # Refusing is part of honouring
|
|
54
|
+
*
|
|
55
|
+
* The adapter assumes validated input (C-845) and performs no limit checking.
|
|
56
|
+
* It does, however, refuse to EMIT an operator the discovered model does not
|
|
57
|
+
* advertise for that field. That is not a second validation pass for the
|
|
58
|
+
* caller's benefit: it is the mechanism C-855 names — an argument is honoured or
|
|
59
|
+
* the variant is omitted — enforced at the emission site, so an operator that
|
|
60
|
+
* discovery decided not to advertise cannot reach a provider by another route.
|
|
61
|
+
* Core has already rejected such a request with `V1006`; if one ever arrives
|
|
62
|
+
* here, the model and the Contract have diverged and that is a defect, not a
|
|
63
|
+
* query.
|
|
64
|
+
*/
|
|
65
|
+
/** A predicate the model does not license, or a key no vocabulary recognises. */
|
|
66
|
+
export declare class PrismaWhereTranslationError extends Error {
|
|
67
|
+
readonly name = "PrismaWhereTranslationError";
|
|
68
|
+
}
|
|
69
|
+
/** A Prisma `<M>WhereInput`, as plain data. */
|
|
70
|
+
export type PrismaWhereInput = Readonly<Record<string, unknown>>;
|
|
71
|
+
export type WhereTranslation = {
|
|
72
|
+
/** The discovered model: field kinds, cardinalities, targets and capabilities. */
|
|
73
|
+
readonly model: AdapterModel;
|
|
74
|
+
/** The Aventara Resource key the predicate is written against. */
|
|
75
|
+
readonly resource: string;
|
|
76
|
+
/** A validated Aventara `WhereInput`, as plain data. */
|
|
77
|
+
readonly where: unknown;
|
|
78
|
+
};
|
|
79
|
+
export declare function translateWhere(input: WhereTranslation): PrismaWhereInput;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { emitScalarValue } from "./scalar.conversion.js";
|
|
2
|
+
/**
|
|
3
|
+
* T-7 — `where` translation: one validated Aventara predicate in, one Prisma
|
|
4
|
+
* `<M>WhereInput` out.
|
|
5
|
+
*
|
|
6
|
+
* # Pure, and that is the property to protect
|
|
7
|
+
*
|
|
8
|
+
* No client, no database, no I/O, no provider name. The only inputs are the
|
|
9
|
+
* discovered model and plain data, which is what makes the whole filter grammar
|
|
10
|
+
* exhaustively testable without an engine (T-7's "single largest testability
|
|
11
|
+
* decision").
|
|
12
|
+
*
|
|
13
|
+
* # One direction, and no Prisma token the framework did not ask for
|
|
14
|
+
*
|
|
15
|
+
* Every emitted key comes from one of the two tables below, both of which are
|
|
16
|
+
* TOTAL over core's own capability unions — a member added to
|
|
17
|
+
* `ScalarFilterOperator` or `RelationFilterCapability` with no row here is a
|
|
18
|
+
* compile error, not a silently dropped predicate. That is C-855's emission half:
|
|
19
|
+
* *"capability derivation advertises an argument only when a named translator
|
|
20
|
+
* step emits it"*, and this module is that step for the filter vocabulary.
|
|
21
|
+
*
|
|
22
|
+
* The inverse matters more. A missing key loses a predicate and shows up as a
|
|
23
|
+
* wrong row count; an INVENTED key silently changes what the query means. So
|
|
24
|
+
* nothing is ever added to the output that the request did not carry: no `equals`
|
|
25
|
+
* wrapper around a bare value, no `is`/`isNot` wrapper around a to-one predicate
|
|
26
|
+
* (§18.5), no `mode` nobody asked for.
|
|
27
|
+
*
|
|
28
|
+
* # Values cross the boundary too (C-834)
|
|
29
|
+
*
|
|
30
|
+
* A predicate is a key AND an operand, and the operand is a value of the field's
|
|
31
|
+
* scalar type. Those values go out through `scalar.conversion.ts`, the one table
|
|
32
|
+
* that knows how each scalar crosses — the same answers `mutation.translator.ts`
|
|
33
|
+
* uses for a write, because it is the same boundary. {@link SCALAR_FILTER_EMISSIONS}
|
|
34
|
+
* records, per operator, *whether* its operand is such a value: `in` carries a
|
|
35
|
+
* list of them, `isEmpty` carries a boolean, `mode` carries a Prisma keyword, and
|
|
36
|
+
* `path`/`string*`/`array*` carry a position inside a JSON document rather than a
|
|
37
|
+
* value of the column. Converting indiscriminately would corrupt the second kind;
|
|
38
|
+
* converting none of them is what let a core `Decimal` reach Prisma, where it was
|
|
39
|
+
* not rejected but **misread** (measured on both engines, 2026-10-02):
|
|
40
|
+
*
|
|
41
|
+
* - `{ sDecimal: { gt: new Decimal("1") } }` → *"PrismaClientValidationError:
|
|
42
|
+
* Argument `_ref` is missing."* — Prisma read the object as a **FieldRef**;
|
|
43
|
+
* - `{ sDecimal: new Decimal("1") }` → **no error and 2 of 2 rows**, the same as
|
|
44
|
+
* no predicate at all, because core's `Decimal` keeps its digits in a private
|
|
45
|
+
* field and `Object.keys` is therefore empty: Prisma read an **empty
|
|
46
|
+
* `DecimalFilter`**. The string `"1"` returned 1 row.
|
|
47
|
+
*
|
|
48
|
+
* The second is why the bare-value branch below follows core's own discriminant —
|
|
49
|
+
* `isPlainRecord`, not "is an object" **[CORE, runtime/scalar-runtime.ts
|
|
50
|
+
* `normalizeScalarFilter`]**. A class instance is a VALUE; reading one as an
|
|
51
|
+
* operator record produced `{}`, and an empty filter constrains nothing.
|
|
52
|
+
*
|
|
53
|
+
* # Refusing is part of honouring
|
|
54
|
+
*
|
|
55
|
+
* The adapter assumes validated input (C-845) and performs no limit checking.
|
|
56
|
+
* It does, however, refuse to EMIT an operator the discovered model does not
|
|
57
|
+
* advertise for that field. That is not a second validation pass for the
|
|
58
|
+
* caller's benefit: it is the mechanism C-855 names — an argument is honoured or
|
|
59
|
+
* the variant is omitted — enforced at the emission site, so an operator that
|
|
60
|
+
* discovery decided not to advertise cannot reach a provider by another route.
|
|
61
|
+
* Core has already rejected such a request with `V1006`; if one ever arrives
|
|
62
|
+
* here, the model and the Contract have diverged and that is a defect, not a
|
|
63
|
+
* query.
|
|
64
|
+
*/
|
|
65
|
+
/** A predicate the model does not license, or a key no vocabulary recognises. */
|
|
66
|
+
export class PrismaWhereTranslationError extends Error {
|
|
67
|
+
name = "PrismaWhereTranslationError";
|
|
68
|
+
}
|
|
69
|
+
const SCALAR_FILTER_EMISSIONS = {
|
|
70
|
+
equals: { token: "equals", operand: "value" },
|
|
71
|
+
not: { token: "not", operand: "value-or-filter" },
|
|
72
|
+
in: { token: "in", operand: "value-list" },
|
|
73
|
+
notIn: { token: "notIn", operand: "value-list" },
|
|
74
|
+
lt: { token: "lt", operand: "value" },
|
|
75
|
+
lte: { token: "lte", operand: "value" },
|
|
76
|
+
gt: { token: "gt", operand: "value" },
|
|
77
|
+
gte: { token: "gte", operand: "value" },
|
|
78
|
+
contains: { token: "contains", operand: "value" },
|
|
79
|
+
startsWith: { token: "startsWith", operand: "value" },
|
|
80
|
+
endsWith: { token: "endsWith", operand: "value" },
|
|
81
|
+
mode: { token: "mode", operand: "opaque" },
|
|
82
|
+
has: { token: "has", operand: "value" },
|
|
83
|
+
hasEvery: { token: "hasEvery", operand: "value-list" },
|
|
84
|
+
hasSome: { token: "hasSome", operand: "value-list" },
|
|
85
|
+
isEmpty: { token: "isEmpty", operand: "opaque" },
|
|
86
|
+
path: { token: "path", operand: "opaque" },
|
|
87
|
+
stringContains: { token: "string_contains", operand: "opaque" },
|
|
88
|
+
stringStartsWith: { token: "string_starts_with", operand: "opaque" },
|
|
89
|
+
stringEndsWith: { token: "string_ends_with", operand: "opaque" },
|
|
90
|
+
arrayContains: { token: "array_contains", operand: "opaque" },
|
|
91
|
+
arrayStartsWith: { token: "array_starts_with", operand: "opaque" },
|
|
92
|
+
arrayEndsWith: { token: "array_ends_with", operand: "opaque" },
|
|
93
|
+
};
|
|
94
|
+
const RELATION_FILTER_EMISSIONS = {
|
|
95
|
+
nested: { kind: "edge-argument" },
|
|
96
|
+
match: { kind: "unwrapped" },
|
|
97
|
+
null: { kind: "null" },
|
|
98
|
+
some: { kind: "key", token: "some" },
|
|
99
|
+
every: { kind: "key", token: "every" },
|
|
100
|
+
none: { kind: "key", token: "none" },
|
|
101
|
+
};
|
|
102
|
+
/** Core's boolean nodes, whose spelling Prisma shares. */
|
|
103
|
+
const BOOLEAN_NODES = { AND: "AND", OR: "OR", NOT: "NOT" };
|
|
104
|
+
export function translateWhere(input) {
|
|
105
|
+
return translateNode(input.model, input.resource, input.where, "where");
|
|
106
|
+
}
|
|
107
|
+
function translateNode(model, resourceKey, node, path) {
|
|
108
|
+
const resource = model.resources[resourceKey];
|
|
109
|
+
if (resource === undefined) {
|
|
110
|
+
throw new PrismaWhereTranslationError(`${path}: Resource "${resourceKey}" is not in the discovered model`);
|
|
111
|
+
}
|
|
112
|
+
if (!isRecord(node)) {
|
|
113
|
+
throw new PrismaWhereTranslationError(`${path} must be an object`);
|
|
114
|
+
}
|
|
115
|
+
const emitted = {};
|
|
116
|
+
for (const [key, value] of Object.entries(node)) {
|
|
117
|
+
if (key === BOOLEAN_NODES.AND || key === BOOLEAN_NODES.OR) {
|
|
118
|
+
if (!Array.isArray(value)) {
|
|
119
|
+
throw new PrismaWhereTranslationError(`${path}.${key} must be an array of predicates`);
|
|
120
|
+
}
|
|
121
|
+
emitted[key] = value.map((member, index) => translateNode(model, resourceKey, member, `${path}.${key}[${index}]`));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (key === BOOLEAN_NODES.NOT) {
|
|
125
|
+
// Aventara's `NOT` is ONE node. Prisma admits an array too; emitting one
|
|
126
|
+
// for a single node would be a shape the request never carried.
|
|
127
|
+
emitted[key] = translateNode(model, resourceKey, value, `${path}.${key}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const field = resource.fields[key];
|
|
131
|
+
if (field === undefined) {
|
|
132
|
+
throw new PrismaWhereTranslationError(`${path}.${key} is not a field of Resource "${resourceKey}"`);
|
|
133
|
+
}
|
|
134
|
+
emitted[key] =
|
|
135
|
+
field.kind === "scalar"
|
|
136
|
+
? translateScalarPredicate(model, field, value, `${path}.${key}`)
|
|
137
|
+
: translateRelationPredicate(model, field, value, `${path}.${key}`);
|
|
138
|
+
}
|
|
139
|
+
return emitted;
|
|
140
|
+
}
|
|
141
|
+
function translateScalarPredicate(model, field, value, path) {
|
|
142
|
+
const advertised = field.capabilities.filter;
|
|
143
|
+
if (advertised === undefined || advertised.length === 0) {
|
|
144
|
+
throw new PrismaWhereTranslationError(`${path}: filtering is not available on this field`);
|
|
145
|
+
}
|
|
146
|
+
// Core's own rule: a value that is not a PLAIN record — `null` and a class
|
|
147
|
+
// instance included — IS equality, and Prisma spells that the same way.
|
|
148
|
+
// Wrapping it in `{ equals: … }` would emit a token the request never carried.
|
|
149
|
+
if (!isPlainRecord(value)) {
|
|
150
|
+
requireOperator(advertised, "equals", path);
|
|
151
|
+
return emitScalarValue(model, field, value, path);
|
|
152
|
+
}
|
|
153
|
+
const operand = {};
|
|
154
|
+
for (const [operator, argument] of Object.entries(value)) {
|
|
155
|
+
if (!isScalarFilterOperator(operator)) {
|
|
156
|
+
throw new PrismaWhereTranslationError(`${path}.${operator} is not an Aventara filter operator`);
|
|
157
|
+
}
|
|
158
|
+
requireOperator(advertised, operator, path);
|
|
159
|
+
const emission = SCALAR_FILTER_EMISSIONS[operator];
|
|
160
|
+
operand[emission.token] = emitOperand(model, field, emission.operand, argument, `${path}.${operator}`);
|
|
161
|
+
}
|
|
162
|
+
return operand;
|
|
163
|
+
}
|
|
164
|
+
/** One operator's operand, converted as the operator's shape says it must be. */
|
|
165
|
+
function emitOperand(model, field, shape, argument, path) {
|
|
166
|
+
switch (shape) {
|
|
167
|
+
case "opaque":
|
|
168
|
+
return argument;
|
|
169
|
+
case "value":
|
|
170
|
+
return emitScalarValue(model, field, argument, path);
|
|
171
|
+
case "value-list":
|
|
172
|
+
if (!Array.isArray(argument)) {
|
|
173
|
+
throw new PrismaWhereTranslationError(`${path} must be an array of values`);
|
|
174
|
+
}
|
|
175
|
+
// `emitScalarValue` converts an array elementwise, which is what a list
|
|
176
|
+
// operand is — and what a LIST column's single value would be too, so the
|
|
177
|
+
// array is handed over whole rather than mapped here.
|
|
178
|
+
return emitScalarValue(model, field, argument, path);
|
|
179
|
+
case "value-or-filter":
|
|
180
|
+
// Core recurses on `not` exactly when its operand is a plain record, so
|
|
181
|
+
// the adapter does too; anything else is a value.
|
|
182
|
+
return isPlainRecord(argument)
|
|
183
|
+
? translateScalarPredicate(model, field, argument, path)
|
|
184
|
+
: emitScalarValue(model, field, argument, path);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function translateRelationPredicate(model, field, value, path) {
|
|
188
|
+
const advertised = field.capabilities.filter ?? [];
|
|
189
|
+
if (value === null) {
|
|
190
|
+
requireRelationFilter(advertised, "null", path);
|
|
191
|
+
assertEmission("null", path);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (field.cardinality === "one") {
|
|
195
|
+
requireRelationFilter(advertised, "match", path);
|
|
196
|
+
assertEmission("match", path);
|
|
197
|
+
// §18.5: the nested predicate, unwrapped. No `is`.
|
|
198
|
+
return translateNode(model, field.target, value, path);
|
|
199
|
+
}
|
|
200
|
+
if (!isRecord(value)) {
|
|
201
|
+
throw new PrismaWhereTranslationError(`${path} must be an object of to-many predicates`);
|
|
202
|
+
}
|
|
203
|
+
const operand = {};
|
|
204
|
+
for (const [capability, argument] of Object.entries(value)) {
|
|
205
|
+
if (!isRelationFilterCapability(capability)) {
|
|
206
|
+
throw new PrismaWhereTranslationError(`${path}.${capability} is not an Aventara relation filter`);
|
|
207
|
+
}
|
|
208
|
+
const emission = RELATION_FILTER_EMISSIONS[capability];
|
|
209
|
+
if (emission.kind !== "key") {
|
|
210
|
+
throw new PrismaWhereTranslationError(`${path}.${capability} is not a to-many predicate`);
|
|
211
|
+
}
|
|
212
|
+
requireRelationFilter(advertised, capability, path);
|
|
213
|
+
operand[emission.token] = translateNode(model, field.target, argument, `${path}.${capability}`);
|
|
214
|
+
}
|
|
215
|
+
return operand;
|
|
216
|
+
}
|
|
217
|
+
/** The two capabilities whose emission is a shape rather than a key. */
|
|
218
|
+
function assertEmission(capability, path) {
|
|
219
|
+
const emission = RELATION_FILTER_EMISSIONS[capability];
|
|
220
|
+
if (emission.kind === "key" || emission.kind === "edge-argument") {
|
|
221
|
+
throw new PrismaWhereTranslationError(`${path}: ${capability} has no inline emission`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function requireOperator(advertised, operator, path) {
|
|
225
|
+
if (!advertised.includes(operator)) {
|
|
226
|
+
throw new PrismaWhereTranslationError(`${path}: "${operator}" is not advertised on this field, so the adapter will not emit it`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function requireRelationFilter(advertised, capability, path) {
|
|
230
|
+
if (!advertised.includes(capability)) {
|
|
231
|
+
throw new PrismaWhereTranslationError(`${path}: "${capability}" is not advertised on this edge, so the adapter will not emit it`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function isScalarFilterOperator(key) {
|
|
235
|
+
return Object.hasOwn(SCALAR_FILTER_EMISSIONS, key);
|
|
236
|
+
}
|
|
237
|
+
function isRelationFilterCapability(key) {
|
|
238
|
+
return Object.hasOwn(RELATION_FILTER_EMISSIONS, key);
|
|
239
|
+
}
|
|
240
|
+
function isRecord(value) {
|
|
241
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Core's discriminant between a filter record and a VALUE, mirrored
|
|
245
|
+
* **[CORE, runtime/scalar-runtime.ts `isPlainRecord`]**: a class instance — a
|
|
246
|
+
* core `Decimal`, a `Date`, a `Uint8Array` — is a value, not a bag of operators.
|
|
247
|
+
*/
|
|
248
|
+
function isPlainRecord(value) {
|
|
249
|
+
if (!isRecord(value)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const prototype = Object.getPrototypeOf(value);
|
|
253
|
+
return prototype === Object.prototype || prototype === null;
|
|
254
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aventara/prisma7-adapter",
|
|
3
|
+
"version": "0.1.0-pilot.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"description": "The Prisma 7 adapter for Aventara.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": "^22.18.0 || >=24.2.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./generate": {
|
|
19
|
+
"types": "./dist/generate/index.d.ts",
|
|
20
|
+
"import": "./dist/generate/index.js",
|
|
21
|
+
"default": "./dist/generate/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"aventara-prisma7-generate": "./dist/generate/discovery.bin.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"LICENSE-ADDITIONAL-PERMISSION.md"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@aventara/core": "0.1.0-pilot.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@prisma/adapter-better-sqlite3": "^7.10.0",
|
|
36
|
+
"@prisma/adapter-pg": "^7.10.0",
|
|
37
|
+
"@prisma/client": "^7.10.0",
|
|
38
|
+
"prisma": "^7.10.0",
|
|
39
|
+
"typescript": ">=5.5.0 <7"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@prisma/adapter-better-sqlite3": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"@prisma/adapter-pg": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"prisma": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"typescript": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"aventara": {
|
|
56
|
+
"adapter": {
|
|
57
|
+
"orm": "prisma",
|
|
58
|
+
"ormLabel": "Prisma",
|
|
59
|
+
"ormPackage": "@prisma/client",
|
|
60
|
+
"providers": [
|
|
61
|
+
"sqlite",
|
|
62
|
+
"postgresql"
|
|
63
|
+
],
|
|
64
|
+
"drivers": {
|
|
65
|
+
"sqlite": "@prisma/adapter-better-sqlite3",
|
|
66
|
+
"postgresql": "@prisma/adapter-pg"
|
|
67
|
+
},
|
|
68
|
+
"generators": [
|
|
69
|
+
"prisma-client"
|
|
70
|
+
],
|
|
71
|
+
"familyPackages": [
|
|
72
|
+
"prisma",
|
|
73
|
+
"@prisma/client",
|
|
74
|
+
"@prisma/prisma7",
|
|
75
|
+
"@prisma/orm-*"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"publishConfig": {
|
|
80
|
+
"access": "public"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@prisma/adapter-better-sqlite3": "7.10.0",
|
|
84
|
+
"@prisma/adapter-pg": "7.10.0",
|
|
85
|
+
"@prisma/client": "7.10.0",
|
|
86
|
+
"@prisma/prisma7": "7.10.0",
|
|
87
|
+
"@types/node": "24.10.1",
|
|
88
|
+
"prisma": "7.10.0",
|
|
89
|
+
"typescript": "^5.9.2",
|
|
90
|
+
"typescript-6": "npm:typescript@6.0.3"
|
|
91
|
+
},
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "node node_modules/typescript/bin/tsc -p tsconfig.json",
|
|
94
|
+
"typecheck": "node node_modules/typescript/bin/tsc -p tsconfig.typecheck.json --noEmit",
|
|
95
|
+
"generate": "node node_modules/typescript/bin/tsc -p tsconfig.generate.json && node .generate/generate/discovery.generate.js && prisma generate --schema src/discovery/__fixtures__/ignore.prisma",
|
|
96
|
+
"db:push": "prisma db push --config prisma.config.sqlite.ts && sh -c 'if [ -n \"$AVENTARA_TEST_POSTGRES_URL\" ]; then prisma db push --config prisma.config.postgres.ts; else echo \"AVENTARA_TEST_POSTGRES_URL is unset: skipping the PostgreSQL schema push\"; fi'",
|
|
97
|
+
"test": "pnpm run generate && pnpm run db:push && vitest run --typecheck --typecheck.checker node_modules/typescript/bin/tsc --no-file-parallelism --config ../../vitest.config.mts --root .",
|
|
98
|
+
"test:sqlite": "pnpm run generate && prisma db push --config prisma.config.sqlite.ts && vitest run --config ../../vitest.config.mts --root . src/environment.spec.ts",
|
|
99
|
+
"test:postgres": "pnpm run generate && prisma db push --config prisma.config.postgres.ts && vitest run --config ../../vitest.config.mts --root . src/environment.spec.ts",
|
|
100
|
+
"lint": "biome lint src"
|
|
101
|
+
}
|
|
102
|
+
}
|