@aventara/prisma7-adapter 1.0.0-rc.3 → 1.0.0-rc.8
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/CHANGELOG.md +145 -1
- package/README.md +43 -14
- package/dist/discovery/discovery-artifact.interface.d.ts +11 -7
- package/dist/discovery/model.discoverer.js +8 -1
- package/dist/execution/operation.executor.js +17 -3
- package/dist/execution/scoped-cursor.resolver.d.ts +11 -0
- package/dist/execution/scoped-cursor.resolver.js +47 -0
- package/dist/generate/artifact.emitter.d.ts +8 -4
- package/dist/generate/artifact.emitter.js +10 -10
- package/dist/generate/computed-fields.import.d.ts +5 -0
- package/dist/generate/computed-fields.import.js +52 -0
- package/dist/generate/discovery.cli.js +7 -11
- package/dist/generate/index.d.ts +3 -5
- package/dist/generate/index.js +0 -1
- package/dist/generate/prisma.generate.d.ts +5 -13
- package/dist/generate/prisma.generate.js +9 -18
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/prisma-adapter.description.d.ts +42 -0
- package/dist/prisma-adapter.description.js +36 -0
- package/dist/prisma.adapter.js +10 -30
- package/dist/translation/operation.translator.d.ts +11 -0
- package/dist/translation/operation.translator.js +54 -8
- package/dist/translation/order-pagination.translator.d.ts +19 -0
- package/dist/translation/order-pagination.translator.js +15 -0
- package/package.json +2 -2
- package/dist/generate/computed-fields.reader.d.ts +0 -19
- package/dist/generate/computed-fields.reader.js +0 -210
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# @aventara/prisma7-adapter
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.8
|
|
4
|
+
|
|
5
|
+
1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- 1c8e4ee: The discovery artifact's referential actions say `referenceFields` where they said `references`: in
|
|
10
|
+
`referentialActions.<Resource>.owningEdges[]`, the fields of the Resource itself that hold the reference
|
|
11
|
+
(`@relation(fields: [...])`). The model's relation fields already use `references` for the target's fields
|
|
12
|
+
(`@relation(references: [...])`) and `referenceFields` for these, so the same word meant two different lists in one
|
|
13
|
+
file. The generated `discovery.artifact.ts` changes on the next `aventara:prepare` (it runs on install, build and
|
|
14
|
+
start), and only in those keys: `modelHash` is unchanged, since the referential actions are outside it, and so is the
|
|
15
|
+
prebuilt Contracts file `aventara-build` writes. Code that read `owningEdges[i].references` reads
|
|
16
|
+
`owningEdges[i].referenceFields`.
|
|
17
|
+
- Updated dependencies [1c8e4ee]
|
|
18
|
+
- @aventara/core@1.0.0-rc.8
|
|
19
|
+
|
|
20
|
+
## 1.0.0-rc.7
|
|
21
|
+
|
|
22
|
+
1.0.0-rc.5 and 1.0.0-rc.6 were tagged but never published; 1.0.0-rc.7 contains everything listed under them and the changes below.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [f917c1c]
|
|
27
|
+
- @aventara/core@1.0.0-rc.7
|
|
28
|
+
|
|
29
|
+
## 1.0.0-rc.6
|
|
30
|
+
|
|
31
|
+
1.0.0-rc.5 was tagged but never published. 1.0.0-rc.6 contains everything listed under 1.0.0-rc.5 and the fix below.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- Updated dependencies [1cf72e0]
|
|
36
|
+
- @aventara/core@1.0.0-rc.6
|
|
37
|
+
|
|
38
|
+
## 1.0.0-rc.5
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- 4795ff1: Two additions for policy engines. A Framework without a policy, and every Contract a caller is served, are unchanged.
|
|
43
|
+
|
|
44
|
+
- A policy engine can check a create whose row reaches another Resource through a relation. Discovery now states, on the end of a relation that holds the reference, which of the target's fields its reference fields name, pair by pair (`Note.campaign`: `referenceFields: ["campaignId"]`, `references: ["id"]`). An engine reads them from the model it is built from (a Prisma discovery artifact's `model`) or from `framework.contracts.core`, and asks one `exists` about the row they name: "a player creates a note only in a campaign they play in". Only the adapter's model and the Core Contract carry them. The Application, Client and Agent Contracts, `/_contract`, the MCP tools and every ClientContract hash are unchanged. The discovery artifact's model hash, and so a prebuilt Contracts file's fingerprint, change: regenerate the artifact and rebuild the prebuilt file.
|
|
45
|
+
- `fieldsOf({ access: "write" })` also names the action that writes the record, `action: "create" | "update"` (an upsert's branches each their own, a nested record its directive's), so an engine can lock a field on update and still let a create write it. A read question names none. An engine that ignores it keeps working.
|
|
46
|
+
|
|
47
|
+
- 9e178ca: Fixes for applications that check requests with a policy engine. A Framework without a policy behaves as before.
|
|
48
|
+
|
|
49
|
+
- A cursor on a row the caller cannot read is answered exactly as a cursor on a row that does not exist, at the root and in a to-many relation, whatever values the caller sends beside the identifier. Before, the page started at the hidden row, so it told that the row exists and where its values sort. The Prisma adapter reads the cursor's row under the read scope first: one statement more for a cursor on a readable row, and only that read for one on a row the caller cannot read.
|
|
50
|
+
- A unique update with a nested write (`$create`, `$connect`, …) on a row the caller can read but not change answers `A4002`, as the same update without one does. It answered `A2003`. A row the caller cannot read still answers `A2003`.
|
|
51
|
+
- The `exists` questions a policy asks select the Resource's primary identifier, never another unique field such as a governed `email`.
|
|
52
|
+
- `policy.principal` may answer with a promise. A synchronous principal works as before; a rejection answers `A3000`, as a throw does.
|
|
53
|
+
- `AvConstructionError.message` names the first problem and how many more there are, for example `Aventara framework configuration validation failed: CONFIG_INVALID_TYPE at adapter.execute: adapter.execute must be a function (+2 more)`. `stage` and `diagnostics` are unchanged.
|
|
54
|
+
|
|
55
|
+
## 1.0.0-rc.4
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- 7c3b65f: Build-time Contracts for serverless and edge cold starts.
|
|
60
|
+
|
|
61
|
+
- New package `@aventara/build`, bin `aventara-build`, a development dependency. It runs `src/aventara.config.ts` and
|
|
62
|
+
`src/aventara.adapter.ts` without a database client, compiles the Contracts with the project's `@aventara/core` and
|
|
63
|
+
writes `src/generated/aventara/contracts.generated.ts` (`aventaraContracts`), which a serverless or edge entry passes
|
|
64
|
+
to `createFramework` as `prebuilt`. A configuration that startup would refuse fails the build with the same
|
|
65
|
+
`CONFIG_…`/`COMPILER_…` diagnostics. `aventara-build --check` writes nothing and exits 1 when the file is missing or
|
|
66
|
+
stale, naming the Resources that changed.
|
|
67
|
+
- `@aventara/prisma7-adapter`: `describePrismaAdapter({ discovery, provider, driver })` states the adapter's passive
|
|
68
|
+
facts (the model, the driver's transaction capability, the referential actions, the computed fields) with no client.
|
|
69
|
+
`createPrismaAdapter({ ...description, client })` builds the adapter from the same facts, and still checks the client
|
|
70
|
+
against the discovery artifact.
|
|
71
|
+
- `@aventara/cli`: the scaffold's `src/aventara.config.ts` exports the configuration as a plain value
|
|
72
|
+
(`export const aventaraConfig = { … } as const`) with no `adapter`. A new `src/aventara.adapter.ts` default-exports
|
|
73
|
+
`describePrismaAdapter(…)`, and `app.module.ts` joins the two:
|
|
74
|
+
`config: { ...aventaraConfig, adapter: await createPrismaAdapter({ ...adapter, client: prisma }) }`. The Nest server
|
|
75
|
+
still compiles its Contracts at startup and passes no `prebuilt`. `aventara:prepare` ends with `&& aventara-build`,
|
|
76
|
+
and `@aventara/build` is an exact development dependency.
|
|
77
|
+
|
|
78
|
+
- 2b1b17e: `aventara-build`'s files no longer land inside a Prisma client generated into `src/generated`.
|
|
79
|
+
|
|
80
|
+
When `aventara init` reuses a project whose Prisma client is generated into `src/generated`, it already puts the
|
|
81
|
+
discovery artifact in `src/aventara/`, because `prisma generate` deletes every file in its output directory that it did
|
|
82
|
+
not write. `aventara-build` still wrote its three files to `src/generated/aventara/`, so each `prisma generate` deleted
|
|
83
|
+
them and the project stopped compiling until the next `aventara:prepare`. Now all of Aventara's generated files are
|
|
84
|
+
in one place.
|
|
85
|
+
|
|
86
|
+
- `@aventara/build`: `aventara-build` writes `resources.generated.ts`, `computed-fields.generated.ts` and
|
|
87
|
+
`contracts.generated.ts` to the directory the configuration imports `aventaraResources` from, so the tree is always
|
|
88
|
+
written where the configuration reads it. A configuration that imports no tree keeps `src/generated/aventara/`.
|
|
89
|
+
The files of a project whose client is generated anywhere else are the same bytes, in the same place.
|
|
90
|
+
- `@aventara/prisma7-adapter`: with `--config`, the artifact imports the computed fields from its sibling,
|
|
91
|
+
`./computed-fields.generated`, wherever `--out` puts it. `--config` is still required for that import and the step
|
|
92
|
+
still checks that the file exists, but the step no longer uses its location. A scaffold's artifact is unchanged.
|
|
93
|
+
- `@aventara/cli`: in that project, `src/aventara.config.ts` imports the tree from `./aventara/resources.generated`,
|
|
94
|
+
and `.gitignore` gets the one `/src/aventara/` line that covers all four files (no separate
|
|
95
|
+
`/src/generated/aventara/contracts.generated.ts` line). The agent skill says where the generated files are in
|
|
96
|
+
both cases.
|
|
97
|
+
|
|
98
|
+
- 3a0dfca: A write the policy checks costs fewer database round trips. What each check decides and refuses is unchanged.
|
|
99
|
+
|
|
100
|
+
- A single-row update or delete carries the caller's scopes inside its own `where` and runs as one statement, with no
|
|
101
|
+
read of its row before it. One that matches nothing still answers `A4002` when the caller can read the row and
|
|
102
|
+
`A2003` when it cannot, with one lookup on that miss only. The Prisma adapter sends it as
|
|
103
|
+
`update({ where: { id, AND: [scope] } })`.
|
|
104
|
+
- A create asks `can({ action: "create", row })` and its `exists` lookups before the insert, without a transaction. A
|
|
105
|
+
transaction around them never closed the race between the lookup and the insert at the database's default
|
|
106
|
+
isolation, and cost a BEGIN and a COMMIT.
|
|
107
|
+
- A transaction opens only when a check after the write may roll it back: an update's after check or move-out, a
|
|
108
|
+
created row held to a write grant, a nested write, or returned rows checked by `exists`. A plain checked create or
|
|
109
|
+
delete is one statement, its interceptors and hooks outside any transaction.
|
|
110
|
+
- The after check asks `can` about the row the update returned, so it reads nothing again. Only a move-out reads the
|
|
111
|
+
changed rows again. An `update.count` runs as the returning `update.many` when the Contract offers it.
|
|
112
|
+
|
|
113
|
+
Round trips per request on PostgreSQL: `create.one` 3 → 1, `delete.unique` 4 → 1, `update.unique` 5 → 3,
|
|
114
|
+
`update.many` 4 → 3, `update.count` 5 → 3. Reads, and a Framework without a policy, are unchanged.
|
|
115
|
+
|
|
116
|
+
- 3813207: The Resource tree and the computed fields come from `aventara-build`.
|
|
117
|
+
|
|
118
|
+
- `@aventara/build`: before it compiles the Contracts, `aventara-build` writes two more files under
|
|
119
|
+
`src/generated/aventara/`. `resources.generated.ts` exports `aventaraResources`: every `defineResource(…)` result
|
|
120
|
+
that the files `src/resources/**/*.resource.ts` export, found by its type (never run), one `resources` tree per
|
|
121
|
+
layer, written as the plain spreads you wrote by hand (`root` always, `application`, `client` and `agent` when some
|
|
122
|
+
Resource has a block for them). Two definitions of one Resource stop the build, naming both. Imports are spelled as
|
|
123
|
+
the project spells its own. `computed-fields.generated.ts` exports `computedFields` and its type `AvComputedFields`:
|
|
124
|
+
which fields the configuration's write compute fills, read off its type after the tree exists, so a fresh install is
|
|
125
|
+
typed right on its first `aventara:prepare`. `--check` covers all three files. `typescript` (`>=5.7.0 <7`) is now a
|
|
126
|
+
peer dependency.
|
|
127
|
+
- `@aventara/prisma7-adapter`: the generate step no longer reads the configuration. With `--config`, the discovery
|
|
128
|
+
artifact imports `computedFields` and `AvComputedFields` from the file `aventara-build` writes beside the
|
|
129
|
+
configuration (`generated/aventara/computed-fields.generated.ts`); a `--config` file that does not exist is still
|
|
130
|
+
refused. `readConfigComputedFields`, `AvPrismaConfigReaderError` and `ConfigComputedFieldsReading` are removed from
|
|
131
|
+
`@aventara/prisma7-adapter/generate`, as are `PrismaGenerateToolchain.readComputedFields` and
|
|
132
|
+
`PrismaGenerateResult.computedFields`.
|
|
133
|
+
- `@aventara/cli`: the scaffold's `src/aventara.config.ts` takes its Resources from the tree,
|
|
134
|
+
`resources: aventaraResources.root`, so a new Resource file needs no configuration edit; the agent skill says so.
|
|
135
|
+
|
|
136
|
+
- Updated dependencies [e3d03a7]
|
|
137
|
+
- Updated dependencies [d10639a]
|
|
138
|
+
- Updated dependencies [f67e68c]
|
|
139
|
+
- Updated dependencies [708556c]
|
|
140
|
+
- Updated dependencies [3a0dfca]
|
|
141
|
+
- Updated dependencies [99169d2]
|
|
142
|
+
- Updated dependencies [0fd2608]
|
|
143
|
+
- Updated dependencies [154e440]
|
|
144
|
+
- Updated dependencies [2fcffdb]
|
|
145
|
+
- @aventara/core@1.0.0-rc.4
|
|
146
|
+
|
|
3
147
|
## 1.0.0-rc.3
|
|
4
148
|
|
|
5
149
|
### Patch Changes
|
|
@@ -21,7 +165,7 @@
|
|
|
21
165
|
`the artifact file /app/src/generated/aventara/discovery.artifact.ts (--out) cannot be written: permission denied (EACCES).`,
|
|
22
166
|
and leaves no staged file behind. Before, every such failure except a directory printed a stack trace.
|
|
23
167
|
|
|
24
|
-
- b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's
|
|
168
|
+
- b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's limit of about 2704 bytes, SQLSTATE
|
|
25
169
|
`54000`) is answered `422 A2004`, not `500 A3001`: one `V1004` issue at `["arguments"]`, as a value too long for its
|
|
26
170
|
column already was. The adapter reads the SQLSTATE, never the message. The issue's message for a value too long now
|
|
27
171
|
reads `The database refused this value: it is longer than the database can hold for its column.`
|
package/README.md
CHANGED
|
@@ -55,6 +55,31 @@ than a declaration that is checked.
|
|
|
55
55
|
`createPrismaAdapter` is `async`. Construction verifies the discovery artifact, and the digest it
|
|
56
56
|
checks is a `crypto.subtle` promise, so every check runs once, here, before an adapter exists.
|
|
57
57
|
|
|
58
|
+
### `describePrismaAdapter`: the same facts, with no client
|
|
59
|
+
|
|
60
|
+
`describePrismaAdapter({ discovery, provider, driver })` returns the adapter's passive facts: the model,
|
|
61
|
+
the driver's transaction capability, the referential actions in Aventara's words and the computed
|
|
62
|
+
fields the generated types were written with. It constructs nothing and connects to nothing, so
|
|
63
|
+
`aventara-build` (`@aventara/build`) compiles the Contracts from it at build time. Spread into
|
|
64
|
+
`createPrismaAdapter` with the client, it is the adapter's options, and the adapter states exactly the
|
|
65
|
+
facts the description does:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// src/aventara.adapter.ts
|
|
69
|
+
export default describePrismaAdapter({
|
|
70
|
+
discovery,
|
|
71
|
+
provider: "postgresql",
|
|
72
|
+
driver: "@prisma/adapter-pg",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// where the framework is created
|
|
76
|
+
import description from "./aventara.adapter.js";
|
|
77
|
+
const adapter = await createPrismaAdapter({ ...description, client });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`createPrismaAdapter` still verifies the artifact and checks the client against it; the description
|
|
81
|
+
does neither, because it has no client to check.
|
|
82
|
+
|
|
58
83
|
### `maxWait`, `timeout` and `isolationLevel` are adapter-owned
|
|
59
84
|
|
|
60
85
|
They are provider concepts. Core's `Adapter.transaction(work)` interface carries no options and will
|
|
@@ -99,16 +124,18 @@ installed keeps `@prisma/prisma7` beside it. With neither usable, the step refus
|
|
|
99
124
|
both. TypeScript 6 is supported; TypeScript 7, which has no classic compiler API, is refused in one
|
|
100
125
|
sentence.
|
|
101
126
|
|
|
102
|
-
`--config` is optional. It names your Aventara configuration module
|
|
103
|
-
|
|
104
|
-
configuration's TypeScript type
|
|
105
|
-
`compute.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
`--config` is optional. It names your Aventara configuration module (`src/aventara.config.ts`). The step
|
|
128
|
+
neither reads nor runs it: `aventara-build` (`@aventara/build`), which `aventara:prepare` runs right after
|
|
129
|
+
this step, reads the configuration's TypeScript type and writes which fields its `compute.create` and
|
|
130
|
+
`compute.update` stages fill, per layer, to `computed-fields.generated.ts` beside the artifact (the
|
|
131
|
+
directory the configuration imports its Resource tree from). The artifact imports that sibling file: `computedFields` (also `discovery.computedFields`) and its type, which the
|
|
132
|
+
model's type carries. A variant's guards, pipes and filters, which run before write compute, may then
|
|
133
|
+
leave exactly those fields out of a create; every other required field stays required. On a fresh install
|
|
134
|
+
the file is not there until `aventara-build` has run. Without `--config`, or when the configuration is not
|
|
135
|
+
a literal where a compute stage is written (`aventara-build` prints where), the artifact states nothing and
|
|
136
|
+
those stages may leave any create field out. When the framework starts, it compares the statement with
|
|
137
|
+
the configuration and warns (`AVENTARA_STALE_GENERATED_TYPES`) if you changed a compute stage without
|
|
138
|
+
running `aventara:prepare` again.
|
|
112
139
|
|
|
113
140
|
`--out` must not be inside `--client`: Prisma's next `prisma generate` clears that directory and would
|
|
114
141
|
delete the artifact. The step refuses such an `--out`, and one that names a directory, in one sentence
|
|
@@ -502,10 +529,10 @@ discovery.referentialActions;
|
|
|
502
529
|
// {
|
|
503
530
|
// Shop: { owningEdges: [] },
|
|
504
531
|
// Item: { owningEdges: [{ field: "shop", target: "Shop",
|
|
505
|
-
// cardinality: "one",
|
|
532
|
+
// cardinality: "one", referenceFields: ["shopId"],
|
|
506
533
|
// onDelete: "cascade", onUpdate: "cascade" }] },
|
|
507
534
|
// Clerk: { owningEdges: [{ field: "shop", target: "Shop",
|
|
508
|
-
// cardinality: "one",
|
|
535
|
+
// cardinality: "one", referenceFields: ["shopId"],
|
|
509
536
|
// onDelete: "set-null", onUpdate: "cascade" }] },
|
|
510
537
|
// }
|
|
511
538
|
```
|
|
@@ -517,7 +544,7 @@ read the enclosing Resource key: above, that is `Item` (cascade) and `Clerk` (se
|
|
|
517
544
|
that sentence are Resource names and nothing in the shape tells them apart, so reading it backwards
|
|
518
545
|
produces no error, only a plan that reasons backwards.
|
|
519
546
|
|
|
520
|
-
`
|
|
547
|
+
`referenceFields` are the edge's `@relation(fields: […])`, the scalars that hold the reference (the relation field's `referenceFields` in the model; its `references` are the target's fields). A record
|
|
521
548
|
that writes both a relation and one of its reference fields (`shopId` and `shop: { $connect: … }`,
|
|
522
549
|
whether the relation is required or optional) is refused before the database is reached: `A2004` with
|
|
523
550
|
a `V1012` issue at the relation, message `Set "shopId" or "shop", not both.` The reference fields are
|
|
@@ -641,7 +668,9 @@ Resource or operation for one to wrap.
|
|
|
641
668
|
- A record written through a relation cannot write that relation, or its reference fields, back: Prisma's nested
|
|
642
669
|
input for it leaves them out (a post created in `author.posts` has no `author` or `authorId`; a category created in
|
|
643
670
|
a post's `category` has no `posts`). The artifact states each relation's other end (`inverse`, paired by relation
|
|
644
|
-
name), the fields that hold its reference (`referenceFields`, from `@relation(fields: …)`) and
|
|
671
|
+
name), the fields that hold its reference (`referenceFields`, from `@relation(fields: …)`) and the target's fields
|
|
672
|
+
they hold, pair by pair (`references`, from `@relation(references: …)`, which `@aventara/core` keeps in the Core
|
|
673
|
+
Contract only, for a policy engine), and no relation
|
|
645
674
|
writable at the `"loopback"` position, so `@aventara/core` refuses such a write before any
|
|
646
675
|
guard or SQL, `A2004` with a `V1012` issue at the field written back, and the types do not offer it. Where a layer
|
|
647
676
|
hides the other end, Prisma's client refuses the write instead and the adapter answers that refusal the same way.
|
|
@@ -42,9 +42,11 @@ export type AvPrismaReferentialEdge = {
|
|
|
42
42
|
readonly cardinality: "one" | "many";
|
|
43
43
|
/**
|
|
44
44
|
* The scalar fields of the enclosing resource that hold the reference: the
|
|
45
|
-
* relation's `@relation(fields: […])`, in schema order.
|
|
45
|
+
* relation's `@relation(fields: […])`, in schema order. Named as the relation
|
|
46
|
+
* field's `referenceFields` in the model: its `references` are the TARGET's
|
|
47
|
+
* fields (`@relation(references: […])`).
|
|
46
48
|
*/
|
|
47
|
-
readonly
|
|
49
|
+
readonly referenceFields: readonly string[];
|
|
48
50
|
/** What deleting a `target` row does to rows of the enclosing resource. */
|
|
49
51
|
readonly onDelete: AvPrismaReferentialAction;
|
|
50
52
|
/** The same for an update of the referenced key. Read independently of `onDelete`. */
|
|
@@ -107,11 +109,13 @@ export type AvPrismaDiscoveryArtifact<M extends AvPrismaAdapterModel = AvPrismaA
|
|
|
107
109
|
*/
|
|
108
110
|
readonly client: PrismaClientShape;
|
|
109
111
|
/**
|
|
110
|
-
* Which fields the configuration's write compute fills, per layer, as
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
112
|
+
* Which fields the configuration's write compute fills, per layer, as
|
|
113
|
+
* `aventara-build` read them off the configuration's type (the artifact
|
|
114
|
+
* imports them when the step was given `--config`); `null` when it was
|
|
115
|
+
* given none, or the build could not read them. The model's type carries
|
|
116
|
+
* the same statement for the pipeline stages' types; this value is compared
|
|
117
|
+
* with the configuration when the framework starts. Not part of
|
|
118
|
+
* `modelHash`.
|
|
115
119
|
*/
|
|
116
120
|
readonly computedFields: AvConfigComputedFields | null;
|
|
117
121
|
};
|
|
@@ -245,10 +245,17 @@ function composeResource(input) {
|
|
|
245
245
|
const referenceFields = firstReference === undefined
|
|
246
246
|
? undefined
|
|
247
247
|
: [firstReference, ...moreReferences];
|
|
248
|
+
const [firstTarget, ...moreTargets] = field.relation?.references ?? [];
|
|
249
|
+
const references = referenceFields === undefined ||
|
|
250
|
+
firstTarget === undefined ||
|
|
251
|
+
moreTargets.length + 1 !== referenceFields.length
|
|
252
|
+
? undefined
|
|
253
|
+
: [firstTarget, ...moreTargets];
|
|
248
254
|
fields[projection.name] = {
|
|
249
255
|
...projection.field,
|
|
250
256
|
...(inverse === undefined ? {} : { inverse: inverse.relation }),
|
|
251
257
|
...(referenceFields === undefined ? {} : { referenceFields }),
|
|
258
|
+
...(references === undefined ? {} : { references }),
|
|
252
259
|
capabilities: deriveRelationCapabilities({
|
|
253
260
|
surface: relationSurface,
|
|
254
261
|
}),
|
|
@@ -259,7 +266,7 @@ function composeResource(input) {
|
|
|
259
266
|
field: projection.edge.field,
|
|
260
267
|
target: projection.edge.target,
|
|
261
268
|
cardinality: projection.edge.cardinality,
|
|
262
|
-
|
|
269
|
+
referenceFields: [...(referenceFields ?? [])],
|
|
263
270
|
onDelete: projection.edge.onDelete,
|
|
264
271
|
onUpdate: projection.edge.onUpdate,
|
|
265
272
|
});
|
|
@@ -2,9 +2,10 @@ import { locateBackReferenceWrite, } from "../errors/back-reference.locator.js";
|
|
|
2
2
|
import { locateUnboundBigint } from "../errors/bigint-binding.locator.js";
|
|
3
3
|
import { classifyPrismaError, prismaNestedWriteFailure, prismaOutcomeFailure, prismaValueFailure, } from "../errors/error.normalizer.js";
|
|
4
4
|
import { prismaDelegates, } from "../prisma-client.interface.js";
|
|
5
|
-
import { PRISMA_VARIANT_DISPATCH, translateOperation, } from "../translation/operation.translator.js";
|
|
5
|
+
import { PRISMA_VARIANT_DISPATCH, translateOperation, translateScopedCursorLookup, } from "../translation/operation.translator.js";
|
|
6
6
|
import { runFirstVariantEmulation } from "./first-variant.emulator.js";
|
|
7
7
|
import { shapeCount, shapeRow, shapeRows, } from "./result.shaper.js";
|
|
8
|
+
import { resolveScopedCursors } from "./scoped-cursor.resolver.js";
|
|
8
9
|
export class AvPrismaExecutionError extends Error {
|
|
9
10
|
name = "AvPrismaExecutionError";
|
|
10
11
|
}
|
|
@@ -21,8 +22,9 @@ export function createPrismaOperationExecutor(dependencies) {
|
|
|
21
22
|
: {}),
|
|
22
23
|
});
|
|
23
24
|
return {
|
|
24
|
-
async execute(
|
|
25
|
-
const result = resultKindOf(dispatchOf(
|
|
25
|
+
async execute(sent) {
|
|
26
|
+
const result = resultKindOf(dispatchOf(sent));
|
|
27
|
+
const operation = await withScopedCursorsResolved(dependencies, sent, (call) => issueClassified(issue, call, sent, locatorFor(sent)));
|
|
26
28
|
const translation = translateOperation({
|
|
27
29
|
model: dependencies.model,
|
|
28
30
|
translation: dependencies.translation,
|
|
@@ -53,6 +55,18 @@ export function createPrismaOperationExecutor(dependencies) {
|
|
|
53
55
|
},
|
|
54
56
|
};
|
|
55
57
|
}
|
|
58
|
+
async function withScopedCursorsResolved(dependencies, operation, issue) {
|
|
59
|
+
const args = operation.arguments;
|
|
60
|
+
const resolved = await resolveScopedCursors(dependencies.model, operation.resource, args, async (resource, cursor) => (await issue(translateScopedCursorLookup({
|
|
61
|
+
model: dependencies.model,
|
|
62
|
+
translation: dependencies.translation,
|
|
63
|
+
resource,
|
|
64
|
+
cursor,
|
|
65
|
+
}))) !== null);
|
|
66
|
+
return resolved === args
|
|
67
|
+
? operation
|
|
68
|
+
: { ...operation, arguments: resolved };
|
|
69
|
+
}
|
|
56
70
|
async function withUnit(dependencies, work) {
|
|
57
71
|
if (dependencies.scope === "transaction") {
|
|
58
72
|
return work(dependencies.client);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AvAdapterModel } from "@aventara/core";
|
|
2
|
+
type Level = Readonly<Record<string, unknown>>;
|
|
3
|
+
/** Whether the row `cursor` (with its `AND`) names on `resource` meets its conditions. */
|
|
4
|
+
export type PrismaScopedCursorLookup = (resource: string, cursor: Level) => Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* The operation's arguments with every cursor that carries `AND` resolved: its
|
|
7
|
+
* address alone when the row meets the conditions, otherwise no cursor and
|
|
8
|
+
* `limit: 0`. The same object when no cursor carries `AND`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveScopedCursors(model: AvAdapterModel, resource: string, args: Level, lookup: PrismaScopedCursorLookup): Promise<Level>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export async function resolveScopedCursors(model, resource, args, lookup) {
|
|
2
|
+
return (await resolveLevel(model, resource, args, lookup)) ?? args;
|
|
3
|
+
}
|
|
4
|
+
async function resolveLevel(model, resource, level, lookup) {
|
|
5
|
+
let resolved;
|
|
6
|
+
const cursor = level.cursor;
|
|
7
|
+
if (isRecord(cursor) && Object.hasOwn(cursor, "AND")) {
|
|
8
|
+
const { AND: _conditions, ...address } = cursor;
|
|
9
|
+
const { cursor: _cursor, ...rest } = level;
|
|
10
|
+
resolved = (await lookup(resource, cursor))
|
|
11
|
+
? { ...rest, cursor: address }
|
|
12
|
+
: { ...rest, limit: 0 };
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(level.select)) {
|
|
15
|
+
const select = await resolveSelect(model, resource, level.select, lookup);
|
|
16
|
+
if (select !== undefined) {
|
|
17
|
+
resolved = { ...(resolved ?? level), select };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
async function resolveSelect(model, resource, select, lookup) {
|
|
23
|
+
let changed = false;
|
|
24
|
+
const entries = [];
|
|
25
|
+
for (const entry of select) {
|
|
26
|
+
if (!isRecord(entry)) {
|
|
27
|
+
entries.push(entry);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const rewritten = {};
|
|
31
|
+
for (const [name, value] of Object.entries(entry)) {
|
|
32
|
+
const field = model.resources[resource]?.fields[name];
|
|
33
|
+
const inner = field?.kind === "relation" && isRecord(value)
|
|
34
|
+
? await resolveLevel(model, field.target, value, lookup)
|
|
35
|
+
: undefined;
|
|
36
|
+
if (inner !== undefined) {
|
|
37
|
+
changed = true;
|
|
38
|
+
}
|
|
39
|
+
rewritten[name] = inner ?? value;
|
|
40
|
+
}
|
|
41
|
+
entries.push(rewritten);
|
|
42
|
+
}
|
|
43
|
+
return changed ? entries : undefined;
|
|
44
|
+
}
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AvConfigComputedFields } from "@aventara/core";
|
|
2
1
|
import { type AvPrismaAdapterModel, type AvPrismaReferentialActionTable, type PrismaClientShape, type PrismaTranslationTable } from "../discovery/discovery-artifact.interface.js";
|
|
3
2
|
import type { AvPrismaProvider } from "../provider/provider-profile.js";
|
|
4
3
|
export type PrismaArtifactEmission = {
|
|
@@ -12,9 +11,14 @@ export type PrismaArtifactEmission = {
|
|
|
12
11
|
/** The generated client's models and field references — beside `model`, outside the hash. */
|
|
13
12
|
readonly client: PrismaClientShape;
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* The specifier the module imports the configuration's computed fields by:
|
|
15
|
+
* the file `aventara-build` writes beside the configuration
|
|
16
|
+
* (`computed-fields.generated.ts`). `null` when the step was given no
|
|
17
|
+
* configuration: the module then states none. Beside `model`, outside the
|
|
18
|
+
* hash.
|
|
17
19
|
*/
|
|
18
|
-
readonly computedFields:
|
|
20
|
+
readonly computedFields: {
|
|
21
|
+
readonly specifier: string;
|
|
22
|
+
} | null;
|
|
19
23
|
};
|
|
20
24
|
export declare function emitDiscoveryArtifactModule(emission: PrismaArtifactEmission): Promise<string>;
|
|
@@ -19,7 +19,7 @@ const REFERENTIAL_ACTIONS_DOC = [
|
|
|
19
19
|
"/**",
|
|
20
20
|
" * The schema's referential actions, per Resource: the relation fields that",
|
|
21
21
|
" * declare `@relation(fields: …)`, each with the fields holding its reference",
|
|
22
|
-
" * (`
|
|
22
|
+
" * (`referenceFields`) and its resolved `onDelete` and `onUpdate`. An edge under",
|
|
23
23
|
" * Resource `R` reads:",
|
|
24
24
|
" *",
|
|
25
25
|
" * deleting a `target` row does `onDelete` to rows of `R`.",
|
|
@@ -60,8 +60,8 @@ const MODEL_TYPE_DOC = [
|
|
|
60
60
|
const COMPUTED_FIELDS_DOC = [
|
|
61
61
|
"/**",
|
|
62
62
|
" * Which fields the configuration's write compute fills (`compute.create`,",
|
|
63
|
-
" * `compute.update`), per layer and Resource,
|
|
64
|
-
" *
|
|
63
|
+
" * `compute.update`), per layer and Resource, as aventara-build read them off",
|
|
64
|
+
" * the configuration's type. A variant's guards, pipes and filters may leave a",
|
|
65
65
|
" * create's computed fields out; every other required field stays required.",
|
|
66
66
|
" * The framework warns at startup when the configuration says otherwise: run",
|
|
67
67
|
" * `aventara:prepare` again. Not part of `modelHash`.",
|
|
@@ -90,10 +90,7 @@ function computedFieldsLines(computedFields) {
|
|
|
90
90
|
}
|
|
91
91
|
return [
|
|
92
92
|
...COMPUTED_FIELDS_DOC,
|
|
93
|
-
|
|
94
|
-
"",
|
|
95
|
-
"/** The type of {@link computedFields}: what `AvModel` carries to the stages' types. */",
|
|
96
|
-
"export type AvComputedFields = typeof computedFields;",
|
|
93
|
+
"export { type AvComputedFields, computedFields };",
|
|
97
94
|
"",
|
|
98
95
|
];
|
|
99
96
|
}
|
|
@@ -102,9 +99,12 @@ export async function emitDiscoveryArtifactModule(emission) {
|
|
|
102
99
|
return `${[
|
|
103
100
|
...BANNER,
|
|
104
101
|
"",
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
'import type { AvAdapterModel } from "@aventara/core";',
|
|
103
|
+
...(emission.computedFields === null
|
|
104
|
+
? []
|
|
105
|
+
: [
|
|
106
|
+
`import { type AvComputedFields, computedFields } from ${JSON.stringify(emission.computedFields.specifier)};`,
|
|
107
|
+
]),
|
|
108
108
|
"",
|
|
109
109
|
`const discoveredModel = ${canonicalLiteral(emission.model)} as const satisfies AvAdapterModel;`,
|
|
110
110
|
"",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
function projectOptions(directory) {
|
|
5
|
+
const tsconfig = ts.findConfigFile(directory, ts.sys.fileExists);
|
|
6
|
+
if (tsconfig === undefined) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
const read = ts.readConfigFile(tsconfig, ts.sys.readFile);
|
|
10
|
+
return read.error !== undefined
|
|
11
|
+
? {}
|
|
12
|
+
: ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(tsconfig))
|
|
13
|
+
.options;
|
|
14
|
+
}
|
|
15
|
+
function packageType(directory) {
|
|
16
|
+
for (let current = path.resolve(directory);;) {
|
|
17
|
+
const manifest = path.join(current, "package.json");
|
|
18
|
+
if (existsSync(manifest)) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(manifest, "utf8")).type;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const parent = path.dirname(current);
|
|
27
|
+
if (parent === current) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
current = parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function importSuffix(directory) {
|
|
34
|
+
const options = projectOptions(directory);
|
|
35
|
+
if (options.allowImportingTsExtensions === true ||
|
|
36
|
+
options
|
|
37
|
+
.rewriteRelativeImportExtensions === true) {
|
|
38
|
+
return ".ts";
|
|
39
|
+
}
|
|
40
|
+
if (options.module === ts.ModuleKind.CommonJS ||
|
|
41
|
+
options.moduleResolution === ts.ModuleResolutionKind.Bundler) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
if (options.module === ts.ModuleKind.Node16 ||
|
|
45
|
+
options.module === ts.ModuleKind.NodeNext) {
|
|
46
|
+
return packageType(directory) === "module" ? ".js" : "";
|
|
47
|
+
}
|
|
48
|
+
return ".js";
|
|
49
|
+
}
|
|
50
|
+
export function computedFieldsSpecifier(artifactFile) {
|
|
51
|
+
return `./computed-fields.generated${importSuffix(path.dirname(path.resolve(artifactFile)))}`;
|
|
52
|
+
}
|
|
@@ -14,10 +14,13 @@ Options:
|
|
|
14
14
|
--driver <name> ${PRISMA_DRIVERS.join(" | ")}
|
|
15
15
|
--out <file> Where to write the emitted artifact module (a .ts file).
|
|
16
16
|
--config <file> Optional: your Aventara configuration module, such as
|
|
17
|
-
src/aventara.config.ts
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
src/aventara.config.ts. aventara-build reads which fields
|
|
18
|
+
it computes and writes them to
|
|
19
|
+
computed-fields.generated.ts beside the artifact; the
|
|
20
|
+
artifact imports them from there, so the stages that
|
|
21
|
+
run before compute may leave exactly those out of a
|
|
22
|
+
create. This step never reads the configuration; it
|
|
23
|
+
only checks that it exists.
|
|
21
24
|
-h, --help Print this and exit 0.
|
|
22
25
|
|
|
23
26
|
The artifact must NOT be written inside --client: the next \`prisma generate\`
|
|
@@ -94,12 +97,6 @@ async function main(argv) {
|
|
|
94
97
|
for (const absence of result.excluded) {
|
|
95
98
|
process.stdout.write(`excluded: ${absence.message}\n`);
|
|
96
99
|
}
|
|
97
|
-
if (result.computedFields?.kind === "read") {
|
|
98
|
-
process.stdout.write(`computed fields: read from ${options.config ?? "the configuration"}\n`);
|
|
99
|
-
}
|
|
100
|
-
else if (result.computedFields?.kind === "unread") {
|
|
101
|
-
process.stdout.write(`computed fields: not read, ${result.computedFields.reason}; the stages before compute may leave any create field out. Write the configuration \`as const\` there.\n`);
|
|
102
|
-
}
|
|
103
100
|
for (const exemption of result.exemptions) {
|
|
104
101
|
process.stdout.write(`exempt (${exemption.kind}) ${exemption.entity}: ${exemption.reason}\n`);
|
|
105
102
|
}
|
|
@@ -114,7 +111,6 @@ export async function runFromProcess() {
|
|
|
114
111
|
(error instanceof Error &&
|
|
115
112
|
(error.name === "AvPrismaGenerateStepError" ||
|
|
116
113
|
error.name === "AvPrismaModelCompositionError" ||
|
|
117
|
-
error.name === "AvPrismaConfigReaderError" ||
|
|
118
114
|
error.name === "AvPrismaSchemaReaderError"));
|
|
119
115
|
process.stderr.write(named
|
|
120
116
|
? `aventara-prisma7-generate: ${error.message}\n`
|
package/dist/generate/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { AvPrismaDriver, AvPrismaProvider } from "../provider/provider-profile.js";
|
|
2
2
|
import type { PrismaGenerateResult, PrismaGenerateToolchain } from "./prisma.generate.js";
|
|
3
3
|
export { AvPrismaModelCompositionError, createPrismaModelComposer, } from "../discovery/model.discoverer.js";
|
|
4
|
-
export type { ConfigComputedFieldsReading } from "./computed-fields.reader.js";
|
|
5
|
-
export { AvPrismaConfigReaderError, readConfigComputedFields, } from "./computed-fields.reader.js";
|
|
6
4
|
export type { PrismaEntityAbsence } from "./existence.gate.js";
|
|
7
5
|
export type { PrismaComposedArtifact, PrismaGenerateRequest, PrismaGenerateResult, PrismaGenerateToolchain, PrismaModelComposer, } from "./prisma.generate.js";
|
|
8
6
|
export { AvPrismaGenerateStepError, createPrismaGenerateToolchain, PINNED_PRISMA_MAJOR, runPrismaGenerateStep, } from "./prisma.generate.js";
|
|
@@ -19,9 +17,9 @@ export type PrismaDiscoveryGenerateRequest = {
|
|
|
19
17
|
/** Where the emitted TypeScript artifact module is written. */
|
|
20
18
|
readonly artifactFile: string;
|
|
21
19
|
/**
|
|
22
|
-
* The Aventara configuration module
|
|
23
|
-
*
|
|
24
|
-
* states none.
|
|
20
|
+
* The Aventara configuration module (`src/aventara.config.ts` in a
|
|
21
|
+
* scaffolded project): the artifact imports the fields it computes from
|
|
22
|
+
* the file `aventara-build` writes beside it. Left out, it states none.
|
|
25
23
|
*/
|
|
26
24
|
readonly config?: string;
|
|
27
25
|
/** Replaces the step's external effects; omit it to use the installed Prisma and TypeScript. */
|
package/dist/generate/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import { createPrismaModelComposer } from "../discovery/model.discoverer.js";
|
|
|
2
2
|
import { resolveProviderProfile } from "../provider/provider-profile.js";
|
|
3
3
|
import { runPrismaGenerateStep } from "./prisma.generate.js";
|
|
4
4
|
export { AvPrismaModelCompositionError, createPrismaModelComposer, } from "../discovery/model.discoverer.js";
|
|
5
|
-
export { AvPrismaConfigReaderError, readConfigComputedFields, } from "./computed-fields.reader.js";
|
|
6
5
|
export { AvPrismaGenerateStepError, createPrismaGenerateToolchain, PINNED_PRISMA_MAJOR, runPrismaGenerateStep, } from "./prisma.generate.js";
|
|
7
6
|
export async function generatePrismaDiscoveryArtifact(request) {
|
|
8
7
|
const profile = resolveProviderProfile({
|