@vsrepo/prisma7-adapter 1.3.1 → 2.0.0-dev

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 (77) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/LICENSE +20 -20
  3. package/README.md +303 -299
  4. package/README.pt-BR.md +303 -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 +8 -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 +7 -45
  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 +2 -10
  14. package/dist/parsers/order-by.parser.js +6 -19
  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 -28
  18. package/dist/parsers/where.parser.js +49 -151
  19. package/dist/prisma7.adapter.d.ts +104 -81
  20. package/dist/prisma7.adapter.js +329 -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 +7 -12
  28. package/dist/types/adapter-config.type.js +1 -3
  29. package/dist/types/adapter-relations.type.d.ts +2 -2
  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-orm-types.type.d.ts +2 -2
  42. package/dist/types/prisma7-orm-types.type.js +1 -3
  43. package/dist/types/relation.type.d.ts +25 -19
  44. package/dist/types/relation.type.js +1 -3
  45. package/dist/validators/is-decimal.validator.d.ts +2 -5
  46. package/dist/validators/is-decimal.validator.js +3 -6
  47. package/dist/validators/is-plain-object.validator.d.ts +1 -1
  48. package/dist/validators/is-plain-object.validator.js +3 -7
  49. package/dist/validators/validate-prisma-client.validator.d.ts +1 -14
  50. package/dist/validators/validate-prisma-client.validator.js +7 -24
  51. package/package.json +108 -91
  52. package/dist/index.js.map +0 -1
  53. package/dist/parsers/data.parser.js.map +0 -1
  54. package/dist/parsers/include.parser.js.map +0 -1
  55. package/dist/parsers/order-by.parser.js.map +0 -1
  56. package/dist/parsers/select.parser.js.map +0 -1
  57. package/dist/parsers/where.parser.js.map +0 -1
  58. package/dist/prisma7.adapter.js.map +0 -1
  59. package/dist/resolvers/map-prisma-error.resolver.js.map +0 -1
  60. package/dist/resolvers/merge-entities.resolver.js.map +0 -1
  61. package/dist/resolvers/resolve-provider.resolver.js.map +0 -1
  62. package/dist/types/adapter-config.type.js.map +0 -1
  63. package/dist/types/adapter-relations.type.js.map +0 -1
  64. package/dist/types/plain-object.type.js.map +0 -1
  65. package/dist/types/prisma-arg-like.type.js.map +0 -1
  66. package/dist/types/prisma-client-models.type.js.map +0 -1
  67. package/dist/types/prisma-provider.type.js.map +0 -1
  68. package/dist/types/prisma-repository-like.type.js.map +0 -1
  69. package/dist/types/prisma7-client-like.type.js.map +0 -1
  70. package/dist/types/prisma7-orm-types.type.js.map +0 -1
  71. package/dist/types/relation.type.js.map +0 -1
  72. package/dist/validators/is-decimal.validator.js.map +0 -1
  73. package/dist/validators/is-plain-object.validator.js.map +0 -1
  74. package/dist/validators/validate-adapter-config.validator.d.ts +0 -8
  75. package/dist/validators/validate-adapter-config.validator.js +0 -78
  76. package/dist/validators/validate-adapter-config.validator.js.map +0 -1
  77. package/dist/validators/validate-prisma-client.validator.js.map +0 -1
package/README.md CHANGED
@@ -1,299 +1,303 @@
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
+ - [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
+ - [Atomic and aggregation methods](#atomic-and-aggregation-methods)
36
+ - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-dont-support-nested-writes)
37
+ - [Provider-specific limitations](#provider-specific-limitations)
38
+ - [Transactions](#transactions)
39
+ - [Logging](#logging)
40
+ - [Requirements](#requirements)
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ npm install vsrepo@dev @prisma/client @vsrepo/prisma7-adapter@dev
48
+ ```
49
+
50
+ While `2.0.0` is in development, install both `vsrepo` and `@vsrepo/prisma7-adapter` with the `dev` tag.
51
+
52
+ ## Basic usage
53
+
54
+ ```typescript
55
+ import { VSRepository, VSLogLevel } from "vsrepo";
56
+ import { Prisma7Adapter, Prisma7OrmTypes } from "@vsrepo/prisma7-adapter";
57
+ import { Prisma, PrismaClient } from "./generated/prisma/client";
58
+ import { prisma } from "./prisma";
59
+
60
+ type User = Prisma.UserGetPayload<{ include: { posts: true } }>;
61
+ type MyOrmTypes = Prisma7OrmTypes<PrismaClient>;
62
+
63
+ class UserRepository extends VSRepository<User, string, MyOrmTypes> {
64
+ constructor() {
65
+ super({
66
+ adapter: new Prisma7Adapter(prisma, {
67
+ modelName: "user",
68
+ pkName: "id",
69
+ relations: {
70
+ posts: { mode: "otm", restriction: "add", pk: "id" },
71
+ },
72
+ logLevel: VSLogLevel.WARN,
73
+ }),
74
+ });
75
+ }
76
+ }
77
+
78
+ const userRepository = new UserRepository();
79
+
80
+ const user = await userRepository.get("...", { relations: { posts: true } }); // by primary key
81
+ ```
82
+
83
+ 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).
84
+
85
+ `Prisma7OrmTypes<DB>` ties `VSRepository`'s `getDbClient()`/`transaction()` return types to your real, generated Prisma types — see [Transactions](#transactions).
86
+
87
+ ## Constructor config
88
+
89
+ ```typescript
90
+ new Prisma7Adapter(prisma, {
91
+ modelName: "user", // required — the Prisma Client model/delegate name, as in `prisma.user`
92
+ pkName: "id", // required — the entity's primary key field name
93
+ relations: { ... }, // optional — see "relations in the constructor (write)" below
94
+ provider: "postgresql", // optional — detected from the Prisma Client when omitted
95
+ logLevel: VSLogLevel.WARN, // optional — default: VSLogLevel.WARN
96
+ });
97
+ ```
98
+
99
+ At construction, the adapter checks the Prisma Client and the model, and resolves the datasource provider:
100
+
101
+ - a `null`/`undefined` client throws a `VSRepoAdapterError` (code `MISSING_DB_CLIENT`);
102
+ - a `modelName` with no matching delegate on the client (`prisma.<modelName>`) throws `MODEL_NOT_FOUND`;
103
+ - 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`).
104
+
105
+ The other fields (`pkName`, `relations`, `logLevel`) are checked by TypeScript only, not at runtime.
106
+
107
+ ## Relations
108
+
109
+ ### The two `relations`
110
+
111
+ The name `relations` appears in **two different places** in the API, with **different shapes and purposes** — easy to confuse. In a nutshell:
112
+
113
+ | | `relations` in the **constructor** | `relations` in **method options** |
114
+ | --- | --- | --- |
115
+ | Where you define it | `new Prisma7Adapter(prisma, { relations: ... })` | `repository.get(where, { relations: ... })` — and other methods |
116
+ | Shape | One **config object** per field: `{ mode, restriction, pk, nullable? }` | One **`true`/sub-object** per field: `{ posts: true }` |
117
+ | 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`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) | **Read** — eager loading: which relations to fetch alongside the result (becomes a Prisma `include`) |
118
+ | Consumed by | `parsePrismaWriteData` (write resolver) | `parsePrismaInclude` (via `resolveReadArg`) |
119
+ | Depends on the other? | No — it only affects writes | No — it works even with no `relations` in the constructor |
120
+
121
+ 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.
122
+
123
+ ### `relations` in the constructor (write)
124
+
125
+ 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`).
126
+
127
+ Each relation is configured by its field name:
128
+
129
+ ```typescript
130
+ relations: {
131
+ posts: { mode: "otm", restriction: "add", pk: "id" },
132
+ address: { mode: "oto", restriction: "set", pk: "id" },
133
+ author: { mode: "mto", restriction: "set", pk: "id", nullable: true },
134
+ }
135
+ ```
136
+
137
+ 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.
138
+
139
+ #### `mode`
140
+
141
+ Cardinality of the relation, from the point of view of the entity that owns the field:
142
+
143
+ | `mode` | Meaning | Field shape you send |
144
+ | --- | --- | --- |
145
+ | `oto` | one-to-one | a single object, or `null` |
146
+ | `mto` | many-to-one | a single object, or `null` |
147
+ | `otm` | one-to-many | an array of objects |
148
+ | `mtm` | many-to-many | an array of objects |
149
+
150
+ #### `restriction`
151
+
152
+ 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:
153
+
154
+ - **`"add"`** — only creates/connects/upserts the items you send. Existing items that aren't in the payload are left untouched.
155
+ - **`"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).
156
+
157
+ #### `pk` and `nullable`
158
+
159
+ - **`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`.
160
+ - **`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.
161
+
162
+ #### How each write method resolves relations
163
+
164
+ | Method | Relations |
165
+ | --- | --- |
166
+ | `create` | `create`/`connectOrCreate` only (no upsert — there's nothing to update yet) |
167
+ | `update` / `upsert` (`update` half) / `save` (upsert branch) | Full resolution: `create`/`connectOrCreate`/`upsert`/`disconnect`/`delete`/`deleteMany`/`set`, per `mode`/`restriction` |
168
+ | `createMany` / `createManyReturning` / `updateMany` / `updateManyReturning` | Not supported — throws a `VSRepoAdapterError` naming the offending field if the payload contains a configured relation |
169
+
170
+ ### `relations` in method options (read)
171
+
172
+ 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:
173
+
174
+ - `true` — load the relation as-is;
175
+ - or another `relations` object — for nested eager loading (relations of the relation).
176
+
177
+ ```typescript
178
+ const user = await userRepository.get(
179
+ "...", // primary key
180
+ {
181
+ relations: {
182
+ posts: true, // fetch posts along
183
+ author: { profile: true }, // and, inside author, its profile (nested eager loading)
184
+ },
185
+ }
186
+ );
187
+ ```
188
+
189
+ 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.
190
+
191
+ 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`):
192
+
193
+ ```ts
194
+ const prismaSelect = options.select && parsePrismaSelect(options.select);
195
+ const prismaInclude = prismaSelect
196
+ ? undefined
197
+ : options.relations && parsePrismaInclude(options.relations);
198
+ ```
199
+
200
+ ## Atomic and aggregation methods
201
+
202
+ 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`.
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
+ - `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.
206
+ - `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()`.
207
+
208
+ ```typescript
209
+ // Atomic — evaluated server-side, no read-modify-write:
210
+ await productRepository.increment("stock", 10, { id: productId });
211
+ await accountRepository.decrement("balance", 50, { id: accountId });
212
+ await productRepository.multiply("price", 1.1, { id: productId }); // e.g. a 10% price bump
213
+ await productRepository.divide("price", 2, { id: productId });
214
+
215
+ // Same, for every record matching `where` — returns `{ count }`:
216
+ await productRepository.incrementMany("stock", 10, { category: "books" });
217
+
218
+ // Aggregation — across every record matching an optional `where` (all if omitted):
219
+ const total = await productRepository.sum("price"); // number | null
220
+ const avgPrice = await productRepository.average("price", { active: true });
221
+ const cheapest = await productRepository.min("price");
222
+ const mostExpensive = await productRepository.max("price");
223
+ ```
224
+
225
+ **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.
226
+
227
+ ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes
228
+
229
+ `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`.
230
+
231
+ > 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.
232
+
233
+ ## Provider-specific limitations
234
+
235
+ 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.
236
+
237
+ | Feature | Supported providers | Not supported |
238
+ | --- | --- | --- |
239
+ | `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 |
240
+ | `createManyReturning` / `updateManyReturning` (built on Prisma's `createManyAndReturn`/`updateManyAndReturn`) | PostgreSQL, CockroachDB, SQLite | MySQL, SQL Server |
241
+ | `ignoreConflicts: true` on `createMany` / `createManyReturning` (Prisma's `skipDuplicates`) | PostgreSQL, MySQL, CockroachDB | SQL Server, SQLite |
242
+
243
+ 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.
244
+
245
+ > 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.
246
+
247
+ ## Transactions
248
+
249
+ **Every** method accepts `options.db` and runs its operation on the client/transaction you pass — the difference is in **how** each one treats it:
250
+
251
+ - 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.
252
+ - `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).
253
+
254
+ 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.
255
+
256
+ ```typescript
257
+ await userRepository.transaction(async tx => {
258
+ // Every repository call made with `{ db: tx }` inside this callback
259
+ // shares the same transaction — including calls to `saveMany`/
260
+ // `updateManyReturning`/`createManyReturning`/`deleteManyReturning`, which
261
+ // will detect `tx` is already a transaction and run directly on it instead
262
+ // of nesting a new one.
263
+ await userRepository.save(user, { db: tx });
264
+ await userRepository.save(otherUsers, { db: tx }); // an array saves several records
265
+ });
266
+ ```
267
+
268
+ ### Concurrency in `deleteManyReturning`
269
+
270
+ `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.
271
+
272
+ 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:
273
+
274
+ - a returned row that was **deleted by someone else** in the meantime is still returned, even though this call didn't delete it;
275
+ - a returned row that **stops matching** the `where` before the `deleteMany` runs is still deleted, since the `where` isn't re-applied.
276
+
277
+ 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`:
278
+
279
+ ```typescript
280
+ import { VSRepoIsolationLevel } from "vsrepo";
281
+
282
+ await userRepository.transaction(async tx => {
283
+ // Atomic: no concurrent insert/update can slip between the findMany
284
+ // and the deleteMany, since SERIALIZABLE isolates this transaction.
285
+ // Always pass `{ db: tx }` so the call runs inside this transaction.
286
+ const deleted = await userRepository.deleteManyReturning({ id: { in: [...] } }, { db: tx });
287
+ }, {
288
+ isolationLevel: VSRepoIsolationLevel.SERIALIZABLE,
289
+ });
290
+ ```
291
+
292
+ > 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.
293
+
294
+ ## Logging
295
+
296
+ 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`.
297
+
298
+ ## Requirements
299
+
300
+ - `vsrepo` ^3.0.0-dev.2
301
+ - `@prisma/client` ^7.10.0
302
+ - Node.js `>=22`
303
+ - A PostgreSQL, CockroachDB, MySQL, SQLite or SQL Server database (all covered by the integration tests)