@aventara/prisma7-adapter 0.1.0-pilot.1 → 0.1.0-pilot.3
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/README.md +33 -20
- package/dist/discovery/artifact.loader.d.ts +6 -2
- package/dist/discovery/artifact.loader.js +12 -57
- package/dist/discovery/capability-surface.interface.d.ts +29 -40
- package/dist/discovery/capability-surface.interface.js +0 -12
- package/dist/discovery/capability.deriver.d.ts +10 -40
- package/dist/discovery/capability.deriver.js +0 -179
- package/dist/discovery/client-shape.verifier.d.ts +13 -0
- package/dist/discovery/client-shape.verifier.js +93 -0
- package/dist/discovery/discovery-artifact.interface.d.ts +55 -30
- package/dist/discovery/enum.mapper.d.ts +6 -14
- package/dist/discovery/enum.mapper.js +1 -2
- package/dist/discovery/field.mapper.d.ts +49 -63
- package/dist/discovery/field.mapper.js +0 -161
- package/dist/discovery/identifier.mapper.d.ts +38 -77
- package/dist/discovery/identifier.mapper.js +0 -64
- package/dist/discovery/model.discoverer.d.ts +18 -35
- package/dist/discovery/model.discoverer.js +66 -141
- package/dist/discovery/operations.deriver.d.ts +10 -38
- package/dist/discovery/operations.deriver.js +0 -40
- package/dist/discovery/scalar.mapper.d.ts +12 -35
- package/dist/discovery/scalar.mapper.js +0 -8
- package/dist/discovery/schema-facts.interface.d.ts +26 -25
- package/dist/discovery/schema-facts.interface.js +0 -10
- package/dist/discovery/schema.reader.d.ts +53 -64
- package/dist/discovery/schema.reader.js +0 -174
- package/dist/discovery/types.reader.d.ts +0 -4
- package/dist/discovery/types.reader.js +2 -177
- package/dist/discovery/vocabulary.dictionary.d.ts +36 -75
- package/dist/discovery/vocabulary.dictionary.js +2 -152
- package/dist/errors/error.normalizer.d.ts +85 -135
- package/dist/errors/error.normalizer.js +1 -70
- package/dist/errors/prisma-error.reader.d.ts +53 -68
- package/dist/errors/prisma-error.reader.js +0 -91
- package/dist/execution/first-variant.emulator.d.ts +11 -22
- package/dist/execution/first-variant.emulator.js +0 -142
- package/dist/execution/operation.executor.d.ts +12 -19
- package/dist/execution/operation.executor.js +0 -92
- package/dist/execution/result.shaper.d.ts +22 -31
- package/dist/execution/result.shaper.js +0 -24
- package/dist/execution/transaction.executor.d.ts +36 -73
- package/dist/generate/artifact.emitter.d.ts +27 -27
- package/dist/generate/artifact.emitter.js +30 -13
- package/dist/generate/declared-support.reader.d.ts +11 -11
- package/dist/generate/declared-support.reader.js +0 -12
- package/dist/generate/discovery.bin.js +0 -10
- package/dist/generate/discovery.cli.js +0 -55
- package/dist/generate/existence.gate.d.ts +26 -32
- package/dist/generate/existence.gate.js +0 -6
- package/dist/generate/index.d.ts +10 -12
- package/dist/generate/index.js +0 -42
- package/dist/generate/node-version.guard.js +0 -12
- package/dist/generate/prisma-cli.resolver.d.ts +9 -11
- package/dist/generate/prisma-cli.resolver.js +0 -13
- package/dist/generate/prisma.generate.d.ts +49 -65
- package/dist/generate/prisma.generate.js +13 -123
- package/dist/generate/reader-crosscheck.d.ts +36 -60
- package/dist/generate/reader-crosscheck.js +5 -41
- package/dist/prisma-client.interface.d.ts +14 -19
- package/dist/prisma-client.interface.js +0 -6
- package/dist/prisma.adapter.d.ts +9 -17
- package/dist/prisma.adapter.js +11 -53
- package/dist/provider/provider-profile.d.ts +6 -23
- package/dist/provider/provider-profile.js +0 -64
- package/dist/translation/identifier.addressing.d.ts +15 -22
- package/dist/translation/identifier.addressing.js +0 -10
- package/dist/translation/mutation.translator.d.ts +19 -43
- package/dist/translation/mutation.translator.js +1 -209
- package/dist/translation/operation.translator.d.ts +36 -63
- package/dist/translation/operation.translator.js +1 -51
- package/dist/translation/order-pagination.translator.d.ts +24 -31
- package/dist/translation/order-pagination.translator.js +0 -43
- package/dist/translation/projection.translator.d.ts +25 -58
- package/dist/translation/projection.translator.js +0 -27
- package/dist/translation/scalar.conversion.d.ts +15 -26
- package/dist/translation/scalar.conversion.js +1 -129
- package/dist/translation/where.translator.d.ts +17 -28
- package/dist/translation/where.translator.js +0 -83
- package/package.json +4 -3
|
@@ -1,42 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Prisma addressing key for a uniqueness criterion: the key a
|
|
3
|
-
* `WhereUniqueInput` accepts, and the key the generated `AtLeast<…, K>` union
|
|
4
|
-
* lists.
|
|
5
|
-
*
|
|
6
|
-
* Computed **once**, here, at generate time — the architect's decision on
|
|
7
|
-
* 2026-09-29: *"store it, less processing and less room for drifting."* Runtime
|
|
8
|
-
* matches the stored key and never re-derives it.
|
|
9
|
-
*
|
|
10
|
-
* The rule, measured at 7.10.0 (regenerated 2026-09-29 over five models):
|
|
11
|
-
*
|
|
12
|
-
* | criterion | key | wraps |
|
|
13
|
-
* | ---------------------------------- | --------- | ----- |
|
|
14
|
-
* | `id String @id` / `@unique` | `id` | no |
|
|
15
|
-
* | `@@unique([b, a])` | `b_a` | yes |
|
|
16
|
-
* | `@@id([a, b])` | `a_b` | yes |
|
|
17
|
-
* | `@@unique([b, a], name: "altKey")` | `altKey` | yes |
|
|
18
|
-
* | `@@id([a, b], name: "pk")` | `pk` | yes |
|
|
19
|
-
* | any of the above, `map: "…"` | unchanged | — |
|
|
20
|
-
*
|
|
21
|
-
* Two of those rows are why this is a function of the PSL facts and not of the
|
|
22
|
-
* members alone. A custom `name:` **replaces** the positional alias rather than
|
|
23
|
-
* adding to it, so `members.join("_")` is simply wrong for a named criterion —
|
|
24
|
-
* which is precisely what made the cross-check refuse a schema Prisma accepts,
|
|
25
|
-
* reporting a correctly-discovered identifier as an invented constraint (fixed
|
|
26
|
-
* 2026-09-29; `map:` stays invisible and stays discarded).
|
|
27
|
-
*
|
|
28
|
-
* What this key is **not** is part of the identifier. `AdapterIdentifier` is a
|
|
29
|
-
* tuple of field names with nowhere to put a name (C-814), and this key lives in
|
|
30
|
-
* the artifact's translation table beside the model, never in it.
|
|
31
|
-
*/
|
|
32
1
|
export function prismaUniqueInputKey(uniqueness) {
|
|
33
2
|
return uniqueness.name ?? uniqueness.members.join("_");
|
|
34
3
|
}
|
|
35
|
-
/**
|
|
36
|
-
* Every uniqueness criterion PSL states, in PSL's own order. A field spelled
|
|
37
|
-
* `@id @unique` yields two criteria with the same key; the caller keeps the
|
|
38
|
-
* first, so `@id` wins and one column does not become two identifiers.
|
|
39
|
-
*/
|
|
40
4
|
function* uniquenessCriteria(model) {
|
|
41
5
|
const criterion = (source, uniqueness) => ({
|
|
42
6
|
source,
|
|
@@ -44,8 +8,6 @@ function* uniquenessCriteria(model) {
|
|
|
44
8
|
key: prismaUniqueInputKey(uniqueness),
|
|
45
9
|
});
|
|
46
10
|
for (const field of model.fields) {
|
|
47
|
-
// A field-level criterion has no `name:` to carry: PSL gives `@id` and
|
|
48
|
-
// `@unique` no such argument, and Prisma keys them by the column.
|
|
49
11
|
if (field.id) {
|
|
50
12
|
yield criterion("@id", { members: [field.name], name: null });
|
|
51
13
|
}
|
|
@@ -60,16 +22,7 @@ function* uniquenessCriteria(model) {
|
|
|
60
22
|
yield criterion("@@unique", blockUnique);
|
|
61
23
|
}
|
|
62
24
|
}
|
|
63
|
-
/**
|
|
64
|
-
* One model's identity, read from PSL alone.
|
|
65
|
-
*
|
|
66
|
-
* A pure function of plain data: no generated client, no `@prisma/client`
|
|
67
|
-
* import, and no TYPE input — which is what lets the cross-check be an
|
|
68
|
-
* independent second opinion rather than a tautology.
|
|
69
|
-
*/
|
|
70
25
|
export function mapPrismaIdentifiers(model) {
|
|
71
|
-
// The scalar columns this adapter can actually name: a relation field, a list
|
|
72
|
-
// and an `@ignore`d column are all keys no `AdapterFieldMap` will carry.
|
|
73
26
|
const columns = new Map();
|
|
74
27
|
for (const field of model.fields) {
|
|
75
28
|
if (field.relation !== null || field.list || field.ignored) {
|
|
@@ -79,10 +32,6 @@ export function mapPrismaIdentifiers(model) {
|
|
|
79
32
|
}
|
|
80
33
|
const identifiers = [];
|
|
81
34
|
const omitted = [];
|
|
82
|
-
// Deduplication is on the MEMBER TUPLE, not on the addressing key: the tuple
|
|
83
|
-
// is what `@id @unique` duplicates, and it identifies a criterion injectively,
|
|
84
|
-
// while a key does not — a custom `name:` can collide with a field name, and
|
|
85
|
-
// keying the seen-set would then silently drop a different criterion.
|
|
86
35
|
const seen = new Set();
|
|
87
36
|
for (const criterion of uniquenessCriteria(model)) {
|
|
88
37
|
const tuple = JSON.stringify(criterion.members);
|
|
@@ -104,19 +53,6 @@ export function mapPrismaIdentifiers(model) {
|
|
|
104
53
|
});
|
|
105
54
|
continue;
|
|
106
55
|
}
|
|
107
|
-
// C-815. SQL uniqueness does not constrain NULLs, so a criterion with any
|
|
108
|
-
// nullable participant does not guarantee AT MOST ONE row and is not an
|
|
109
|
-
// `Identifier`. Recorded, never dropped: the generator still emits an
|
|
110
|
-
// `AtLeast` key for it, and the TYPE cross-check needs the omission to
|
|
111
|
-
// explain that key.
|
|
112
|
-
//
|
|
113
|
-
// C-815 is typed a Derived Decision because no source states it. Prisma
|
|
114
|
-
// states the same rule from its own side, which is the nearest thing to a
|
|
115
|
-
// source there is: a model whose ONLY criterion has a nullable participant
|
|
116
|
-
// is rejected at validate time with P1012, "Each model must have at least
|
|
117
|
-
// one unique criteria that has only required fields … The following unique
|
|
118
|
-
// criterias were not considered as they contain fields that are not
|
|
119
|
-
// required: - a, b" (7.10.0, measured 2026-09-28).
|
|
120
56
|
const nullableMembers = criterion.members.filter((member) => columns.get(member)?.optional === true);
|
|
121
57
|
if (nullableMembers.length > 0) {
|
|
122
58
|
omitted.push({
|
|
@@ -3,16 +3,8 @@ import type { PrismaComposedArtifact, PrismaModelComposer } from "../generate/pr
|
|
|
3
3
|
import type { PrismaCapabilitySurface } from "./capability-surface.interface.js";
|
|
4
4
|
import type { PrismaSchemaFacts } from "./schema-facts.interface.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Every rule it applies belongs to somebody else. That is the design, not a
|
|
10
|
-
* shortcut: the scalar table is `scalar.mapper.ts`'s, the field projection and the
|
|
11
|
-
* lifecycle traits are `field.mapper.ts`'s, identity is `identifier.mapper.ts`'s,
|
|
12
|
-
* capabilities are `capability.deriver.ts`'s, the §8.3 variant gate is
|
|
13
|
-
* `operations.deriver.ts`'s and nowhere else, and the enum comparison is
|
|
14
|
-
* `enum.mapper.ts`'s. This module **orders and assembles**, and it owns exactly
|
|
15
|
-
* two judgements of its own:
|
|
6
|
+
* Every rule it applies belongs to somebody else. This module **orders and
|
|
7
|
+
* assembles**, and it owns exactly two judgements of its own:
|
|
16
8
|
*
|
|
17
9
|
* 1. **Which diagnostics are fatal.** A mapper answers with a diagnostic; whether
|
|
18
10
|
* the build stops is composition's call, because only composition knows that a
|
|
@@ -49,31 +41,22 @@ import type { PrismaSchemaFacts } from "./schema-facts.interface.js";
|
|
|
49
41
|
*
|
|
50
42
|
* The composer is created with a {@link TransactionCapability} and nothing else.
|
|
51
43
|
* `provider` is not a member anywhere on the path, so `if (provider === …)` is a
|
|
52
|
-
* compile error rather than a convention
|
|
53
|
-
*
|
|
54
|
-
* fact and not a branch.
|
|
55
|
-
*
|
|
56
|
-
* `transactions` is a **driver** fact (T-2) and the driver is a runtime choice, so
|
|
57
|
-
* it is declared to the composer at generate time. The two cannot silently
|
|
58
|
-
* disagree: an artifact that says `"interactive"` loaded by an adapter whose
|
|
59
|
-
* declared driver says `"none"` mounts no `transaction`, and core refuses
|
|
60
|
-
* construction with `DISCOVERY_TRANSACTION_CAPABILITY_MISMATCH` **[CORE,
|
|
61
|
-
* framework.factory.ts]** — a hard, legible failure rather than a silent one.
|
|
62
|
-
*
|
|
63
|
-
* # The C-818 conflict this header used to record is closed
|
|
44
|
+
* compile error rather than a convention. The generate step keeps `provider` only
|
|
45
|
+
* to stamp the artifact, which is a recorded fact and not a branch.
|
|
64
46
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* `
|
|
69
|
-
*
|
|
47
|
+
* `transactions` is a **driver** fact and the driver is a runtime choice, so it is
|
|
48
|
+
* declared to the composer at generate time. The two cannot silently disagree: an
|
|
49
|
+
* artifact that says `"interactive"` loaded by an adapter whose declared driver
|
|
50
|
+
* says `"none"` mounts no `transaction`, and core refuses construction with
|
|
51
|
+
* `DISCOVERY_TRANSACTION_CAPABILITY_MISMATCH` **[CORE, framework.factory.ts]** — a
|
|
52
|
+
* hard, legible failure rather than a silent one.
|
|
70
53
|
*
|
|
71
54
|
* The capability members are **scope-qualified** now, so the one record holds both
|
|
72
|
-
* answers: the create stage is advertised at the `nested` write scope and not at
|
|
73
|
-
* bulk one. Composition asks for no variant, passes no `operation`, and
|
|
74
|
-
* nothing it cannot honour. The mutation translator keeps its refusal —
|
|
75
|
-
* belt to core's braces, not the only guard
|
|
76
|
-
* place the restriction exists.
|
|
55
|
+
* answers: the create stage is advertised at the `nested` write scope and not at
|
|
56
|
+
* the bulk one. Composition asks for no variant, passes no `operation`, and
|
|
57
|
+
* advertises nothing it cannot honour. The mutation translator keeps its refusal —
|
|
58
|
+
* it is the belt to core's braces, not the only guard — but it is no longer the
|
|
59
|
+
* ONLY place the restriction exists.
|
|
77
60
|
*/
|
|
78
61
|
/** Composition refuses: the collected diagnostics, named and reported together. */
|
|
79
62
|
export declare class PrismaModelCompositionError extends Error {
|
|
@@ -83,9 +66,9 @@ export declare class PrismaModelCompositionError extends Error {
|
|
|
83
66
|
}
|
|
84
67
|
export type PrismaModelCompositionOptions = {
|
|
85
68
|
/**
|
|
86
|
-
* The driver's transaction capability
|
|
87
|
-
*
|
|
88
|
-
* `
|
|
69
|
+
* The driver's transaction capability. The ONLY declared fact composition accepts,
|
|
70
|
+
* and deliberately not the whole profile: a `PrismaProviderProfile` carries
|
|
71
|
+
* `provider`.
|
|
89
72
|
*/
|
|
90
73
|
readonly transactions: TransactionCapability;
|
|
91
74
|
};
|
|
@@ -4,80 +4,6 @@ import { mapPrismaEnum } from "./enum.mapper.js";
|
|
|
4
4
|
import { crossCheckCreateOptional, projectPrismaField, } from "./field.mapper.js";
|
|
5
5
|
import { mapPrismaIdentifiers } from "./identifier.mapper.js";
|
|
6
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
7
|
export class PrismaModelCompositionError extends Error {
|
|
82
8
|
name = "PrismaModelCompositionError";
|
|
83
9
|
diagnostics;
|
|
@@ -89,13 +15,6 @@ export class PrismaModelCompositionError extends Error {
|
|
|
89
15
|
this.diagnostics = diagnostics;
|
|
90
16
|
}
|
|
91
17
|
}
|
|
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
18
|
export function createPrismaModelComposer(options) {
|
|
100
19
|
return (input) => discoverPrismaModel({ ...input, ...options });
|
|
101
20
|
}
|
|
@@ -113,8 +32,6 @@ export function discoverPrismaModel(input) {
|
|
|
113
32
|
const model = declared.get(name);
|
|
114
33
|
const exposed = input.surface.resources[name];
|
|
115
34
|
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
35
|
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
36
|
continue;
|
|
120
37
|
}
|
|
@@ -144,17 +61,77 @@ export function discoverPrismaModel(input) {
|
|
|
144
61
|
},
|
|
145
62
|
translation,
|
|
146
63
|
referentialActions,
|
|
64
|
+
createRequirements: composeCreateRequirements(declared, input.surface, resources),
|
|
147
65
|
};
|
|
148
66
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
67
|
+
function composeCreateRequirements(declared, surface, resources) {
|
|
68
|
+
const table = {};
|
|
69
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
70
|
+
const model = declared.get(name);
|
|
71
|
+
const exposed = surface.resources[name];
|
|
72
|
+
if (model === undefined || exposed === undefined) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const requiredScalars = new Set(Object.entries(resource.fields)
|
|
76
|
+
.filter(([field, shape]) => {
|
|
77
|
+
const scalar = exposed.scalarFields[field];
|
|
78
|
+
return (shape.kind === "scalar" &&
|
|
79
|
+
scalar !== undefined &&
|
|
80
|
+
scalar.createOperations !== null &&
|
|
81
|
+
!scalar.createOptional);
|
|
82
|
+
})
|
|
83
|
+
.map(([field]) => field));
|
|
84
|
+
const requirements = [];
|
|
85
|
+
const references = new Set();
|
|
86
|
+
for (const field of model.fields) {
|
|
87
|
+
const shape = resource.fields[field.name];
|
|
88
|
+
if (shape?.kind !== "relation") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const owned = field.relation?.fields ?? [];
|
|
92
|
+
for (const scalar of owned) {
|
|
93
|
+
references.add(scalar);
|
|
94
|
+
}
|
|
95
|
+
if (exposed.relationFields[field.name]?.createRequired !== true) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const orFields = owned.filter((scalar) => requiredScalars.has(scalar));
|
|
99
|
+
if (orFields.length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const inverse = inverseOf(declared, name, field);
|
|
103
|
+
requirements.push({
|
|
104
|
+
kind: "relation",
|
|
105
|
+
relation: field.name,
|
|
106
|
+
orFields,
|
|
107
|
+
...(inverse === undefined ? {} : { inverse }),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const fields = model.fields
|
|
111
|
+
.map((field) => field.name)
|
|
112
|
+
.filter((field) => requiredScalars.has(field) && !references.has(field))
|
|
113
|
+
.map((field) => ({ kind: "field", field }));
|
|
114
|
+
table[name] = [...fields, ...requirements];
|
|
115
|
+
}
|
|
116
|
+
return table;
|
|
117
|
+
}
|
|
118
|
+
function inverseOf(declared, model, field) {
|
|
119
|
+
const target = declared.get(field.type);
|
|
120
|
+
if (target === undefined) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const name = field.relation?.name ?? null;
|
|
124
|
+
const partners = target.fields.filter((candidate) => candidate.type === model &&
|
|
125
|
+
(candidate.relation?.name ?? null) === name &&
|
|
126
|
+
!(target.name === model && candidate.name === field.name));
|
|
127
|
+
const [partner] = partners;
|
|
128
|
+
return partners.length === 1 && partner !== undefined
|
|
129
|
+
? { resource: target.name, relation: partner.name }
|
|
130
|
+
: undefined;
|
|
131
|
+
}
|
|
152
132
|
function composeEnums(input, diagnostics) {
|
|
153
133
|
const enums = {};
|
|
154
134
|
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
135
|
const mapped = mapPrismaEnum({
|
|
159
136
|
declared,
|
|
160
137
|
exposed: input.surface.enums[declared.name],
|
|
@@ -166,24 +143,14 @@ function composeEnums(input, diagnostics) {
|
|
|
166
143
|
if (mapped.outcome === "diverged") {
|
|
167
144
|
diagnostics.push(mapped.diagnostic.message);
|
|
168
145
|
}
|
|
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
146
|
}
|
|
175
147
|
return enums;
|
|
176
148
|
}
|
|
177
|
-
/* ------------------------------------------------------------------ *
|
|
178
|
-
* One resource *
|
|
179
|
-
* ------------------------------------------------------------------ */
|
|
180
149
|
function composeResource(input) {
|
|
181
150
|
const { model, exposed, diagnostics } = input;
|
|
182
151
|
const fields = {};
|
|
183
|
-
/** Owning edges only, in PSL field-declaration order (see this module's header). */
|
|
184
152
|
const owningEdges = [];
|
|
185
153
|
const unknowns = [];
|
|
186
|
-
/** What the create-optional cross-check consumes: PSL's own answers. */
|
|
187
154
|
const projectedScalars = [];
|
|
188
155
|
for (const field of model.fields) {
|
|
189
156
|
const projection = projectPrismaField({
|
|
@@ -196,8 +163,6 @@ function composeResource(input) {
|
|
|
196
163
|
});
|
|
197
164
|
switch (projection.outcome) {
|
|
198
165
|
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
166
|
continue;
|
|
202
167
|
case "unmapped":
|
|
203
168
|
diagnostics.push(projection.diagnostic.message);
|
|
@@ -211,9 +176,6 @@ function composeResource(input) {
|
|
|
211
176
|
}
|
|
212
177
|
const type = projection.field.type;
|
|
213
178
|
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
179
|
const scalar = BUILT_IN_SCALARS.find((candidate) => candidate === type.scalar);
|
|
218
180
|
if (scalar === undefined) {
|
|
219
181
|
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(", ")}).`);
|
|
@@ -240,31 +202,19 @@ function composeResource(input) {
|
|
|
240
202
|
continue;
|
|
241
203
|
}
|
|
242
204
|
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
205
|
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
206
|
continue;
|
|
247
207
|
}
|
|
248
208
|
fields[projection.name] = {
|
|
249
209
|
...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
210
|
capabilities: deriveRelationCapabilities({
|
|
254
211
|
surface: relationSurface,
|
|
255
212
|
}),
|
|
256
213
|
};
|
|
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
214
|
if (projection.edge.onDelete !== null &&
|
|
263
215
|
projection.edge.onUpdate !== null) {
|
|
264
216
|
owningEdges.push({
|
|
265
217
|
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
218
|
target: projection.edge.target,
|
|
269
219
|
cardinality: projection.edge.cardinality,
|
|
270
220
|
onDelete: projection.edge.onDelete,
|
|
@@ -276,15 +226,10 @@ function composeResource(input) {
|
|
|
276
226
|
}
|
|
277
227
|
}
|
|
278
228
|
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
229
|
for (const unknown of unknowns) {
|
|
282
230
|
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
231
|
}
|
|
284
232
|
}
|
|
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
233
|
const createOptional = Object.entries(exposed.scalarFields)
|
|
289
234
|
.filter(([, scalar]) => scalar.createOptional)
|
|
290
235
|
.map(([name]) => name);
|
|
@@ -300,36 +245,16 @@ function composeResource(input) {
|
|
|
300
245
|
resource: {
|
|
301
246
|
fields,
|
|
302
247
|
identifiers: members,
|
|
303
|
-
// The §8.3 gate's single home. Composition supplies its three inputs and
|
|
304
|
-
// omits nothing itself.
|
|
305
248
|
operations: deriveResourceOperations({
|
|
306
249
|
operations: exposed.operations,
|
|
307
250
|
identifiers: members,
|
|
308
251
|
transactions: input.transactions,
|
|
309
252
|
}),
|
|
310
253
|
},
|
|
311
|
-
// The same list's other projection: `members` above, the whole address here.
|
|
312
254
|
translation: { identifierKeys: identifiers.identifiers },
|
|
313
255
|
referentialActions: { owningEdges },
|
|
314
256
|
};
|
|
315
257
|
}
|
|
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
258
|
function composeScalarRegistry(used) {
|
|
334
259
|
const scalars = {};
|
|
335
260
|
for (const scalar of BUILT_IN_SCALARS) {
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import type { AdapterResource, ResourceOperations, TransactionCapability } from "@aventara/core";
|
|
2
2
|
import type { PrismaAdapterModel } from "./discovery-artifact.interface.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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.
|
|
4
|
+
* No mapper, no discoverer, no emitter and no executor may drop a variant, because
|
|
5
|
+
* an omission in two places is an omission nobody can audit.
|
|
12
6
|
*
|
|
13
7
|
* # Three inputs, and `provider` is not one of them
|
|
14
8
|
*
|
|
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
9
|
* ```text
|
|
21
10
|
* findUnique, findUniqueOrThrow, findFirst, findFirstOrThrow, findMany, create,
|
|
22
11
|
* createMany, createManyAndReturn, delete, update, deleteMany, updateMany,
|
|
@@ -29,46 +18,29 @@ import type { PrismaAdapterModel } from "./discovery-artifact.interface.js";
|
|
|
29
18
|
* needs. The whole `PrismaProviderProfile` is deliberately NOT accepted: it
|
|
30
19
|
* carries `provider`, and a module that cannot be handed a provider cannot branch
|
|
31
20
|
* on one (dictionary invariant 6). `isolationLevels` is not a precondition of any
|
|
32
|
-
* variant
|
|
21
|
+
* variant.
|
|
33
22
|
*
|
|
34
23
|
* `supportsEmulatedFirstVariants` is **derived, exactly `transactions !== "none"`,
|
|
35
|
-
* and stored nowhere
|
|
36
|
-
*
|
|
24
|
+
* and stored nowhere**. A stored copy would be a second source of truth for a
|
|
25
|
+
* one-line derivation.
|
|
37
26
|
*
|
|
38
27
|
* # What is NOT gated here, and why that is deliberate
|
|
39
28
|
*
|
|
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
29
|
* So `update.first` reads `emulatedFirst && exposes("update")` and `delete.first`
|
|
57
30
|
* reads `emulatedFirst && exposes("delete")`. Under Route B those cells would read
|
|
58
31
|
* `updateManyAndReturn` and `deleteMany` instead — which is exactly why the write
|
|
59
|
-
* call is NOT folded into `emulatedFirst`: a shared precondition naming one
|
|
60
|
-
* call would assert a gate the other route does not have, an assertion
|
|
61
|
-
* fail for the right reason
|
|
62
|
-
* these two cells and to the translator's one, and to nothing else here.
|
|
32
|
+
* call is NOT folded into `emulatedFirst`: a shared precondition naming one
|
|
33
|
+
* route's call would assert a gate the other route does not have, an assertion
|
|
34
|
+
* that cannot fail for the right reason.
|
|
63
35
|
*/
|
|
64
36
|
/** Core's own identifier tuples for a discovered Resource. */
|
|
65
37
|
type DiscoveredIdentifiers = AdapterResource<PrismaAdapterModel>["identifiers"];
|
|
66
38
|
export type ResourceOperationsInput = {
|
|
67
|
-
/** `<M>Delegate`'s own methods, as the types reader read them
|
|
39
|
+
/** `<M>Delegate`'s own methods, as the types reader read them. */
|
|
68
40
|
readonly operations: readonly string[];
|
|
69
41
|
/** The model's usable identifiers; empty means no unique addressing exists. */
|
|
70
42
|
readonly identifiers: DiscoveredIdentifiers;
|
|
71
|
-
/** The one declared profile row this gate reads
|
|
43
|
+
/** The one declared profile row this gate reads. */
|
|
72
44
|
readonly transactions: TransactionCapability;
|
|
73
45
|
};
|
|
74
46
|
export declare function deriveResourceOperations(input: ResourceOperationsInput): ResourceOperations;
|
|
@@ -1,50 +1,14 @@
|
|
|
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
1
|
const SINGLE_RECORD = true;
|
|
32
|
-
/** `createManyAndReturn` / `updateManyAndReturn`: both axes are the bulk ones. */
|
|
33
2
|
const WRITES_MANY_AND_RETURNS = {
|
|
34
3
|
projection: "bulkWrite",
|
|
35
4
|
write: "bulkWrite",
|
|
36
5
|
};
|
|
37
|
-
/** `createMany` / `updateMany`: the same flat data argument, and no projection. */
|
|
38
6
|
const WRITES_MANY_AND_COUNTS = { write: "bulkWrite" };
|
|
39
|
-
/** `count` / `deleteMany`: no projection argument and no data argument. */
|
|
40
7
|
const QUALIFIES_NO_ARGUMENT = true;
|
|
41
8
|
export function deriveResourceOperations(input) {
|
|
42
9
|
const exposes = (method) => input.operations.includes(method);
|
|
43
10
|
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
11
|
const emulates = input.transactions !== "none";
|
|
47
|
-
/** A first-style variant is a read, an identifier to write by, and a transaction. */
|
|
48
12
|
const emulatedFirst = exposes("findFirst") && addressable && emulates;
|
|
49
13
|
const find = {
|
|
50
14
|
...(exposes("findFirst") ? { first: SINGLE_RECORD } : {}),
|
|
@@ -54,7 +18,6 @@ export function deriveResourceOperations(input) {
|
|
|
54
18
|
};
|
|
55
19
|
const create = {
|
|
56
20
|
...(exposes("create") ? { one: SINGLE_RECORD } : {}),
|
|
57
|
-
// C-817: the row-returning bulk write is what `create.many` owes its caller.
|
|
58
21
|
...(exposes("createManyAndReturn")
|
|
59
22
|
? { many: WRITES_MANY_AND_RETURNS }
|
|
60
23
|
: {}),
|
|
@@ -71,8 +34,6 @@ export function deriveResourceOperations(input) {
|
|
|
71
34
|
const remove = {
|
|
72
35
|
...(emulatedFirst && exposes("delete") ? { first: SINGLE_RECORD } : {}),
|
|
73
36
|
...(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
37
|
...(exposes("deleteMany") ? { count: QUALIFIES_NO_ARGUMENT } : {}),
|
|
77
38
|
};
|
|
78
39
|
const upsert = {
|
|
@@ -86,7 +47,6 @@ export function deriveResourceOperations(input) {
|
|
|
86
47
|
...(nonEmpty(upsert) ? { upsert } : {}),
|
|
87
48
|
};
|
|
88
49
|
}
|
|
89
|
-
/** A family with no available variant is absent, not an empty object. */
|
|
90
50
|
function nonEmpty(family) {
|
|
91
51
|
return Object.keys(family).length > 0;
|
|
92
52
|
}
|