@vsrepo/prisma7-adapter 1.3.2 → 2.0.0-dev.1

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 (79) hide show
  1. package/CHANGELOG.md +535 -0
  2. package/LICENSE +20 -20
  3. package/README.md +394 -299
  4. package/README.pt-BR.md +394 -299
  5. package/dist/constants/provider-capabilities.constant.d.ts +17 -0
  6. package/dist/constants/provider-capabilities.constant.js +27 -0
  7. package/dist/index.d.ts +9 -8
  8. package/dist/index.js +1 -7
  9. package/dist/parsers/data.parser.d.ts +3 -33
  10. package/dist/parsers/data.parser.js +70 -69
  11. package/dist/parsers/include.parser.d.ts +1 -1
  12. package/dist/parsers/include.parser.js +3 -7
  13. package/dist/parsers/order-by.parser.d.ts +1 -10
  14. package/dist/parsers/order-by.parser.js +3 -16
  15. package/dist/parsers/select.parser.d.ts +1 -1
  16. package/dist/parsers/select.parser.js +3 -7
  17. package/dist/parsers/where.parser.d.ts +4 -29
  18. package/dist/parsers/where.parser.js +49 -148
  19. package/dist/prisma7.adapter.d.ts +116 -81
  20. package/dist/prisma7.adapter.js +345 -362
  21. package/dist/resolvers/map-prisma-error.resolver.d.ts +0 -11
  22. package/dist/resolvers/map-prisma-error.resolver.js +48 -72
  23. package/dist/resolvers/merge-entities.resolver.d.ts +11 -15
  24. package/dist/resolvers/merge-entities.resolver.js +29 -38
  25. package/dist/resolvers/resolve-provider.resolver.d.ts +2 -22
  26. package/dist/resolvers/resolve-provider.resolver.js +10 -33
  27. package/dist/types/adapter-config.type.d.ts +21 -10
  28. package/dist/types/adapter-config.type.js +1 -3
  29. package/dist/types/adapter-relations.type.d.ts +14 -8
  30. package/dist/types/adapter-relations.type.js +1 -3
  31. package/dist/types/plain-object.type.js +1 -3
  32. package/dist/types/prisma-arg-like.type.js +1 -3
  33. package/dist/types/prisma-client-models.type.d.ts +4 -4
  34. package/dist/types/prisma-client-models.type.js +1 -3
  35. package/dist/types/prisma-provider.type.d.ts +1 -1
  36. package/dist/types/prisma-provider.type.js +1 -3
  37. package/dist/types/prisma-repository-like.type.d.ts +1 -1
  38. package/dist/types/prisma-repository-like.type.js +1 -3
  39. package/dist/types/prisma7-client-like.type.d.ts +1 -8
  40. package/dist/types/prisma7-client-like.type.js +1 -3
  41. package/dist/types/prisma7-global-config.type.d.ts +17 -0
  42. package/dist/types/prisma7-global-config.type.js +1 -0
  43. package/dist/types/prisma7-orm-types.type.d.ts +2 -2
  44. package/dist/types/prisma7-orm-types.type.js +1 -3
  45. package/dist/types/relation.type.d.ts +119 -25
  46. package/dist/types/relation.type.js +1 -3
  47. package/dist/validators/is-decimal.validator.d.ts +2 -5
  48. package/dist/validators/is-decimal.validator.js +3 -6
  49. package/dist/validators/is-plain-object.validator.d.ts +1 -1
  50. package/dist/validators/is-plain-object.validator.js +3 -7
  51. package/dist/validators/validate-prisma-client.validator.d.ts +1 -14
  52. package/dist/validators/validate-prisma-client.validator.js +7 -24
  53. package/package.json +108 -91
  54. package/dist/index.js.map +0 -1
  55. package/dist/parsers/data.parser.js.map +0 -1
  56. package/dist/parsers/include.parser.js.map +0 -1
  57. package/dist/parsers/order-by.parser.js.map +0 -1
  58. package/dist/parsers/select.parser.js.map +0 -1
  59. package/dist/parsers/where.parser.js.map +0 -1
  60. package/dist/prisma7.adapter.js.map +0 -1
  61. package/dist/resolvers/map-prisma-error.resolver.js.map +0 -1
  62. package/dist/resolvers/merge-entities.resolver.js.map +0 -1
  63. package/dist/resolvers/resolve-provider.resolver.js.map +0 -1
  64. package/dist/types/adapter-config.type.js.map +0 -1
  65. package/dist/types/adapter-relations.type.js.map +0 -1
  66. package/dist/types/plain-object.type.js.map +0 -1
  67. package/dist/types/prisma-arg-like.type.js.map +0 -1
  68. package/dist/types/prisma-client-models.type.js.map +0 -1
  69. package/dist/types/prisma-provider.type.js.map +0 -1
  70. package/dist/types/prisma-repository-like.type.js.map +0 -1
  71. package/dist/types/prisma7-client-like.type.js.map +0 -1
  72. package/dist/types/prisma7-orm-types.type.js.map +0 -1
  73. package/dist/types/relation.type.js.map +0 -1
  74. package/dist/validators/is-decimal.validator.js.map +0 -1
  75. package/dist/validators/is-plain-object.validator.js.map +0 -1
  76. package/dist/validators/validate-adapter-config.validator.d.ts +0 -8
  77. package/dist/validators/validate-adapter-config.validator.js +0 -78
  78. package/dist/validators/validate-adapter-config.validator.js.map +0 -1
  79. package/dist/validators/validate-prisma-client.validator.js.map +0 -1
package/README.md CHANGED
@@ -1,299 +1,394 @@
1
- <div align="center">
2
- <img src="https://res.cloudinary.com/ddbfifdxd/image/upload/w_200,q_auto,f_auto/v1786386427/VS_logo_TextoAbaixo_yev4tq.png" alt="VSRepository Logo" width="200"/>
3
-
4
- <p style="margin-top: 12px;">
5
- <img src="https://img.shields.io/npm/v/@vsrepo/prisma7-adapter?style=flat-square" alt="npm version"/>
6
- <img src="https://img.shields.io/npm/l/@vsrepo/prisma7-adapter?style=flat-square" alt="npm license"/>
7
- <img src="https://img.shields.io/npm/dt/@vsrepo/prisma7-adapter?style=flat-square" alt="npm downloads"/>
8
- <img src="https://img.shields.io/badge/inspired%20by-JpaRepository-E73121?style=flat-square" alt="inspired by JpaRepository"/>
9
- </p>
10
- </div>
11
-
12
- # VSRepoPrisma7Adapter
13
-
14
- 🇧🇷 [Ler em português](./README.pt-BR.md)
15
-
16
- > ✅ **Released.** This adapter is published to npm as `@vsrepo/prisma7-adapter`, targeting [VSRepository](https://github.com/jaobrabo123/VSRepository). It's currently the only officially published adapter for v2 — adapters for other ORMs are planned but not published yet (see [Adapter status](https://github.com/jaobrabo123/VSRepository#adapter-status) in the VSRepository README).
17
-
18
- `VSRepoAdapter` implementation for [VSRepository v2](https://github.com/jaobrabo123/VSRepository) backed by [Prisma 7](https://www.prisma.io/). It translates every `VSRepository` operation into Prisma Client calls, resolving `VSRepoWhere`, `Ordering`, `select`/`relations` through dedicated parsers, and — when a `relations` config is provided — resolving relation fields on `create`/`update`/`upsert`/`merge` the same way the v1 of VSRepository used to.
19
-
20
- ---
21
-
22
- ## Table of contents
23
-
24
- - [Installation](#installation)
25
- - [Basic usage](#basic-usage)
26
- - [Constructor config](#constructor-config)
27
- - [Relations](#relations)
28
- - [The two `relations`](#the-two-relations)
29
- - [`relations` in the constructor (write)](#relations-in-the-constructor-write)
30
- - [`mode`](#mode)
31
- - [`restriction`](#restriction)
32
- - [`pk` and `nullable`](#pk-and-nullable)
33
- - [How each write method resolves relations](#how-each-write-method-resolves-relations)
34
- - [`relations` in method options (read)](#relations-in-method-options-read)
35
- - [`merge`](#merge)
36
- - [Atomic and aggregation methods](#atomic-and-aggregation-methods)
37
- - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-dont-support-nested-writes)
38
- - [Provider-specific limitations](#provider-specific-limitations)
39
- - [Transactions](#transactions)
40
- - [Logging](#logging)
41
- - [Requirements](#requirements)
42
-
43
- ---
44
-
45
- ## Installation
46
-
47
- ```bash
48
- npm install vsrepo @prisma/client @vsrepo/prisma7-adapter
49
- ```
50
-
51
- Both `vsrepo` and `@vsrepo/prisma7-adapter` are published to npm and ready to use.
52
-
53
- ## Basic usage
54
-
55
- ```typescript
56
- import { VSRepository, VSLogLevel } from "vsrepo";
57
- import { Prisma7Adapter, Prisma7OrmTypes } from "@vsrepo/prisma7-adapter";
58
- import { Prisma, PrismaClient } from "./generated/prisma/client";
59
- import { prisma } from "./prisma";
60
-
61
- type User = Prisma.UserGetPayload<{ include: { posts: true } }>;
62
- type MyOrmTypes = Prisma7OrmTypes<PrismaClient>;
63
-
64
- class UserRepository extends VSRepository<User, string, MyOrmTypes> {
65
- constructor() {
66
- super({
67
- adapter: new Prisma7Adapter(prisma, {
68
- tableName: "user",
69
- pkName: "id",
70
- relations: {
71
- posts: { mode: "otm", restriction: "add", pk: "id" },
72
- },
73
- logLevel: VSLogLevel.WARN,
74
- }),
75
- });
76
- }
77
- }
78
-
79
- const userRepository = new UserRepository();
80
-
81
- const user = await userRepository.get({ id: "..." }, { relations: { posts: true } });
82
- ```
83
-
84
- Here the `relations` you pass in the method `options` — shaped like `{ field: true }` — is transformed into a Prisma `include` by the adapter (see [`relations` in method options (read)](#relations-in-method-options-read)). If you supply `select`, the `relations`/`include` is ignored. Don't confuse it with the constructor-config `relations`, which describes how relation fields are resolved in write payloads — the difference is explained in [The two `relations`](#the-two-relations).
85
-
86
- `Prisma7OrmTypes<DB>` ties `VSRepository`'s `getDbClient()`/`transaction()` return types to your real, generated Prisma types — see [Transactions](#transactions).
87
-
88
- ## Constructor config
89
-
90
- ```typescript
91
- new Prisma7Adapter(prisma, {
92
- tableName: "user", // required — the Prisma Client model/delegate name, as in `prisma.user`
93
- pkName: "id", // required — the entity's primary key field name
94
- relations: { ... }, // optional — see "relations in the constructor (write)" below
95
- logLevel: VSLogLevel.WARN, // optional — default: VSLogLevel.WARN
96
- });
97
- ```
98
-
99
- The config is validated at construction time — an invalid `tableName`/`pkName`/`relations`/`logLevel` throws a `VSRepoAdapterError` naming the offending field.
100
-
101
- ## Relations
102
-
103
- ### The two `relations`
104
-
105
- The name `relations` appears in **two different places** in the API, with **different shapes and purposes** — easy to confuse. In a nutshell:
106
-
107
- | | `relations` in the **constructor** | `relations` in **method options** |
108
- | --- | --- | --- |
109
- | Where you define it | `new Prisma7Adapter(prisma, { relations: ... })` | `repository.get(where, { relations: ... })` — and other methods |
110
- | Shape | One **config object** per field: `{ mode, restriction, pk, nullable? }` | One **`true`/sub-object** per field: `{ posts: true }` |
111
- | Purpose | **Write** — when a `create`/`update`/`upsert`/`save`/`merge` payload contains a relation field, tells the adapter how to turn it into a Prisma nested write (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) | **Read** — eager loading: which relations to fetch alongside the result (becomes a Prisma `include`) |
112
- | Consumed by | `parsePrismaWriteData` / `mergeEntities` (write resolvers) | `parsePrismaInclude` (via `resolveReadArg`) |
113
- | Depends on the other? | No — it only affects writes/`merge` | No — it works even with no `relations` in the constructor |
114
-
115
- Neither depends on the other: the method-options `relations` does eager loading even when the constructor has no `relations`, and the constructor `relations` governs write behavior even if you never pass `relations` in options. The two subsections below cover each one.
116
-
117
- ### `relations` in the constructor (write)
118
-
119
- The `relations` config on the constructor describes, for each relation field of your entity, **how the adapter should resolve that field when it shows up in write payloads** (`create`/`update`/`upsert`/`save`/`merge`) — the same behavior the v1 of VSRepository had.
120
-
121
- Each relation is configured by its field name:
122
-
123
- ```typescript
124
- relations: {
125
- posts: { mode: "otm", restriction: "add", pk: "id" },
126
- address: { mode: "oto", restriction: "set", pk: "id" },
127
- author: { mode: "mto", restriction: "set", pk: "id", nullable: true },
128
- }
129
- ```
130
-
131
- Without `relations`, every field — including relation fields — is passed straight through to Prisma's `where`/`data`/`create`/`update`, as-is. That's fine for scalar fields, but Prisma expects a very specific nested-write shape (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) for relation fields, so if your entity has relations you'll usually want to configure them.
132
-
133
- #### `mode`
134
-
135
- Cardinality of the relation, from the point of view of the entity that owns the field:
136
-
137
- | `mode` | Meaning | Field shape you send |
138
- | --- | --- | --- |
139
- | `oto` | one-to-one | a single object, or `null` |
140
- | `mto` | many-to-one | a single object, or `null` |
141
- | `otm` | one-to-many | an array of objects |
142
- | `mtm` | many-to-many | an array of objects |
143
-
144
- #### `restriction`
145
-
146
- Controls how `save`/`update`/`upsert` handle relation items that already exist (matched by `pk`) and, for to-many relations, items that were **not** included in the payload:
147
-
148
- - **`"add"`** — only creates/connects/upserts the items you send. Existing items that aren't in the payload are left untouched.
149
- - **`"set"`** — same as `"add"`, but also removes what wasn't sent: for `otm` it runs a `deleteMany` on the items missing from the array; for `mtm` it resets the join (`set: []`) before reconnecting; for `oto`/`mto` with `nullable: true`, sending `null` deletes/disconnects the relation (see below).
150
-
151
- #### `pk` and `nullable`
152
-
153
- - **`pk`** — the field used to identify an existing related record. An item **with** `pk` in the payload becomes a `connectOrCreate`/`upsert`; an item **without** it becomes a plain `create`.
154
- - **`nullable`** — relevant for `oto`/`mto` to-one relations. When `true`, sending `null` for the field resolves to `disconnect` on update (`delete` instead for `oto` with `restriction: "set"` — an `oto` side can't just be "disconnected", the owned row is removed). When omitted/`false`, sending `null` for a to-one relation throws a `VSRepoAdapterError` (code `INVALID_DATA`) telling you the relation is not nullable.
155
-
156
- #### How each write method resolves relations
157
-
158
- | Method | Relations |
159
- | --- | --- |
160
- | `create` | `create`/`connectOrCreate` only (no upsert — there's nothing to update yet) |
161
- | `update` / `upsert` (`update` half) / `save` (upsert branch) | Full resolution: `create`/`connectOrCreate`/`upsert`/`disconnect`/`delete`/`deleteMany`/`set`, per `mode`/`restriction` |
162
- | `createMany` / `createManyReturning` / `updateMany` / `updateManyReturning` | Not supported — throws a `VSRepoAdapterError` naming the offending field if the payload contains a configured relation |
163
-
164
- ### `relations` in method options (read)
165
-
166
- This is the `relations` you pass in the `options` of a `VSRepository` method (`get`, `find`, `findOne`, etc.). The shape is much simpler: an object where each relation field accepts:
167
-
168
- - `true` — load the relation as-is;
169
- - or another `relations` object — for nested eager loading (relations of the relation).
170
-
171
- ```typescript
172
- const user = await userRepository.get(
173
- { id: "..." },
174
- {
175
- relations: {
176
- posts: true, // fetch posts along
177
- author: { profile: true }, // and, inside author, its profile (nested eager loading)
178
- },
179
- }
180
- );
181
- ```
182
-
183
- This object is turned into a Prisma `include` by the adapter (`parsePrismaInclude`). It does **not** use the constructor's `relations` config: it's purely a read-side option and works even with no `relations` in the config.
184
-
185
- The `select` and `relations` you pass in the options are turned into a Prisma `select` (`parsePrismaSelect`) and a Prisma `include` (`parsePrismaInclude`), respectively. When `select` is provided, the `include` derived from `relations` is discarded (`undefined`), because Prisma doesn't allow combining `include` and `select` in a single query. In other words, if you're already using `select`, the `relations` object becomes redundant — so it's optional. Here's the resolved logic (from `resolveReadArg`):
186
-
187
- ```ts
188
- const prismaSelect = options.select && parsePrismaSelect(options.select);
189
- const prismaInclude = prismaSelect
190
- ? undefined
191
- : options.relations && parsePrismaInclude(options.relations);
192
- ```
193
-
194
- ## `merge`
195
-
196
- `merge(where, obj, options)` fetches the record matching `where` and returns it **deep-merged, in memory**, with `obj` — it does **not** write anything to the database. This mirrors how `merge` worked in the v1 of VSRepository: it's meant to build a full, merged entity that you then pass to `save`/`update` yourself, not to persist a partial update directly.
197
-
198
- For to-many relations (`otm`/`mtm`), items in the stored record and items in `obj` are matched by the configured `pk`: a match merges the two items, a new `pk` (or no `pk`) is appended. Nothing is ever removed by `merge`.
199
-
200
- ## Atomic and aggregation methods
201
-
202
- The adapter implements the 8 abstract methods `VSRepository`'s `increment`/`decrement`/`multiply`/`divide`/`sum`/`average`/`min`/`max` delegate to: `incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max`.
203
-
204
- - `incrementOne`/`decrementOne`/`multiplyOne`/`divideOne` translate directly into Prisma's native single-field atomic write — `data: { [field]: { increment: value } }` (and `decrement`/`multiply`/`divide`) — so the operation is evaluated **server-side** against the row's *current* value (`UPDATE ... SET field = field + value`), not as a fetch-then-save round trip on the client. Prisma's `update()` already returns the row reflecting the state *after* the write, so the adapter never issues a follow-up read.
205
- - `sum`/`average`/`min`/`max` translate into Prisma's `aggregate()` with `_sum`/`_avg`/`_min`/`_max: { [field]: true }`. The raw result (`number`, `bigint`, a `Decimal` instance, or `null`) is normalized to `number | null` — `null` is returned as-is (mirroring SQL's `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL`, not `0`, over an empty set), and a `Decimal`/`DecimalLike` value is converted via its `.toNumber()`.
206
-
207
- ```typescript
208
- // Atomic — evaluated server-side, no read-modify-write:
209
- await productRepository.increment(productId, "stock", 10);
210
- await accountRepository.decrement(accountId, "balance", 50);
211
- await productRepository.multiply(productId, "price", 1.1); // e.g. a 10% price bump
212
- await productRepository.divide(productId, "price", 2);
213
-
214
- // Aggregation — across every record matching an optional `where` (all if omitted):
215
- const total = await productRepository.sum("price"); // number | null
216
- const avgPrice = await productRepository.average("price", { active: true });
217
- const cheapest = await productRepository.min("price");
218
- const mostExpensive = await productRepository.max("price");
219
- ```
220
-
221
- **Decimal fields** (`Prisma.Decimal`) are fully supported as the `field` for all 8 methods — `value` for the atomic methods is passed straight through to Prisma (which accepts a `number`, `string`, or `Decimal`/`DecimalJsLike`), and the aggregate methods always normalize the result to a `number`, never a `Decimal` instance.
222
-
223
- ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes
224
-
225
- `createMany`, `createManyReturning`, `updateMany` and `updateManyReturning` only accept scalar fields in their `data`. If your payload includes a field configured in `relations` (regardless of its value), the adapter throws a `VSRepoAdapterError` naming the offending field. For a full nested write, use `create`/`update`/`save` one record at a time, or wrap several `save` calls in a `saveMany`/`transaction`.
226
-
227
- > Note on return order: `createManyReturning` don't guarantee the returned records follow the order of the input payload (`objs`). Their result comes from a second `findMany` (re-querying the inserted/updated rows by primary key), so the order is only guaranteed when you pass `order` in the options.
228
-
229
- ## Provider-specific limitations
230
-
231
- Some Prisma features this adapter relies on aren't available on every database provider. If you use one of these features on an unsupported provider, Prisma itself will throw a validation error (or, in future versions, the adapter may validate this upfront — see below).
232
-
233
- | Feature | Supported providers | Not supported |
234
- | --- | --- | --- |
235
- | `mode: "insensitive"` (used by `findByXIgnoreCase` dynamic methods) | PostgreSQL, MongoDB | MySQL, SQLite, SQL Server, CockroachDB — MySQL and SQL Server are case-insensitive by default, so no `mode` is needed there; SQLite is only case-insensitive for ASCII characters |
236
- | `createManyReturning` / `updateManyReturning` (built on Prisma's `createManyAndReturn`/`updateManyAndReturn`) | PostgreSQL, CockroachDB, SQLite | MySQL, SQL Server, MongoDB |
237
- | `skipDuplicates` on `createManyIgnoreConflicts` / `createManyReturningIgnoreConflicts` | PostgreSQL, MySQL, CockroachDB | MongoDB, SQL Server, SQLite |
238
-
239
- > These limitations come from Prisma itself, not from this adapter — see [Prisma's case sensitivity docs](https://www.prisma.io/docs/orm/v7/reference/prisma-client-reference#mode) and [CRUD reference](https://www.prisma.io/docs/orm/v7/prisma-client/queries/crud) for details.
240
-
241
- ## Transactions
242
-
243
- **Every** method accepts `options.db` and runs its operation on the client/transaction you pass — the difference is in **how** each one treats it:
244
-
245
- - Most methods (single, one-call Prisma operations) simply run directly on `options?.db`: hand them a transaction client and the call participates in that transaction, starting nothing new.
246
- - `saveMany`, `updateManyReturning`, `createManyReturning` and `deleteManyReturning` need to run **more than one** Prisma operation atomically (`saveMany` saves each record individually; `updateManyReturning`/`createManyReturning` do an `updateManyAndReturn`/`createManyAndReturn` + `findMany`; `deleteManyReturning` does a `findMany` + `deleteMany`), so they go through `runTransactional`. If `options.db` is already an active transaction client, it's reused — no nested transaction is started; otherwise, a new transaction is created (on `options.db` if provided, or on the root client otherwise).
247
-
248
- A transaction client is told apart from the root `PrismaClient` by the `$on` method: Prisma's root client exposes it (for event listeners), the interactive transaction client doesn't. This is what `Prisma7ClientLike`/`Prisma7OrmTypes` encode in their types.
249
-
250
- ```typescript
251
- await userRepository.transaction(async tx => {
252
- // Every repository call made with `{ db: tx }` inside this callback
253
- // shares the same transaction — including calls to `saveMany`/
254
- // `updateManyReturning`/`createManyReturning`/`deleteManyReturning`, which
255
- // will detect `tx` is already a transaction and run directly on it instead
256
- // of nesting a new one.
257
- await userRepository.save(user, { db: tx });
258
- await userRepository.saveList(otherUsers, { db: tx });
259
- });
260
- ```
261
-
262
- ### Concurrency in `deleteManyReturning`
263
-
264
- `deleteManyReturning` runs a `findMany` on the given `where` (to capture the records it will return) and then **re-applies the same `where`** to a `deleteMany`. Prisma has no native `deleteManyAndReturn`, which is why the delete is driven by the `where` instead of by the records fetched in the `findMany`.
265
-
266
- Because of this two-step shape, a concurrent change between the `findMany` and the `deleteMany` can make them diverge:
267
-
268
- - a row **inserted after** the `findMany` that matches the `where` will still be deleted, even though it wasn't returned;
269
- - a row that **stops matching** the `where` before the `deleteMany` runs won't be deleted, even though it was returned.
270
-
271
- In other words, the returned records and the rows actually deleted are not guaranteed to be identical under concurrency.
272
-
273
- If you need to guarantee there are no concurrency issues, run this method inside a `repository.transaction()` at a higher isolation level — e.g. `SERIALIZABLE`:
274
-
275
- ```typescript
276
- import { VSRepository, TransactionIsolationLevel } from "vsrepo";
277
-
278
- await userRepository.transaction(async tx => {
279
- // Atomic: no concurrent insert/update can slip between the findMany
280
- // and the deleteMany, since SERIALIZABLE isolates this transaction.
281
- // `deleteManyReturning` is reached through the dynamic method you mapped
282
- // to the adapter (e.g. a `@DynamicMethod()` exposing `deleteManyReturning`),
283
- // always passing `{ db: tx }` so it runs inside this transaction.
284
- const deleted = await userRepository.deleteManyReturningByIdIn([...], { db: tx });
285
- }, {
286
- isolationLevel: TransactionIsolationLevel.SERIALIZABLE,
287
- });
288
- ```
289
-
290
- > The isolation level applies only when the repository call actually runs **inside** that transaction (i.e. with `{ db: tx }`). When `deleteManyReturning` starts its own internal transaction (no `options.db` passed), it falls back to the default isolation level.
291
-
292
- ## Logging
293
-
294
- The adapter uses `VSLogger` (from `vsrepo`) internally: every method logs a `DEBUG` line with the resolved Prisma arg, and start/end performance logs (surfacing a `WARN` for slow operations). Set `logLevel` in the constructor config to control verbosity; it defaults to `VSLogLevel.WARN`.
295
-
296
- ## Requirements
297
-
298
- - `vsrepo` ^2.7.2
299
- - `@prisma/client` ^7.10.0
1
+ <div align="center">
2
+ <img src="https://res.cloudinary.com/ddbfifdxd/image/upload/w_200,q_auto,f_auto/v1786386427/VS_logo_TextoAbaixo_yev4tq.png" alt="VSRepository Logo" width="200"/>
3
+
4
+ <p style="margin-top: 12px;">
5
+ <img src="https://img.shields.io/npm/v/@vsrepo/prisma7-adapter?style=flat-square" alt="npm version"/>
6
+ <img src="https://img.shields.io/npm/l/@vsrepo/prisma7-adapter?style=flat-square" alt="npm license"/>
7
+ <img src="https://img.shields.io/npm/dt/@vsrepo/prisma7-adapter?style=flat-square" alt="npm downloads"/>
8
+ <img src="https://img.shields.io/badge/inspired%20by-JpaRepository-E73121?style=flat-square" alt="inspired by JpaRepository"/>
9
+ </p>
10
+ </div>
11
+
12
+ # VSRepoPrisma7Adapter
13
+
14
+ 🇧🇷 [Ler em português](./README.pt-BR.md)
15
+
16
+ > 🚧 **In development.** This is the `v2` of `@vsrepo/prisma7-adapter`, targeting [VSRepository v3](https://github.com/VSRepository/VSRepository). It's a development version, not a stable release yet, so its API may still change. For VSRepository v2, use the 1.x line (latest: `1.3.2`). It's currently the only officially supported adapter — adapters for other ORMs are planned but not published yet (see [Adapter status](https://github.com/VSRepository/VSRepository#adapter-status) in the VSRepository README).
17
+
18
+ `VSRepoAdapter` implementation for [VSRepository v3](https://github.com/VSRepository/VSRepository) backed by [Prisma 7](https://www.prisma.io/). It translates every `VSRepository` operation into Prisma Client calls, resolving `VSRepoWhere`, `Ordering`, `select`/`relations` through dedicated parsers, and — when a `relations` config is provided — resolving relation fields on `create`/`update`/`upsert`/`save`.
19
+
20
+ ---
21
+
22
+ ## Table of contents
23
+
24
+ - [Installation](#installation)
25
+ - [Basic usage](#basic-usage)
26
+ - [Constructor config](#constructor-config)
27
+ - [Global config](#global-config)
28
+ - [Relations](#relations)
29
+ - [The two `relations`](#the-two-relations)
30
+ - [`relations` in the constructor (write)](#relations-in-the-constructor-write)
31
+ - [`mode`](#mode)
32
+ - [`pk`](#pk)
33
+ - [`onUpdate`](#onupdate)
34
+ - [`onDisconnect`](#ondisconnect)
35
+ - [Ignoring a relation (`"ignore"`)](#ignoring-a-relation-ignore)
36
+ - [`fkHere` and `fkThere`](#fkhere-and-fkthere)
37
+ - [`allowOmitPk`](#allowomitpk)
38
+ - [How each write method resolves relations](#how-each-write-method-resolves-relations)
39
+ - [`relations` in method options (read)](#relations-in-method-options-read)
40
+ - [Atomic and aggregation methods](#atomic-and-aggregation-methods)
41
+ - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-dont-support-nested-writes)
42
+ - [Provider-specific limitations](#provider-specific-limitations)
43
+ - [Transactions](#transactions)
44
+ - [Logging](#logging)
45
+ - [Requirements](#requirements)
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ npm install vsrepo@dev @prisma/client @vsrepo/prisma7-adapter@dev
53
+ ```
54
+
55
+ While `2.0.0` is in development, install both `vsrepo` and `@vsrepo/prisma7-adapter` with the `dev` tag.
56
+
57
+ ## Basic usage
58
+
59
+ ```typescript
60
+ import { VSRepository, VSLogLevel } from "vsrepo";
61
+ import { Prisma7Adapter, Prisma7OrmTypes } from "@vsrepo/prisma7-adapter";
62
+ import { Prisma, PrismaClient } from "./generated/prisma/client";
63
+ import { prisma } from "./prisma";
64
+
65
+ type User = Prisma.UserGetPayload<{ include: { posts: true } }>;
66
+ type MyOrmTypes = Prisma7OrmTypes<PrismaClient>;
67
+
68
+ class UserRepository extends VSRepository<User, string, MyOrmTypes> {
69
+ constructor() {
70
+ super({
71
+ adapter: new Prisma7Adapter(prisma, {
72
+ modelName: "user",
73
+ pkName: "id",
74
+ relations: {
75
+ posts: { mode: "otm", pk: "id" },
76
+ },
77
+ logLevel: VSLogLevel.WARN,
78
+ }),
79
+ });
80
+ }
81
+ }
82
+
83
+ const userRepository = new UserRepository();
84
+
85
+ const user = await userRepository.get("...", { relations: { posts: true } }); // by primary key
86
+ ```
87
+
88
+ Here the `relations` you pass in the method `options` — shaped like `{ field: true }` — is transformed into a Prisma `include` by the adapter (see [`relations` in method options (read)](#relations-in-method-options-read)). If you supply `select`, the `relations`/`include` is ignored. Don't confuse it with the constructor-config `relations`, which describes how relation fields are resolved in write payloads — the difference is explained in [The two `relations`](#the-two-relations).
89
+
90
+ `Prisma7OrmTypes<DB>` ties `VSRepository`'s `getDbClient()`/`transaction()` return types to your real, generated Prisma types — see [Transactions](#transactions).
91
+
92
+ ## Constructor config
93
+
94
+ ```typescript
95
+ new Prisma7Adapter(prisma, {
96
+ modelName: "user", // required — the Prisma Client model/delegate name, as in `prisma.user`
97
+ pkName: "id", // required — the entity's primary key field name
98
+ relations: { ... }, // optional — see "relations in the constructor (write)" below
99
+ provider: "postgresql", // optional — detected from the Prisma Client when omitted
100
+ logLevel: VSLogLevel.WARN, // optional — default: VSLogLevel.WARN
101
+ allowOmitPk: false, // optional — default: false. Read "`allowOmitPk`" before turning it on
102
+ });
103
+ ```
104
+
105
+ At construction, the adapter checks the Prisma Client and the model, and resolves the datasource provider:
106
+
107
+ - a `null`/`undefined` client throws a `VSRepoAdapterError` (code `MISSING_DB_CLIENT`);
108
+ - a `modelName` with no matching delegate on the client (`prisma.<modelName>`) throws `MODEL_NOT_FOUND`;
109
+ - the provider comes from `provider` when set; otherwise it's detected from the generated client (`_engineConfig.activeProvider`, falling back to the driver adapter's `provider`). If it can't be detected, construction throws `NOT_SUPPORTED` — set `provider` explicitly in that case (one of `postgresql`, `cockroachdb`, `mysql`, `sqlite`, `sqlserver`).
110
+
111
+ The other fields (`pkName`, `relations`, `logLevel`, `allowOmitPk`) are checked by TypeScript only, not at runtime.
112
+
113
+ ## Global config
114
+
115
+ `provider`, `logLevel` and `allowOmitPk` can be set once for every adapter, through the static `setGlobalConfig`:
116
+
117
+ ```typescript
118
+ import { VSLogLevel } from "vsrepo";
119
+ import { Prisma7Adapter } from "@vsrepo/prisma7-adapter";
120
+
121
+ Prisma7Adapter.setGlobalConfig({ logLevel: VSLogLevel.WARN });
122
+ ```
123
+
124
+ - It only affects adapters created **after** the call, so call it before instantiating your repositories.
125
+ - Each call **replaces** the previous global config — it doesn't merge with it.
126
+ - A value set in an adapter's own config always wins over the global one.
127
+
128
+ ## Relations
129
+
130
+ ### The two `relations`
131
+
132
+ The name `relations` appears in **two different places** in the API, with **different shapes and purposes** — easy to confuse. In a nutshell:
133
+
134
+ | | `relations` in the **constructor** | `relations` in **method options** |
135
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
136
+ | Where you define it | `new Prisma7Adapter(prisma, { relations: ... })` | `repository.get(where, { relations: ... })` — and other methods |
137
+ | Shape | One **config object** per field (`{ mode, pk, onUpdate?, onDisconnect?, fkHere?, fkThere? }`), or `"ignore"` | One **`true`/sub-object** per field: `{ posts: true }` |
138
+ | Purpose | **Write** — when a `create`/`update`/`upsert`/`save` payload contains a relation field, tells the adapter how to turn it into a Prisma nested write (`create`/`connect`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) | **Read** — eager loading: which relations to fetch alongside the result (becomes a Prisma `include`) |
139
+ | Consumed by | `parsePrismaWriteData` (write resolver) | `parsePrismaInclude` (via `resolveReadArg`) |
140
+ | Depends on the other? | No — it only affects writes | No — it works even with no `relations` in the constructor |
141
+
142
+ Neither depends on the other: the method-options `relations` does eager loading even when the constructor has no `relations`, and the constructor `relations` governs write behavior even if you never pass `relations` in options. The two subsections below cover each one.
143
+
144
+ ### `relations` in the constructor (write)
145
+
146
+ The `relations` config on the constructor describes, for each relation field of your entity, **how the adapter should resolve that field when it shows up in write payloads** (`create`/`update`/`upsert`/`save`).
147
+
148
+ Each relation is configured by its field name:
149
+
150
+ ```typescript
151
+ relations: {
152
+ posts: { mode: "otm", pk: "id", onUpdate: "set" },
153
+ address: { mode: "oto", pk: "id", onUpdate: "upsert", onDisconnect: "delete" },
154
+ author: { mode: "mto", pk: "id" }, // every option has a default
155
+ audit: "ignore", // never written through this repository
156
+ }
157
+ ```
158
+
159
+ Without `relations`, every field — including relation fields — is passed straight through to Prisma's `where`/`data`/`create`/`update`, as-is. That's fine for scalar fields, but Prisma expects a very specific nested-write shape (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) for relation fields, so if your entity has relations you'll usually want to configure them.
160
+
161
+ #### `mode`
162
+
163
+ Cardinality of the relation, from the point of view of the entity that owns the field:
164
+
165
+ | `mode` | Meaning | Field shape you send | Options |
166
+ | ------ | ------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
167
+ | `oto` | one-to-one | a single object, or `null` | `pk`, `onUpdate` (to-one), `onDisconnect` (`"disconnect"`/`"delete"`), `fkHere`, `fkThere` |
168
+ | `mto` | many-to-one | a single object, or `null` | `pk`, `onUpdate` (to-one), `onDisconnect` (`"disconnect"`/`"delete"`), `fkHere`, `fkThere` |
169
+ | `otm` | one-to-many | an array of objects | `pk`, `onUpdate` (to-many), `fkThere` |
170
+ | `mtm` | many-to-many | an array of objects | `pk`, `onUpdate` (to-many), `fkThere` |
171
+
172
+ #### `pk`
173
+
174
+ The field used to identify an existing related record. An item **with** `pk` in the payload is connected to (or upserted into) the existing record; an item **without** it is created.
175
+
176
+ #### `onUpdate`
177
+
178
+ How `save`/`update`/`upsert` write the relation on update. The values accepted depend on the `mode`.
179
+
180
+ **To-one (`oto`/`mto`)**, when the related object has its `pk`:
181
+
182
+ - **`"connect"`** never creates nor overwrites the related record: it only points the relation to it.
183
+ - **`"connectOrCreate"`** creates the related record when it is missing.
184
+ - **`"upsert"`** is the one that **overwrites** the related record with the fields you sent.
185
+ - When the owner record itself is being created (`create`, or the `create` branch of `save`), a related object with `pk` always becomes a `connectOrCreate`.
186
+
187
+ **To-many (`otm`/`mtm`)**, for the items that already exist (matched by `pk`) and for the ones that were **not** sent:
188
+
189
+ - **`"add"`** (default) — only creates/connects/upserts the items you send. Existing items that aren't in the payload are left untouched.
190
+ - **`"set"`** — same as `"add"`, but also removes what wasn't sent: `otm` runs a `deleteMany` on the items missing from the array, and `mtm` resets the join (`set: []`) before reconnecting.
191
+
192
+ #### `onDisconnect`
193
+
194
+ What sending `null` to a to-one relation does on update:
195
+
196
+ - **`"disconnect"`** (default) — detaches the related record and keeps it. Prisma rejects it when the relation is required.
197
+ - **`"delete"`** — deletes the related record. Available for `oto` and `mto`, and always opt-in. Be careful on an `mto`: the related record is shared with other rows, so deleting it affects all of them, and Prisma rejects the write while other rows still reference it (unless the FK has a referential action such as `onDelete: Cascade`).
198
+
199
+ #### Ignoring a relation (`"ignore"`)
200
+
201
+ Setting a relation to `"ignore"` leaves that field out of every write (`create`, `update`, `save`...): the object (or array) is simply dropped. It is useful for a relation you **load** but don't want to **persist** through this entity.
202
+
203
+ ```typescript
204
+ // AddressRepository
205
+ relations: {
206
+ user: "ignore";
207
+ }
208
+ ```
209
+
210
+ Since the relation object is dropped, a scalar FK in the payload (`userId`) goes through as is, together with the primary key. That makes it the simplest way around the [`allowOmitPk` problem](#allowomitpk) when you only want to keep the FK: `address.save(addressLoadedWithItsUser)` updates the address and never touches the user.
211
+
212
+ #### `fkHere` and `fkThere`
213
+
214
+ In a one-to-one or many-to-one relation, only one side keeps the foreign key column. For `Address.user`, that's `Address.userId`. Tell the adapter which side each entity is on:
215
+
216
+ | Entity | Relation field | Holds the FK? | Config |
217
+ | ---------------------- | -------------- | --------------------------------------------------- | ------------------- |
218
+ | `Address` (owner side) | `user` | **yes** — `userId` is a column of `Address` | `fkHere: "userId"` |
219
+ | `User` (inverse side) | `address` | no — the FK lives in the related entity (`Address`) | `fkThere: "userId"` |
220
+
221
+ ```typescript
222
+ // UserRepository
223
+ relations: { address: { mode: "oto", pk: "id", fkThere: "userId" } }
224
+
225
+ // AddressRepository
226
+ relations: { user: { mode: "oto", pk: "id", fkHere: "userId" } }
227
+ ```
228
+
229
+ - **`fkThere`** — name of the FK that lives in the **related** entity. A related object loaded with `get`/`find` usually carries it (`address: { id, street, userId }`), but the relation already sets it, and Prisma doesn't accept it inside the nested write. The adapter strips it from the related payload(s) before building the write.
230
+ - **`fkHere`** — name of the FK that lives in **this** entity. Prisma doesn't accept the scalar FK together with the relation object, so when the relation field is in the payload the adapter drops this field from `create` and `update`. A payload that carries only the scalar FK (and no relation object) is left as it is.
231
+
232
+ `fkHere` has one more consequence, for the primary key — read the next section.
233
+
234
+ #### `allowOmitPk`
235
+
236
+ `allowOmitPk` is an adapter option (it can also be set in the [global config](#global-config)), but it only matters for relations configured with `fkHere`.
237
+
238
+ **The problem.** Take an `Address` loaded together with its `user` (`relations: { user: true }`) and sent back to `save`. The payload has the primary key (`id`) **and** a relation that holds its FK in the entity itself. Once the relation object is there, Prisma won't take the scalar FK nor the primary key (`Unknown argument 'id'`), so the adapter can't send everything.
239
+
240
+ **What the adapter does:**
241
+
242
+ - **`allowOmitPk: false`** (default) — throws a `VSRepoAdapterError` (`INVALID_DATA`) **before** touching the database, explaining what to do. Payloads without a primary key (e.g. a plain `create`), or without the relation object, are not affected.
243
+ - **`allowOmitPk: true`** — drops the primary key from `create` and `update`. Records that already exist are still found through `where: { id }`, so **updates work normally**. The catch is the `create` branch of `save`: if the record doesn't exist yet, it is created with a **database-generated primary key, not the one you sent** — and nothing warns you.
244
+
245
+ **When to turn it on.** Only if you really need it, and only if it's not your application that manages the ids — i.e. the database generates them (`autoincrement`, `uuid()`/`cuid()` defaults, sequences...). If your application assigns the ids (client-generated UUIDs, ids imported from another system, ids that other records already reference), keep it **off**: a record that doesn't exist would silently be stored under a different id.
246
+
247
+ **Ways around it, without `allowOmitPk`:**
248
+
249
+ 1. **Ignore the relation** when you only want to keep the FK: `relations: { user: "ignore" }` (see [Ignoring a relation](#ignoring-a-relation-ignore)). The payload keeps `id` and `userId`, and the related entity is not written.
250
+ 2. **Send the scalar FK instead of the relation object.** `address.save({ id, city, userId })` keeps both `id` and `userId`, since there's no relation object. Persist the related entity on its own (through its repository) when it also changed — ideally in one `transaction`.
251
+ 3. **Send only what changed.** `address.update({ id }, { city })` carries neither the primary key nor the relation.
252
+ 4. **Save from the other side.** `user.save({ ...user, address })` keeps the primary keys (configure `fkThere` there): the FK lives in `Address`, so `User` has no FK of its own to conflict with.
253
+ 5. **Let the database generate the key for new records.** Don't send `id` when creating: `address.create({ city, user })`.
254
+
255
+ #### How each write method resolves relations
256
+
257
+ | Method | Relations |
258
+ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | `create` | `create`/`connectOrCreate` only (no upsert — there's nothing to update yet) |
260
+ | `update` / `upsert` (`update` half) / `save` (upsert branch) | Full resolution: `create`/`connect`/`connectOrCreate`/`upsert`/`disconnect`/`delete`/`deleteMany`/`set`, per `mode`/`onUpdate`/`onDisconnect` |
261
+ | `createMany` / `createManyReturning` / `updateMany` / `updateManyReturning` | Not supported — throws a `VSRepoAdapterError` naming the offending field if the payload contains a configured relation |
262
+
263
+ ### `relations` in method options (read)
264
+
265
+ This is the `relations` you pass in the `options` of a `VSRepository` method (`get`, `find`, `findOne`, etc.). The shape is much simpler: an object where each relation field accepts:
266
+
267
+ - `true` — load the relation as-is;
268
+ - or another `relations` object — for nested eager loading (relations of the relation).
269
+
270
+ ```typescript
271
+ const user = await userRepository.get(
272
+ "...", // primary key
273
+ {
274
+ relations: {
275
+ posts: true, // fetch posts along
276
+ author: { profile: true }, // and, inside author, its profile (nested eager loading)
277
+ },
278
+ },
279
+ );
280
+ ```
281
+
282
+ This object is turned into a Prisma `include` by the adapter (`parsePrismaInclude`). It does **not** use the constructor's `relations` config: it's purely a read-side option and works even with no `relations` in the config.
283
+
284
+ The `select` and `relations` you pass in the options are turned into a Prisma `select` (`parsePrismaSelect`) and a Prisma `include` (`parsePrismaInclude`), respectively. When `select` is provided, the `include` derived from `relations` is discarded (`undefined`), because Prisma doesn't allow combining `include` and `select` in a single query. In other words, if you're already using `select`, the `relations` object becomes redundant — so it's optional. Here's the resolved logic (from `resolveReadArg`):
285
+
286
+ ```ts
287
+ const prismaSelect = options.select && parsePrismaSelect(options.select);
288
+ const prismaInclude = prismaSelect ? undefined : options.relations && parsePrismaInclude(options.relations);
289
+ ```
290
+
291
+ ## Atomic and aggregation methods
292
+
293
+ The adapter implements the 12 abstract methods `VSRepository`'s `increment`/`decrement`/`multiply`/`divide`, their `*Many` counterparts, and `sum`/`average`/`min`/`max` delegate to: `incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `incrementMany`, `decrementMany`, `multiplyMany`, `divideMany`, `sum`, `average`, `min`, `max`.
294
+
295
+ - `incrementOne`/`decrementOne`/`multiplyOne`/`divideOne` translate directly into Prisma's native single-field atomic write — `data: { [field]: { increment: value } }` (and `decrement`/`multiply`/`divide`) — so the operation is evaluated **server-side** against the row's _current_ value (`UPDATE ... SET field = field + value`), not as a fetch-then-save round trip on the client. Prisma's `update()` already returns the row reflecting the state _after_ the write, so the adapter never issues a follow-up read.
296
+ - `incrementMany`/`decrementMany`/`multiplyMany`/`divideMany` apply that same native write to **every** record matching `where`, through Prisma's `updateMany`, also evaluated server-side. They return a `{ count }` (`CountResult`) instead of the records.
297
+ - `sum`/`average`/`min`/`max` translate into Prisma's `aggregate()` with `_sum`/`_avg`/`_min`/`_max: { [field]: true }`. The raw result (`number`, `bigint`, a `Decimal` instance, or `null`) is normalized to `number | null` — `null` is returned as-is (mirroring SQL's `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL`, not `0`, over an empty set), and a `Decimal`/`DecimalLike` value is converted via its `.toNumber()`.
298
+
299
+ ```typescript
300
+ // Atomic — evaluated server-side, no read-modify-write:
301
+ await productRepository.increment("stock", 10, { id: productId });
302
+ await accountRepository.decrement("balance", 50, { id: accountId });
303
+ await productRepository.multiply("price", 1.1, { id: productId }); // e.g. a 10% price bump
304
+ await productRepository.divide("price", 2, { id: productId });
305
+
306
+ // Same, for every record matching `where` — returns `{ count }`:
307
+ await productRepository.incrementMany("stock", 10, { category: "books" });
308
+
309
+ // Aggregation — across every record matching an optional `where` (all if omitted):
310
+ const total = await productRepository.sum("price"); // number | null
311
+ const avgPrice = await productRepository.average("price", { active: true });
312
+ const cheapest = await productRepository.min("price");
313
+ const mostExpensive = await productRepository.max("price");
314
+ ```
315
+
316
+ **Decimal fields** (`Prisma.Decimal`) are fully supported as the `field` for all of these methods — `value` for the atomic methods is passed straight through to Prisma (which accepts a `number`, `string`, or `Decimal`/`DecimalJsLike`), and the aggregate methods always normalize the result to a `number`, never a `Decimal` instance.
317
+
318
+ ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes
319
+
320
+ `createMany`, `createManyReturning`, `updateMany` and `updateManyReturning` only accept scalar fields in their `data`. If your payload includes a field configured in `relations` (regardless of its value), the adapter throws a `VSRepoAdapterError` naming the offending field. For a full nested write, use `create`/`update`/`save` one record at a time, or wrap several `save` calls in a `saveMany`/`transaction`.
321
+
322
+ > Note on return order: `createManyReturning` and `updateManyReturning` don't guarantee the returned records follow the order of the input payload (`objs`). Without `select`/`relations` they are a single `createManyAndReturn`/`updateManyAndReturn` call; with them, the result comes from a second `findMany` (re-querying the inserted/updated rows by primary key, inside a transaction). Either way, don't rely on the order matching the input.
323
+
324
+ ## Provider-specific limitations
325
+
326
+ Some features this adapter relies on aren't available on every database provider. The adapter checks every feature below upfront and throws a `VSRepoAdapterError` (code `NOT_SUPPORTED`) before touching the database.
327
+
328
+ | Feature | Supported providers | Not supported |
329
+ | ------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
330
+ | `ignoreCase` (translated to Prisma's `mode: "insensitive"`; used by `findByXIgnoreCase` dynamic methods) | PostgreSQL, CockroachDB | MySQL, SQLite, SQL Server — MySQL and SQL Server are case-insensitive by default (depending on the collation), so no `mode` is needed there; SQLite is only case-insensitive for ASCII characters |
331
+ | `createManyReturning` / `updateManyReturning` (built on Prisma's `createManyAndReturn`/`updateManyAndReturn`) | PostgreSQL, CockroachDB, SQLite | MySQL, SQL Server |
332
+ | `ignoreConflicts: true` on `createMany` / `createManyReturning` (Prisma's `skipDuplicates`) | PostgreSQL, MySQL, CockroachDB | SQL Server, SQLite |
333
+
334
+ The `like` operator isn't supported on any provider — Prisma has no equivalent, so the adapter throws `NOT_SUPPORTED`. Use `contains`, `startsWith` or `endsWith` instead.
335
+
336
+ > These limitations come from Prisma itself, not from this adapter — see [Prisma's case sensitivity docs](https://www.prisma.io/docs/orm/v7/reference/prisma-client-reference#mode) and [CRUD reference](https://www.prisma.io/docs/orm/v7/prisma-client/queries/crud) for details. The adapter is covered by integration tests on PostgreSQL, MySQL, SQLite, SQL Server and CockroachDB.
337
+
338
+ ## Transactions
339
+
340
+ **Every** method accepts `options.db` and runs its operation on the client/transaction you pass — the difference is in **how** each one treats it:
341
+
342
+ - Most methods (single, one-call Prisma operations) simply run directly on `options?.db`: hand them a transaction client and the call participates in that transaction, starting nothing new.
343
+ - `saveMany` and `deleteManyReturning` — and `updateManyReturning`/`createManyReturning` when you pass `select` or `relations` — need to run **more than one** Prisma operation atomically (`saveMany` saves each record individually; `updateManyReturning`/`createManyReturning` do an `updateManyAndReturn`/`createManyAndReturn` + `findMany`; `deleteManyReturning` does a `findMany` + `deleteMany`), so they go through `runTransactional`. Without `select`/`relations`, `updateManyReturning`/`createManyReturning` are a single Prisma call. If `options.db` is already an active transaction client, it's reused — no nested transaction is started; otherwise, a new transaction is created (on `options.db` if provided, or on the root client otherwise).
344
+
345
+ A transaction client is told apart from the root `PrismaClient` by the `$on` method: Prisma's root client exposes it (for event listeners), the interactive transaction client doesn't. This is what `Prisma7ClientLike`/`Prisma7OrmTypes` encode in their types.
346
+
347
+ ```typescript
348
+ await userRepository.transaction(async tx => {
349
+ // Every repository call made with `{ db: tx }` inside this callback
350
+ // shares the same transaction — including calls to `saveMany`/
351
+ // `updateManyReturning`/`createManyReturning`/`deleteManyReturning`, which
352
+ // will detect `tx` is already a transaction and run directly on it instead
353
+ // of nesting a new one.
354
+ await userRepository.save(user, { db: tx });
355
+ await userRepository.save(otherUsers, { db: tx }); // an array saves several records
356
+ });
357
+ ```
358
+
359
+ ### Concurrency in `deleteManyReturning`
360
+
361
+ `deleteManyReturning` runs a `findMany` on the given `where` (to capture the records it will return) and then deletes **those same records by primary key** with a `deleteMany`, all inside one transaction. Prisma has no native `deleteManyAndReturn`, which is why it takes these two steps. If your `select` leaves the primary key out, the adapter adds it to the query internally and strips it from the returned records.
362
+
363
+ Since the delete is driven by the primary keys that were actually fetched, a row **inserted after** the `findMany` is never deleted by mistake. A concurrent change between the two steps can still make them diverge, though:
364
+
365
+ - a returned row that was **deleted by someone else** in the meantime is still returned, even though this call didn't delete it;
366
+ - a returned row that **stops matching** the `where` before the `deleteMany` runs is still deleted, since the `where` isn't re-applied.
367
+
368
+ If you need to guarantee there are no concurrency issues, run this method inside a `repository.transaction()` at a higher isolation level — e.g. `SERIALIZABLE`:
369
+
370
+ ```typescript
371
+ import { VSRepoIsolationLevel } from "vsrepo";
372
+
373
+ await userRepository.transaction(async tx => {
374
+ // Atomic: no concurrent insert/update can slip between the findMany
375
+ // and the deleteMany, since SERIALIZABLE isolates this transaction.
376
+ // Always pass `{ db: tx }` so the call runs inside this transaction.
377
+ const deleted = await userRepository.deleteManyReturning({ id: { in: [...] } }, { db: tx });
378
+ }, {
379
+ isolationLevel: VSRepoIsolationLevel.SERIALIZABLE,
380
+ });
381
+ ```
382
+
383
+ > The isolation level applies only when the repository call actually runs **inside** that transaction (i.e. with `{ db: tx }`). When `deleteManyReturning` starts its own internal transaction (no `options.db` passed), it falls back to the default isolation level.
384
+
385
+ ## Logging
386
+
387
+ The adapter uses `VSLogger` (from `vsrepo`) internally: every method logs a `DEBUG` line with the resolved Prisma arg, and start/end performance logs. Set `logLevel` in the constructor config to control verbosity; it defaults to `VSLogLevel.WARN`.
388
+
389
+ ## Requirements
390
+
391
+ - `vsrepo` ^3.0.0-dev.3
392
+ - `@prisma/client` ^7.10.1
393
+ - Node.js `>=22`
394
+ - A PostgreSQL, CockroachDB, MySQL, SQLite or SQL Server database (all covered by the integration tests)