@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,341 @@
|
|
|
1
|
+
import { BUILT_IN_SCALARS, } from "@aventara/core";
|
|
2
|
+
import { deriveRelationCapabilities, deriveScalarCapabilities, } from "./capability.deriver.js";
|
|
3
|
+
import { mapPrismaEnum } from "./enum.mapper.js";
|
|
4
|
+
import { crossCheckCreateOptional, projectPrismaField, } from "./field.mapper.js";
|
|
5
|
+
import { mapPrismaIdentifiers } from "./identifier.mapper.js";
|
|
6
|
+
import { deriveResourceOperations } from "./operations.deriver.js";
|
|
7
|
+
/**
|
|
8
|
+
* Task 6 โ **composition**: the one place the two readers' facts become the two
|
|
9
|
+
* halves of the discovery artifact.
|
|
10
|
+
*
|
|
11
|
+
* Every rule it applies belongs to somebody else. That is the design, not a
|
|
12
|
+
* shortcut: the scalar table is `scalar.mapper.ts`'s, the field projection and the
|
|
13
|
+
* lifecycle traits are `field.mapper.ts`'s, identity is `identifier.mapper.ts`'s,
|
|
14
|
+
* capabilities are `capability.deriver.ts`'s, the ยง8.3 variant gate is
|
|
15
|
+
* `operations.deriver.ts`'s and nowhere else, and the enum comparison is
|
|
16
|
+
* `enum.mapper.ts`'s. This module **orders and assembles**, and it owns exactly
|
|
17
|
+
* two judgements of its own:
|
|
18
|
+
*
|
|
19
|
+
* 1. **Which diagnostics are fatal.** A mapper answers with a diagnostic; whether
|
|
20
|
+
* the build stops is composition's call, because only composition knows that a
|
|
21
|
+
* model with an unmapped column cannot produce a Contract at all.
|
|
22
|
+
* 2. **The scalar registry.** `ScalarRegistry` is the set of Aventara scalar
|
|
23
|
+
* identities the discovered fields actually use โ a fact no single field can
|
|
24
|
+
* state and only the whole pass can.
|
|
25
|
+
*
|
|
26
|
+
* # Three halves, from one pass over one field list
|
|
27
|
+
*
|
|
28
|
+
* `model` is core's: canonicalized, hashed, compiled into the Contract.
|
|
29
|
+
* `translation` is the adapter's Prisma addressing, and `referentialActions` is
|
|
30
|
+
* what the resolved `@relation(onDelete:/onUpdate:)` arguments say. All three are
|
|
31
|
+
* built from the SAME pass: the translation table from the same
|
|
32
|
+
* `mapPrismaIdentifiers` answer the model's `identifiers` come from โ `.members`
|
|
33
|
+
* into the model, the whole address into the table โ and the action table from the
|
|
34
|
+
* same `projectPrismaField` answer the model's relation fields come from. Two
|
|
35
|
+
* parallel passes would have to be kept in step; one pass cannot drift.
|
|
36
|
+
*
|
|
37
|
+
* `PrismaTranslationTable<M>` and `PrismaReferentialActionTable<M>` are mapped
|
|
38
|
+
* types over the model's resources, so a resource this module adds to the model
|
|
39
|
+
* and forgets to address โ or forgets to answer the actions for โ is a **compile
|
|
40
|
+
* error** rather than a runtime `undefined` at the first unique read or the first
|
|
41
|
+
* cascade question.
|
|
42
|
+
*
|
|
43
|
+
* The action table carries **only owning edges**, because only an owning edge has
|
|
44
|
+
* an action: PSL forbids the back-reference from declaring one, so the
|
|
45
|
+
* back-reference's answer is its partner's. Its direction is the one documented on
|
|
46
|
+
* `PrismaReferentialEdge` and on `field.mapper.ts`'s `PrismaRelationEdge` โ
|
|
47
|
+
* *deleting a `target` does `onDelete` to rows of the enclosing resource* โ and it
|
|
48
|
+
* is carried out of field projection unchanged rather than re-derived here.
|
|
49
|
+
*
|
|
50
|
+
* # No provider reaches this module, and that is enforced by the input
|
|
51
|
+
*
|
|
52
|
+
* The composer is created with a {@link TransactionCapability} and nothing else.
|
|
53
|
+
* `provider` is not a member anywhere on the path, so `if (provider === โฆ)` is a
|
|
54
|
+
* compile error rather than a convention (dictionary invariant 6, D-802). The
|
|
55
|
+
* generate step keeps `provider` only to stamp the artifact, which is a recorded
|
|
56
|
+
* fact and not a branch.
|
|
57
|
+
*
|
|
58
|
+
* `transactions` is a **driver** fact (T-2) and the driver is a runtime choice, so
|
|
59
|
+
* it is declared to the composer at generate time. The two cannot silently
|
|
60
|
+
* disagree: an artifact that says `"interactive"` loaded by an adapter whose
|
|
61
|
+
* declared driver says `"none"` mounts no `transaction`, and core refuses
|
|
62
|
+
* construction with `DISCOVERY_TRANSACTION_CAPABILITY_MISMATCH` **[CORE,
|
|
63
|
+
* framework.factory.ts]** โ a hard, legible failure rather than a silent one.
|
|
64
|
+
*
|
|
65
|
+
* # The C-818 conflict this header used to record is closed
|
|
66
|
+
*
|
|
67
|
+
* It read: a relation carries create directives for `create.one` and none for
|
|
68
|
+
* `create.many` **[DOC7]**, `AdapterRelationField.capabilities` is one record per
|
|
69
|
+
* field with a single `create` member, so composition had to ask
|
|
70
|
+
* `deriveRelationCapabilities` for the `"create.one"` answer โ the permissive one โ
|
|
71
|
+
* and C-818's restriction was unrepresentable in the Contract.
|
|
72
|
+
*
|
|
73
|
+
* The capability members are **scope-qualified** now, so the one record holds both
|
|
74
|
+
* answers: the create stage is advertised at the `nested` write scope and not at the
|
|
75
|
+
* bulk one. Composition asks for no variant, passes no `operation`, and advertises
|
|
76
|
+
* nothing it cannot honour. The mutation translator keeps its refusal โ it is the
|
|
77
|
+
* belt to core's braces, not the only guard (C-855) โ but it is no longer the ONLY
|
|
78
|
+
* place the restriction exists.
|
|
79
|
+
*/
|
|
80
|
+
/** Composition refuses: the collected diagnostics, named and reported together. */
|
|
81
|
+
export class PrismaModelCompositionError extends Error {
|
|
82
|
+
name = "PrismaModelCompositionError";
|
|
83
|
+
diagnostics;
|
|
84
|
+
constructor(diagnostics) {
|
|
85
|
+
super([
|
|
86
|
+
"discovery cannot compose an AdapterModel from this schema and client:",
|
|
87
|
+
...diagnostics.map((diagnostic) => ` - ${diagnostic}`),
|
|
88
|
+
].join("\n"));
|
|
89
|
+
this.diagnostics = diagnostics;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The generate step's `composeModel` seam, filled.
|
|
94
|
+
*
|
|
95
|
+
* A factory rather than a bare function because the step's seam takes only the
|
|
96
|
+
* readers' output โ order and refusal over external effects is the step's job, and
|
|
97
|
+
* the declared driver fact is the caller's.
|
|
98
|
+
*/
|
|
99
|
+
export function createPrismaModelComposer(options) {
|
|
100
|
+
return (input) => discoverPrismaModel({ ...input, ...options });
|
|
101
|
+
}
|
|
102
|
+
export function discoverPrismaModel(input) {
|
|
103
|
+
const diagnostics = [];
|
|
104
|
+
const declared = new Map(input.facts.models.map((m) => [m.name, m]));
|
|
105
|
+
const modelNames = new Set(declared.keys());
|
|
106
|
+
const admitted = new Set(input.admitted);
|
|
107
|
+
const enums = composeEnums(input, diagnostics);
|
|
108
|
+
const resources = {};
|
|
109
|
+
const translation = {};
|
|
110
|
+
const referentialActions = {};
|
|
111
|
+
const usedScalars = new Set();
|
|
112
|
+
for (const name of input.admitted) {
|
|
113
|
+
const model = declared.get(name);
|
|
114
|
+
const exposed = input.surface.resources[name];
|
|
115
|
+
if (model === undefined || exposed === undefined) {
|
|
116
|
+
// The existence gate admitted it, so both halves must be there. A
|
|
117
|
+
// diagnostic rather than a throw: the caller gets the whole list.
|
|
118
|
+
diagnostics.push(`\`${name}\` was admitted by the existence gate and is absent from ${model === undefined ? "the PSL facts" : "the capability surface"}, which the gate cannot produce โ this is a reader defect.`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const composed = composeResource({
|
|
122
|
+
model,
|
|
123
|
+
exposed,
|
|
124
|
+
surface: input.surface,
|
|
125
|
+
modelNames,
|
|
126
|
+
admitted,
|
|
127
|
+
transactions: input.transactions,
|
|
128
|
+
diagnostics,
|
|
129
|
+
usedScalars,
|
|
130
|
+
});
|
|
131
|
+
resources[name] = composed.resource;
|
|
132
|
+
translation[name] = composed.translation;
|
|
133
|
+
referentialActions[name] = composed.referentialActions;
|
|
134
|
+
}
|
|
135
|
+
if (diagnostics.length > 0) {
|
|
136
|
+
throw new PrismaModelCompositionError(diagnostics);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
model: {
|
|
140
|
+
scalars: composeScalarRegistry(usedScalars),
|
|
141
|
+
enums,
|
|
142
|
+
resources,
|
|
143
|
+
transactions: input.transactions,
|
|
144
|
+
},
|
|
145
|
+
translation,
|
|
146
|
+
referentialActions,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/* ------------------------------------------------------------------ *
|
|
150
|
+
* Enums *
|
|
151
|
+
* ------------------------------------------------------------------ */
|
|
152
|
+
function composeEnums(input, diagnostics) {
|
|
153
|
+
const enums = {};
|
|
154
|
+
for (const declared of input.facts.enums) {
|
|
155
|
+
// The mapper's judgement, forwarded. Declaration order is semantic and
|
|
156
|
+
// enters the Contract hash, so "diverged" is fatal here for the same reason
|
|
157
|
+
// `reader-crosscheck.ts` reports it: the client is from another schema.
|
|
158
|
+
const mapped = mapPrismaEnum({
|
|
159
|
+
declared,
|
|
160
|
+
exposed: input.surface.enums[declared.name],
|
|
161
|
+
});
|
|
162
|
+
if (mapped.outcome === "enum") {
|
|
163
|
+
enums[mapped.name] = mapped.definition;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (mapped.outcome === "diverged") {
|
|
167
|
+
diagnostics.push(mapped.diagnostic.message);
|
|
168
|
+
}
|
|
169
|
+
// `excluded` is the existence rule applied to an enum: declared in PSL and
|
|
170
|
+
// absent from the generated `enums.ts`. It is NOT fatal, exactly as an
|
|
171
|
+
// `@ignore`d model is not: an enum the client cannot name has no place in a
|
|
172
|
+
// Contract, and any column typed by it is itself absent from the ๐ข surface
|
|
173
|
+
// and therefore already excluded by the field-level gate.
|
|
174
|
+
}
|
|
175
|
+
return enums;
|
|
176
|
+
}
|
|
177
|
+
/* ------------------------------------------------------------------ *
|
|
178
|
+
* One resource *
|
|
179
|
+
* ------------------------------------------------------------------ */
|
|
180
|
+
function composeResource(input) {
|
|
181
|
+
const { model, exposed, diagnostics } = input;
|
|
182
|
+
const fields = {};
|
|
183
|
+
/** Owning edges only, in PSL field-declaration order (see this module's header). */
|
|
184
|
+
const owningEdges = [];
|
|
185
|
+
const unknowns = [];
|
|
186
|
+
/** What the create-optional cross-check consumes: PSL's own answers. */
|
|
187
|
+
const projectedScalars = [];
|
|
188
|
+
for (const field of model.fields) {
|
|
189
|
+
const projection = projectPrismaField({
|
|
190
|
+
resource: model.name,
|
|
191
|
+
field,
|
|
192
|
+
siblings: model.fields,
|
|
193
|
+
surface: exposed,
|
|
194
|
+
enums: input.surface.enums,
|
|
195
|
+
models: input.modelNames,
|
|
196
|
+
});
|
|
197
|
+
switch (projection.outcome) {
|
|
198
|
+
case "excluded":
|
|
199
|
+
// The existence gate has already decided this absence is explained;
|
|
200
|
+
// re-deciding it here would be a second owner for C-808.
|
|
201
|
+
continue;
|
|
202
|
+
case "unmapped":
|
|
203
|
+
diagnostics.push(projection.diagnostic.message);
|
|
204
|
+
continue;
|
|
205
|
+
case "scalar": {
|
|
206
|
+
unknowns.push(...projection.unknowns);
|
|
207
|
+
const scalarSurface = exposed.scalarFields[field.name];
|
|
208
|
+
if (scalarSurface === undefined) {
|
|
209
|
+
diagnostics.push(`\`${model.name}.${field.name}\` projected as a scalar and carries no scalar surface, which is a reader defect.`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const type = projection.field.type;
|
|
213
|
+
if ("scalar" in type) {
|
|
214
|
+
// Narrowed by membership rather than asserted: `AdapterScalarField`'s
|
|
215
|
+
// type ref is `keyof M["scalars"]`, which is `string` on the general
|
|
216
|
+
// model, and the registry must carry built-in identities only.
|
|
217
|
+
const scalar = BUILT_IN_SCALARS.find((candidate) => candidate === type.scalar);
|
|
218
|
+
if (scalar === undefined) {
|
|
219
|
+
diagnostics.push(`\`${model.name}.${field.name}\` mapped to the scalar identity \`${type.scalar}\`, which is not one of Aventara's built-in scalars (${BUILT_IN_SCALARS.join(", ")}).`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
input.usedScalars.add(scalar);
|
|
223
|
+
}
|
|
224
|
+
fields[projection.name] = {
|
|
225
|
+
...projection.field,
|
|
226
|
+
capabilities: deriveScalarCapabilities({ surface: scalarSurface }),
|
|
227
|
+
};
|
|
228
|
+
projectedScalars.push({
|
|
229
|
+
name: projection.name,
|
|
230
|
+
nullable: projection.field.nullable,
|
|
231
|
+
list: projection.field.list,
|
|
232
|
+
lifecycle: projection.field.lifecycle,
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
case "relation": {
|
|
237
|
+
const relationSurface = exposed.relationFields[field.name];
|
|
238
|
+
if (relationSurface === undefined) {
|
|
239
|
+
diagnostics.push(`\`${model.name}.${field.name}\` projected as a relation and carries no relation surface, which is a reader defect.`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!input.admitted.has(projection.field.target)) {
|
|
243
|
+
// An edge to a resource the model does not carry would compile into a
|
|
244
|
+
// Contract that references a Resource that is not there.
|
|
245
|
+
diagnostics.push(`\`${model.name}.${field.name}\` targets \`${projection.field.target}\`, which the existence gate did not admit, so the edge would point at a Resource the model does not carry.`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
fields[projection.name] = {
|
|
249
|
+
...projection.field,
|
|
250
|
+
// No variant is named: the scope-qualified members carry both create
|
|
251
|
+
// answers at once. See this module's header for the conflict that
|
|
252
|
+
// closed.
|
|
253
|
+
capabilities: deriveRelationCapabilities({
|
|
254
|
+
surface: relationSurface,
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
// The edge's resolved actions, carried out of the SAME projection the
|
|
258
|
+
// field above came from. Both members are non-null exactly on an owning
|
|
259
|
+
// edge, and PSL forbids a back-reference from declaring either โ so the
|
|
260
|
+
// pair is tested together rather than one member at a time, and a
|
|
261
|
+
// back-reference contributes nothing instead of contributing two nulls.
|
|
262
|
+
if (projection.edge.onDelete !== null &&
|
|
263
|
+
projection.edge.onUpdate !== null) {
|
|
264
|
+
owningEdges.push({
|
|
265
|
+
field: projection.edge.field,
|
|
266
|
+
// The PARENT. Deleting one of these does `onDelete` to rows of
|
|
267
|
+
// `model.name`, which is the key this list hangs under.
|
|
268
|
+
target: projection.edge.target,
|
|
269
|
+
cardinality: projection.edge.cardinality,
|
|
270
|
+
onDelete: projection.edge.onDelete,
|
|
271
|
+
onUpdate: projection.edge.onUpdate,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (unknowns.length > 0) {
|
|
279
|
+
// T-3.1 invariant 5: an unrecognised construct is a build stop, never a
|
|
280
|
+
// column Aventara quietly decides the client must supply.
|
|
281
|
+
for (const unknown of unknowns) {
|
|
282
|
+
diagnostics.push(`${unknown.spelling} (${unknown.kind}) in \`${unknown.block ?? "<top level>"}\` at ${unknown.site.file}:${unknown.site.line}: ${unknown.text} โ the reader does not model it, and a silently dropped construct is how a grammar change becomes wrong output instead of a build stop.`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Task 4's cross-check, run from the SURFACE's own `createOptional` bit. The
|
|
286
|
+
// marker is one bit over several causes and is never read as a trait; this is
|
|
287
|
+
// the one direction it is read in.
|
|
288
|
+
const createOptional = Object.entries(exposed.scalarFields)
|
|
289
|
+
.filter(([, scalar]) => scalar.createOptional)
|
|
290
|
+
.map(([name]) => name);
|
|
291
|
+
const creatability = crossCheckCreateOptional({
|
|
292
|
+
resource: model.name,
|
|
293
|
+
fields: projectedScalars,
|
|
294
|
+
createOptional,
|
|
295
|
+
});
|
|
296
|
+
diagnostics.push(...creatability.messages);
|
|
297
|
+
const identifiers = mapPrismaIdentifiers(model);
|
|
298
|
+
const members = identifiers.identifiers.map((address) => address.members);
|
|
299
|
+
return {
|
|
300
|
+
resource: {
|
|
301
|
+
fields,
|
|
302
|
+
identifiers: members,
|
|
303
|
+
// The ยง8.3 gate's single home. Composition supplies its three inputs and
|
|
304
|
+
// omits nothing itself.
|
|
305
|
+
operations: deriveResourceOperations({
|
|
306
|
+
operations: exposed.operations,
|
|
307
|
+
identifiers: members,
|
|
308
|
+
transactions: input.transactions,
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
// The same list's other projection: `members` above, the whole address here.
|
|
312
|
+
translation: { identifierKeys: identifiers.identifiers },
|
|
313
|
+
referentialActions: { owningEdges },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/* ------------------------------------------------------------------ *
|
|
317
|
+
* The scalar registry *
|
|
318
|
+
* ------------------------------------------------------------------ */
|
|
319
|
+
/**
|
|
320
|
+
* The Aventara scalar identities the discovered fields actually use, in core's
|
|
321
|
+
* own `BUILT_IN_SCALARS` declaration order.
|
|
322
|
+
*
|
|
323
|
+
* Ordered by the union rather than by field declaration order for the reason
|
|
324
|
+
* `capability.deriver.ts` orders its lists that way: the canonical form must be a
|
|
325
|
+
* property of the model and not of how a schema happened to be written. (The
|
|
326
|
+
* emitted artifact is canonicalized again before hashing, so this order is for
|
|
327
|
+
* readers of the model in memory; it costs nothing and removes a question.)
|
|
328
|
+
*
|
|
329
|
+
* An enum-backed column contributes NO scalar: C-810 makes it a ScalarField whose
|
|
330
|
+
* type ref takes the `enum` arm, and the enum registry is where its identity
|
|
331
|
+
* lives.
|
|
332
|
+
*/
|
|
333
|
+
function composeScalarRegistry(used) {
|
|
334
|
+
const scalars = {};
|
|
335
|
+
for (const scalar of BUILT_IN_SCALARS) {
|
|
336
|
+
if (used.has(scalar)) {
|
|
337
|
+
scalars[scalar] = { builtin: true };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return scalars;
|
|
341
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { AdapterResource, ResourceOperations, TransactionCapability } from "@aventara/core";
|
|
2
|
+
import type { PrismaAdapterModel } from "./discovery-artifact.interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* T-6 โ `deriveResourceOperations`, **the single home of the ยง8.3 gate**.
|
|
5
|
+
*
|
|
6
|
+
* ยง8.3: *"an adapter โฆ MUST advertise the variant only when it can preserve
|
|
7
|
+
* framework semantics atomically enough for the providerโฆ If the provider cannot
|
|
8
|
+
* guarantee the required semantics, the variant is omitted."* Every omission in
|
|
9
|
+
* Phase 8 is decided here. No mapper, no discoverer, no emitter and no executor
|
|
10
|
+
* may drop a variant, because an omission in two places is an omission nobody can
|
|
11
|
+
* audit.
|
|
12
|
+
*
|
|
13
|
+
* # Three inputs, and `provider` is not one of them
|
|
14
|
+
*
|
|
15
|
+
* The available operation set is read from the client's own `<M>Delegate` (T-3) โ
|
|
16
|
+
* at 7.10.0, identically on both Phase 8 engines and on all six models (four
|
|
17
|
+
* when this was first measured; re-measured over `User` and `Profile` too when
|
|
18
|
+
* the one-to-one fixture landed, and the set is unchanged):
|
|
19
|
+
*
|
|
20
|
+
* ```text
|
|
21
|
+
* findUnique, findUniqueOrThrow, findFirst, findFirstOrThrow, findMany, create,
|
|
22
|
+
* createMany, createManyAndReturn, delete, update, deleteMany, updateMany,
|
|
23
|
+
* updateManyAndReturn, upsert, count, aggregate, groupBy
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* The second input is the model's identifiers, because `find.unique`,
|
|
27
|
+
* `update.unique`, `delete.unique` and `upsert.unique` are meaningless without
|
|
28
|
+
* one. The third is `transactions` โ the **only** declared profile row this gate
|
|
29
|
+
* needs. The whole `PrismaProviderProfile` is deliberately NOT accepted: it
|
|
30
|
+
* carries `provider`, and a module that cannot be handed a provider cannot branch
|
|
31
|
+
* on one (dictionary invariant 6). `isolationLevels` is not a precondition of any
|
|
32
|
+
* variant (C-830: Phase 8 escalates no isolation).
|
|
33
|
+
*
|
|
34
|
+
* `supportsEmulatedFirstVariants` is **derived, exactly `transactions !== "none"`,
|
|
35
|
+
* and stored nowhere** (T-2's amendment, D-824). A stored copy would be a second
|
|
36
|
+
* source of truth for a one-line derivation.
|
|
37
|
+
*
|
|
38
|
+
* # What is NOT gated here, and why that is deliberate
|
|
39
|
+
*
|
|
40
|
+
* `delete.many` is absent on **every** profile (C-829, D-826): 7.10.0 has no
|
|
41
|
+
* `deleteManyAndReturn`, the architect ruled the variant omitted rather than
|
|
42
|
+
* emulated, and the omission does **not** vary with `transactions`. Pairing it
|
|
43
|
+
* with the emulation gate would re-teach the read-capability-depends-on-
|
|
44
|
+
* transaction-capability coupling C-829 records as no longer existing.
|
|
45
|
+
*
|
|
46
|
+
* The first-style variants' preconditions are the **read**, an identifier, a
|
|
47
|
+
* transaction โ the three `emulatedFirst` carries, because all three are shared โ
|
|
48
|
+
* and then **the chosen route's own write call**, which each family contributes
|
|
49
|
+
* itself. ยง14e's route is **Route A**: the read selects the identifier and the
|
|
50
|
+
* write is Prisma's own `update`/`delete` narrowed to it with the caller's
|
|
51
|
+
* predicate re-asserted under `AND`. Task 10 measured both routes on the real
|
|
52
|
+
* 7.10.0 client on both engines, every row came out for Route A, and the choice is
|
|
53
|
+
* propagated across the 21 rows of `translation/operation.translator.ts`'s table;
|
|
54
|
+
* `FIRST_VARIANT_WRITE_ROUTE` there is the one cell that names it.
|
|
55
|
+
*
|
|
56
|
+
* So `update.first` reads `emulatedFirst && exposes("update")` and `delete.first`
|
|
57
|
+
* reads `emulatedFirst && exposes("delete")`. Under Route B those cells would read
|
|
58
|
+
* `updateManyAndReturn` and `deleteMany` instead โ which is exactly why the write
|
|
59
|
+
* call is NOT folded into `emulatedFirst`: a shared precondition naming one route's
|
|
60
|
+
* call would assert a gate the other route does not have, an assertion that cannot
|
|
61
|
+
* fail for the right reason (C-817, T-7.1.1). A reversal of ยง14e is an edit to
|
|
62
|
+
* these two cells and to the translator's one, and to nothing else here.
|
|
63
|
+
*/
|
|
64
|
+
/** Core's own identifier tuples for a discovered Resource. */
|
|
65
|
+
type DiscoveredIdentifiers = AdapterResource<PrismaAdapterModel>["identifiers"];
|
|
66
|
+
export type ResourceOperationsInput = {
|
|
67
|
+
/** `<M>Delegate`'s own methods, as the types reader read them (T-3). */
|
|
68
|
+
readonly operations: readonly string[];
|
|
69
|
+
/** The model's usable identifiers; empty means no unique addressing exists. */
|
|
70
|
+
readonly identifiers: DiscoveredIdentifiers;
|
|
71
|
+
/** The one declared profile row this gate reads (T-2's amendment). */
|
|
72
|
+
readonly transactions: TransactionCapability;
|
|
73
|
+
};
|
|
74
|
+
export declare function deriveResourceOperations(input: ResourceOperationsInput): ResourceOperations;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* ------------------------------------------------------------------ *
|
|
2
|
+
* The scopes each advertised variant qualifies its arguments with *
|
|
3
|
+
* ------------------------------------------------------------------ */
|
|
4
|
+
/**
|
|
5
|
+
* The scope follows the **Prisma call the gate below routes the variant to**, and
|
|
6
|
+
* those calls fall into four argument shapes. The scope therefore sits beside the
|
|
7
|
+
* gate that chose the call, where a reversal of ยง14e's route is visible as an edit
|
|
8
|
+
* to one cell โ not in a constant in core, which would be a tabulated provider fact
|
|
9
|
+
* and wrong for the next adapter (D-802, ยง4 Q2).
|
|
10
|
+
*
|
|
11
|
+
* **The narrowest honest encoding (F-821).** Two of the four shapes are the
|
|
12
|
+
* unrestricted positions โ the `"record"` projection and the `"nested"` data
|
|
13
|
+
* argument โ which is exactly what core's readers (`projectionScopeOf`,
|
|
14
|
+
* `writeScopeOf`) answer for `true`. A descriptor stating them carries no
|
|
15
|
+
* information: core's canonicalizer collapsed it before any Contract value or
|
|
16
|
+
* hash, but it lengthened the artifact and moved its `modelHash`. So those
|
|
17
|
+
* variants are `true`, and a descriptor is stated only where it narrows: the two
|
|
18
|
+
* bulk shapes. `true` also stays the answer for the variants with no
|
|
19
|
+
* scope-qualified argument at all (`count` / `deleteMany` return a number, and the
|
|
20
|
+
* delete family has no data argument) โ `OperationVariantScopes` requires at least
|
|
21
|
+
* one scope, so nothing else is representable there.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* A row-returning read (`findFirst`, `findUnique`, `findMany`), and a
|
|
25
|
+
* row-returning write or delete of ONE addressed record (`create`, `update`,
|
|
26
|
+
* `upsert`, `delete`, and the emulated first-style variants, whose ยง14e Route A
|
|
27
|
+
* write is Prisma's own `update` / `delete` narrowed to the identifier): the
|
|
28
|
+
* unrestricted projection and, for a write, the nested data argument โ what
|
|
29
|
+
* `true` already means.
|
|
30
|
+
*/
|
|
31
|
+
const SINGLE_RECORD = true;
|
|
32
|
+
/** `createManyAndReturn` / `updateManyAndReturn`: both axes are the bulk ones. */
|
|
33
|
+
const WRITES_MANY_AND_RETURNS = {
|
|
34
|
+
projection: "bulkWrite",
|
|
35
|
+
write: "bulkWrite",
|
|
36
|
+
};
|
|
37
|
+
/** `createMany` / `updateMany`: the same flat data argument, and no projection. */
|
|
38
|
+
const WRITES_MANY_AND_COUNTS = { write: "bulkWrite" };
|
|
39
|
+
/** `count` / `deleteMany`: no projection argument and no data argument. */
|
|
40
|
+
const QUALIFIES_NO_ARGUMENT = true;
|
|
41
|
+
export function deriveResourceOperations(input) {
|
|
42
|
+
const exposes = (method) => input.operations.includes(method);
|
|
43
|
+
const addressable = input.identifiers.length > 0;
|
|
44
|
+
// Derived, never stored: the emulated read-then-write sequence runs inside one
|
|
45
|
+
// interactive transaction (D-807), so a driver without one cannot carry it.
|
|
46
|
+
const emulates = input.transactions !== "none";
|
|
47
|
+
/** A first-style variant is a read, an identifier to write by, and a transaction. */
|
|
48
|
+
const emulatedFirst = exposes("findFirst") && addressable && emulates;
|
|
49
|
+
const find = {
|
|
50
|
+
...(exposes("findFirst") ? { first: SINGLE_RECORD } : {}),
|
|
51
|
+
...(exposes("findUnique") && addressable ? { unique: SINGLE_RECORD } : {}),
|
|
52
|
+
...(exposes("findMany") ? { many: SINGLE_RECORD } : {}),
|
|
53
|
+
...(exposes("count") ? { count: QUALIFIES_NO_ARGUMENT } : {}),
|
|
54
|
+
};
|
|
55
|
+
const create = {
|
|
56
|
+
...(exposes("create") ? { one: SINGLE_RECORD } : {}),
|
|
57
|
+
// C-817: the row-returning bulk write is what `create.many` owes its caller.
|
|
58
|
+
...(exposes("createManyAndReturn")
|
|
59
|
+
? { many: WRITES_MANY_AND_RETURNS }
|
|
60
|
+
: {}),
|
|
61
|
+
...(exposes("createMany") ? { count: WRITES_MANY_AND_COUNTS } : {}),
|
|
62
|
+
};
|
|
63
|
+
const update = {
|
|
64
|
+
...(emulatedFirst && exposes("update") ? { first: SINGLE_RECORD } : {}),
|
|
65
|
+
...(exposes("update") && addressable ? { unique: SINGLE_RECORD } : {}),
|
|
66
|
+
...(exposes("updateManyAndReturn")
|
|
67
|
+
? { many: WRITES_MANY_AND_RETURNS }
|
|
68
|
+
: {}),
|
|
69
|
+
...(exposes("updateMany") ? { count: WRITES_MANY_AND_COUNTS } : {}),
|
|
70
|
+
};
|
|
71
|
+
const remove = {
|
|
72
|
+
...(emulatedFirst && exposes("delete") ? { first: SINGLE_RECORD } : {}),
|
|
73
|
+
...(exposes("delete") && addressable ? { unique: SINGLE_RECORD } : {}),
|
|
74
|
+
// `many` is ABSENT unconditionally (C-829). `delete.count` is the supported
|
|
75
|
+
// way to delete a filtered set.
|
|
76
|
+
...(exposes("deleteMany") ? { count: QUALIFIES_NO_ARGUMENT } : {}),
|
|
77
|
+
};
|
|
78
|
+
const upsert = {
|
|
79
|
+
...(exposes("upsert") && addressable ? { unique: SINGLE_RECORD } : {}),
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
...(nonEmpty(find) ? { find } : {}),
|
|
83
|
+
...(nonEmpty(create) ? { create } : {}),
|
|
84
|
+
...(nonEmpty(update) ? { update } : {}),
|
|
85
|
+
...(nonEmpty(remove) ? { delete: remove } : {}),
|
|
86
|
+
...(nonEmpty(upsert) ? { upsert } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** A family with no available variant is absent, not an empty object. */
|
|
90
|
+
function nonEmpty(family) {
|
|
91
|
+
return Object.keys(family).length > 0;
|
|
92
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { PrismaResourceSurface } from "./capability-surface.interface.js";
|
|
2
|
+
import type { PrismaSchemaField } from "./schema-facts.interface.js";
|
|
3
|
+
import { type PrismaDiscoveryDiagnostic, type PrismaScalarMapping } from "./vocabulary.dictionary.js";
|
|
4
|
+
/**
|
|
5
|
+
* T-5 / C-809 โ one Prisma column, answered in Aventara's words.
|
|
6
|
+
*
|
|
7
|
+
* # Four answers, no fifth, and no guess
|
|
8
|
+
*
|
|
9
|
+
* The outcome is a closed union because every way this can end is a decision
|
|
10
|
+
* somebody has to be able to read back:
|
|
11
|
+
*
|
|
12
|
+
* - `scalar` โ the Prisma type name had a row in the dictionary's total table.
|
|
13
|
+
* - `enum` โ the written type is an enum `enums.ts` confirms. It is **not** a
|
|
14
|
+
* third field kind: C-810 has exactly two, and an enum-backed column is a
|
|
15
|
+
* ScalarField. `enum.mapper.ts`'s `projectEnumBackedColumn` is where that
|
|
16
|
+
* becomes a field type ref.
|
|
17
|
+
* - `excluded` โ PSL declares the column and the ๐ข types do not expose it. This
|
|
18
|
+
* is the scalar half of C-808's existence gate, and the reason travels with it:
|
|
19
|
+
* an exclusion nobody can explain is indistinguishable from a bug.
|
|
20
|
+
* - `unmapped` โ a type name in neither table. A **diagnostic**, never a silent
|
|
21
|
+
* drop and never a nearest-neighbour guess (C-809).
|
|
22
|
+
*
|
|
23
|
+
* # Why `format` is emitted here at all โ and why so little of it is
|
|
24
|
+
*
|
|
25
|
+
* T-3.1's table says `format?` is `CFG`, "policy, not discovery". Spec ยง6.1 says
|
|
26
|
+
* UUID "is a string scalar plus passive format metadata **when the adapter can
|
|
27
|
+
* prove the format**"; ยง18.3's discovery mapping says "scalar type โ stable
|
|
28
|
+
* framework scalar + optional format"; C-809 says "native types normalize to the
|
|
29
|
+
* nearest stable scalar plus optional format"; and `AdapterScalarField.format?`
|
|
30
|
+
* exists **[CORE]**, which would be a member with no producer if discovery never
|
|
31
|
+
* emitted one. Three specification statements and a type beat one plan line, so
|
|
32
|
+
* discovery does emit `format` โ and T-3.1's row is corrected to `PSL` + `CFG`
|
|
33
|
+
* in `vocabulary.dictionary.ts`.
|
|
34
|
+
*
|
|
35
|
+
* What the plan row was *right* about is that most of what looks like a format is
|
|
36
|
+
* policy. `format` is **enforced**: `normalizeScalarFieldValue` turns
|
|
37
|
+
* `format: "uuid"` into a `V1003` rejection of request input **[CORE,
|
|
38
|
+
* `runtime/scalar-runtime.ts`]**. So the bar for discovering one is not "the
|
|
39
|
+
* schema suggests it" but "the column itself rejects what Aventara would then
|
|
40
|
+
* reject". That admits `@db.Uuid` โ PostgreSQL's `uuid` column refuses
|
|
41
|
+
* `'not-a-uuid'` where `text` accepts it **[RUN8 โ 2026-09-28, postgres:18]** โ
|
|
42
|
+
* and it excludes `@default(uuid())`, which T-5 also named: on a `text` column
|
|
43
|
+
* only *generated* values are UUIDs and a client may legally write any string, so
|
|
44
|
+
* emitting the format there would make Aventara refuse writes the database
|
|
45
|
+
* accepts. That intent belongs in `FrameworkScalarFieldConfig.format` **[CORE]**,
|
|
46
|
+
* which the Contract compiler already layers over the adapter's value.
|
|
47
|
+
*
|
|
48
|
+
* It follows that no `format` is emitted for either Phase 8 schema, whose only
|
|
49
|
+
* UUID column is `Author.id String @default(uuid())` with no native type. That is
|
|
50
|
+
* the correct answer, not a gap.
|
|
51
|
+
*
|
|
52
|
+
* # Nothing here branches on a provider
|
|
53
|
+
*
|
|
54
|
+
* `@db.Uuid` is unwritable on SQLite, so the SQLite schema simply never carries
|
|
55
|
+
* it and the same table gives the same right answer on both engines without a
|
|
56
|
+
* provider arm (T-3.1 invariant 6).
|
|
57
|
+
*/
|
|
58
|
+
export type ScalarMappingInput = {
|
|
59
|
+
/** The PSL model name, carried so a diagnostic can name the column in full. */
|
|
60
|
+
readonly resource: string;
|
|
61
|
+
/** PSL โ what the column IS. */
|
|
62
|
+
readonly field: PrismaSchemaField;
|
|
63
|
+
/** TYPE โ what this client exposes. The existence gate (C-808). */
|
|
64
|
+
readonly surface: PrismaResourceSurface;
|
|
65
|
+
/** TYPE โ `enums.ts`, which is what makes an enum name an enum name. */
|
|
66
|
+
readonly enums: Readonly<Record<string, readonly string[]>>;
|
|
67
|
+
};
|
|
68
|
+
export type ScalarMappingOutcome = {
|
|
69
|
+
readonly outcome: "scalar";
|
|
70
|
+
readonly mapping: PrismaScalarMapping;
|
|
71
|
+
} | {
|
|
72
|
+
readonly outcome: "enum";
|
|
73
|
+
readonly enum: string;
|
|
74
|
+
} | {
|
|
75
|
+
readonly outcome: "excluded";
|
|
76
|
+
readonly diagnostic: PrismaDiscoveryDiagnostic;
|
|
77
|
+
} | {
|
|
78
|
+
readonly outcome: "unmapped";
|
|
79
|
+
readonly diagnostic: PrismaDiscoveryDiagnostic;
|
|
80
|
+
};
|
|
81
|
+
export declare function mapPrismaScalarField(input: ScalarMappingInput): ScalarMappingOutcome;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PRISMA_NATIVE_FORMATS, PRISMA_SCALAR_TABLE, } from "./vocabulary.dictionary.js";
|
|
2
|
+
export function mapPrismaScalarField(input) {
|
|
3
|
+
const { resource, field, surface, enums } = input;
|
|
4
|
+
const column = `${resource}.${field.name}`;
|
|
5
|
+
if (!Object.hasOwn(surface.scalarFields, field.name)) {
|
|
6
|
+
return {
|
|
7
|
+
outcome: "excluded",
|
|
8
|
+
diagnostic: {
|
|
9
|
+
code: "not-exposed",
|
|
10
|
+
message: `${column} is declared in schema.prisma and absent from the generated ๐ข types, so it is omitted: ${explainAbsence(field, surface)}`,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (Object.hasOwn(enums, field.type)) {
|
|
15
|
+
// C-810: this is a ScalarField whose type ref takes the `enum` arm.
|
|
16
|
+
return { outcome: "enum", enum: field.type };
|
|
17
|
+
}
|
|
18
|
+
const mapping = PRISMA_SCALAR_TABLE[field.type];
|
|
19
|
+
if (mapping === undefined) {
|
|
20
|
+
return {
|
|
21
|
+
outcome: "unmapped",
|
|
22
|
+
diagnostic: {
|
|
23
|
+
code: "unmapped-prisma-type",
|
|
24
|
+
message: `${column} is written as \`${field.type}\`, which is neither a built-in Prisma scalar (${Object.keys(PRISMA_SCALAR_TABLE).join(", ")}) nor an enum the generated \`enums.ts\` exposes. Discovery does not guess a nearest scalar.`,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Matched by exact written spelling, never parsed: `@db.Decimal(10, 2)`
|
|
29
|
+
// matches no key and therefore proves no format, with no argument list split.
|
|
30
|
+
const proven = field.native
|
|
31
|
+
.map((native) => PRISMA_NATIVE_FORMATS[native])
|
|
32
|
+
.find((format) => format !== undefined);
|
|
33
|
+
return {
|
|
34
|
+
outcome: "scalar",
|
|
35
|
+
mapping: proven === undefined ? mapping : { ...mapping, format: proven },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The gate's message is built from evidence, never from an assumption: `@ignore`
|
|
40
|
+
* is reported only where PSL actually wrote one, because "present in schema,
|
|
41
|
+
* absent from client" with a made-up cause is worse than the bare fact.
|
|
42
|
+
*/
|
|
43
|
+
function explainAbsence(field, surface) {
|
|
44
|
+
if (field.ignored) {
|
|
45
|
+
return "it carries `@ignore`";
|
|
46
|
+
}
|
|
47
|
+
if (Object.hasOwn(surface.relationFields, field.name)) {
|
|
48
|
+
return "the ๐ข types expose it as a relation edge, not a scalar column";
|
|
49
|
+
}
|
|
50
|
+
return "no schema attribute explains it โ a provider drop, an `Unsupported(โฆ)` column or a preview-feature exclusion";
|
|
51
|
+
}
|