@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,244 @@
|
|
|
1
|
+
import { type AdapterLifecycleTrait, type AdapterRelationField, type AdapterScalarField } from "@aventara/core";
|
|
2
|
+
import type { PrismaResourceSurface } from "./capability-surface.interface.js";
|
|
3
|
+
import type { PrismaAdapterModel, PrismaReferentialAction } from "./discovery-artifact.interface.js";
|
|
4
|
+
import type { PrismaSchemaField, PrismaSchemaUnknown } from "./schema-facts.interface.js";
|
|
5
|
+
import { type PrismaDiscoveryDiagnostic, type PrismaEvidence } from "./vocabulary.dictionary.js";
|
|
6
|
+
/**
|
|
7
|
+
* Task 4 — one PSL field, answered as an `AdapterScalarField` or an
|
|
8
|
+
* `AdapterRelationField` (C-808, C-811, C-813).
|
|
9
|
+
*
|
|
10
|
+
* # Nullability comes from PSL, and the required `Json` column is why
|
|
11
|
+
*
|
|
12
|
+
* `nullable` is `PrismaSchemaField.optional` — PSL's `?` — and **nothing else**
|
|
13
|
+
* (T-3.1 invariant 4). The type surface cannot answer the question, and the
|
|
14
|
+
* damage is not hypothetical: `$MetricPayload.scalars.sJson` resolves to
|
|
15
|
+
* `null | string | number | false | true | JsonObject | JsonArray` while PSL says
|
|
16
|
+
* `sJson Json`, required, because JSON null is a *value*. The reader's own facts
|
|
17
|
+
* point the same wrong way — `JsonFilterBase.array_contains` admits `| null` and
|
|
18
|
+
* `equals` admits `JsonNullValueFilter` on that same required column — so a
|
|
19
|
+
* plausible "the operands mention null, therefore the column is nullable" rule
|
|
20
|
+
* is wrong for **every** `Json` field while being right everywhere else. That is
|
|
21
|
+
* the whole reason this rule is stated as a rule rather than left to taste.
|
|
22
|
+
*
|
|
23
|
+
* # The create-optional marker is a cross-check, never a source
|
|
24
|
+
*
|
|
25
|
+
* {@link crossCheckCreateOptional} is a separate function that *consumes* this
|
|
26
|
+
* projection, and the data flows one way only: the marker never reaches
|
|
27
|
+
* {@link projectPrismaField}. `<M>UncheckedCreateInput`'s `?` is **one bit over
|
|
28
|
+
* several causes** — `@default(uuid())`, `@default(BRONZE)`, `@default(now())`,
|
|
29
|
+
* `@updatedAt`, plain nullability, and a scalar list, which needs no value
|
|
30
|
+
* because the empty list is one — and nothing on the 🟢 surface tells them
|
|
31
|
+
* apart. Deriving a trait *from* the marker would invert the check into the bug
|
|
32
|
+
* it exists to catch: it would make the two readers agree by construction and
|
|
33
|
+
* detect nothing.
|
|
34
|
+
*
|
|
35
|
+
* # `@default` is matched, never parsed
|
|
36
|
+
*
|
|
37
|
+
* {@link matchPrismaDefault} walks `PRISMA_DEFAULT_TABLE` over the raw argument
|
|
38
|
+
* text the PSL reader carried across unchanged. B1 deferred this matcher to this
|
|
39
|
+
* task deliberately: the dictionary owns the data, and the lookup belongs where
|
|
40
|
+
* a red test can drive it.
|
|
41
|
+
*
|
|
42
|
+
* The matcher adds **one** discriminant the dictionary's table does not state,
|
|
43
|
+
* and it is a shape test rather than a parse: an argument that begins with an
|
|
44
|
+
* identifier followed by `(` is a *generator form*, and anything else is a
|
|
45
|
+
* *static value*. Both are `DEFAULTED`; only an unrecognised generator form
|
|
46
|
+
* records an unknown. Without that split, `tier Tier @default(BRONZE)` — a field
|
|
47
|
+
* of the Phase 8 schemas — would record an unknown and fail the generate step,
|
|
48
|
+
* while C-811's own acceptance criteria say `@default("x")` is `["DEFAULTED"]`
|
|
49
|
+
* with nothing recorded. The test never reads *inside* the argument, so
|
|
50
|
+
* `dbgenerated("gen_random_uuid()")` remains a key and `"a, b // not a comment"`
|
|
51
|
+
* remains opaque text.
|
|
52
|
+
*
|
|
53
|
+
* # A relation carries four members, and the FK is not one of them
|
|
54
|
+
*
|
|
55
|
+
* `target`, `cardinality`, `nullable`, and (from Task 6) `capabilities`. The
|
|
56
|
+
* `@relation(fields: […], references: […])` scalars are read, used to identify
|
|
57
|
+
* the FK column and check it against the edge's own optionality, and then
|
|
58
|
+
* **discarded** (C-813). `@relation`'s *name* is not an Aventara concept either,
|
|
59
|
+
* and its referential actions are not core's yet, so both travel beside the
|
|
60
|
+
* projection as {@link PrismaRelationEdge} rather than inside it: pairing is
|
|
61
|
+
* discovery-internal, and it is **PSL-only** — the name
|
|
62
|
+
* appears nowhere on the 🟢 surface, so from the types alone `Author` has two
|
|
63
|
+
* `Author` edges with nothing to pair them.
|
|
64
|
+
*
|
|
65
|
+
* # Two failure vocabularies, kept apart
|
|
66
|
+
*
|
|
67
|
+
* A **diagnostic** is about the schema: PSL declares a column the 🟢 types do
|
|
68
|
+
* not expose (C-808's existence gate, whose fatality the generate step decides),
|
|
69
|
+
* or a Prisma type name in no table. A **throw** is about the readers: the two
|
|
70
|
+
* sources disagree about a fact they both state, which no schema can cause and
|
|
71
|
+
* no diagnostic vocabulary describes. The types reader draws the same line.
|
|
72
|
+
*/
|
|
73
|
+
/** A reader-versus-reader disagreement: not a schema problem, and not recoverable. */
|
|
74
|
+
export declare class PrismaFieldProjectionError extends Error {
|
|
75
|
+
readonly name = "PrismaFieldProjectionError";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The projection minus `capabilities`, which is Task 6's and is derived from the
|
|
79
|
+
* capability surface rather than from PSL. `Omit` rather than a restatement, so
|
|
80
|
+
* the composed field is the core type by construction.
|
|
81
|
+
*/
|
|
82
|
+
export type ProjectedScalarField = Omit<AdapterScalarField<PrismaAdapterModel>, "capabilities">;
|
|
83
|
+
export type ProjectedRelationField = Omit<AdapterRelationField<PrismaAdapterModel>, "capabilities">;
|
|
84
|
+
/**
|
|
85
|
+
* One relation edge as PSL states it — the pairing evidence and the referential
|
|
86
|
+
* actions, neither of which the projected field carries.
|
|
87
|
+
*
|
|
88
|
+
* # Why the actions ride here and not on `AdapterRelationField`
|
|
89
|
+
*
|
|
90
|
+
* `AdapterRelationField` is core's type and its members are C-813's (`target`,
|
|
91
|
+
* `cardinality`, `nullable`, `capabilities`). Wiring a referential action into
|
|
92
|
+
* the Contract is a core change and belongs to the task that owns core's
|
|
93
|
+
* transaction planner, not to discovery. Until then the fact lives where
|
|
94
|
+
* discovery can state it truthfully.
|
|
95
|
+
*
|
|
96
|
+
* # The lookup is inverted, and the edge is shaped so a consumer need not invert
|
|
97
|
+
*
|
|
98
|
+
* `onDelete` is declared on the CHILD — `Kid.parent @relation(…, onDelete:
|
|
99
|
+
* Cascade)` — and fires when the PARENT is deleted. So an edge reads:
|
|
100
|
+
* **deleting a `target` does `onDelete` to rows of `resource`.** A consumer
|
|
101
|
+
* asking "what does deleting `X` remove?" filters `target === X && onDelete ===
|
|
102
|
+
* "cascade"` and reads off `resource`; it does not have to re-derive which side
|
|
103
|
+
* of a pair declared what.
|
|
104
|
+
*
|
|
105
|
+
* `null` on both members means this edge is not the owning side (it declares no
|
|
106
|
+
* `fields:`), so the actions are on its partner — never that no action applies.
|
|
107
|
+
*/
|
|
108
|
+
export type PrismaRelationEdge = {
|
|
109
|
+
/** The model DECLARING the edge — the child, where an owning edge is concerned. */
|
|
110
|
+
readonly resource: string;
|
|
111
|
+
readonly field: string;
|
|
112
|
+
/** The model the edge points AT — the parent, whose deletion fires `onDelete`. */
|
|
113
|
+
readonly target: string;
|
|
114
|
+
readonly cardinality: "one" | "many";
|
|
115
|
+
/** `@relation("Name")`, or `null` where PSL wrote none. PSL-only. */
|
|
116
|
+
readonly relationName: string | null;
|
|
117
|
+
/**
|
|
118
|
+
* What deleting a `target` row does to this `resource` row, resolved (an
|
|
119
|
+
* absent PSL argument already carries Prisma's measured default). `null` on a
|
|
120
|
+
* non-owning edge.
|
|
121
|
+
*/
|
|
122
|
+
readonly onDelete: PrismaReferentialAction | null;
|
|
123
|
+
/** The same for an update of the referenced key. `null` on a non-owning edge. */
|
|
124
|
+
readonly onUpdate: PrismaReferentialAction | null;
|
|
125
|
+
};
|
|
126
|
+
export type FieldProjectionInput = {
|
|
127
|
+
readonly resource: string;
|
|
128
|
+
/** PSL — what the field IS. */
|
|
129
|
+
readonly field: PrismaSchemaField;
|
|
130
|
+
/** The model's other fields: the only way to identify an FK scalar by name. */
|
|
131
|
+
readonly siblings: readonly PrismaSchemaField[];
|
|
132
|
+
/** TYPE — what this client exposes. The existence gate (C-808). */
|
|
133
|
+
readonly surface: PrismaResourceSurface;
|
|
134
|
+
/** TYPE — `enums.ts`, which is what makes an enum name an enum name. */
|
|
135
|
+
readonly enums: Readonly<Record<string, readonly string[]>>;
|
|
136
|
+
/** PSL — the model names, which is what makes a written type name a relation. */
|
|
137
|
+
readonly models: ReadonlySet<string>;
|
|
138
|
+
};
|
|
139
|
+
export type FieldProjection = {
|
|
140
|
+
readonly outcome: "scalar";
|
|
141
|
+
readonly name: string;
|
|
142
|
+
readonly field: ProjectedScalarField;
|
|
143
|
+
/** Recorded unknowns; a non-empty list FAILS the generate step (T-3.1 invariant 5). */
|
|
144
|
+
readonly unknowns: readonly PrismaSchemaUnknown[];
|
|
145
|
+
} | {
|
|
146
|
+
readonly outcome: "relation";
|
|
147
|
+
readonly name: string;
|
|
148
|
+
readonly field: ProjectedRelationField;
|
|
149
|
+
readonly edge: PrismaRelationEdge;
|
|
150
|
+
} | {
|
|
151
|
+
readonly outcome: "excluded";
|
|
152
|
+
readonly diagnostic: PrismaDiscoveryDiagnostic;
|
|
153
|
+
} | {
|
|
154
|
+
readonly outcome: "unmapped";
|
|
155
|
+
readonly diagnostic: PrismaDiscoveryDiagnostic;
|
|
156
|
+
};
|
|
157
|
+
export declare function projectPrismaField(input: FieldProjectionInput): FieldProjection;
|
|
158
|
+
export type PrismaDefaultOutcome =
|
|
159
|
+
/** A row of `PRISMA_DEFAULT_TABLE` matched, and its evidence travels with it. */
|
|
160
|
+
{
|
|
161
|
+
readonly form: "table";
|
|
162
|
+
readonly traits: readonly AdapterLifecycleTrait[];
|
|
163
|
+
readonly evidence: PrismaEvidence;
|
|
164
|
+
}
|
|
165
|
+
/** A static value — a literal, a number, a boolean, an enum member, a list. */
|
|
166
|
+
| {
|
|
167
|
+
readonly form: "static";
|
|
168
|
+
readonly traits: readonly AdapterLifecycleTrait[];
|
|
169
|
+
}
|
|
170
|
+
/** A generator form the table does not carry: `DEFAULTED`, plus a recorded unknown. */
|
|
171
|
+
| {
|
|
172
|
+
readonly form: "unrecognised-generator";
|
|
173
|
+
readonly traits: readonly AdapterLifecycleTrait[];
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* T-3.1's `@default` table, looked up — never evaluated. The argument arrives as
|
|
177
|
+
* opaque text precisely so that this function can be a lookup.
|
|
178
|
+
*/
|
|
179
|
+
export declare function matchPrismaDefault(argument: string): PrismaDefaultOutcome;
|
|
180
|
+
/** Why a field is legitimately optional in `<M>UncheckedCreateInput`. */
|
|
181
|
+
export type CreateOptionalCause = AdapterLifecycleTrait | "nullable"
|
|
182
|
+
/** A scalar list needs no value: the empty list is one. */
|
|
183
|
+
| "list";
|
|
184
|
+
export type CreateOptionalCrossCheckInput = {
|
|
185
|
+
readonly resource: string;
|
|
186
|
+
/**
|
|
187
|
+
* The **projected** scalar fields — PSL's answers, already computed. Relation
|
|
188
|
+
* edges are not passed: every edge is optional in a create input regardless of
|
|
189
|
+
* anything PSL says, so they carry no information for this check.
|
|
190
|
+
*/
|
|
191
|
+
readonly fields: readonly {
|
|
192
|
+
readonly name: string;
|
|
193
|
+
readonly nullable: boolean;
|
|
194
|
+
readonly list: boolean;
|
|
195
|
+
readonly lifecycle: readonly AdapterLifecycleTrait[];
|
|
196
|
+
}[];
|
|
197
|
+
/** TYPE — `<M>UncheckedCreateInput`'s optional members. */
|
|
198
|
+
readonly createOptional: readonly string[];
|
|
199
|
+
};
|
|
200
|
+
export type CreateOptionalCrossCheck = {
|
|
201
|
+
readonly explained: readonly {
|
|
202
|
+
readonly name: string;
|
|
203
|
+
readonly cause: CreateOptionalCause;
|
|
204
|
+
}[];
|
|
205
|
+
/** Marked optional by the types with nothing in PSL to explain it. */
|
|
206
|
+
readonly unexplained: readonly string[];
|
|
207
|
+
readonly messages: readonly string[];
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* TYPE's only contribution to lifecycle: a check, run in one direction.
|
|
211
|
+
*
|
|
212
|
+
* Every field the 🟢 `<M>UncheckedCreateInput` marks optional must carry a PSL
|
|
213
|
+
* cause. A field the types mark optional with no cause means **one of the two
|
|
214
|
+
* readers is wrong** — that is the entire signal, and it is why the marker is
|
|
215
|
+
* never read as a trait. Names the types mark that this model does not project
|
|
216
|
+
* as a scalar (relation edges) are skipped rather than reported: their
|
|
217
|
+
* optionality is structural.
|
|
218
|
+
*/
|
|
219
|
+
export declare function crossCheckCreateOptional(input: CreateOptionalCrossCheckInput): CreateOptionalCrossCheck;
|
|
220
|
+
export type PrismaRelationPair = readonly [
|
|
221
|
+
PrismaRelationEdge,
|
|
222
|
+
PrismaRelationEdge
|
|
223
|
+
];
|
|
224
|
+
export type PrismaRelationPairing = {
|
|
225
|
+
readonly pairs: readonly PrismaRelationPair[];
|
|
226
|
+
/** Edges nothing pairs: a back-reference the schema never declared, or an ambiguity. */
|
|
227
|
+
readonly unpaired: readonly PrismaRelationEdge[];
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Pairs each edge with its back-reference, which **only PSL can do**.
|
|
231
|
+
*
|
|
232
|
+
* An explicit `@relation("Name")` is the pairing evidence, and for the two
|
|
233
|
+
* `Author` → `Author` edges it is the *only* evidence: the name appears nowhere
|
|
234
|
+
* on the 🟢 surface, so from the types alone there are two self-edges and
|
|
235
|
+
* nothing to say they are one relation.
|
|
236
|
+
*
|
|
237
|
+
* Unnamed edges are paired by the unordered model pair, which is unambiguous
|
|
238
|
+
* because PSL requires a name as soon as two relations connect the same two
|
|
239
|
+
* models. Where an unordered pair still has more than two edges, they are left
|
|
240
|
+
* unpaired rather than guessed: the implicit name Prisma would compute lives in
|
|
241
|
+
* the 🛑 runtime data model, and re-deriving it here would be this module
|
|
242
|
+
* reimplementing the generator.
|
|
243
|
+
*/
|
|
244
|
+
export declare function pairPrismaRelationEdges(edges: readonly PrismaRelationEdge[]): PrismaRelationPairing;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { LIFECYCLE_TRAITS, } from "@aventara/core";
|
|
2
|
+
import { projectEnumBackedColumn } from "./enum.mapper.js";
|
|
3
|
+
import { mapPrismaScalarField } from "./scalar.mapper.js";
|
|
4
|
+
import { PRISMA_DEFAULT_TABLE, PRISMA_UNMATCHED_DEFAULT_TRAITS, } from "./vocabulary.dictionary.js";
|
|
5
|
+
/**
|
|
6
|
+
* Task 4 — one PSL field, answered as an `AdapterScalarField` or an
|
|
7
|
+
* `AdapterRelationField` (C-808, C-811, C-813).
|
|
8
|
+
*
|
|
9
|
+
* # Nullability comes from PSL, and the required `Json` column is why
|
|
10
|
+
*
|
|
11
|
+
* `nullable` is `PrismaSchemaField.optional` — PSL's `?` — and **nothing else**
|
|
12
|
+
* (T-3.1 invariant 4). The type surface cannot answer the question, and the
|
|
13
|
+
* damage is not hypothetical: `$MetricPayload.scalars.sJson` resolves to
|
|
14
|
+
* `null | string | number | false | true | JsonObject | JsonArray` while PSL says
|
|
15
|
+
* `sJson Json`, required, because JSON null is a *value*. The reader's own facts
|
|
16
|
+
* point the same wrong way — `JsonFilterBase.array_contains` admits `| null` and
|
|
17
|
+
* `equals` admits `JsonNullValueFilter` on that same required column — so a
|
|
18
|
+
* plausible "the operands mention null, therefore the column is nullable" rule
|
|
19
|
+
* is wrong for **every** `Json` field while being right everywhere else. That is
|
|
20
|
+
* the whole reason this rule is stated as a rule rather than left to taste.
|
|
21
|
+
*
|
|
22
|
+
* # The create-optional marker is a cross-check, never a source
|
|
23
|
+
*
|
|
24
|
+
* {@link crossCheckCreateOptional} is a separate function that *consumes* this
|
|
25
|
+
* projection, and the data flows one way only: the marker never reaches
|
|
26
|
+
* {@link projectPrismaField}. `<M>UncheckedCreateInput`'s `?` is **one bit over
|
|
27
|
+
* several causes** — `@default(uuid())`, `@default(BRONZE)`, `@default(now())`,
|
|
28
|
+
* `@updatedAt`, plain nullability, and a scalar list, which needs no value
|
|
29
|
+
* because the empty list is one — and nothing on the 🟢 surface tells them
|
|
30
|
+
* apart. Deriving a trait *from* the marker would invert the check into the bug
|
|
31
|
+
* it exists to catch: it would make the two readers agree by construction and
|
|
32
|
+
* detect nothing.
|
|
33
|
+
*
|
|
34
|
+
* # `@default` is matched, never parsed
|
|
35
|
+
*
|
|
36
|
+
* {@link matchPrismaDefault} walks `PRISMA_DEFAULT_TABLE` over the raw argument
|
|
37
|
+
* text the PSL reader carried across unchanged. B1 deferred this matcher to this
|
|
38
|
+
* task deliberately: the dictionary owns the data, and the lookup belongs where
|
|
39
|
+
* a red test can drive it.
|
|
40
|
+
*
|
|
41
|
+
* The matcher adds **one** discriminant the dictionary's table does not state,
|
|
42
|
+
* and it is a shape test rather than a parse: an argument that begins with an
|
|
43
|
+
* identifier followed by `(` is a *generator form*, and anything else is a
|
|
44
|
+
* *static value*. Both are `DEFAULTED`; only an unrecognised generator form
|
|
45
|
+
* records an unknown. Without that split, `tier Tier @default(BRONZE)` — a field
|
|
46
|
+
* of the Phase 8 schemas — would record an unknown and fail the generate step,
|
|
47
|
+
* while C-811's own acceptance criteria say `@default("x")` is `["DEFAULTED"]`
|
|
48
|
+
* with nothing recorded. The test never reads *inside* the argument, so
|
|
49
|
+
* `dbgenerated("gen_random_uuid()")` remains a key and `"a, b // not a comment"`
|
|
50
|
+
* remains opaque text.
|
|
51
|
+
*
|
|
52
|
+
* # A relation carries four members, and the FK is not one of them
|
|
53
|
+
*
|
|
54
|
+
* `target`, `cardinality`, `nullable`, and (from Task 6) `capabilities`. The
|
|
55
|
+
* `@relation(fields: […], references: […])` scalars are read, used to identify
|
|
56
|
+
* the FK column and check it against the edge's own optionality, and then
|
|
57
|
+
* **discarded** (C-813). `@relation`'s *name* is not an Aventara concept either,
|
|
58
|
+
* and its referential actions are not core's yet, so both travel beside the
|
|
59
|
+
* projection as {@link PrismaRelationEdge} rather than inside it: pairing is
|
|
60
|
+
* discovery-internal, and it is **PSL-only** — the name
|
|
61
|
+
* appears nowhere on the 🟢 surface, so from the types alone `Author` has two
|
|
62
|
+
* `Author` edges with nothing to pair them.
|
|
63
|
+
*
|
|
64
|
+
* # Two failure vocabularies, kept apart
|
|
65
|
+
*
|
|
66
|
+
* A **diagnostic** is about the schema: PSL declares a column the 🟢 types do
|
|
67
|
+
* not expose (C-808's existence gate, whose fatality the generate step decides),
|
|
68
|
+
* or a Prisma type name in no table. A **throw** is about the readers: the two
|
|
69
|
+
* sources disagree about a fact they both state, which no schema can cause and
|
|
70
|
+
* no diagnostic vocabulary describes. The types reader draws the same line.
|
|
71
|
+
*/
|
|
72
|
+
/** A reader-versus-reader disagreement: not a schema problem, and not recoverable. */
|
|
73
|
+
export class PrismaFieldProjectionError extends Error {
|
|
74
|
+
name = "PrismaFieldProjectionError";
|
|
75
|
+
}
|
|
76
|
+
export function projectPrismaField(input) {
|
|
77
|
+
return input.models.has(input.field.type)
|
|
78
|
+
? projectRelationField(input)
|
|
79
|
+
: projectScalarField(input);
|
|
80
|
+
}
|
|
81
|
+
/* ------------------------------------------------------------------ *
|
|
82
|
+
* Scalars *
|
|
83
|
+
* ------------------------------------------------------------------ */
|
|
84
|
+
function projectScalarField(input) {
|
|
85
|
+
const { resource, field, surface, enums } = input;
|
|
86
|
+
const mapped = mapPrismaScalarField({ resource, field, surface, enums });
|
|
87
|
+
if (mapped.outcome === "excluded" || mapped.outcome === "unmapped") {
|
|
88
|
+
// The existence gate and the unmapped-type diagnostic are the scalar
|
|
89
|
+
// mapper's own answers, forwarded whole: restating them here would be a
|
|
90
|
+
// second wording of one decision.
|
|
91
|
+
return mapped;
|
|
92
|
+
}
|
|
93
|
+
const exposed = surface.scalarFields[field.name];
|
|
94
|
+
if (exposed === undefined) {
|
|
95
|
+
throw new PrismaFieldProjectionError(`${resource}.${field.name} passed the existence gate and has no scalar surface`);
|
|
96
|
+
}
|
|
97
|
+
assertTokenAgrees(resource, field, exposed.token);
|
|
98
|
+
const lifecycle = projectLifecycle(resource, field);
|
|
99
|
+
const type = mapped.outcome === "enum"
|
|
100
|
+
? projectEnumBackedColumn(mapped.enum).type
|
|
101
|
+
: { scalar: mapped.mapping.scalar };
|
|
102
|
+
const format = mapped.outcome === "scalar" ? mapped.mapping.format : undefined;
|
|
103
|
+
return {
|
|
104
|
+
outcome: "scalar",
|
|
105
|
+
name: field.name,
|
|
106
|
+
field: {
|
|
107
|
+
kind: "scalar",
|
|
108
|
+
type,
|
|
109
|
+
...(format === undefined ? {} : { format }),
|
|
110
|
+
// PSL's `?`, and nothing else (T-3.1 invariant 4).
|
|
111
|
+
nullable: field.optional,
|
|
112
|
+
list: field.list,
|
|
113
|
+
lifecycle: lifecycle.traits,
|
|
114
|
+
},
|
|
115
|
+
unknowns: lifecycle.unknowns,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The `FieldRef` token against the written PSL type.
|
|
120
|
+
*
|
|
121
|
+
* **A token carries its list suffix** — `Post.tags` reads as `String[]`, not
|
|
122
|
+
* `String` — so the suffix is split off before the names are compared and is
|
|
123
|
+
* then the TYPE half of `list`. Comparing the raw token to the PSL type name
|
|
124
|
+
* would make every list field look like a type mismatch, and a provider-drop
|
|
125
|
+
* check built on that would fire on a field that is present.
|
|
126
|
+
*/
|
|
127
|
+
function assertTokenAgrees(resource, field, token) {
|
|
128
|
+
const listed = token.endsWith("[]");
|
|
129
|
+
const base = listed ? token.slice(0, -2) : token;
|
|
130
|
+
if (base !== field.type) {
|
|
131
|
+
throw new PrismaFieldProjectionError(`${resource}.${field.name} is written \`${field.type}\` in schema.prisma and the 🟢 types carry the FieldRef token \`${token}\`. The two readers describe one instant, so this is a stale generated client or a reader defect, not a schema the adapter can translate.`);
|
|
132
|
+
}
|
|
133
|
+
if (listed !== field.list) {
|
|
134
|
+
throw new PrismaFieldProjectionError(`${resource}.${field.name} is ${field.list ? "a list" : "not a list"} in schema.prisma and the 🟢 types carry the FieldRef token \`${token}\`, which says the opposite.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/* ------------------------------------------------------------------ *
|
|
138
|
+
* Lifecycle (C-811) *
|
|
139
|
+
* ------------------------------------------------------------------ */
|
|
140
|
+
/**
|
|
141
|
+
* The three adapter traits in core's own declaration order, so a trait list has
|
|
142
|
+
* one canonical order and it is not this module's invention. Order is semantic:
|
|
143
|
+
* it enters the Contract hash.
|
|
144
|
+
*/
|
|
145
|
+
const ADAPTER_TRAIT_ORDER = LIFECYCLE_TRAITS.filter((trait) => trait === "DEFAULTED" ||
|
|
146
|
+
trait === "GENERATED_ON_CREATE" ||
|
|
147
|
+
trait === "GENERATED_ON_UPDATE");
|
|
148
|
+
/** An argument that begins with an identifier and an open paren: a generator form. */
|
|
149
|
+
const GENERATOR_FORM = /^[A-Za-z_][A-Za-z0-9_]*\(/;
|
|
150
|
+
/**
|
|
151
|
+
* T-3.1's `@default` table, looked up — never evaluated. The argument arrives as
|
|
152
|
+
* opaque text precisely so that this function can be a lookup.
|
|
153
|
+
*/
|
|
154
|
+
export function matchPrismaDefault(argument) {
|
|
155
|
+
for (const row of PRISMA_DEFAULT_TABLE) {
|
|
156
|
+
const matched = "literal" in row.match
|
|
157
|
+
? row.match.literal === argument
|
|
158
|
+
: row.match.pattern.test(argument);
|
|
159
|
+
if (matched) {
|
|
160
|
+
return { form: "table", traits: row.traits, evidence: row.evidence };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return GENERATOR_FORM.test(argument)
|
|
164
|
+
? {
|
|
165
|
+
form: "unrecognised-generator",
|
|
166
|
+
traits: PRISMA_UNMATCHED_DEFAULT_TRAITS,
|
|
167
|
+
}
|
|
168
|
+
: { form: "static", traits: PRISMA_UNMATCHED_DEFAULT_TRAITS };
|
|
169
|
+
}
|
|
170
|
+
function projectLifecycle(resource, field) {
|
|
171
|
+
const present = new Set();
|
|
172
|
+
const unknowns = [];
|
|
173
|
+
if (field.defaultArgument !== null) {
|
|
174
|
+
const outcome = matchPrismaDefault(field.defaultArgument);
|
|
175
|
+
for (const trait of outcome.traits) {
|
|
176
|
+
present.add(trait);
|
|
177
|
+
}
|
|
178
|
+
if (outcome.form === "unrecognised-generator") {
|
|
179
|
+
// Recorded as the same `PrismaSchemaUnknown` the PSL reader records, so
|
|
180
|
+
// it fails the generate step by construction rather than through a second
|
|
181
|
+
// rule written for it. An unrecognised generator function is a build stop,
|
|
182
|
+
// not a column Aventara quietly decides the client must supply.
|
|
183
|
+
unknowns.push({
|
|
184
|
+
kind: "attribute-argument",
|
|
185
|
+
spelling: "@default",
|
|
186
|
+
text: field.defaultArgument,
|
|
187
|
+
block: resource,
|
|
188
|
+
site: field.site,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (field.updatedAt) {
|
|
193
|
+
present.add("GENERATED_ON_UPDATE");
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
traits: ADAPTER_TRAIT_ORDER.filter((trait) => present.has(trait)),
|
|
197
|
+
unknowns,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* TYPE's only contribution to lifecycle: a check, run in one direction.
|
|
202
|
+
*
|
|
203
|
+
* Every field the 🟢 `<M>UncheckedCreateInput` marks optional must carry a PSL
|
|
204
|
+
* cause. A field the types mark optional with no cause means **one of the two
|
|
205
|
+
* readers is wrong** — that is the entire signal, and it is why the marker is
|
|
206
|
+
* never read as a trait. Names the types mark that this model does not project
|
|
207
|
+
* as a scalar (relation edges) are skipped rather than reported: their
|
|
208
|
+
* optionality is structural.
|
|
209
|
+
*/
|
|
210
|
+
export function crossCheckCreateOptional(input) {
|
|
211
|
+
const byName = new Map(input.fields.map((field) => [field.name, field]));
|
|
212
|
+
const explained = [];
|
|
213
|
+
const unexplained = [];
|
|
214
|
+
const messages = [];
|
|
215
|
+
for (const name of input.createOptional) {
|
|
216
|
+
const projected = byName.get(name);
|
|
217
|
+
if (projected === undefined) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const cause = causeOf(projected);
|
|
221
|
+
if (cause === null) {
|
|
222
|
+
unexplained.push(name);
|
|
223
|
+
messages.push(`\`${input.resource}.${name}\` is optional in the generated \`${input.resource}UncheckedCreateInput\` and schema.prisma gives no reason: it carries no \`@default\`, no \`@updatedAt\`, no \`?\` and no \`[]\`. The marker is one bit over several causes and is never read as a trait, so a field optional in the types with none of them in PSL means one of the two readers is wrong.`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
explained.push({ name, cause });
|
|
227
|
+
}
|
|
228
|
+
return { explained, unexplained, messages };
|
|
229
|
+
}
|
|
230
|
+
function causeOf(projected) {
|
|
231
|
+
// Lifecycle first, in core's order, so a field with both a default and a
|
|
232
|
+
// nullable column reports the stronger cause.
|
|
233
|
+
const trait = ADAPTER_TRAIT_ORDER.filter((candidate) => projected.lifecycle.includes(candidate));
|
|
234
|
+
// `GENERATED_ON_CREATE` outranks the `DEFAULTED` that always accompanies it:
|
|
235
|
+
// it is the more specific reason the client may omit the value.
|
|
236
|
+
const strongest = trait.includes("GENERATED_ON_CREATE")
|
|
237
|
+
? "GENERATED_ON_CREATE"
|
|
238
|
+
: trait[0];
|
|
239
|
+
if (strongest !== undefined) {
|
|
240
|
+
return strongest;
|
|
241
|
+
}
|
|
242
|
+
if (projected.nullable) {
|
|
243
|
+
return "nullable";
|
|
244
|
+
}
|
|
245
|
+
return projected.list ? "list" : null;
|
|
246
|
+
}
|
|
247
|
+
/* ------------------------------------------------------------------ *
|
|
248
|
+
* Relations (C-813) *
|
|
249
|
+
* ------------------------------------------------------------------ */
|
|
250
|
+
function projectRelationField(input) {
|
|
251
|
+
const { resource, field, siblings, surface } = input;
|
|
252
|
+
const column = `${resource}.${field.name}`;
|
|
253
|
+
const exposed = surface.relationFields[field.name];
|
|
254
|
+
if (exposed === undefined) {
|
|
255
|
+
return {
|
|
256
|
+
outcome: "excluded",
|
|
257
|
+
diagnostic: {
|
|
258
|
+
code: "not-exposed",
|
|
259
|
+
message: `${column} is declared in schema.prisma as an edge to \`${field.type}\` and absent from the generated 🟢 types, so it is omitted: ${field.ignored ? "it carries `@ignore`" : "no schema attribute explains it — a provider drop or a preview-feature exclusion"}.`,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const cardinality = field.list ? "many" : "one";
|
|
264
|
+
// A to-many edge is a collection, and an absent collection is an empty one:
|
|
265
|
+
// never nullable. A to-one edge is nullable exactly where PSL wrote `?`.
|
|
266
|
+
const nullable = cardinality === "many" ? false : field.optional;
|
|
267
|
+
// TYPE is the cross-check, from `$<M>Payload.objects`: `mentor:
|
|
268
|
+
// $AuthorPayload | null`, `mentees: $AuthorPayload[]`.
|
|
269
|
+
if (exposed.many !== field.list) {
|
|
270
|
+
throw new PrismaFieldProjectionError(`${column} is ${field.list ? "a list" : "a single edge"} in schema.prisma and the 🟢 payload says the opposite.`);
|
|
271
|
+
}
|
|
272
|
+
if (exposed.nullable !== nullable) {
|
|
273
|
+
throw new PrismaFieldProjectionError(`${column} is ${nullable ? "optional" : "required"} in schema.prisma and the 🟢 payload ${exposed.nullable ? "admits" : "refuses"} \`null\`.`);
|
|
274
|
+
}
|
|
275
|
+
if (exposed.target !== field.type) {
|
|
276
|
+
throw new PrismaFieldProjectionError(`${column} targets \`${field.type}\` in schema.prisma and \`${exposed.target}\` on the 🟢 payload.`);
|
|
277
|
+
}
|
|
278
|
+
assertForeignKeyAgrees(column, field, siblings, nullable);
|
|
279
|
+
return {
|
|
280
|
+
outcome: "relation",
|
|
281
|
+
name: field.name,
|
|
282
|
+
// Four members. No target field schema, no FK, no join-table name, no
|
|
283
|
+
// owning-side metadata (C-813).
|
|
284
|
+
field: {
|
|
285
|
+
kind: "relation",
|
|
286
|
+
target: field.type,
|
|
287
|
+
cardinality,
|
|
288
|
+
nullable,
|
|
289
|
+
},
|
|
290
|
+
edge: {
|
|
291
|
+
resource,
|
|
292
|
+
field: field.name,
|
|
293
|
+
target: field.type,
|
|
294
|
+
cardinality,
|
|
295
|
+
relationName: field.relation?.name ?? null,
|
|
296
|
+
// Carried, not projected: the action changes what a delete DOES, so it
|
|
297
|
+
// is evidence a transaction planner needs, and it is not a member of
|
|
298
|
+
// `AdapterRelationField` (see the edge's own doc).
|
|
299
|
+
onDelete: field.relation?.onDelete ?? null,
|
|
300
|
+
onUpdate: field.relation?.onUpdate ?? null,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The FK, read and then discarded.
|
|
306
|
+
*
|
|
307
|
+
* `fields: […]` is used for exactly two things, both of them checks: each named
|
|
308
|
+
* scalar must be a column this model declares, and an **optional FK forces an
|
|
309
|
+
* optional edge**. PSL enforces the second itself — `prisma validate` 7.10.0
|
|
310
|
+
* answers *"The relation field `author` uses the scalar fields authorId. At
|
|
311
|
+
* least one of those fields is optional. Hence the relation field must be
|
|
312
|
+
* optional as well."* — so a violation can only come from a reader defect.
|
|
313
|
+
*
|
|
314
|
+
* The converse does **not** hold, and assuming it would be wrong: `mentor
|
|
315
|
+
* Author? @relation(fields: [mentorId])` with `mentorId String` **required** is
|
|
316
|
+
* valid PSL at 7.10.0 and its client still types `mentor: $AuthorPayload |
|
|
317
|
+
* null`. So the FK is not the source of nullability — the edge's own `?` is.
|
|
318
|
+
*
|
|
319
|
+
* Nothing about the FK enters the projection.
|
|
320
|
+
*/
|
|
321
|
+
function assertForeignKeyAgrees(column, field, siblings, nullable) {
|
|
322
|
+
for (const name of field.relation?.fields ?? []) {
|
|
323
|
+
const scalar = siblings.find((sibling) => sibling.name === name);
|
|
324
|
+
if (scalar === undefined) {
|
|
325
|
+
throw new PrismaFieldProjectionError(`${column} names \`${name}\` in \`@relation(fields: …)\` and the model declares no such column.`);
|
|
326
|
+
}
|
|
327
|
+
if (scalar.optional && !nullable) {
|
|
328
|
+
throw new PrismaFieldProjectionError(`${column} is required and its foreign key \`${name}\` is optional. PSL forbids this combination, so the schema cannot have said it: one of the readers is wrong.`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Pairs each edge with its back-reference, which **only PSL can do**.
|
|
334
|
+
*
|
|
335
|
+
* An explicit `@relation("Name")` is the pairing evidence, and for the two
|
|
336
|
+
* `Author` → `Author` edges it is the *only* evidence: the name appears nowhere
|
|
337
|
+
* on the 🟢 surface, so from the types alone there are two self-edges and
|
|
338
|
+
* nothing to say they are one relation.
|
|
339
|
+
*
|
|
340
|
+
* Unnamed edges are paired by the unordered model pair, which is unambiguous
|
|
341
|
+
* because PSL requires a name as soon as two relations connect the same two
|
|
342
|
+
* models. Where an unordered pair still has more than two edges, they are left
|
|
343
|
+
* unpaired rather than guessed: the implicit name Prisma would compute lives in
|
|
344
|
+
* the 🛑 runtime data model, and re-deriving it here would be this module
|
|
345
|
+
* reimplementing the generator.
|
|
346
|
+
*/
|
|
347
|
+
export function pairPrismaRelationEdges(edges) {
|
|
348
|
+
const groups = new Map();
|
|
349
|
+
for (const edge of edges) {
|
|
350
|
+
const key = edge.relationName === null
|
|
351
|
+
? `implicit:${[edge.resource, edge.target].sort().join("<->")}`
|
|
352
|
+
: `named:${edge.relationName}`;
|
|
353
|
+
const group = groups.get(key);
|
|
354
|
+
if (group === undefined) {
|
|
355
|
+
groups.set(key, [edge]);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
group.push(edge);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const pairs = [];
|
|
362
|
+
const unpaired = [];
|
|
363
|
+
for (const group of [...groups.keys()].sort().map((key) => groups.get(key))) {
|
|
364
|
+
const [first, second, ...rest] = group ?? [];
|
|
365
|
+
if (first === undefined) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (second === undefined || rest.length > 0) {
|
|
369
|
+
unpaired.push(...(group ?? []));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (first.resource !== second.target || first.target !== second.resource) {
|
|
373
|
+
unpaired.push(first, second);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
pairs.push(sortKey(first) <= sortKey(second) ? [first, second] : [second, first]);
|
|
377
|
+
}
|
|
378
|
+
// Sorted by the first edge, so the answer's order is a property of the model
|
|
379
|
+
// and not of how the groups happened to be keyed.
|
|
380
|
+
return {
|
|
381
|
+
pairs: [...pairs].sort((left, right) => sortKey(left[0]).localeCompare(sortKey(right[0]))),
|
|
382
|
+
unpaired,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function sortKey(edge) {
|
|
386
|
+
return `${edge.resource}.${edge.field}`;
|
|
387
|
+
}
|