@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,86 @@
|
|
|
1
|
+
import { type AdapterModel } from "@aventara/core";
|
|
2
|
+
import type { PrismaIdentifierAddress } from "../discovery/discovery-artifact.interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* T-7 — `orderBy` and pagination translation. Pure functions over plain data:
|
|
5
|
+
* no client, no database, no provider name.
|
|
6
|
+
*
|
|
7
|
+
* # C-831 is a one-directional rename, and this module is the only place it happens
|
|
8
|
+
*
|
|
9
|
+
* The public names are `limit`, `offset` and `cursor`. `take` and `skip` are
|
|
10
|
+
* Prisma's and appear nowhere in this adapter's public types — they are produced
|
|
11
|
+
* here and nowhere else. The tables below are TOTAL over core's own unions, so a
|
|
12
|
+
* pagination capability or a reducer added to core with no emission site is a
|
|
13
|
+
* compile error (C-855).
|
|
14
|
+
*
|
|
15
|
+
* # An absent argument and an `undefined` one are different arguments
|
|
16
|
+
*
|
|
17
|
+
* `take: undefined` is a present key with no value, and `"take" in args` is what
|
|
18
|
+
* a provider sees. Every key below is added only when its Aventara argument is
|
|
19
|
+
* present, which is also why `0` is carried through rather than tested for
|
|
20
|
+
* truthiness.
|
|
21
|
+
*/
|
|
22
|
+
/** An order or cursor the discovered model does not license. */
|
|
23
|
+
export declare class PrismaOrderTranslationError extends Error {
|
|
24
|
+
readonly name = "PrismaOrderTranslationError";
|
|
25
|
+
}
|
|
26
|
+
/** A Prisma `<M>OrderByWithRelationInput`, as plain data. */
|
|
27
|
+
export type PrismaOrderByInput = Readonly<Record<string, unknown>>;
|
|
28
|
+
/** `take` / `skip` / `cursor`, exactly as many of them as were asked for. */
|
|
29
|
+
export type PrismaPaginationArguments = Readonly<Record<string, unknown>>;
|
|
30
|
+
export type PaginationTranslation = {
|
|
31
|
+
readonly limit?: number;
|
|
32
|
+
readonly offset?: number;
|
|
33
|
+
/**
|
|
34
|
+
* The Aventara cursor: the active ordered scalar fields and their values.
|
|
35
|
+
*
|
|
36
|
+
* **Only the identifier subset is emitted, and that is faithful translation
|
|
37
|
+
* rather than a silent drop (C-855).** Settled 2026-10-02; do not re-open
|
|
38
|
+
* without reading `validateCursor` first.
|
|
39
|
+
*
|
|
40
|
+
* Core's `CursorInput` type is `Partial<Record<ScalarFieldKey, unknown>>`,
|
|
41
|
+
* which reads as permissive, but `operation-validator-core.ts`'s
|
|
42
|
+
* `validateCursor` pins it exactly: the cursor's keys must **equal** the
|
|
43
|
+
* active ordered scalar fields, and `orderBy` must include a complete
|
|
44
|
+
* Resource identifier as a tie-breaker. So the non-identifier keys are not
|
|
45
|
+
* optional context a caller happened to send — core requires every one of
|
|
46
|
+
* them, and has already verified they match `orderBy`.
|
|
47
|
+
*
|
|
48
|
+
* Prisma's `cursor` is a `<M>WhereUniqueInput`: it locates a *row*, and the
|
|
49
|
+
* ordering comes from `orderBy`, which this module translates alongside. The
|
|
50
|
+
* identifier therefore determines the position completely, and the remaining
|
|
51
|
+
* ordered keys are already expressed in the emitted `orderBy`. Nothing is
|
|
52
|
+
* lost — which is why this is translation and not the MikroORM failure mode
|
|
53
|
+
* C-855 names.
|
|
54
|
+
*
|
|
55
|
+
* Two things follow, both load-bearing. Passing the extra keys through would
|
|
56
|
+
* make them equality members of a where-unique, *narrowing* the lookup: a key
|
|
57
|
+
* whose value no longer matches the stored row would resolve the cursor to
|
|
58
|
+
* nothing and return an empty page instead of a correct one. And narrowing
|
|
59
|
+
* core's `CursorInput` to the identifier — considered and rejected — would
|
|
60
|
+
* break the determinism contract `validateCursor` enforces, since core needs
|
|
61
|
+
* the full ordered set to know the position is unambiguous.
|
|
62
|
+
*
|
|
63
|
+
* The type is loose because the Contract is passive data; the precision lives
|
|
64
|
+
* in the validator. That is the pattern throughout `operation-arguments.ts`,
|
|
65
|
+
* and reading the type without its validator is how this was briefly
|
|
66
|
+
* mis-analysed.
|
|
67
|
+
*/
|
|
68
|
+
readonly cursor?: Readonly<Record<string, unknown>>;
|
|
69
|
+
/**
|
|
70
|
+
* The Resource's Prisma addressing, from the generate-time translation table.
|
|
71
|
+
*
|
|
72
|
+
* Required rather than optional because Prisma's `cursor` is a
|
|
73
|
+
* `<M>WhereUniqueInput`, not a free scalar map: a compound identifier is keyed
|
|
74
|
+
* by its members joined in attribute order and WRAPPED, and that key is
|
|
75
|
+
* recorded at generate time and matched here, never re-derived.
|
|
76
|
+
*/
|
|
77
|
+
readonly identifiers: readonly PrismaIdentifierAddress[];
|
|
78
|
+
};
|
|
79
|
+
export type OrderByTranslation = {
|
|
80
|
+
readonly model: AdapterModel;
|
|
81
|
+
readonly resource: string;
|
|
82
|
+
/** A validated Aventara `OrderByInput`: one entry, or a precedence list. */
|
|
83
|
+
readonly orderBy: unknown;
|
|
84
|
+
};
|
|
85
|
+
export declare function translatePagination(input: PaginationTranslation): PrismaPaginationArguments;
|
|
86
|
+
export declare function translateOrderBy(input: OrderByTranslation): PrismaOrderByInput | readonly PrismaOrderByInput[];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { advertisedReducers, } from "@aventara/core";
|
|
2
|
+
import { addressIdentifier } from "./identifier.addressing.js";
|
|
3
|
+
/**
|
|
4
|
+
* T-7 — `orderBy` and pagination translation. Pure functions over plain data:
|
|
5
|
+
* no client, no database, no provider name.
|
|
6
|
+
*
|
|
7
|
+
* # C-831 is a one-directional rename, and this module is the only place it happens
|
|
8
|
+
*
|
|
9
|
+
* The public names are `limit`, `offset` and `cursor`. `take` and `skip` are
|
|
10
|
+
* Prisma's and appear nowhere in this adapter's public types — they are produced
|
|
11
|
+
* here and nowhere else. The tables below are TOTAL over core's own unions, so a
|
|
12
|
+
* pagination capability or a reducer added to core with no emission site is a
|
|
13
|
+
* compile error (C-855).
|
|
14
|
+
*
|
|
15
|
+
* # An absent argument and an `undefined` one are different arguments
|
|
16
|
+
*
|
|
17
|
+
* `take: undefined` is a present key with no value, and `"take" in args` is what
|
|
18
|
+
* a provider sees. Every key below is added only when its Aventara argument is
|
|
19
|
+
* present, which is also why `0` is carried through rather than tested for
|
|
20
|
+
* truthiness.
|
|
21
|
+
*/
|
|
22
|
+
/** An order or cursor the discovered model does not license. */
|
|
23
|
+
export class PrismaOrderTranslationError extends Error {
|
|
24
|
+
name = "PrismaOrderTranslationError";
|
|
25
|
+
}
|
|
26
|
+
const PAGINATION_TOKENS = {
|
|
27
|
+
limit: "take",
|
|
28
|
+
offset: "skip",
|
|
29
|
+
cursor: "cursor",
|
|
30
|
+
};
|
|
31
|
+
/** The reducer vocabulary, on the ORDER side (`<M>OrderByRelationAggregateInput`). */
|
|
32
|
+
const REDUCER_TOKENS = {
|
|
33
|
+
$count: "_count",
|
|
34
|
+
};
|
|
35
|
+
export function translatePagination(input) {
|
|
36
|
+
const emitted = {};
|
|
37
|
+
if (input.limit !== undefined) {
|
|
38
|
+
emitted[PAGINATION_TOKENS.limit] = input.limit;
|
|
39
|
+
}
|
|
40
|
+
if (input.offset !== undefined) {
|
|
41
|
+
emitted[PAGINATION_TOKENS.offset] = input.offset;
|
|
42
|
+
}
|
|
43
|
+
if (input.cursor !== undefined) {
|
|
44
|
+
emitted[PAGINATION_TOKENS.cursor] = addressCursor(input.cursor, input.identifiers);
|
|
45
|
+
}
|
|
46
|
+
return emitted;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The Aventara cursor carries the active ordered scalar fields; core guarantees
|
|
50
|
+
* that they include a complete identifier (`V1009` — *"orderBy must include a
|
|
51
|
+
* complete Resource identifier as an explicit tie-breaker"*). Prisma's `cursor`
|
|
52
|
+
* is an ADDRESS, so the identifier is what is emitted: the remaining ordered
|
|
53
|
+
* fields are the ordering, which `orderBy` already carries, and adding them as
|
|
54
|
+
* equality members of a where-unique would narrow the address rather than
|
|
55
|
+
* sharpen it.
|
|
56
|
+
*/
|
|
57
|
+
function addressCursor(cursor, identifiers) {
|
|
58
|
+
// `"subset"`: the cursor carries every active ordered scalar field, of which
|
|
59
|
+
// the identifier is one part (`validateCursor`), so extra keys are expected.
|
|
60
|
+
// The values are already Prisma-shaped, so the emitter is the identity.
|
|
61
|
+
const addressed = addressIdentifier({
|
|
62
|
+
addresses: identifiers,
|
|
63
|
+
values: cursor,
|
|
64
|
+
match: "subset",
|
|
65
|
+
emit: (_member, value) => value,
|
|
66
|
+
});
|
|
67
|
+
if (addressed === undefined) {
|
|
68
|
+
throw new PrismaOrderTranslationError(`cursor: no identifier is complete in { ${Object.keys(cursor).join(", ")} }, so it addresses no row`);
|
|
69
|
+
}
|
|
70
|
+
return addressed.where;
|
|
71
|
+
}
|
|
72
|
+
export function translateOrderBy(input) {
|
|
73
|
+
if (Array.isArray(input.orderBy)) {
|
|
74
|
+
// The precedence list keeps its shape: Prisma reads an array as precedence
|
|
75
|
+
// too, and collapsing it would reorder the request.
|
|
76
|
+
return input.orderBy.map((entry, index) => translateEntry(input.model, input.resource, entry, `orderBy[${index}]`));
|
|
77
|
+
}
|
|
78
|
+
return translateEntry(input.model, input.resource, input.orderBy, "orderBy");
|
|
79
|
+
}
|
|
80
|
+
function translateEntry(model, resourceKey, entry, path) {
|
|
81
|
+
const resource = model.resources[resourceKey];
|
|
82
|
+
if (resource === undefined) {
|
|
83
|
+
throw new PrismaOrderTranslationError(`${path}: Resource "${resourceKey}" is not in the discovered model`);
|
|
84
|
+
}
|
|
85
|
+
if (!isRecord(entry)) {
|
|
86
|
+
throw new PrismaOrderTranslationError(`${path} must be an object`);
|
|
87
|
+
}
|
|
88
|
+
const keys = Object.keys(entry);
|
|
89
|
+
// Core's rule, mirrored: one precedence entry is one field, because the order
|
|
90
|
+
// of keys in an object is not a precedence a caller can rely on.
|
|
91
|
+
if (keys.length !== 1) {
|
|
92
|
+
throw new PrismaOrderTranslationError(`${path} must contain exactly one field`);
|
|
93
|
+
}
|
|
94
|
+
const fieldName = keys[0];
|
|
95
|
+
const field = resource.fields[fieldName];
|
|
96
|
+
if (field === undefined) {
|
|
97
|
+
throw new PrismaOrderTranslationError(`${path}.${fieldName} is not a field of Resource "${resourceKey}"`);
|
|
98
|
+
}
|
|
99
|
+
if (field.capabilities.order !== true) {
|
|
100
|
+
throw new PrismaOrderTranslationError(`${path}.${fieldName}: ordering is not advertised on this field`);
|
|
101
|
+
}
|
|
102
|
+
const value = entry[fieldName];
|
|
103
|
+
return {
|
|
104
|
+
[fieldName]: field.kind === "scalar"
|
|
105
|
+
? translateScalarOrder(field, value, `${path}.${fieldName}`)
|
|
106
|
+
: translateRelationOrder(model, field, value, `${path}.${fieldName}`),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function translateScalarOrder(field, value, path) {
|
|
110
|
+
if (value === "asc" || value === "desc") {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
if (!isRecord(value)) {
|
|
114
|
+
throw new PrismaOrderTranslationError(`${path} must be "asc", "desc" or an explicit null order`);
|
|
115
|
+
}
|
|
116
|
+
// C-836: the extended form is `SortOrderInput`, and the deriver advertises
|
|
117
|
+
// `orderNulls` only where the generated types admit it for THIS field. Prisma's
|
|
118
|
+
// spelling is the same `{ sort, nulls }`, so the translation is the gate.
|
|
119
|
+
if (field.capabilities.orderNulls !== true) {
|
|
120
|
+
throw new PrismaOrderTranslationError(`${path}: explicit null ordering is not advertised on this field`);
|
|
121
|
+
}
|
|
122
|
+
const { sort, nulls } = value;
|
|
123
|
+
if (Object.keys(value).length !== 2 ||
|
|
124
|
+
(sort !== "asc" && sort !== "desc") ||
|
|
125
|
+
(nulls !== "first" && nulls !== "last")) {
|
|
126
|
+
throw new PrismaOrderTranslationError(`${path}: an explicit null order is { sort, nulls }`);
|
|
127
|
+
}
|
|
128
|
+
return { sort, nulls };
|
|
129
|
+
}
|
|
130
|
+
function translateRelationOrder(model, field, value, path) {
|
|
131
|
+
if (!isRecord(value)) {
|
|
132
|
+
throw new PrismaOrderTranslationError(`${path} must be an object`);
|
|
133
|
+
}
|
|
134
|
+
if (field.cardinality === "one") {
|
|
135
|
+
// A to-one edge orders by the target's own fields.
|
|
136
|
+
return translateEntry(model, field.target, value, path);
|
|
137
|
+
}
|
|
138
|
+
// A to-many edge orders by its advertised reducer and by nothing else:
|
|
139
|
+
// `<M>OrderByRelationAggregateInput` carries `_count` alone.
|
|
140
|
+
const keys = Object.keys(value);
|
|
141
|
+
const [reducer] = keys;
|
|
142
|
+
if (keys.length !== 1 || reducer === undefined || !isReducer(reducer)) {
|
|
143
|
+
throw new PrismaOrderTranslationError(`${path}: a to-many edge orders only by an advertised reducer`);
|
|
144
|
+
}
|
|
145
|
+
if (!(advertisedReducers(field.capabilities.reducers) ?? []).includes(reducer)) {
|
|
146
|
+
throw new PrismaOrderTranslationError(`${path}.${reducer} is not advertised on this edge`);
|
|
147
|
+
}
|
|
148
|
+
const direction = value[reducer];
|
|
149
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
150
|
+
throw new PrismaOrderTranslationError(`${path}.${reducer} must be "asc" or "desc"`);
|
|
151
|
+
}
|
|
152
|
+
return { [REDUCER_TOKENS[reducer]]: direction };
|
|
153
|
+
}
|
|
154
|
+
function isReducer(key) {
|
|
155
|
+
return Object.hasOwn(REDUCER_TOKENS, key);
|
|
156
|
+
}
|
|
157
|
+
function isRecord(value) {
|
|
158
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
159
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { RelationReducer } from "@aventara/core";
|
|
2
|
+
/**
|
|
3
|
+
* Task 8 — the projection half of T-7: Aventara `select`/`include`/`$count` →
|
|
4
|
+
* Prisma `select`/`include`/`_count` (C-832, C-833).
|
|
5
|
+
*
|
|
6
|
+
* **What arrives here is core's plan, not a caller's request.** By the time an
|
|
7
|
+
* `AdapterOperation` reaches the adapter, `planReadOperation` has resolved
|
|
8
|
+
* `include` into `select`, expanded every relation it could resolve into object
|
|
9
|
+
* form, and dropped VIRTUAL scalars from the adapter's half while keeping them
|
|
10
|
+
* in the projection plan it validates against
|
|
11
|
+
* **[CORE, runtime/computed-read-runtime.ts]**. So this module reads one shape:
|
|
12
|
+
* `select` as an array whose entries are field-name strings, the reducer string
|
|
13
|
+
* `"$count"`, or single-key objects carrying a relation's own arguments beside a
|
|
14
|
+
* nested `select`.
|
|
15
|
+
*
|
|
16
|
+
* **Three translations are worth stating because they are not obvious:**
|
|
17
|
+
*
|
|
18
|
+
* 1. *The default projection emits nothing at all.* C-833's claim is that
|
|
19
|
+
* Prisma's default read projection — every scalar, no relations — IS the
|
|
20
|
+
* framework default (§10.1), so the honest translation of "no `select`" is
|
|
21
|
+
* an empty argument fragment rather than a reconstructed scalar list. A
|
|
22
|
+
* reconstructed list would have to be derived from the model and would drift
|
|
23
|
+
* from it; this cannot.
|
|
24
|
+
* 2. *A string entry becomes `true`, whether it names a scalar or a relation.*
|
|
25
|
+
* §10.2 says a relation string uses the target Resource's default
|
|
26
|
+
* projection, and `posts: true` is exactly how Prisma spells that. Since the
|
|
27
|
+
* two emissions coincide, the translator needs no model lookup to tell a
|
|
28
|
+
* scalar from a relation — and therefore cannot get that lookup wrong.
|
|
29
|
+
* 3. *`$count` is lifted to the level that owns the edge.* Aventara writes the
|
|
30
|
+
* reducer inside the relation's own `select` (§10.3); Prisma writes it in the
|
|
31
|
+
* PARENT's `select`, under `_count`. The `where` placed in `_count` is the
|
|
32
|
+
* relation's *translated* `where` and carries no pagination, which is what
|
|
33
|
+
* makes `count` the total matching the nested `where` before
|
|
34
|
+
* limit/offset/cursor (C-832).
|
|
35
|
+
*
|
|
36
|
+
* **One call, not two (D-822, measured 2026-10-02).** D-822 left open whether
|
|
37
|
+
* Prisma's relation `_count` is affected by a `take`/`skip` on the same relation
|
|
38
|
+
* in the same query, and required Task 8 to establish it before choosing a
|
|
39
|
+
* shape. Measured against real databases on both engines — `@prisma/client`
|
|
40
|
+
* 7.10.0, `_count: { select: { posts: { where } } }` beside
|
|
41
|
+
* `posts: { where, take, skip }` — the count was `3` in every case while the row
|
|
42
|
+
* set was `3`, `1`, `2`, `1` and `1` for no pagination, `take: 1`, `skip: 1`,
|
|
43
|
+
* `take: 1 + skip: 1` and `take: 2 + skip: 2`, and `3` again with
|
|
44
|
+
* `cursor + take: 1`. SQLite and PostgreSQL agreed exactly. The filtered-count
|
|
45
|
+
* claim ("since 4.3.0") was re-measured in the same run: `_count` of the same
|
|
46
|
+
* edge answered `5` unfiltered and `3` with the `where`. So the translation is
|
|
47
|
+
* one call and the two-call fallback D-822 allowed for is not needed.
|
|
48
|
+
*
|
|
49
|
+
* **The relation-argument translator is injected.** `where`, `orderBy`,
|
|
50
|
+
* `limit`, `offset` and `cursor` are Task 7's vocabulary
|
|
51
|
+
* (`where.translator.ts`, `order-pagination.translator.ts`); this module owns
|
|
52
|
+
* the projection and nothing else. Injecting the seam also makes the `_count`
|
|
53
|
+
* filter the *same* translated `where` the relation read uses, rather than a
|
|
54
|
+
* second translation of the same predicate that could drift from it.
|
|
55
|
+
*/
|
|
56
|
+
/** Core's own word for the reducer set, so the adapter cannot invent a second one. */
|
|
57
|
+
export type PrismaRelationProjection = {
|
|
58
|
+
readonly field: string;
|
|
59
|
+
/** Non-empty exactly when the edge requested a reducer. */
|
|
60
|
+
readonly reducers: readonly RelationReducer[];
|
|
61
|
+
/** Whether any row field was requested beside the reducer (§10.3's third shape). */
|
|
62
|
+
readonly rows: boolean;
|
|
63
|
+
readonly projection: PrismaProjectionPlan;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* What T-8 called `ReadProjectionPlanLike`, renamed because it is not a view of
|
|
67
|
+
* a plan: `AdapterOperation` carries only `{ family, variant, resource,
|
|
68
|
+
* arguments }` **[CORE]**, so the adapter *reconstructs* the reducer positions
|
|
69
|
+
* from the arguments core planned, and this module is their owner. It records
|
|
70
|
+
* only the edges that need reshaping — an edge with no reducer anywhere beneath
|
|
71
|
+
* it is absent, which is what lets the shaper return such a row untouched.
|
|
72
|
+
*/
|
|
73
|
+
export type PrismaProjectionPlan = {
|
|
74
|
+
readonly relations: readonly PrismaRelationProjection[];
|
|
75
|
+
};
|
|
76
|
+
/** Where in the projection a relation sits, for the injected translator's diagnostics. */
|
|
77
|
+
export type PrismaProjectionContext = {
|
|
78
|
+
readonly path: readonly string[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Task 7's `where`/`orderBy`/pagination translation, as this module's one
|
|
82
|
+
* dependency: Aventara relation arguments in, Prisma relation arguments out.
|
|
83
|
+
*/
|
|
84
|
+
export type PrismaRelationArgumentTranslator = (args: Readonly<Record<string, unknown>>, context: PrismaProjectionContext) => Readonly<Record<string, unknown>>;
|
|
85
|
+
export type PrismaProjectionDependencies = {
|
|
86
|
+
readonly translateRelationArguments: PrismaRelationArgumentTranslator;
|
|
87
|
+
};
|
|
88
|
+
export type PrismaProjectionTranslation = {
|
|
89
|
+
/**
|
|
90
|
+
* The projection fragment of the Prisma call: `{}` for the default
|
|
91
|
+
* projection, `{ select: … }` otherwise. The operation translator merges it
|
|
92
|
+
* with the rest of the arguments.
|
|
93
|
+
*/
|
|
94
|
+
readonly args: Readonly<Record<string, unknown>>;
|
|
95
|
+
readonly plan: PrismaProjectionPlan;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* A projection the adapter will not translate.
|
|
99
|
+
*
|
|
100
|
+
* Every case is one core's validation should already have rejected, and each is
|
|
101
|
+
* raised rather than worked around: translating an `include` as "no relations",
|
|
102
|
+
* or an empty `select` as "every scalar", would answer a different question in
|
|
103
|
+
* silence, which is C-855's failure mode seen from the translation side.
|
|
104
|
+
*/
|
|
105
|
+
export declare class PrismaProjectionTranslationError extends Error {
|
|
106
|
+
constructor(reason: string, path: readonly string[]);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Translates core's planned projection into the Prisma projection arguments and
|
|
110
|
+
* the plan the result shaper needs to place every `$count` envelope.
|
|
111
|
+
*
|
|
112
|
+
* @throws {PrismaProjectionTranslationError} for a projection the adapter will
|
|
113
|
+
* not translate rather than silently reinterpret.
|
|
114
|
+
*/
|
|
115
|
+
export declare function translateProjection(args: Readonly<Record<string, unknown>>, dependencies: PrismaProjectionDependencies): PrismaProjectionTranslation;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A projection the adapter will not translate.
|
|
3
|
+
*
|
|
4
|
+
* Every case is one core's validation should already have rejected, and each is
|
|
5
|
+
* raised rather than worked around: translating an `include` as "no relations",
|
|
6
|
+
* or an empty `select` as "every scalar", would answer a different question in
|
|
7
|
+
* silence, which is C-855's failure mode seen from the translation side.
|
|
8
|
+
*/
|
|
9
|
+
export class PrismaProjectionTranslationError extends Error {
|
|
10
|
+
constructor(reason, path) {
|
|
11
|
+
const where = path.length === 0 ? "the root projection" : path.join(".");
|
|
12
|
+
super(`${reason} (at ${where}).`);
|
|
13
|
+
this.name = "PrismaProjectionTranslationError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Prisma's spelling of the reducer; the dictionary's `_count` → `$count` row, read backwards. */
|
|
17
|
+
const PRISMA_COUNT_KEY = "_count";
|
|
18
|
+
const AVENTARA_COUNT_REDUCER = "$count";
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
/** The single-key `{ relation: { … } }` entry core emits for a customized edge. */
|
|
23
|
+
function relationEntry(entry, path) {
|
|
24
|
+
const keys = Object.keys(entry);
|
|
25
|
+
const field = keys[0];
|
|
26
|
+
if (keys.length !== 1 || field === undefined) {
|
|
27
|
+
throw new PrismaProjectionTranslationError("A relation projection entry must carry exactly one relation name", path);
|
|
28
|
+
}
|
|
29
|
+
return [field, entry[field]];
|
|
30
|
+
}
|
|
31
|
+
function relationArguments(value) {
|
|
32
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "select" && key !== "include"));
|
|
33
|
+
}
|
|
34
|
+
function nestedSelect(value, path) {
|
|
35
|
+
if (value.include !== undefined) {
|
|
36
|
+
throw new PrismaProjectionTranslationError("A relation projection must use `select`; core resolves `include` before the adapter", path);
|
|
37
|
+
}
|
|
38
|
+
if (value.select === undefined) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(value.select)) {
|
|
42
|
+
throw new PrismaProjectionTranslationError("A relation `select` must be an array of projection entries", path);
|
|
43
|
+
}
|
|
44
|
+
return value.select;
|
|
45
|
+
}
|
|
46
|
+
function translateRelation(field, value, dependencies, path) {
|
|
47
|
+
const here = [...path, field];
|
|
48
|
+
if (!isRecord(value)) {
|
|
49
|
+
throw new PrismaProjectionTranslationError("A customized relation must use object form", here);
|
|
50
|
+
}
|
|
51
|
+
const entries = nestedSelect(value, here);
|
|
52
|
+
const reducerRequested = entries?.includes(AVENTARA_COUNT_REDUCER) === true;
|
|
53
|
+
const rowEntries = entries?.filter((entry) => entry !== AVENTARA_COUNT_REDUCER);
|
|
54
|
+
const translated = dependencies.translateRelationArguments(relationArguments(value), { path: here });
|
|
55
|
+
const nested = rowEntries === undefined || rowEntries.length === 0
|
|
56
|
+
? undefined
|
|
57
|
+
: translateLevel(rowEntries, dependencies, here);
|
|
58
|
+
// An edge whose only requested entries were row fields keeps Prisma's own
|
|
59
|
+
// default projection when none were named at all (§10.2's relation string).
|
|
60
|
+
const rows = entries === undefined || (rowEntries?.length ?? 0) > 0;
|
|
61
|
+
const selected = rows
|
|
62
|
+
? {
|
|
63
|
+
...translated,
|
|
64
|
+
...(nested === undefined ? {} : { select: nested.select }),
|
|
65
|
+
}
|
|
66
|
+
: undefined;
|
|
67
|
+
const node = reducerRequested || (nested?.plan.relations.length ?? 0) > 0
|
|
68
|
+
? {
|
|
69
|
+
field,
|
|
70
|
+
reducers: reducerRequested ? [AVENTARA_COUNT_REDUCER] : [],
|
|
71
|
+
rows,
|
|
72
|
+
projection: nested?.plan ?? { relations: [] },
|
|
73
|
+
}
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
// `true` rather than `{}`: an edge with neither arguments nor a nested
|
|
77
|
+
// projection is exactly "the target's default projection".
|
|
78
|
+
selected: selected === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: Object.keys(selected).length === 0
|
|
81
|
+
? true
|
|
82
|
+
: selected,
|
|
83
|
+
// The count carries the translated `where` and NOTHING else, which is what
|
|
84
|
+
// makes it the pre-pagination total (C-832).
|
|
85
|
+
counted: reducerRequested
|
|
86
|
+
? translated.where === undefined
|
|
87
|
+
? true
|
|
88
|
+
: { where: translated.where }
|
|
89
|
+
: undefined,
|
|
90
|
+
node,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function translateLevel(select, dependencies, path) {
|
|
94
|
+
if (select.length === 0) {
|
|
95
|
+
throw new PrismaProjectionTranslationError("A projection must request at least one field; Prisma cannot express an empty `select`", path);
|
|
96
|
+
}
|
|
97
|
+
const prismaSelect = {};
|
|
98
|
+
const counts = {};
|
|
99
|
+
const relations = [];
|
|
100
|
+
for (const entry of select) {
|
|
101
|
+
if (typeof entry === "string") {
|
|
102
|
+
if (entry === AVENTARA_COUNT_REDUCER) {
|
|
103
|
+
throw new PrismaProjectionTranslationError("`$count` is an edge reducer and is only valid inside a relation projection", path);
|
|
104
|
+
}
|
|
105
|
+
// A scalar and a relation string translate alike, and Prisma reads
|
|
106
|
+
// `relation: true` as the target's default projection (§10.2).
|
|
107
|
+
prismaSelect[entry] = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!isRecord(entry)) {
|
|
111
|
+
throw new PrismaProjectionTranslationError("A projection entry must be a field name or a single-key relation object", path);
|
|
112
|
+
}
|
|
113
|
+
const [field, value] = relationEntry(entry, path);
|
|
114
|
+
const relation = translateRelation(field, value, dependencies, path);
|
|
115
|
+
if (relation.selected !== undefined) {
|
|
116
|
+
prismaSelect[field] = relation.selected;
|
|
117
|
+
}
|
|
118
|
+
if (relation.counted !== undefined) {
|
|
119
|
+
counts[field] = relation.counted;
|
|
120
|
+
}
|
|
121
|
+
if (relation.node !== undefined) {
|
|
122
|
+
relations.push(relation.node);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (Object.keys(counts).length > 0) {
|
|
126
|
+
prismaSelect[PRISMA_COUNT_KEY] = { select: counts };
|
|
127
|
+
}
|
|
128
|
+
return { select: prismaSelect, plan: { relations } };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Translates core's planned projection into the Prisma projection arguments and
|
|
132
|
+
* the plan the result shaper needs to place every `$count` envelope.
|
|
133
|
+
*
|
|
134
|
+
* @throws {PrismaProjectionTranslationError} for a projection the adapter will
|
|
135
|
+
* not translate rather than silently reinterpret.
|
|
136
|
+
*/
|
|
137
|
+
export function translateProjection(args, dependencies) {
|
|
138
|
+
if (args.include !== undefined) {
|
|
139
|
+
throw new PrismaProjectionTranslationError("An operation projection must arrive as `select`; core resolves `include` before the adapter", []);
|
|
140
|
+
}
|
|
141
|
+
if (args.select === undefined) {
|
|
142
|
+
// C-833: Prisma's default read projection is the framework default, so the
|
|
143
|
+
// translation is to request nothing.
|
|
144
|
+
return { args: {}, plan: { relations: [] } };
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(args.select)) {
|
|
147
|
+
throw new PrismaProjectionTranslationError("An operation `select` must be an array of projection entries", []);
|
|
148
|
+
}
|
|
149
|
+
const level = translateLevel(args.select, dependencies, []);
|
|
150
|
+
return { args: { select: level.select }, plan: level.plan };
|
|
151
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type AdapterField, type AdapterModel } 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 declare class PrismaScalarConversionError extends Error {
|
|
79
|
+
readonly name = "PrismaScalarConversionError";
|
|
80
|
+
}
|
|
81
|
+
/** Which way across the adapter boundary a value is travelling. */
|
|
82
|
+
export type PrismaScalarDirection = "outbound" | "inbound";
|
|
83
|
+
/** A scalar-shaped discovered field: the only thing that names a scalar. */
|
|
84
|
+
export type PrismaScalarField = Extract<AdapterField<AdapterModel>, {
|
|
85
|
+
kind: "scalar";
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* One value out of Aventara and into a Prisma argument.
|
|
89
|
+
*
|
|
90
|
+
* @throws {PrismaScalarConversionError} for a value the adapter will not
|
|
91
|
+
* represent rather than let a provider misread.
|
|
92
|
+
*/
|
|
93
|
+
export declare function emitScalarValue(model: AdapterModel, field: PrismaScalarField, value: unknown, path: string): unknown;
|
|
94
|
+
/**
|
|
95
|
+
* One value out of a Prisma result and into the runtime form core's output
|
|
96
|
+
* validator accepts.
|
|
97
|
+
*
|
|
98
|
+
* @throws {PrismaScalarConversionError} for a provider value the adapter cannot
|
|
99
|
+
* honestly read as that scalar.
|
|
100
|
+
*/
|
|
101
|
+
export declare function receiveScalarValue(model: AdapterModel, field: PrismaScalarField, value: unknown, path: string): unknown;
|