@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,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
+ }