@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 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 8191 bytes, SQLSTATE
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: its `aventaraConfig` export (a
103
- function, as the `aventara` scaffold writes it, or a value) or its default export. The step reads the
104
- configuration's TypeScript type, never runs it, and states in the artifact which fields its
105
- `compute.create` and `compute.update` stages fill, per layer (`computedFields`). A variant's guards,
106
- pipes and filters, which run before write compute, may then leave exactly those fields out of a create;
107
- every other required field stays required. Without `--config`, or when the configuration is not a
108
- literal where a compute stage is written (the step prints where), the artifact states nothing and those
109
- stages may leave any create field out. When the framework starts, it compares the statement with the
110
- configuration and warns (`AVENTARA_STALE_GENERATED_TYPES`) if you changed a compute stage without
111
- running the step again.
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", references: ["shopId"],
532
+ // cardinality: "one", referenceFields: ["shopId"],
506
533
  // onDelete: "cascade", onUpdate: "cascade" }] },
507
534
  // Clerk: { owningEdges: [{ field: "shop", target: "Shop",
508
- // cardinality: "one", references: ["shopId"],
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
- `references` are the edge's `@relation(fields: […])`, the scalars that hold the reference. A record
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 no relation
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 references: readonly string[];
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 the
111
- * generate step read them off the configuration it was given (`--config`);
112
- * `null` when it was given none. The model's type carries the same statement
113
- * for the pipeline stages' types; this value is compared with the
114
- * configuration when the framework starts. Not part of `modelHash`.
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
- references: [...(referenceFields ?? [])],
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(operation) {
25
- const result = resultKindOf(dispatchOf(operation));
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
- * What the configuration's write compute fills, read off its type; `null`
16
- * when the step was given no configuration. Beside `model`, outside the hash.
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: AvConfigComputedFields | null;
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
- " * (`references`) and its resolved `onDelete` and `onUpdate`. An edge under",
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, read off the configuration's type",
64
- " * by the generate step. A variant's guards, pipes and filters may leave a",
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
- `export const computedFields = ${canonicalLiteral(computedFields)} as const satisfies AvConfigComputedFields;`,
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
- emission.computedFields === null
106
- ? 'import type { AvAdapterModel } from "@aventara/core";'
107
- : 'import type { AvAdapterModel, AvConfigComputedFields } from "@aventara/core";',
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,5 @@
1
+ /**
2
+ * The specifier the artifact at `artifactFile` imports the computed fields by:
3
+ * the sibling `computed-fields.generated` module.
4
+ */
5
+ export declare function computedFieldsSpecifier(artifactFile: string): string;
@@ -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 (its \`aventaraConfig\` export, or
18
- its default). The artifact states which fields it
19
- computes, so the stages that run before compute may leave
20
- exactly those out of a create.
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`
@@ -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 whose computed fields the artifact
23
- * states (`src/aventara.config.ts` in a scaffolded project); left out, it
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. */
@@ -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({