@aventara/prisma7-adapter 1.0.0-rc.1 → 1.0.0-rc.2

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 (44) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +65 -18
  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/errors/back-reference.locator.d.ts +25 -0
  14. package/dist/errors/back-reference.locator.js +100 -0
  15. package/dist/errors/bigint-binding.locator.d.ts +7 -0
  16. package/dist/errors/bigint-binding.locator.js +27 -0
  17. package/dist/errors/error.normalizer.d.ts +34 -2
  18. package/dist/errors/error.normalizer.js +69 -2
  19. package/dist/errors/prisma-error.reader.d.ts +4 -0
  20. package/dist/errors/prisma-error.reader.js +2 -0
  21. package/dist/execution/operation.executor.d.ts +6 -0
  22. package/dist/execution/operation.executor.js +21 -8
  23. package/dist/execution/transaction.executor.d.ts +4 -0
  24. package/dist/execution/transaction.executor.js +2 -0
  25. package/dist/generate/artifact.emitter.d.ts +6 -0
  26. package/dist/generate/artifact.emitter.js +56 -2
  27. package/dist/generate/computed-fields.reader.d.ts +19 -0
  28. package/dist/generate/computed-fields.reader.js +210 -0
  29. package/dist/generate/declared-support.reader.d.ts +1 -1
  30. package/dist/generate/discovery.cli.d.ts +1 -0
  31. package/dist/generate/discovery.cli.js +20 -4
  32. package/dist/generate/index.d.ts +8 -0
  33. package/dist/generate/index.js +3 -0
  34. package/dist/generate/prisma.generate.d.ts +17 -0
  35. package/dist/generate/prisma.generate.js +41 -3
  36. package/dist/prisma.adapter.d.ts +4 -4
  37. package/dist/prisma.adapter.js +37 -2
  38. package/dist/provider/provider-profile.d.ts +19 -3
  39. package/dist/provider/provider-profile.js +21 -0
  40. package/dist/translation/json-null.sentinel.d.ts +7 -0
  41. package/dist/translation/json-null.sentinel.js +17 -0
  42. package/dist/translation/mutation.translator.js +7 -0
  43. package/dist/translation/where.translator.js +100 -17
  44. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,150 @@
1
1
  # @aventara/prisma7-adapter
2
2
 
3
+ ## 1.0.0-rc.2
4
+
5
+ ### Patch Changes
6
+
7
+ - On Cloudflare D1, a `BigInt` value beyond ±(2^53 − 1) is answered `A2004`, not `A3001`. `@prisma/adapter-d1`
8
+ binds a bigint as a JavaScript number and refuses one beyond that range before the statement runs, written,
9
+ compared in a filter or used in an `$increment`, and Prisma reports it as a database error (`P2039`). The adapter
10
+ answers that refusal as a value the database refused: `422 A2004` with one `V1004` issue, `The database refused
11
+ this value: it is outside the range its column can hold.`, at the value in the arguments (for example
12
+ `["arguments", "where", "views", "gt"]`). Nothing is written by the refused statement. It reads the arguments only
13
+ after the driver refused them, and only for the D1 driver; every other D1 database error stays `A3001`, and so does
14
+ a raw `ctx.$sql` statement binding such a value. Within ±(2^53 − 1) a bigint round-trips exactly on D1.
15
+ In rc.1 the refusal answered `500 A3001`. Measured on 7.10.0 through Miniflare's D1 binding.
16
+ - The README's D1 notes are corrected: D1 is measured in process through Miniflare, where Prisma's client refuses
17
+ an interactive `$transaction` and the D1 driver runs Prisma's implicit transaction for a nested write as single
18
+ statements, so a nested write is not atomic on D1.
19
+ - The optional `typescript` peer range is `>=5.7.0 <7` (it was `>=5.5.0 <7`). Under TypeScript 5.5 and 5.6 a
20
+ variant's stages written inline in `createFramework` are told `unknown` arguments, so `ctx.$args.data` and the
21
+ documented spread pipe `(args) => ({ ...args, data: { ...args.data, title: "t" } })` do not compile; TypeScript 5.7
22
+ and later type them. A project on TypeScript 5.5 or 5.6 upgrades to 5.7 or later.
23
+ - `createPrismaAdapter` checks the installed `@prisma/client`'s major against the discovery artifact's, as the
24
+ README says. It reads the version from `@prisma/client/package.json`, resolved from the adapter's own location and
25
+ then from the working directory, and refuses an artifact generated against another major with
26
+ `AvPrismaDiscoveryArtifactError`. In rc.1 the artifact's version stood in for the installed one when no
27
+ `installedPrismaVersion` was passed, which a scaffolded project never does, so the check compared the artifact with
28
+ itself. Where the version cannot be read (a runtime without Node's module loader, a bundle deployed without
29
+ `@prisma/client`), the adapter starts and emits a warning, code `AVENTARA_PRISMA_VERSION_UNCHECKED`, saying the
30
+ majors were not compared. `installedPrismaVersion` still overrides the version read.
31
+ - `aventara-prisma7-generate` refuses an `--out` inside `--client`, before `prisma generate` runs: exit 1 and one
32
+ sentence naming both paths. Prisma's next `prisma generate` clears the client's directory, so an artifact written
33
+ there was deleted. In rc.1 the step wrote it and exited 0. The same refusal applies to
34
+ `generatePrismaDiscoveryArtifact` and `runPrismaGenerateStep`.
35
+ - `aventara-prisma7-generate` refuses an `--out` that names a directory in one sentence (exit 1), before
36
+ `prisma generate` runs. In rc.1 it ran Prisma and then failed with an `EISDIR` stack trace.
37
+ - A nested write that writes its relation back is answered `A2004`, not `A3001`. Prisma writes a record reached
38
+ through a relation without that relation's other end, so it refuses a post created in `author.posts` that names
39
+ `author` or `authorId`, and a category created in a post's `category` that nests `posts`. The discovery artifact
40
+ now states each relation's other end (`inverse`, paired from the schema by relation name) and no relation writable
41
+ at the `"loopback"` position, so `@aventara/core` refuses such a write before any guard or SQL: `A2004`, `V1012` at
42
+ the field written back. It holds for `$create`, `$connectOrCreate`, `$update` and `$upsert`, to-one and to-many,
43
+ self-relations and one-to-ones. Where a layer hides the relation's other end, its Contract states no pairing and
44
+ Prisma's client is the one that refuses: the adapter answers that refusal as the caller's too, `A2004` with a
45
+ `V1012` issue at the field and the message `The data source refused this nested write: a record written through a
46
+ relation cannot write that relation or its reference fields back.` It reads the arguments only after Prisma
47
+ refused them; every other refusal of Prisma's client stays `A3001`. In rc.1 every case answered `500 A3001`.
48
+ Measured on 7.10.0, SQLite and PostgreSQL. Run `npm run aventara:prepare` to regenerate the artifact: the model's
49
+ hash and every Contract's hash change.
50
+ - The discovery artifact states each relation's reference fields (`referenceFields`, read from the schema's
51
+ `@relation(fields: …)`), on optional and required relations alike, and none on the end that holds no reference.
52
+ So the types leave `categoryId` out of a post created in `Category.posts`, as `@aventara/core` refuses it. Run
53
+ `npm run aventara:prepare` to regenerate the artifact: the model's hash and every Contract's hash change.
54
+ - A `$divide` by zero is answered `A2004`, not `A3001`. It reaches the database as given: PostgreSQL refuses the
55
+ division (`P2039`, `22012`) and the adapter answers `V1004` `The database refused this value: it divides by zero.`;
56
+ SQLite computes NULL and the `NOT NULL` column refuses it (`P2011`), answered `V1004` `The database refused this
57
+ value: the write computes no value for a column that requires one, as a division by zero does.` Both at
58
+ `["arguments"]`, nothing written. `P2011` reaches the adapter only this way: core refuses a `null` for a required
59
+ field and a missing one, and so does Prisma's client before the query. In rc.1 both answered `500 A3001`. Measured
60
+ on 7.10.0 for `Int`, `BigInt`, `Float` and `Decimal`, in `update` and `updateMany`.
61
+ - `null` on a nullable `Json` field is the database NULL, as on every other nullable field. A write of `null`
62
+ (create, update, upsert, bulk, nested, a plan step) is sent as Prisma's `DbNull`; `{ meta: null }` and
63
+ `{ meta: { equals: null } }` are sent as Prisma's `AnyNull` and match every row that reads back `null` (a database
64
+ NULL or a JSON `null` stored by other means), and `{ meta: { not: null } }` matches the other rows. A JSON `null`
65
+ inside a value is sent as written, and so is a `null` beside a `path`: Prisma matches the documents whose value at
66
+ that path is a JSON `null`. In rc.1 a write of `null` stored a JSON `null`, `equals: null` matched only the stored
67
+ JSON nulls, and `{ meta: null }` failed with `500 A3001`. Reads are unchanged. Measured on 7.10.0, SQLite and
68
+ PostgreSQL.
69
+ - A to-one relation filter is sent under Prisma's `is`. `{ category: {} }` matches the rows that have a related row and
70
+ `{ NOT: { category: {} } }` the rows that have none. In rc.1 the empty predicate was sent unwrapped, which Prisma
71
+ reads as no condition: both matched every row, and `Post.delete.count({ where: { NOT: { author: {} } } })` deleted
72
+ every post although each has its required author. A non-empty nested `where` and `null` match what they did.
73
+ - A value the database refuses is answered `A2004`, not `A3001`. Text-match operands and written values reach Prisma
74
+ as given (`%` and `_` are what the engine's `LIKE` makes of them, a column rounds or keeps what it does); when
75
+ PostgreSQL refuses a caller's value, the adapter answers `A2004` with one issue at `["arguments"]` (Prisma's error
76
+ names no column): `V1004` for a value too long for its column (`P2000`), out of its column's range (`P2020`: a
77
+ `Decimal` past its precision, `Number.MAX_VALUE` in a `Float`, an `Int` `$increment` past 2^31) or a `DateTime`
78
+ outside PostgreSQL's range (`P2039`, `22009`); `V1001` for U+0000 in text or `json` (`P2039`, `22021` and `22P05`),
79
+ a lone surrogate in `json` (`P2007`) and a `LIKE` pattern ending in `\` (`P2039`, `22025`). The issue's message
80
+ says which of these the database refused, in core's words and none of the database's (`The database refused this
81
+ value: it is longer than its column can hold.`), not the code's generic text. Inside a plan the
82
+ issue stands at the step (`["operations", i, "args"]`) and the plan rolls back. Every other database failure stays
83
+ `A3001`. In rc.1 each of these answered `500 A3001`. Measured on 7.10.0, SQLite and PostgreSQL; SQLite refused none
84
+ of these values.
85
+ - A field's `not` holding an operator object now means the negation of the whole object, on every field type, on
86
+ both engines, in reads and in bulk writes. `{ f: { not: X } }` matches exactly the rows `{ NOT: { f: X } }` matches:
87
+ a row where `X` is neither true nor false (the column is `NULL`, a `json` `path` is not in the document, a `json`
88
+ value is not the kind the operator reads) matches neither `X` nor `not: X`, as SQL's `NOT` reads it. The adapter
89
+ sends it to Prisma as `NOT` of the field's filter, ANDed with the rest of the `where`, because Prisma's own
90
+ field-level `not` says less (measured on 7.10.0, SQLite and PostgreSQL):
91
+ - In rc.1, a `json` field's `not` holding operators matched every row on both engines, because Prisma reads that
92
+ `not` as a JSON value to compare the document with. That covers `{ meta: { not: { path: ["label"],
93
+ stringContains: "x" } } }`, `{ meta: { not: { equals: { a: 1 } } } }` and every other operator under it, and an
94
+ `update.many`, `update.count`, `delete.count` or `delete.first` under such a filter wrote or deleted every row
95
+ the rest of the filter allowed.
96
+ - In rc.1, two or more operators under `not` were negated one at a time and ANDed, so
97
+ `{ views: { not: { gt: 2, lte: 5 } } }` matched no row. It now matches the rows outside that range.
98
+ - In rc.1 on PostgreSQL, a `mode` inside `not` (`{ title: { not: { contains: "x", mode: "insensitive" } } }`)
99
+ failed with `A3001`, because Prisma's filter under `not` has no `mode`. It now matches the titles that do not
100
+ contain `x` in any case.
101
+
102
+ The object under `not` is its own level: nothing from the object around it reaches into it. In rc.1 on
103
+ PostgreSQL, `{ title: { mode: "insensitive", not: { contains: "x" } } }` excluded `X` as well as `x`, because Prisma
104
+ applied the outer `mode` inside its own `not`. Core now refuses that filter (`A2004`, `V1001` at the `mode`); write
105
+ `{ title: { not: { contains: "x", mode: "insensitive" } } }`. A plain value under `not` (`{ not: "x" }`,
106
+ `{ not: null }`) is equality negated and is sent as before.
107
+ - A `json` field's bare value in a filter is equality on both engines: `{ meta: "x" }` and `{ meta: [1, 2] }` are sent
108
+ to Prisma as `{ equals: … }`. In rc.1 they failed with `A3001`, because Prisma's Json filter takes no bare value.
109
+ - Filter modes, measured on each engine. PostgreSQL offers `"default"`, `"sensitive"` and `"insensitive"` on a string
110
+ column and on a `json` value at a `path`; on a whole `json` document it offers `"default"` and `"sensitive"`, because
111
+ Prisma accepts an insensitive `stringContains` there and PostgreSQL then fails (`42883 function lower(jsonb) does not
112
+ exist`, answered `A3001`). That filter is now refused before any query (`A2004`, `V1006` at the `mode`). SQLite, and
113
+ so D1, offers `"default"` only: its `LIKE` folds ASCII case and nothing else, and `equals` folds none, so neither
114
+ promise holds. Every string field with a text operator now offers `mode` (on SQLite with `"default"` alone), and
115
+ `"default"` and `"sensitive"` are sent to Prisma as no `mode` at all. Regenerate the discovery artifact
116
+ (`aventara:prepare`).
117
+ - 9366e16: The discovery artifact names the model's type: it exports `AvModel`, and `model` is declared with it
118
+ (`typeof model` is `AvModel`). A project that emits declarations (Nest's `declaration: true`) now writes that
119
+ name wherever an exported declaration holds the model, instead of the whole model. Over 50 models the scaffold's
120
+ `aventara.config.d.ts` goes from 485 KB to 0.4 KB, and a `buildFramework` / `AppFramework` module from 931 KB to
121
+ 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
122
+ gate). Regenerate the artifact (`aventara:prepare`, which runs on install, build and start) to get it; Contract bytes
123
+ and hashes are unchanged.
124
+ - New: `aventara-prisma7-generate --config <file>`, optional. Given your Aventara configuration module
125
+ (`src/aventara.config.ts` in a project set up by `aventara`; its `aventaraConfig` export, a function or a value, or
126
+ its default export), the artifact states which fields it computes on create and on update, per layer. They are read
127
+ off the configuration's TypeScript type, never by running it, so nothing connects to a database and no environment
128
+ is needed. The artifact exports them as `computedFields` (type `AvComputedFields`), carries them on `AvModel` so a
129
+ variant's guards, pipes and filters may leave exactly those fields out of a create, and hands them to the framework
130
+ through `discovery.computedFields`, which it compares with the configuration at startup. A configuration that is
131
+ not a literal where a compute stage is written (one annotated with the wide `AvConfig`, a conditional spread) is not
132
+ read: the step prints where, and the artifact states nothing, so those stages may leave any create field out. A
133
+ `--config` file that does not exist, or exports neither `aventaraConfig` nor a default, stops the step.
134
+ `generatePrismaDiscoveryArtifact` and `runPrismaGenerateStep` take the same `config`, and
135
+ `readConfigComputedFields` is exported from `@aventara/prisma7-adapter/generate`.
136
+ - The discovery artifact's format is now 4 (`computedFields` is new): an artifact written before it is refused with
137
+ the regeneration instruction. `aventara:prepare` regenerates it on install, build and start.
138
+ - A transaction that expired (or was closed) under a step is answered `A3002`, as when the COMMIT finds it expired:
139
+ Prisma's `P2028`, raised by the step's own call, is the transaction's failure, at that step (`cause.operation`),
140
+ and the plan rolls back. In rc.1 it was unclassified and answered `500 A3001`, an adapter failure. Measured on
141
+ 7.10.0, SQLite and PostgreSQL.
142
+ - Updated dependencies [9366e16]
143
+ - Updated dependencies [9366e16]
144
+ - Updated dependencies [9366e16]
145
+ - Updated dependencies [9366e16]
146
+ - @aventara/core@1.0.0-rc.2
147
+
3
148
  ## 1.0.0-rc.1
4
149
 
5
150
  ### 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,7 +82,8 @@ 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
89
  `--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact. The
@@ -97,9 +98,20 @@ installed keeps `@prisma/prisma7` beside it. With neither usable, the step refus
97
98
  both. TypeScript 6 is supported; TypeScript 7, which has no classic compiler API, is refused in one
98
99
  sentence.
99
100
 
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.
101
+ `--config` is optional. It names your Aventara configuration module: its `aventaraConfig` export (a
102
+ function, as the `aventara` scaffold writes it, or a value) or its default export. The step reads the
103
+ configuration's TypeScript type, never runs it, and states in the artifact which fields its
104
+ `compute.create` and `compute.update` stages fill, per layer (`computedFields`). A variant's guards,
105
+ pipes and filters, which run before write compute, may then leave exactly those fields out of a create;
106
+ every other required field stays required. Without `--config`, or when the configuration is not a
107
+ literal where a compute stage is written (the step prints where), the artifact states nothing and those
108
+ stages may leave any create field out. When the framework starts, it compares the statement with the
109
+ configuration and warns (`AVENTARA_STALE_GENERATED_TYPES`) if you changed a compute stage without
110
+ running the step again.
111
+
112
+ `--out` must not be inside `--client`: Prisma's next `prisma generate` clears that directory and would
113
+ delete the artifact. The step refuses such an `--out`, and one that names a directory, in one sentence
114
+ before Prisma runs.
103
115
 
104
116
  The same step can run from code, for a project that already has a build script:
105
117
 
@@ -146,19 +158,31 @@ at runtime, after your pipes and computed behaviours have run, it is answered `A
146
158
  issue at its path (`["arguments", "data", "title"]`, or `["arguments", "data", "author"]` for a
147
159
  required relation) before the database can refuse it.
148
160
 
161
+ The module also exports the model's type by name, `AvModel` (`typeof model` is that type). A
162
+ project that emits declarations (Nest's `declaration: true`) writes that name wherever an exported
163
+ declaration holds the model, such as the scaffold's `aventaraConfig()`, an `AppFramework` alias or
164
+ `defineResource<typeof model>()`, so those declarations stay under a kilobyte at any schema size. Before
165
+ the name, each mention wrote the whole model out, about 0.5 MB over 50 models.
166
+
149
167
  `modelHash` digests the model and nothing else. The addressing table, the referential actions and the
150
168
  client record sit beside it and outside the hash. A database-side rename or a foreign-key action
151
169
  changes no semantics; inside the hash, either would move the Contract identity and force every
152
170
  generated client to be regenerated for nothing.
153
171
 
154
172
  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.
173
+ reads no file: the artifact arrives as a value you import. The adapter reads one: the installed
174
+ `@prisma/client`'s `package.json`, for its version.
156
175
 
157
176
  At construction, before the application listens, the adapter checks three things and fails hard with
158
177
  an instruction to regenerate if any is wrong:
159
178
 
160
179
  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;
180
+ 2. the installed `@prisma/client` major is the one the artifact was generated against. The version is
181
+ read from `@prisma/client/package.json`, resolved from the adapter's own location and then from the
182
+ working directory. Where it cannot be read (a runtime without Node's module loader, a bundle
183
+ deployed without `@prisma/client`), the adapter starts and emits a warning, code
184
+ `AVENTARA_PRISMA_VERSION_UNCHECKED`, saying the majors were not compared;
185
+ `installedPrismaVersion` overrides the version read;
162
186
  3. the Prisma client you pass is the one the artifact was generated from: the same models, and the
163
187
  same field references (`prisma.<model>.fields`) on each. A schema change followed by
164
188
  `prisma generate` but not by the generate step (a model or field added, removed or renamed) is
@@ -213,10 +237,13 @@ On PostgreSQL and SQLite, read from the generated types:
213
237
  | `create.many` / `update.many` (row-returning bulk writes) | advertised | advertised | `createManyAndReturn` / `updateManyAndReturn` on the delegate |
214
238
  | Transactions | `"interactive"` | `"interactive"` | the driver, not the provider (see below) |
215
239
 
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.
240
+ Nothing beyond PostgreSQL and SQLite is measured. Cloudflare D1 is SQLite through `@prisma/adapter-d1`:
241
+ it reads the same generated client, so its capabilities are SQLite's, and it is measured in process
242
+ through Miniflare's D1 binding, where it differs from SQLite in two ways (no transactions, and a bigint
243
+ within ±(2^53 − 1); see below). MySQL, SQL Server, MongoDB and CockroachDB were not generated against.
244
+ Because every capability is read from the client you actually generated, a provider that lacks
245
+ something advertises less instead of failing at runtime, but this release makes no claim about those
246
+ providers.
220
247
 
221
248
  ### `update.many` and `create.many` cannot project a to-many relation
222
249
 
@@ -372,8 +399,7 @@ commits, do not perform it in an after hook.
372
399
  ### `P2028` covers seven situations, and three of them are separable
373
400
 
374
401
  `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.
402
+ `A3002 TRANSACTION_ERROR` wherever it is met, and never reads the message text to decide anything.
377
403
 
378
404
  What can be told apart from the error's structure, measured on PostgreSQL and SQLite:
379
405
 
@@ -391,8 +417,9 @@ timeout cannot be told apart from them. That limit is Prisma's, and it is record
391
417
  works around it by parsing messages.
392
418
 
393
419
  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.
420
+ raised by a delegate call (or a raw statement) inside the callback, such as a step that runs after the
421
+ `timeout` passed, is in the closed code table and also becomes `A3002`, attributed to that operation
422
+ (`cause.operation`). In rc.1 that second case was unclassified and answered `A3001`.
396
423
 
397
424
  ### Everything unrecognised becomes `A3001`
398
425
 
@@ -438,9 +465,12 @@ observe that it did. A commit failure is different: it rejects like a callback t
438
465
 
439
466
  ### `transactions` is a driver fact, and it reaches the ClientContract hash
440
467
 
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.
468
+ `@prisma/adapter-d1` declares `transactions: "none"`: Prisma's client refuses an interactive
469
+ `$transaction` on D1 itself. Prisma still opens its own implicit transaction for a nested write (a
470
+ `$create` under a relation, a `$connect`), and the D1 driver runs it as single statements, so a nested
471
+ write is not atomic on D1: when a later statement fails, the rows the earlier ones wrote are kept.
472
+ `@prisma/adapter-pg` and `@prisma/adapter-better-sqlite3` declare `"interactive"`, observed by running
473
+ one.
444
474
 
445
475
  Plan for the consequence. The `transactions` capability is part of the ClientContract hash, and a
446
476
  generated client fails hard on a hash mismatch. So one schema, one framework configuration and two
@@ -597,6 +627,23 @@ Resource or operation for one to wrap.
597
627
  - `limit`/`offset`/`cursor` are the public names. Prisma's `take`/`skip` never surface.
598
628
  - Decimal values are converted in both directions. Prisma's bundled decimal object is not an Aventara
599
629
  public type and does not reach a caller as one.
630
+ - Values and filter operands reach Prisma as given: a `contains`, `startsWith` or `endsWith` operand is the
631
+ engine's `LIKE` pattern (`%` and `_` included), and a column stores, rounds or widens a value as the engine does.
632
+ A value PostgreSQL refuses (too long for its column, out of its range, U+0000 in text, a `LIKE` pattern ending in
633
+ `\`) answers `A2004` with one issue at `["arguments"]` (`V1004` or `V1001`), not `A3001`.
634
+ - On D1, a `BigInt` value must lie within ±(2^53 − 1) (±9,007,199,254,740,991), not the full signed 64-bit
635
+ range: the D1 driver binds a bigint as a JavaScript number and refuses one beyond that before the
636
+ statement runs. A value beyond it, written, compared in a filter or used in an `$increment`, answers
637
+ `A2004` with one `V1004` issue at the value (`["arguments", "data", "views"]`), and the statement that
638
+ carries it writes nothing. A `ctx.$sql` statement binding such a value answers `A3001`: its SQL is
639
+ yours.
640
+ - A record written through a relation cannot write that relation, or its reference fields, back: Prisma's nested
641
+ input for it leaves them out (a post created in `author.posts` has no `author` or `authorId`; a category created in
642
+ a post's `category` has no `posts`). The artifact states each relation's other end (`inverse`, paired by relation
643
+ name), the fields that hold its reference (`referenceFields`, from `@relation(fields: …)`) and no relation
644
+ writable at the `"loopback"` position, so `@aventara/core` refuses such a write before any
645
+ guard or SQL, `A2004` with a `V1012` issue at the field written back, and the types do not offer it. Where a layer
646
+ hides the other end, Prisma's client refuses the write instead and the adapter answers that refusal the same way.
600
647
  - Count variants return numbers, including zero. `create.many` and `update.many` return arrays.
601
648
  - `find.unique`, `update.unique` and `delete.unique` answer `A2003 NOT_FOUND` on a missing target;
602
649
  `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
  });
@@ -0,0 +1,25 @@
1
+ import type { AvAdapterModel } from "@aventara/core";
2
+ /** A relation's other end, as seen from the record written through the relation. */
3
+ export type PrismaBackReference = {
4
+ /** The target's relation field that points back at the record it was written from. */
5
+ readonly relation: string;
6
+ /** The target's scalar fields holding that relation's reference, when the target owns it. */
7
+ readonly references: readonly string[];
8
+ };
9
+ /** By Resource, then by relation field: the back-reference a record written through it cannot write. */
10
+ export type PrismaBackReferenceTable = Readonly<Record<string, Readonly<Record<string, PrismaBackReference>>>>;
11
+ /** A path inside an operation's arguments. */
12
+ export type PrismaArgumentPath = readonly (string | number)[];
13
+ /**
14
+ * Pairs every relation field with its other end, once, at construction: the
15
+ * model's own `inverse` (the generate step pairs each relation from the schema,
16
+ * by relation name), and that end's `referenceFields`. A relation the model
17
+ * states no `inverse` for is left out, and its refusal stays `A3001`.
18
+ */
19
+ export declare function derivePrismaBackReferences(model: AvAdapterModel): PrismaBackReferenceTable;
20
+ /**
21
+ * Where the caller's write names a relation's other end (or its reference
22
+ * fields) inside a record written through that relation, as a path in the
23
+ * arguments; `undefined` when it does not. The first such place, in key order.
24
+ */
25
+ export declare function locateBackReferenceWrite(table: PrismaBackReferenceTable, model: AvAdapterModel, resource: string, args: unknown): PrismaArgumentPath | undefined;