@aventara/prisma7-adapter 1.0.0-rc.1 → 1.0.0-rc.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/README.md +68 -20
  3. package/dist/discovery/artifact.loader.d.ts +6 -2
  4. package/dist/discovery/artifact.loader.js +6 -1
  5. package/dist/discovery/capability.deriver.d.ts +4 -1
  6. package/dist/discovery/capability.deriver.js +38 -3
  7. package/dist/discovery/discovery-artifact.interface.d.ts +10 -2
  8. package/dist/discovery/discovery-artifact.interface.js +1 -1
  9. package/dist/discovery/installed-client.locator.d.ts +13 -0
  10. package/dist/discovery/installed-client.locator.js +43 -0
  11. package/dist/discovery/model.discoverer.d.ts +3 -0
  12. package/dist/discovery/model.discoverer.js +14 -2
  13. package/dist/discovery/schema-facts.interface.d.ts +8 -0
  14. package/dist/discovery/schema.reader.js +23 -3
  15. package/dist/errors/back-reference.locator.d.ts +25 -0
  16. package/dist/errors/back-reference.locator.js +100 -0
  17. package/dist/errors/bigint-binding.locator.d.ts +7 -0
  18. package/dist/errors/bigint-binding.locator.js +27 -0
  19. package/dist/errors/error.normalizer.d.ts +34 -2
  20. package/dist/errors/error.normalizer.js +70 -2
  21. package/dist/errors/prisma-error.reader.d.ts +4 -0
  22. package/dist/errors/prisma-error.reader.js +2 -0
  23. package/dist/execution/operation.executor.d.ts +6 -0
  24. package/dist/execution/operation.executor.js +21 -8
  25. package/dist/execution/transaction.executor.d.ts +4 -0
  26. package/dist/execution/transaction.executor.js +2 -0
  27. package/dist/generate/artifact.emitter.d.ts +6 -0
  28. package/dist/generate/artifact.emitter.js +56 -2
  29. package/dist/generate/computed-fields.reader.d.ts +19 -0
  30. package/dist/generate/computed-fields.reader.js +210 -0
  31. package/dist/generate/declared-support.reader.d.ts +1 -1
  32. package/dist/generate/discovery.cli.d.ts +1 -0
  33. package/dist/generate/discovery.cli.js +20 -4
  34. package/dist/generate/index.d.ts +8 -0
  35. package/dist/generate/index.js +3 -0
  36. package/dist/generate/prisma.generate.d.ts +17 -0
  37. package/dist/generate/prisma.generate.js +100 -7
  38. package/dist/prisma.adapter.d.ts +4 -4
  39. package/dist/prisma.adapter.js +37 -2
  40. package/dist/provider/provider-profile.d.ts +19 -3
  41. package/dist/provider/provider-profile.js +21 -0
  42. package/dist/translation/json-null.sentinel.d.ts +7 -0
  43. package/dist/translation/json-null.sentinel.js +17 -0
  44. package/dist/translation/mutation.translator.js +7 -0
  45. package/dist/translation/where.translator.js +100 -17
  46. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,184 @@
1
1
  # @aventara/prisma7-adapter
2
2
 
3
+ ## 1.0.0-rc.3
4
+
5
+ ### Patch Changes
6
+
7
+ - b38034d: `aventara-prisma7-generate` refuses a `--provider` that the schema's `datasource` block contradicts, or a block naming
8
+ a provider the adapter does not support, before `prisma generate` runs, so neither the Prisma client nor the artifact
9
+ is written:
10
+
11
+ ```text
12
+ aventara-prisma7-generate: --provider postgresql contradicts the schema's datasource `db` (prisma/schema.prisma:12), which names provider "sqlite"; pass --provider sqlite.
13
+ ```
14
+
15
+ PSL's `postgres` spelling counts as `postgresql`, and a provider that is not a string literal is not compared.
16
+ Before, the flag was taken on trust, and a wrong one wrote the wrong provider into the artifact and the wrong filter
17
+ modes into the Contract.
18
+
19
+ When the file system refuses the artifact file (`--out`), the step says so in one sentence naming the file and why,
20
+ such as
21
+ `the artifact file /app/src/generated/aventara/discovery.artifact.ts (--out) cannot be written: permission denied (EACCES).`,
22
+ and leaves no staged file behind. Before, every such failure except a directory printed a stack trace.
23
+
24
+ - b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's 8191 bytes, SQLSTATE
25
+ `54000`) is answered `422 A2004`, not `500 A3001`: one `V1004` issue at `["arguments"]`, as a value too long for its
26
+ column already was. The adapter reads the SQLSTATE, never the message. The issue's message for a value too long now
27
+ reads `The database refused this value: it is longer than the database can hold for its column.`
28
+ (it said `longer than its column can hold`, which an index refusal is not).
29
+ - Updated dependencies [b38034d]
30
+ - Updated dependencies [b38034d]
31
+ - Updated dependencies [b38034d]
32
+ - Updated dependencies [b38034d]
33
+ - Updated dependencies [b38034d]
34
+ - Updated dependencies [b38034d]
35
+ - @aventara/core@1.0.0-rc.3
36
+
37
+ ## 1.0.0-rc.2
38
+
39
+ ### Patch Changes
40
+
41
+ - On Cloudflare D1, a `BigInt` value beyond ±(2^53 − 1) is answered `A2004`, not `A3001`. `@prisma/adapter-d1`
42
+ binds a bigint as a JavaScript number and refuses one beyond that range before the statement runs, written,
43
+ compared in a filter or used in an `$increment`, and Prisma reports it as a database error (`P2039`). The adapter
44
+ answers that refusal as a value the database refused: `422 A2004` with one `V1004` issue, `The database refused
45
+ this value: it is outside the range its column can hold.`, at the value in the arguments (for example
46
+ `["arguments", "where", "views", "gt"]`). Nothing is written by the refused statement. It reads the arguments only
47
+ after the driver refused them, and only for the D1 driver; every other D1 database error stays `A3001`, and so does
48
+ a raw `ctx.$sql` statement binding such a value. Within ±(2^53 − 1) a bigint round-trips exactly on D1.
49
+ In rc.1 the refusal answered `500 A3001`. Measured on 7.10.0 through Miniflare's D1 binding.
50
+ - The README's D1 notes are corrected: D1 is measured in process through Miniflare, where Prisma's client refuses
51
+ an interactive `$transaction` and the D1 driver runs Prisma's implicit transaction for a nested write as single
52
+ statements, so a nested write is not atomic on D1.
53
+ - The optional `typescript` peer range is `>=5.7.0 <7` (it was `>=5.5.0 <7`). Under TypeScript 5.5 and 5.6 a
54
+ variant's stages written inline in `createFramework` are told `unknown` arguments, so `ctx.$args.data` and the
55
+ documented spread pipe `(args) => ({ ...args, data: { ...args.data, title: "t" } })` do not compile; TypeScript 5.7
56
+ and later type them. A project on TypeScript 5.5 or 5.6 upgrades to 5.7 or later.
57
+ - `createPrismaAdapter` checks the installed `@prisma/client`'s major against the discovery artifact's, as the
58
+ README says. It reads the version from `@prisma/client/package.json`, resolved from the adapter's own location and
59
+ then from the working directory, and refuses an artifact generated against another major with
60
+ `AvPrismaDiscoveryArtifactError`. In rc.1 the artifact's version stood in for the installed one when no
61
+ `installedPrismaVersion` was passed, which a scaffolded project never does, so the check compared the artifact with
62
+ itself. Where the version cannot be read (a runtime without Node's module loader, a bundle deployed without
63
+ `@prisma/client`), the adapter starts and emits a warning, code `AVENTARA_PRISMA_VERSION_UNCHECKED`, saying the
64
+ majors were not compared. `installedPrismaVersion` still overrides the version read.
65
+ - `aventara-prisma7-generate` refuses an `--out` inside `--client`, before `prisma generate` runs: exit 1 and one
66
+ sentence naming both paths. Prisma's next `prisma generate` clears the client's directory, so an artifact written
67
+ there was deleted. In rc.1 the step wrote it and exited 0. The same refusal applies to
68
+ `generatePrismaDiscoveryArtifact` and `runPrismaGenerateStep`.
69
+ - `aventara-prisma7-generate` refuses an `--out` that names a directory in one sentence (exit 1), before
70
+ `prisma generate` runs. In rc.1 it ran Prisma and then failed with an `EISDIR` stack trace.
71
+ - A nested write that writes its relation back is answered `A2004`, not `A3001`. Prisma writes a record reached
72
+ through a relation without that relation's other end, so it refuses a post created in `author.posts` that names
73
+ `author` or `authorId`, and a category created in a post's `category` that nests `posts`. The discovery artifact
74
+ now states each relation's other end (`inverse`, paired from the schema by relation name) and no relation writable
75
+ at the `"loopback"` position, so `@aventara/core` refuses such a write before any guard or SQL: `A2004`, `V1012` at
76
+ the field written back. It holds for `$create`, `$connectOrCreate`, `$update` and `$upsert`, to-one and to-many,
77
+ self-relations and one-to-ones. Where a layer hides the relation's other end, its Contract states no pairing and
78
+ Prisma's client is the one that refuses: the adapter answers that refusal as the caller's too, `A2004` with a
79
+ `V1012` issue at the field and the message `The data source refused this nested write: a record written through a
80
+ relation cannot write that relation or its reference fields back.` It reads the arguments only after Prisma
81
+ refused them; every other refusal of Prisma's client stays `A3001`. In rc.1 every case answered `500 A3001`.
82
+ Measured on 7.10.0, SQLite and PostgreSQL. Run `npm run aventara:prepare` to regenerate the artifact: the model's
83
+ hash and every Contract's hash change.
84
+ - The discovery artifact states each relation's reference fields (`referenceFields`, read from the schema's
85
+ `@relation(fields: …)`), on optional and required relations alike, and none on the end that holds no reference.
86
+ So the types leave `categoryId` out of a post created in `Category.posts`, as `@aventara/core` refuses it. Run
87
+ `npm run aventara:prepare` to regenerate the artifact: the model's hash and every Contract's hash change.
88
+ - A `$divide` by zero is answered `A2004`, not `A3001`. It reaches the database as given: PostgreSQL refuses the
89
+ division (`P2039`, `22012`) and the adapter answers `V1004` `The database refused this value: it divides by zero.`;
90
+ SQLite computes NULL and the `NOT NULL` column refuses it (`P2011`), answered `V1004` `The database refused this
91
+ value: the write computes no value for a column that requires one, as a division by zero does.` Both at
92
+ `["arguments"]`, nothing written. `P2011` reaches the adapter only this way: core refuses a `null` for a required
93
+ field and a missing one, and so does Prisma's client before the query. In rc.1 both answered `500 A3001`. Measured
94
+ on 7.10.0 for `Int`, `BigInt`, `Float` and `Decimal`, in `update` and `updateMany`.
95
+ - `null` on a nullable `Json` field is the database NULL, as on every other nullable field. A write of `null`
96
+ (create, update, upsert, bulk, nested, a plan step) is sent as Prisma's `DbNull`; `{ meta: null }` and
97
+ `{ meta: { equals: null } }` are sent as Prisma's `AnyNull` and match every row that reads back `null` (a database
98
+ NULL or a JSON `null` stored by other means), and `{ meta: { not: null } }` matches the other rows. A JSON `null`
99
+ inside a value is sent as written, and so is a `null` beside a `path`: Prisma matches the documents whose value at
100
+ that path is a JSON `null`. In rc.1 a write of `null` stored a JSON `null`, `equals: null` matched only the stored
101
+ JSON nulls, and `{ meta: null }` failed with `500 A3001`. Reads are unchanged. Measured on 7.10.0, SQLite and
102
+ PostgreSQL.
103
+ - A to-one relation filter is sent under Prisma's `is`. `{ category: {} }` matches the rows that have a related row and
104
+ `{ NOT: { category: {} } }` the rows that have none. In rc.1 the empty predicate was sent unwrapped, which Prisma
105
+ reads as no condition: both matched every row, and `Post.delete.count({ where: { NOT: { author: {} } } })` deleted
106
+ every post although each has its required author. A non-empty nested `where` and `null` match what they did.
107
+ - A value the database refuses is answered `A2004`, not `A3001`. Text-match operands and written values reach Prisma
108
+ as given (`%` and `_` are what the engine's `LIKE` makes of them, a column rounds or keeps what it does); when
109
+ PostgreSQL refuses a caller's value, the adapter answers `A2004` with one issue at `["arguments"]` (Prisma's error
110
+ names no column): `V1004` for a value too long for its column (`P2000`), out of its column's range (`P2020`: a
111
+ `Decimal` past its precision, `Number.MAX_VALUE` in a `Float`, an `Int` `$increment` past 2^31) or a `DateTime`
112
+ outside PostgreSQL's range (`P2039`, `22009`); `V1001` for U+0000 in text or `json` (`P2039`, `22021` and `22P05`),
113
+ a lone surrogate in `json` (`P2007`) and a `LIKE` pattern ending in `\` (`P2039`, `22025`). The issue's message
114
+ says which of these the database refused, in core's words and none of the database's (`The database refused this
115
+ value: it is longer than its column can hold.`), not the code's generic text. Inside a plan the
116
+ issue stands at the step (`["operations", i, "args"]`) and the plan rolls back. Every other database failure stays
117
+ `A3001`. In rc.1 each of these answered `500 A3001`. Measured on 7.10.0, SQLite and PostgreSQL; SQLite refused none
118
+ of these values.
119
+ - A field's `not` holding an operator object now means the negation of the whole object, on every field type, on
120
+ both engines, in reads and in bulk writes. `{ f: { not: X } }` matches exactly the rows `{ NOT: { f: X } }` matches:
121
+ a row where `X` is neither true nor false (the column is `NULL`, a `json` `path` is not in the document, a `json`
122
+ value is not the kind the operator reads) matches neither `X` nor `not: X`, as SQL's `NOT` reads it. The adapter
123
+ sends it to Prisma as `NOT` of the field's filter, ANDed with the rest of the `where`, because Prisma's own
124
+ field-level `not` says less (measured on 7.10.0, SQLite and PostgreSQL):
125
+ - In rc.1, a `json` field's `not` holding operators matched every row on both engines, because Prisma reads that
126
+ `not` as a JSON value to compare the document with. That covers `{ meta: { not: { path: ["label"],
127
+ stringContains: "x" } } }`, `{ meta: { not: { equals: { a: 1 } } } }` and every other operator under it, and an
128
+ `update.many`, `update.count`, `delete.count` or `delete.first` under such a filter wrote or deleted every row
129
+ the rest of the filter allowed.
130
+ - In rc.1, two or more operators under `not` were negated one at a time and ANDed, so
131
+ `{ views: { not: { gt: 2, lte: 5 } } }` matched no row. It now matches the rows outside that range.
132
+ - In rc.1 on PostgreSQL, a `mode` inside `not` (`{ title: { not: { contains: "x", mode: "insensitive" } } }`)
133
+ failed with `A3001`, because Prisma's filter under `not` has no `mode`. It now matches the titles that do not
134
+ contain `x` in any case.
135
+
136
+ The object under `not` is its own level: nothing from the object around it reaches into it. In rc.1 on
137
+ PostgreSQL, `{ title: { mode: "insensitive", not: { contains: "x" } } }` excluded `X` as well as `x`, because Prisma
138
+ applied the outer `mode` inside its own `not`. Core now refuses that filter (`A2004`, `V1001` at the `mode`); write
139
+ `{ title: { not: { contains: "x", mode: "insensitive" } } }`. A plain value under `not` (`{ not: "x" }`,
140
+ `{ not: null }`) is equality negated and is sent as before.
141
+ - A `json` field's bare value in a filter is equality on both engines: `{ meta: "x" }` and `{ meta: [1, 2] }` are sent
142
+ to Prisma as `{ equals: … }`. In rc.1 they failed with `A3001`, because Prisma's Json filter takes no bare value.
143
+ - Filter modes, measured on each engine. PostgreSQL offers `"default"`, `"sensitive"` and `"insensitive"` on a string
144
+ column and on a `json` value at a `path`; on a whole `json` document it offers `"default"` and `"sensitive"`, because
145
+ Prisma accepts an insensitive `stringContains` there and PostgreSQL then fails (`42883 function lower(jsonb) does not
146
+ exist`, answered `A3001`). That filter is now refused before any query (`A2004`, `V1006` at the `mode`). SQLite, and
147
+ so D1, offers `"default"` only: its `LIKE` folds ASCII case and nothing else, and `equals` folds none, so neither
148
+ promise holds. Every string field with a text operator now offers `mode` (on SQLite with `"default"` alone), and
149
+ `"default"` and `"sensitive"` are sent to Prisma as no `mode` at all. Regenerate the discovery artifact
150
+ (`aventara:prepare`).
151
+ - 9366e16: The discovery artifact names the model's type: it exports `AvModel`, and `model` is declared with it
152
+ (`typeof model` is `AvModel`). A project that emits declarations (Nest's `declaration: true`) now writes that
153
+ name wherever an exported declaration holds the model, instead of the whole model. Over 50 models the scaffold's
154
+ `aventara.config.d.ts` goes from 485 KB to 0.4 KB, and a `buildFramework` / `AppFramework` module from 931 KB to
155
+ 0.6 KB (1.2 MB to 0.6 KB, and 2.4 MB to 22 KB for exported configurations holding typed stages, in the consumer-build
156
+ gate). Regenerate the artifact (`aventara:prepare`, which runs on install, build and start) to get it; Contract bytes
157
+ and hashes are unchanged.
158
+ - New: `aventara-prisma7-generate --config <file>`, optional. Given your Aventara configuration module
159
+ (`src/aventara.config.ts` in a project set up by `aventara`; its `aventaraConfig` export, a function or a value, or
160
+ its default export), the artifact states which fields it computes on create and on update, per layer. They are read
161
+ off the configuration's TypeScript type, never by running it, so nothing connects to a database and no environment
162
+ is needed. The artifact exports them as `computedFields` (type `AvComputedFields`), carries them on `AvModel` so a
163
+ variant's guards, pipes and filters may leave exactly those fields out of a create, and hands them to the framework
164
+ through `discovery.computedFields`, which it compares with the configuration at startup. A configuration that is
165
+ not a literal where a compute stage is written (one annotated with the wide `AvConfig`, a conditional spread) is not
166
+ read: the step prints where, and the artifact states nothing, so those stages may leave any create field out. A
167
+ `--config` file that does not exist, or exports neither `aventaraConfig` nor a default, stops the step.
168
+ `generatePrismaDiscoveryArtifact` and `runPrismaGenerateStep` take the same `config`, and
169
+ `readConfigComputedFields` is exported from `@aventara/prisma7-adapter/generate`.
170
+ - The discovery artifact's format is now 4 (`computedFields` is new): an artifact written before it is refused with
171
+ the regeneration instruction. `aventara:prepare` regenerates it on install, build and start.
172
+ - A transaction that expired (or was closed) under a step is answered `A3002`, as when the COMMIT finds it expired:
173
+ Prisma's `P2028`, raised by the step's own call, is the transaction's failure, at that step (`cause.operation`),
174
+ and the plan rolls back. In rc.1 it was unclassified and answered `500 A3001`, an adapter failure. Measured on
175
+ 7.10.0, SQLite and PostgreSQL.
176
+ - Updated dependencies [9366e16]
177
+ - Updated dependencies [9366e16]
178
+ - Updated dependencies [9366e16]
179
+ - Updated dependencies [9366e16]
180
+ - @aventara/core@1.0.0-rc.2
181
+
3
182
  ## 1.0.0-rc.1
4
183
 
5
184
  ### Patch Changes
package/README.md CHANGED
@@ -11,7 +11,7 @@ suggest. Everything you need is in the body of this file, not in footnotes.
11
11
  - Status: release candidate. Two engines are proven, PostgreSQL and SQLite. Every other Prisma provider is
12
12
  unmeasured, and this file says so where it matters.
13
13
  - Peer dependencies: `@prisma/client` `^7.10.0` (required); `prisma`, `@prisma/adapter-better-sqlite3`
14
- and `@prisma/adapter-pg` `^7.10.0` and `typescript` `>=5.5.0 <7` (optional). The error map targets
14
+ and `@prisma/adapter-pg` `^7.10.0` and `typescript` `>=5.7.0 <7` (optional). The error map targets
15
15
  7.10.0, and the adapter is bound to Prisma 7's error surface; see [Prisma 7 only](#prisma-7-only).
16
16
 
17
17
  ---
@@ -82,11 +82,13 @@ npx aventara-prisma7-generate \
82
82
  --client src/generated \
83
83
  --provider postgresql \
84
84
  --driver @prisma/adapter-pg \
85
- --out src/discovery.artifact.ts
85
+ --out src/discovery.artifact.ts \
86
+ --config src/aventara.config.ts
86
87
  ```
87
88
 
88
- `--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact. The
89
- driver decides `AvAdapterModel.transactions`, a driver fact the schema cannot state. Declare the driver
89
+ `--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact, and the
90
+ step refuses a `--provider` that the schema's `datasource` block contradicts (`provider = "sqlite"` with
91
+ `--provider postgresql`), naming the flag to pass. The driver decides `AvAdapterModel.transactions`, a driver fact the schema cannot state. Declare the driver
90
92
  the application will actually construct the adapter with. The two cannot drift silently, but they can
91
93
  disagree, and core refuses construction when they do.
92
94
 
@@ -97,9 +99,20 @@ installed keeps `@prisma/prisma7` beside it. With neither usable, the step refus
97
99
  both. TypeScript 6 is supported; TypeScript 7, which has no classic compiler API, is refused in one
98
100
  sentence.
99
101
 
100
- `--out` must not be inside `--client`. The types reader reads every `.ts` file under the client root
101
- and refuses one that is not generated-client output, so an artifact written beside the client would
102
- break the next run.
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.
112
+
113
+ `--out` must not be inside `--client`: Prisma's next `prisma generate` clears that directory and would
114
+ delete the artifact. The step refuses such an `--out`, and one that names a directory, in one sentence
115
+ before Prisma runs.
103
116
 
104
117
  The same step can run from code, for a project that already has a build script:
105
118
 
@@ -146,19 +159,31 @@ at runtime, after your pipes and computed behaviours have run, it is answered `A
146
159
  issue at its path (`["arguments", "data", "title"]`, or `["arguments", "data", "author"]` for a
147
160
  required relation) before the database can refuse it.
148
161
 
162
+ The module also exports the model's type by name, `AvModel` (`typeof model` is that type). A
163
+ project that emits declarations (Nest's `declaration: true`) writes that name wherever an exported
164
+ declaration holds the model, such as the scaffold's `aventaraConfig()`, an `AppFramework` alias or
165
+ `defineResource<typeof model>()`, so those declarations stay under a kilobyte at any schema size. Before
166
+ the name, each mention wrote the whole model out, about 0.5 MB over 50 models.
167
+
149
168
  `modelHash` digests the model and nothing else. The addressing table, the referential actions and the
150
169
  client record sit beside it and outside the hash. A database-side rename or a foreign-key action
151
170
  changes no semantics; inside the hash, either would move the Contract identity and force every
152
171
  generated client to be regenerated for nothing.
153
172
 
154
173
  On the runtime path there is no TypeScript program, no PSL parser and no Prisma tooling. The loader
155
- reads no file: the artifact arrives as a value you import.
174
+ reads no file: the artifact arrives as a value you import. The adapter reads one: the installed
175
+ `@prisma/client`'s `package.json`, for its version.
156
176
 
157
177
  At construction, before the application listens, the adapter checks three things and fails hard with
158
178
  an instruction to regenerate if any is wrong:
159
179
 
160
180
  1. the artifact matches itself: the `modelHash` over its own canonicalized model;
161
- 2. the installed `@prisma/client` major is the one the artifact was generated against;
181
+ 2. the installed `@prisma/client` major is the one the artifact was generated against. The version is
182
+ read from `@prisma/client/package.json`, resolved from the adapter's own location and then from the
183
+ working directory. Where it cannot be read (a runtime without Node's module loader, a bundle
184
+ deployed without `@prisma/client`), the adapter starts and emits a warning, code
185
+ `AVENTARA_PRISMA_VERSION_UNCHECKED`, saying the majors were not compared;
186
+ `installedPrismaVersion` overrides the version read;
162
187
  3. the Prisma client you pass is the one the artifact was generated from: the same models, and the
163
188
  same field references (`prisma.<model>.fields`) on each. A schema change followed by
164
189
  `prisma generate` but not by the generate step (a model or field added, removed or renamed) is
@@ -213,10 +238,13 @@ On PostgreSQL and SQLite, read from the generated types:
213
238
  | `create.many` / `update.many` (row-returning bulk writes) | advertised | advertised | `createManyAndReturn` / `updateManyAndReturn` on the delegate |
214
239
  | Transactions | `"interactive"` | `"interactive"` | the driver, not the provider (see below) |
215
240
 
216
- Nothing beyond PostgreSQL and SQLite is measured. MySQL, SQL Server, MongoDB, CockroachDB and D1 were
217
- not generated against. Because every capability is read from the client you actually generated, a
218
- provider that lacks something advertises less instead of failing at runtime, but this release makes no
219
- claim about those providers.
241
+ Nothing beyond PostgreSQL and SQLite is measured. Cloudflare D1 is SQLite through `@prisma/adapter-d1`:
242
+ it reads the same generated client, so its capabilities are SQLite's, and it is measured in process
243
+ through Miniflare's D1 binding, where it differs from SQLite in two ways (no transactions, and a bigint
244
+ within ±(2^53 − 1); see below). MySQL, SQL Server, MongoDB and CockroachDB were not generated against.
245
+ Because every capability is read from the client you actually generated, a provider that lacks
246
+ something advertises less instead of failing at runtime, but this release makes no claim about those
247
+ providers.
220
248
 
221
249
  ### `update.many` and `create.many` cannot project a to-many relation
222
250
 
@@ -372,8 +400,7 @@ commits, do not perform it in an after hook.
372
400
  ### `P2028` covers seven situations, and three of them are separable
373
401
 
374
402
  `P2028` is Prisma 7's transaction code, and seven internal classes share it. The adapter answers
375
- `A3002 TRANSACTION_ERROR` for the failure that ends the transaction, and never reads the message text
376
- to decide anything.
403
+ `A3002 TRANSACTION_ERROR` wherever it is met, and never reads the message text to decide anything.
377
404
 
378
405
  What can be told apart from the error's structure, measured on PostgreSQL and SQLite:
379
406
 
@@ -391,8 +418,9 @@ timeout cannot be told apart from them. That limit is Prisma's, and it is record
391
418
  works around it by parsing messages.
392
419
 
393
420
  A `P2028` that ends the transaction rejects out of `$transaction` and becomes `A3002`. A `P2028`
394
- raised by a delegate call inside the callback is not in the closed code table, so it becomes `A3001`
395
- attributed to that operation.
421
+ raised by a delegate call (or a raw statement) inside the callback, such as a step that runs after the
422
+ `timeout` passed, is in the closed code table and also becomes `A3002`, attributed to that operation
423
+ (`cause.operation`). In rc.1 that second case was unclassified and answered `A3001`.
396
424
 
397
425
  ### Everything unrecognised becomes `A3001`
398
426
 
@@ -438,9 +466,12 @@ observe that it did. A commit failure is different: it rejects like a callback t
438
466
 
439
467
  ### `transactions` is a driver fact, and it reaches the ClientContract hash
440
468
 
441
- `@prisma/adapter-d1` declares `transactions: "none"`: Cloudflare D1 rejects interactive transactions,
442
- and Prisma says so itself. `@prisma/adapter-pg` and `@prisma/adapter-better-sqlite3` declare
443
- `"interactive"`, observed by running one.
469
+ `@prisma/adapter-d1` declares `transactions: "none"`: Prisma's client refuses an interactive
470
+ `$transaction` on D1 itself. Prisma still opens its own implicit transaction for a nested write (a
471
+ `$create` under a relation, a `$connect`), and the D1 driver runs it as single statements, so a nested
472
+ write is not atomic on D1: when a later statement fails, the rows the earlier ones wrote are kept.
473
+ `@prisma/adapter-pg` and `@prisma/adapter-better-sqlite3` declare `"interactive"`, observed by running
474
+ one.
444
475
 
445
476
  Plan for the consequence. The `transactions` capability is part of the ClientContract hash, and a
446
477
  generated client fails hard on a hash mismatch. So one schema, one framework configuration and two
@@ -597,6 +628,23 @@ Resource or operation for one to wrap.
597
628
  - `limit`/`offset`/`cursor` are the public names. Prisma's `take`/`skip` never surface.
598
629
  - Decimal values are converted in both directions. Prisma's bundled decimal object is not an Aventara
599
630
  public type and does not reach a caller as one.
631
+ - Values and filter operands reach Prisma as given: a `contains`, `startsWith` or `endsWith` operand is the
632
+ engine's `LIKE` pattern (`%` and `_` included), and a column stores, rounds or widens a value as the engine does.
633
+ A value PostgreSQL refuses (too long for its column, out of its range, U+0000 in text, a `LIKE` pattern ending in
634
+ `\`) answers `A2004` with one issue at `["arguments"]` (`V1004` or `V1001`), not `A3001`.
635
+ - On D1, a `BigInt` value must lie within ±(2^53 − 1) (±9,007,199,254,740,991), not the full signed 64-bit
636
+ range: the D1 driver binds a bigint as a JavaScript number and refuses one beyond that before the
637
+ statement runs. A value beyond it, written, compared in a filter or used in an `$increment`, answers
638
+ `A2004` with one `V1004` issue at the value (`["arguments", "data", "views"]`), and the statement that
639
+ carries it writes nothing. A `ctx.$sql` statement binding such a value answers `A3001`: its SQL is
640
+ yours.
641
+ - A record written through a relation cannot write that relation, or its reference fields, back: Prisma's nested
642
+ input for it leaves them out (a post created in `author.posts` has no `author` or `authorId`; a category created in
643
+ 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
645
+ writable at the `"loopback"` position, so `@aventara/core` refuses such a write before any
646
+ guard or SQL, `A2004` with a `V1012` issue at the field written back, and the types do not offer it. Where a layer
647
+ hides the other end, Prisma's client refuses the write instead and the adapter answers that refusal the same way.
600
648
  - Count variants return numbers, including zero. `create.many` and `update.many` return arrays.
601
649
  - `find.unique`, `update.unique` and `delete.unique` answer `A2003 NOT_FOUND` on a missing target;
602
650
  `find.first`, `update.first` and `delete.first` answer `A1001 NO_MATCH` with `null` data. The
@@ -9,8 +9,12 @@ export declare class AvPrismaDiscoveryArtifactError extends Error {
9
9
  }
10
10
  export type PrismaDiscoveryArtifactLoad = {
11
11
  readonly artifact: unknown;
12
- /** The `version` of the `@prisma/client` actually installed beside the caller. */
13
- readonly installedPrismaVersion: string;
12
+ /**
13
+ * The `version` of the `@prisma/client` actually installed beside the caller;
14
+ * `undefined` when it could not be read, and the majors are then not compared
15
+ * (the adapter says so at startup).
16
+ */
17
+ readonly installedPrismaVersion: string | undefined;
14
18
  };
15
19
  /**
16
20
  * The identity of a discovered model: one canonicalization, one digest — over
@@ -49,6 +49,9 @@ function assertArtifactShape(artifact) {
49
49
  throw new AvPrismaDiscoveryArtifactError(`The discovery artifact's \`${key}\` is not a JSON object.`);
50
50
  }
51
51
  }
52
+ if (artifact.computedFields !== null && !isRecord(artifact.computedFields)) {
53
+ throw new AvPrismaDiscoveryArtifactError("The discovery artifact's `computedFields` is neither a JSON object nor null.");
54
+ }
52
55
  for (const key of ["scalars", "enums", "resources"]) {
53
56
  if (!isRecord(artifact.model[key])) {
54
57
  throw new AvPrismaDiscoveryArtifactError(`The discovery artifact's \`model.${key}\` is not a JSON object.`);
@@ -66,7 +69,9 @@ export async function loadDiscoveryArtifact(load) {
66
69
  const { artifact, installedPrismaVersion } = load;
67
70
  assertArtifactShape(artifact);
68
71
  const artifactMajor = majorOf(artifact.prismaVersion);
69
- const installedMajor = majorOf(installedPrismaVersion);
72
+ const installedMajor = installedPrismaVersion === undefined
73
+ ? artifactMajor
74
+ : majorOf(installedPrismaVersion);
70
75
  if (artifactMajor === undefined || installedMajor === undefined) {
71
76
  throw new AvPrismaDiscoveryArtifactError(`A Prisma version is not a major.minor.patch version: the artifact declares \`${artifact.prismaVersion}\` and the installed client reports \`${installedPrismaVersion}\`.`);
72
77
  }
@@ -1,4 +1,5 @@
1
1
  import type { AvRelationFieldCapabilities, AvScalarFieldCapabilities, AvScalarFilterOperator } from "@aventara/core";
2
+ import type { PrismaFilterModeFacts } from "../provider/provider-profile.js";
2
3
  import type { PrismaRelationSurface, PrismaScalarSurface } from "./capability-surface.interface.js";
3
4
  /** A Prisma spelling that no dictionary table recognises: a build stop, never a drop. */
4
5
  export declare class AvPrismaCapabilityDerivationError extends Error {
@@ -6,6 +7,8 @@ export declare class AvPrismaCapabilityDerivationError extends Error {
6
7
  }
7
8
  export type ScalarCapabilityInput = {
8
9
  readonly surface: PrismaScalarSurface;
10
+ /** The `mode`s the engine honours (see this module's header). */
11
+ readonly filterModes: PrismaFilterModeFacts;
9
12
  };
10
13
  /** A relation's capabilities are answered for the field, not for an operation. */
11
14
  export type RelationCapabilityInput = {
@@ -13,5 +16,5 @@ export type RelationCapabilityInput = {
13
16
  };
14
17
  export declare function deriveScalarCapabilities(input: ScalarCapabilityInput): AvScalarFieldCapabilities;
15
18
  /** The Prisma operand for each ADVERTISED operator, keyed by the Aventara name. */
16
- export declare function deriveScalarFilterOperands(input: ScalarCapabilityInput): Readonly<Partial<Record<AvScalarFilterOperator, string>>>;
19
+ export declare function deriveScalarFilterOperands(input: Pick<ScalarCapabilityInput, "surface">): Readonly<Partial<Record<AvScalarFilterOperator, string>>>;
17
20
  export declare function deriveRelationCapabilities(input: RelationCapabilityInput): AvRelationFieldCapabilities;
@@ -61,18 +61,50 @@ const PAGINATION_ORDER = [
61
61
  export function deriveScalarCapabilities(input) {
62
62
  const { surface } = input;
63
63
  const operands = deriveScalarFilterOperands(input);
64
- const filter = canonical(FILTER_OPERATOR_ORDER, surface.filters
64
+ const derived = surface.filters
65
65
  .map((key) => look(PRISMA_FILTER_OPERATORS, key, "a scalar filter operator"))
66
- .filter((operator) => translatable(operator, operands)));
66
+ .filter((operator) => translatable(operator, operands));
67
+ const modes = filterModes(derived, input.filterModes);
68
+ const filter = canonical(FILTER_OPERATOR_ORDER, [
69
+ ...derived,
70
+ ...(modes === undefined ? [] : ["mode"]),
71
+ ]);
67
72
  return {
68
73
  select: true,
69
74
  ...(filter.length > 0 ? { filter } : {}),
75
+ ...(modes ?? {}),
70
76
  order: surface.orderable,
71
77
  ...(surface.nullsOrderable ? { orderNulls: true } : {}),
72
78
  ...directives("create", surface.createOperations),
73
79
  ...directives("update", surface.updateOperations),
74
80
  };
75
81
  }
82
+ const TEXT_OPERATORS = [
83
+ "contains",
84
+ "startsWith",
85
+ "endsWith",
86
+ ];
87
+ const JSON_TEXT_OPERATORS = [
88
+ "stringContains",
89
+ "stringStartsWith",
90
+ "stringEndsWith",
91
+ ];
92
+ function filterModes(operators, facts) {
93
+ const admitted = (modes) => modes.filter((mode) => mode !== "insensitive" || operators.includes("mode"));
94
+ if (operators.some((operator) => JSON_TEXT_OPERATORS.includes(operator))) {
95
+ return {
96
+ filterModes: admitted(facts.jsonDocument),
97
+ ...(operators.includes("path")
98
+ ? { pathFilterModes: admitted(facts.jsonPath) }
99
+ : {}),
100
+ };
101
+ }
102
+ if (operators.includes("mode") ||
103
+ operators.some((operator) => TEXT_OPERATORS.includes(operator))) {
104
+ return { filterModes: admitted(facts.text) };
105
+ }
106
+ return undefined;
107
+ }
76
108
  export function deriveScalarFilterOperands(input) {
77
109
  const operands = {};
78
110
  for (const operator of FILTER_OPERATOR_ORDER) {
@@ -126,7 +158,10 @@ function reducersIn(reducers, bulk) {
126
158
  return bulk ? reducers : { reducers, scopes: ["record"] };
127
159
  }
128
160
  function writableIn(directives, bulk) {
129
- return bulk ? directives : { directives, scopes: ["nested"] };
161
+ return {
162
+ directives,
163
+ scopes: bulk ? ["nested", "bulkWrite"] : ["nested"],
164
+ };
130
165
  }
131
166
  export function deriveRelationCapabilities(input) {
132
167
  const { surface } = input;
@@ -1,4 +1,4 @@
1
- import type { AvAdapterModel, AvEnumDefinition, AvScalarRegistry, AvTransactionCapability } from "@aventara/core";
1
+ import type { AvAdapterModel, AvConfigComputedFields, AvEnumDefinition, AvScalarRegistry, AvTransactionCapability } from "@aventara/core";
2
2
  import type { AvAdapterResource } from "@aventara/core/internal";
3
3
  import type { AvPrismaProvider } from "../provider/provider-profile.js";
4
4
  /** The adapter model a discovery artifact carries, in Aventara's vocabulary. */
@@ -73,7 +73,7 @@ export type AvPrismaReferentialActionTable<M extends AvPrismaAdapterModel = AvPr
73
73
  */
74
74
  export type PrismaClientShape = Readonly<Record<string, readonly string[]>>;
75
75
  /** The current discovery artifact format. */
76
- export declare const DISCOVERY_ARTIFACT_FORMAT = 3;
76
+ export declare const DISCOVERY_ARTIFACT_FORMAT = 4;
77
77
  /**
78
78
  * The discovery artifact: the module `aventara-prisma7-generate` writes from
79
79
  * your schema and generated client. Import its `discovery` export and pass it
@@ -106,4 +106,12 @@ export type AvPrismaDiscoveryArtifact<M extends AvPrismaAdapterModel = AvPrismaA
106
106
  * against; not part of `modelHash`.
107
107
  */
108
108
  readonly client: PrismaClientShape;
109
+ /**
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`.
115
+ */
116
+ readonly computedFields: AvConfigComputedFields | null;
109
117
  };
@@ -1 +1 @@
1
- export const DISCOVERY_ARTIFACT_FORMAT = 3;
1
+ export const DISCOVERY_ARTIFACT_FORMAT = 4;
@@ -0,0 +1,13 @@
1
+ /** What the locator found: the version, or why it has none. */
2
+ export type InstalledPrismaClientVersion = {
3
+ readonly kind: "read";
4
+ readonly version: string;
5
+ } | {
6
+ readonly kind: "unread";
7
+ readonly reason: string;
8
+ };
9
+ /**
10
+ * Reads the installed `@prisma/client`'s version, resolved as described above.
11
+ * Never throws: a version it cannot read is answered `unread` with the reason.
12
+ */
13
+ export declare function locateInstalledPrismaClientVersion(): InstalledPrismaClientVersion;
@@ -0,0 +1,43 @@
1
+ const MANIFEST = "@prisma/client/package.json";
2
+ function resolutionBases(process) {
3
+ const bases = [];
4
+ try {
5
+ if (typeof import.meta.url === "string") {
6
+ bases.push(import.meta.url);
7
+ }
8
+ }
9
+ catch {
10
+ }
11
+ const cwd = process?.cwd?.();
12
+ if (typeof cwd === "string") {
13
+ bases.push(`${cwd.replace(/[\\/]+$/, "")}/package.json`);
14
+ }
15
+ return bases;
16
+ }
17
+ export function locateInstalledPrismaClientVersion() {
18
+ const process = globalThis.process;
19
+ const moduleApi = process?.getBuiltinModule?.("node:module");
20
+ if (typeof moduleApi?.createRequire !== "function") {
21
+ return {
22
+ kind: "unread",
23
+ reason: "this runtime has no Node module loader to resolve it with",
24
+ };
25
+ }
26
+ const tried = [];
27
+ for (const base of resolutionBases(process)) {
28
+ try {
29
+ const manifest = moduleApi.createRequire(base)(MANIFEST);
30
+ if (typeof manifest.version === "string") {
31
+ return { kind: "read", version: manifest.version };
32
+ }
33
+ tried.push(`${base} (its package.json states no version)`);
34
+ }
35
+ catch {
36
+ tried.push(base);
37
+ }
38
+ }
39
+ return {
40
+ kind: "unread",
41
+ reason: `\`${MANIFEST}\` does not resolve from ${tried.length === 0 ? "anywhere this runtime names" : tried.join(" or ")}`,
42
+ };
43
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AvTransactionCapability } from "@aventara/core";
2
2
  import type { PrismaComposedArtifact, PrismaModelComposer } from "../generate/prisma.generate.js";
3
+ import type { PrismaFilterModeFacts } from "../provider/provider-profile.js";
3
4
  import type { PrismaCapabilitySurface } from "./capability-surface.interface.js";
4
5
  import type { PrismaSchemaFacts } from "./schema-facts.interface.js";
5
6
  /** Thrown when the schema cannot be composed into a model; lists every problem. */
@@ -10,6 +11,8 @@ export declare class AvPrismaModelCompositionError extends Error {
10
11
  }
11
12
  export type PrismaModelCompositionOptions = {
12
13
  readonly transactions: AvTransactionCapability;
14
+ /** The `mode`s the engine's comparisons honour (`provider-profile.ts`). */
15
+ readonly filterModes: PrismaFilterModeFacts;
13
16
  };
14
17
  export type PrismaModelDiscoveryInput = PrismaModelCompositionOptions & {
15
18
  /** PSL — what the schema says. */
@@ -42,8 +42,10 @@ export function discoverPrismaModel(input) {
42
42
  modelNames,
43
43
  admitted,
44
44
  transactions: input.transactions,
45
+ filterModes: input.filterModes,
45
46
  diagnostics,
46
47
  usedScalars,
48
+ declared,
47
49
  });
48
50
  resources[name] = composed.resource;
49
51
  translation[name] = composed.translation;
@@ -215,7 +217,10 @@ function composeResource(input) {
215
217
  }
216
218
  fields[projection.name] = {
217
219
  ...projection.field,
218
- capabilities: deriveScalarCapabilities({ surface: scalarSurface }),
220
+ capabilities: deriveScalarCapabilities({
221
+ surface: scalarSurface,
222
+ filterModes: input.filterModes,
223
+ }),
219
224
  };
220
225
  projectedScalars.push({
221
226
  name: projection.name,
@@ -235,8 +240,15 @@ function composeResource(input) {
235
240
  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.`);
236
241
  continue;
237
242
  }
243
+ const inverse = inverseOf(input.declared, model.name, field);
244
+ const [firstReference, ...moreReferences] = field.relation?.fields ?? [];
245
+ const referenceFields = firstReference === undefined
246
+ ? undefined
247
+ : [firstReference, ...moreReferences];
238
248
  fields[projection.name] = {
239
249
  ...projection.field,
250
+ ...(inverse === undefined ? {} : { inverse: inverse.relation }),
251
+ ...(referenceFields === undefined ? {} : { referenceFields }),
240
252
  capabilities: deriveRelationCapabilities({
241
253
  surface: relationSurface,
242
254
  }),
@@ -247,7 +259,7 @@ function composeResource(input) {
247
259
  field: projection.edge.field,
248
260
  target: projection.edge.target,
249
261
  cardinality: projection.edge.cardinality,
250
- references: [...(field.relation?.fields ?? [])],
262
+ references: [...(referenceFields ?? [])],
251
263
  onDelete: projection.edge.onDelete,
252
264
  onUpdate: projection.edge.onUpdate,
253
265
  });