@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.
Files changed (86) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +564 -0
  4. package/dist/discovery/artifact.loader.d.ts +26 -0
  5. package/dist/discovery/artifact.loader.js +124 -0
  6. package/dist/discovery/capability-surface.interface.d.ts +191 -0
  7. package/dist/discovery/capability-surface.interface.js +13 -0
  8. package/dist/discovery/capability.deriver.d.ts +92 -0
  9. package/dist/discovery/capability.deriver.js +350 -0
  10. package/dist/discovery/discovery-artifact.interface.d.ts +210 -0
  11. package/dist/discovery/discovery-artifact.interface.js +1 -0
  12. package/dist/discovery/enum.mapper.d.ts +59 -0
  13. package/dist/discovery/enum.mapper.js +32 -0
  14. package/dist/discovery/field.mapper.d.ts +244 -0
  15. package/dist/discovery/field.mapper.js +387 -0
  16. package/dist/discovery/identifier.mapper.d.ts +188 -0
  17. package/dist/discovery/identifier.mapper.js +132 -0
  18. package/dist/discovery/model.discoverer.d.ts +108 -0
  19. package/dist/discovery/model.discoverer.js +341 -0
  20. package/dist/discovery/operations.deriver.d.ts +75 -0
  21. package/dist/discovery/operations.deriver.js +92 -0
  22. package/dist/discovery/scalar.mapper.d.ts +81 -0
  23. package/dist/discovery/scalar.mapper.js +51 -0
  24. package/dist/discovery/schema-facts.interface.d.ts +174 -0
  25. package/dist/discovery/schema-facts.interface.js +11 -0
  26. package/dist/discovery/schema.reader.d.ts +162 -0
  27. package/dist/discovery/schema.reader.js +692 -0
  28. package/dist/discovery/types.reader.d.ts +24 -0
  29. package/dist/discovery/types.reader.js +617 -0
  30. package/dist/discovery/vocabulary.dictionary.d.ts +258 -0
  31. package/dist/discovery/vocabulary.dictionary.js +429 -0
  32. package/dist/errors/error.normalizer.d.ts +250 -0
  33. package/dist/errors/error.normalizer.js +135 -0
  34. package/dist/errors/prisma-error.reader.d.ts +144 -0
  35. package/dist/errors/prisma-error.reader.js +167 -0
  36. package/dist/execution/first-variant.emulator.d.ts +64 -0
  37. package/dist/execution/first-variant.emulator.js +205 -0
  38. package/dist/execution/operation.executor.d.ts +87 -0
  39. package/dist/execution/operation.executor.js +210 -0
  40. package/dist/execution/result.shaper.d.ts +103 -0
  41. package/dist/execution/result.shaper.js +141 -0
  42. package/dist/execution/transaction.executor.d.ts +147 -0
  43. package/dist/execution/transaction.executor.js +13 -0
  44. package/dist/generate/artifact.emitter.d.ts +50 -0
  45. package/dist/generate/artifact.emitter.js +105 -0
  46. package/dist/generate/declared-support.reader.d.ts +33 -0
  47. package/dist/generate/declared-support.reader.js +74 -0
  48. package/dist/generate/discovery.bin.d.ts +2 -0
  49. package/dist/generate/discovery.bin.js +15 -0
  50. package/dist/generate/discovery.cli.d.ts +24 -0
  51. package/dist/generate/discovery.cli.js +163 -0
  52. package/dist/generate/existence.gate.d.ts +73 -0
  53. package/dist/generate/existence.gate.js +91 -0
  54. package/dist/generate/index.d.ts +81 -0
  55. package/dist/generate/index.js +65 -0
  56. package/dist/generate/node-version.guard.d.ts +8 -0
  57. package/dist/generate/node-version.guard.js +59 -0
  58. package/dist/generate/prisma-cli.resolver.d.ts +48 -0
  59. package/dist/generate/prisma-cli.resolver.js +95 -0
  60. package/dist/generate/prisma.generate.d.ts +164 -0
  61. package/dist/generate/prisma.generate.js +278 -0
  62. package/dist/generate/reader-crosscheck.d.ts +156 -0
  63. package/dist/generate/reader-crosscheck.js +202 -0
  64. package/dist/index.d.ts +5 -0
  65. package/dist/index.js +1 -0
  66. package/dist/prisma-client.interface.d.ts +48 -0
  67. package/dist/prisma-client.interface.js +9 -0
  68. package/dist/prisma.adapter.d.ts +55 -0
  69. package/dist/prisma.adapter.js +105 -0
  70. package/dist/provider/provider-profile.d.ts +53 -0
  71. package/dist/provider/provider-profile.js +91 -0
  72. package/dist/translation/identifier.addressing.d.ts +80 -0
  73. package/dist/translation/identifier.addressing.js +30 -0
  74. package/dist/translation/mutation.translator.d.ts +115 -0
  75. package/dist/translation/mutation.translator.js +580 -0
  76. package/dist/translation/operation.translator.d.ts +273 -0
  77. package/dist/translation/operation.translator.js +377 -0
  78. package/dist/translation/order-pagination.translator.d.ts +86 -0
  79. package/dist/translation/order-pagination.translator.js +159 -0
  80. package/dist/translation/projection.translator.d.ts +115 -0
  81. package/dist/translation/projection.translator.js +151 -0
  82. package/dist/translation/scalar.conversion.d.ts +101 -0
  83. package/dist/translation/scalar.conversion.js +221 -0
  84. package/dist/translation/where.translator.d.ts +79 -0
  85. package/dist/translation/where.translator.js +254 -0
  86. 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;