@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.pt-BR.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
- 🇺🇸 [Read in English](./README.md)
15
-
16
- > ✅ **Lançado.** Este adapter já foi publicado no npm como `@vsrepo/prisma7-adapter`, feito para o [VSRepository](https://github.com/jaobrabo123/VSRepository). Por enquanto é o único adapter oficial publicado para a v2 — adapters para outros ORMs estão planejados, mas ainda não foram publicados (veja [Status dos adapters](https://github.com/jaobrabo123/VSRepository/blob/main/README.pt-BR.md#status-dos-adapters) no README do VSRepository).
17
-
18
- Implementação de `VSRepoAdapter` para o [VSRepository v2](https://github.com/jaobrabo123/VSRepository) usando [Prisma 7](https://www.prisma.io/). Traduz toda operação do `VSRepository` em chamadas do Prisma Client, resolvendo `VSRepoWhere`, `Ordering`, `select`/`relations` através de parsers dedicados e — quando uma config de `relations` é fornecida — resolvendo campos de relação em `create`/`update`/`upsert`/`merge` da mesma forma que a v1 do VSRepository fazia.
19
-
20
- ---
21
-
22
- ## Sumário
23
-
24
- - [Instalação](#instalação)
25
- - [Uso básico](#uso-básico)
26
- - [Config do construtor](#config-do-construtor)
27
- - [Relations](#relations)
28
- - [Os dois `relations`](#os-dois-relations)
29
- - [`relations` no construtor (escrita)](#relations-no-construtor-escrita)
30
- - [`mode`](#mode)
31
- - [`restriction`](#restriction)
32
- - [`pk` e `nullable`](#pk-e-nullable)
33
- - [Como cada método de escrita resolve relations](#como-cada-método-de-escrita-resolve-relations)
34
- - [`relations` nas options (leitura)](#relations-nas-options-leitura)
35
- - [`merge`](#merge)
36
- - [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação)
37
- - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-não-suportam-nested-writes)
38
- - [Limitações por provider](#limitações-por-provider)
39
- - [Transactions](#transactions)
40
- - [Logging](#logging)
41
- - [Requisitos](#requisitos)
42
-
43
- ---
44
-
45
- ## Instalação
46
-
47
- ```bash
48
- npm install vsrepo @prisma/client @vsrepo/prisma7-adapter
49
- ```
50
-
51
- Tanto o `vsrepo` quanto o `@vsrepo/prisma7-adapter` já foram publicados no npm e estão prontos para uso.
52
-
53
- ## Uso básico
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
- Aqui, o `relations` passado no `options` do método — com a forma `{ campo: true }` — é transformado num `include` do Prisma pelo adapter (ver [`relations` nas options (leitura)](#relations-nas-options-leitura)). Se você fornecer `select`, o `relations`/`include` é ignorado. Não confunda com o `relations` da config do construtor (que descreve como campos de relação são resolvidos em escritas) — a diferença é explicada em [Os dois `relations`](#os-dois-relations).
85
-
86
- `Prisma7OrmTypes<DB>` amarra os tipos de retorno de `getDbClient()`/`transaction()` do `VSRepository` aos seus tipos reais e gerados do Prisma — ver [Transactions](#transactions).
87
-
88
- ## Config do construtor
89
-
90
- ```typescript
91
- new Prisma7Adapter(prisma, {
92
- tableName: "user", // obrigatório — nome do model/delegate do Prisma Client, como em `prisma.user`
93
- pkName: "id", // obrigatório — nome do campo de primary key da entidade
94
- relations: { ... }, // opcional — ver "relations no construtor (escrita)" abaixo
95
- logLevel: VSLogLevel.WARN, // opcional — default: VSLogLevel.WARN
96
- });
97
- ```
98
-
99
- A config é validada na construção — um `tableName`/`pkName`/`relations`/`logLevel` inválido lança um `VSRepoAdapterError` apontando o campo problemático.
100
-
101
- ## Relations
102
-
103
- ### Os dois `relations`
104
-
105
- O nome `relations` aparece em **dois lugares diferentes** da API, com **formas e propósitos diferentes** — fácil de confundir. Em resumo:
106
-
107
- | | `relations` no **construtor** | `relations` nas **options** |
108
- | --- | --- | --- |
109
- | Onde você define | `new Prisma7Adapter(prisma, { relations: ... })` | `repository.get(where, { relations: ... })` — e demais métodos |
110
- | Formato | Um objeto de **configuração** por campo: `{ mode, restriction, pk, nullable? }` | Um objeto por campo **só com `true` ou sub-objeto**: `{ posts: true }` |
111
- | Propósito | **Escrita** — quando um payload de `create`/`update`/`upsert`/`save`/`merge` tem campo de relação, diz como transformá-lo num nested write do Prisma (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) | **Leitura** — eager loading: quais relations trazer junto no resultado (vira um `include` do Prisma) |
112
- | Quem consome | `parsePrismaWriteData` / `mergeEntities` (resolvers de escrita) | `parsePrismaInclude` (via `resolveReadArg`) |
113
- | Precisa da outra? | Não — só afeta escritas/`merge` | Não — funciona mesmo sem `relations` no construtor |
114
-
115
- Nenhum depende do outro: o `relations` das options faz eager loading de leitura mesmo quando o construtor não tem `relations`; e o `relations` do construtor governa o comportamento de escrita mesmo que você nunca passe `relations` nas options. As duas subseções abaixo detalham cada um.
116
-
117
- ### `relations` no construtor (escrita)
118
-
119
- A config `relations` do construtor descreve, para cada campo de relação da entidade, **como o adapter deve resolver esse campo quando ele aparecer nos payloads de escrita** (`create`/`update`/`upsert`/`save`/`merge`) — o mesmo comportamento que a v1 do VSRepository tinha.
120
-
121
- Cada relation é configurada pelo nome do campo:
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
- Sem `relations`, todo campo — incluindo campos de relação — é repassado direto pro `where`/`data`/`create`/`update` do Prisma, como está. Isso funciona bem pra campos escalares, mas o Prisma espera um formato bem específico de nested write (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) pra campos de relação — então, se sua entidade tem relations, normalmente você vai querer configurá-las.
132
-
133
- #### `mode`
134
-
135
- Cardinalidade da relation, do ponto de vista da entidade dona do campo:
136
-
137
- | `mode` | Significado | Formato do campo que você envia |
138
- | --- | --- | --- |
139
- | `oto` | one-to-one | um único objeto, ou `null` |
140
- | `mto` | many-to-one | um único objeto, ou `null` |
141
- | `otm` | one-to-many | um array de objetos |
142
- | `mtm` | many-to-many | um array de objetos |
143
-
144
- #### `restriction`
145
-
146
- Controla como `save`/`update`/`upsert` tratam itens de relação que já existem (casados pela `pk`) e, pra relations to-many, itens que **não** vieram no payload:
147
-
148
- - **`"add"`** — só cria/conecta/faz upsert dos itens enviados. Itens já existentes que não estão no payload permanecem intocados.
149
- - **`"set"`** — igual a `"add"`, mas também remove o que não foi enviado: em `otm` roda um `deleteMany` nos itens que faltam no array; em `mtm` reseta o vínculo (`set: []`) antes de reconectar; em `oto`/`mto` com `nullable: true`, enviar `null` apaga/desconecta a relation (ver abaixo).
150
-
151
- #### `pk` e `nullable`
152
-
153
- - **`pk`** — o campo usado pra identificar um registro relacionado já existente. Um item **com** `pk` no payload vira um `connectOrCreate`/`upsert`; um item **sem** `pk` vira um `create` simples.
154
- - **`nullable`** — relevante pras relações to-one `oto`/`mto`. Quando `true`, enviar `null` no campo resolve pra `disconnect` no update (`delete` no caso de `oto` com `restriction: "set"` — um lado `oto` não dá só pra "desconectar", a linha possuída é removida). Quando omitido/`false`, enviar `null` numa relação to-one lança um `VSRepoAdapterError` (code `INVALID_DATA`) avisando que a relation não é nullable.
155
-
156
- #### Como cada método de escrita resolve relations
157
-
158
- | Método | Relations |
159
- | --- | --- |
160
- | `create` | Só `create`/`connectOrCreate` (sem upsert — ainda não existe nada pra atualizar) |
161
- | `update` / `upsert` (metade do `update`) / `save` (branch de upsert) | Resolução completa: `create`/`connectOrCreate`/`upsert`/`disconnect`/`delete`/`deleteMany`/`set`, conforme `mode`/`restriction` |
162
- | `createMany` / `createManyReturning` / `updateMany` / `updateManyReturning` | Não suportado — lança um `VSRepoAdapterError` apontando o campo problemático se o payload tiver uma relation configurada |
163
-
164
- ### `relations` nas options (leitura)
165
-
166
- Esse é o `relations` que você passa no `options` de um método do `VSRepository` (`get`, `find`, `findOne`, etc.). A forma é bem mais simples: um objeto onde cada campo de relação aceita:
167
-
168
- - `true` — carrega a relation completa;
169
- - ou outro objeto `relations` — para eager loading aninhado (relations da relation).
170
-
171
- ```typescript
172
- const user = await userRepository.get(
173
- { id: "..." },
174
- {
175
- relations: {
176
- posts: true, // carrega os posts junto
177
- author: { profile: true }, // e, dentro do author, o profile (eager loading aninhado)
178
- },
179
- }
180
- );
181
- ```
182
-
183
- Esse objeto é transformado num `include` do Prisma pelo adapter (`parsePrismaInclude`). Ele **não** usa a config `relations` do construtor: é puramente uma opção de leitura e funciona mesmo sem `relations` na config.
184
-
185
- O `select` e o `relations` que você passa nas options são transformados num `select` do Prisma (`parsePrismaSelect`) e num `include` (`parsePrismaInclude`), respectivamente. Quando o `select` é fornecido, o `include` derivado do `relations` é descartado (fica `undefined`), porque o Prisma não permite combinar `include` e `select` na mesma query. Ou seja, se você já estiver usando `select`, o objeto `relations` vira redundante — por isso ele é facultativo. Essa é a lógica resolvida (de `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)` busca o registro que casa com `where` e devolve ele **deep-merged, em memória**, com `obj` — ele **não** escreve nada no banco. Isso reflete exatamente como o `merge` funcionava na v1 do VSRepository: ele serve pra montar uma entidade completa e mesclada, que você depois passa pro `save`/`update`, e não pra persistir um update parcial diretamente.
197
-
198
- Pra relations to-many (`otm`/`mtm`), os itens do registro salvo e os itens de `obj` são casados pela `pk` configurada: um match faz merge dos dois itens, uma `pk` nova (ou sem `pk`) é apenas adicionada. `merge` nunca remove nada.
199
-
200
- ## Métodos atômicos e de agregação
201
-
202
- O adapter implementa os 8 métodos abstratos pros quais `increment`/`decrement`/`multiply`/`divide`/`sum`/`average`/`min`/`max` do `VSRepository` delegam: `incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max`.
203
-
204
- - `incrementOne`/`decrementOne`/`multiplyOne`/`divideOne` traduzem direto pra escrita atômica nativa de campo único do Prisma — `data: { [field]: { increment: value } }` (e `decrement`/`multiply`/`divide`) — então a operação é avaliada **server-side** contra o valor *atual* do registro (`UPDATE ... SET field = field + value`), e não como um fetch-then-save no cliente. O `update()` do Prisma já retorna a linha refletindo o estado *após* a escrita, então o adapter nunca precisa fazer uma leitura extra.
205
- - `sum`/`average`/`min`/`max` traduzem pro `aggregate()` do Prisma, com `_sum`/`_avg`/`_min`/`_max: { [field]: true }`. O resultado bruto (`number`, `bigint`, uma instância de `Decimal`, ou `null`) é normalizado pra `number | null` — `null` é repassado como está (espelhando o `SUM()`/`AVG()`/`MIN()`/`MAX()` do SQL, que retornam `NULL`, não `0`, sobre um conjunto vazio), e um valor `Decimal`/`DecimalLike` é convertido via seu `.toNumber()`.
206
-
207
- ```typescript
208
- // Atômico — avaliado server-side, sem 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); // ex: um reajuste de 10% no preço
212
- await productRepository.divide(productId, "price", 2);
213
-
214
- // Agregação — entre todos os registros que casam com um `where` opcional (todos se omitido):
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
- **Campos Decimal** (`Prisma.Decimal`) são totalmente suportados como o `field` dos 8 métodos — o `value` dos métodos atômicos é repassado direto pro Prisma (que aceita `number`, `string`, ou `Decimal`/`DecimalJsLike`), e os métodos de agregação sempre normalizam o resultado pra `number`, nunca uma instância de `Decimal`.
222
-
223
- ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes
224
-
225
- `createMany`, `createManyReturning`, `updateMany` e `updateManyReturning` só aceitam campos escalares no `data`. Se seu payload incluir um campo configurado em `relations` (independente do valor), o adapter lança um `VSRepoAdapterError` apontando o campo problemático. Pra um nested write completo, use `create`/`update`/`save` registro por registro, ou envolva várias chamadas de `save` num `saveMany`/`transaction`.
226
-
227
- > Nota sobre a ordem de retorno: `createManyReturning` não garante que os registros devolvidos seguem a ordem do payload de entrada (`objs`). O resultado vem de um segundo `findMany` (re-buscando as linhas inseridas/atualizadas pela primary key), então a ordem só é garantida quando você passa `order` nas options.
228
-
229
- ## Limitações por provider
230
-
231
- Alguns recursos do Prisma que esse adapter usa não estão disponíveis em todo banco. Se você usar um desses recursos num provider sem suporte, o próprio Prisma lança um erro de validação (ou, em versões futuras, o adapter pode validar isso antecipadamente — ver abaixo).
232
-
233
- | Recurso | Providers com suporte | Sem suporte |
234
- | --- | --- | --- |
235
- | `mode: "insensitive"` (usado pelos métodos dinâmicos `findByXIgnoreCase`) | PostgreSQL, MongoDB | MySQL, SQLite, SQL Server, CockroachDB — MySQL e SQL Server já são case-insensitive por padrão, então não precisam de `mode`; SQLite só é case-insensitive pra caracteres ASCII |
236
- | `createManyReturning` / `updateManyReturning` (baseados no `createManyAndReturn`/`updateManyAndReturn` do Prisma) | PostgreSQL, CockroachDB, SQLite | MySQL, SQL Server, MongoDB |
237
- | `skipDuplicates` no `createManyIgnoreConflicts` / `createManyReturningIgnoreConflicts` | PostgreSQL, MySQL, CockroachDB | MongoDB, SQL Server, SQLite |
238
-
239
- > Essas limitações vêm do próprio Prisma, não do adapter — veja a [doc de case sensitivity](https://www.prisma.io/docs/orm/v7/reference/prisma-client-reference#mode) e a [referência de CRUD](https://www.prisma.io/docs/orm/v7/prisma-client/queries/crud) do Prisma pra mais detalhes.
240
-
241
- ## Transactions
242
-
243
- **Todos** os métodos aceitam `options.db` e executam a operação no client/transaction passado — a diferença está em **como** cada um o trata:
244
-
245
- - A maioria (operações de uma única chamada Prisma) roda direto em `options?.db`: você passa um transaction client e a chamada participa da transaction, sem iniciar nada novo.
246
- - `saveMany`, `updateManyReturning`, `createManyReturning` e `deleteManyReturning` precisam rodar **mais de uma operação** do Prisma atomicamente (`saveMany` salva cada registro individualmente; `updateManyReturning`/`createManyReturning` fazem um `updateManyAndReturn`/`createManyAndReturn` + `findMany`; `deleteManyReturning` faz um `findMany` + `deleteMany`), então passam por `runTransactional`. Se `options.db` já for um transaction client ativo, ele é reaproveitado — nenhuma transaction aninhada é criada; caso contrário, uma nova transaction é criada (em `options.db`, se fornecido, ou no client raiz caso contrário).
247
-
248
- Um transaction client é diferenciado do `PrismaClient` raiz pelo método `$on`: o client raiz do Prisma o expõe (pra event listeners), o transaction client interativo não. É isso que `Prisma7ClientLike`/`Prisma7OrmTypes` codificam nos seus tipos.
249
-
250
- ```typescript
251
- await userRepository.transaction(async tx => {
252
- // Toda chamada de repository feita com `{ db: tx }` dentro desse
253
- // callback compartilha a mesma transaction — incluindo chamadas a
254
- // `saveMany`/`updateManyReturning`/`createManyReturning`/`deleteManyReturning`,
255
- // que vão detectar que `tx` já é uma transaction e rodar direto nele, sem
256
- // aninhar uma nova.
257
- await userRepository.save(user, { db: tx });
258
- await userRepository.saveList(outrosUsuarios, { db: tx });
259
- });
260
- ```
261
-
262
- ### Concorrência em `deleteManyReturning`
263
-
264
- O `deleteManyReturning` roda um `findMany` no `where` informado (pra capturar os registros que vai devolver) e depois **re-aplica o mesmo `where`** num `deleteMany`. Como o Prisma não tem um `deleteManyAndReturn` nativo, o delete é guiado pelo `where` — e não pelos registros retornados no `findMany`.
265
-
266
- Por causa desse formato em duas etapas, uma alteração concorrente entre o `findMany` e o `deleteMany` pode fazer os dois divergirem:
267
-
268
- - uma linha **inserida depois** do `findMany` que bate com o `where` ainda será deletada, mesmo não tendo sido retornada;
269
- - uma linha que **deixa de bater** com o `where` antes do `deleteMany` rodar não será deletada, mesmo tendo sido retornada.
270
-
271
- Ou seja: os registros retornados e as linhas realmente deletadas não têm garantia de serem idênticos sob concorrência.
272
-
273
- Se você quiser garantir que não haverá problemas de concorrência, rode esse método **dentro** de um `repository.transaction()` num nível de isolamento mais alto — ex.: `SERIALIZABLE`:
274
-
275
- ```typescript
276
- import { VSRepository, TransactionIsolationLevel } from "vsrepo";
277
-
278
- await userRepository.transaction(async tx => {
279
- // Atômico: nenhum insert/update concorrente consegue entrar entre o
280
- // findMany e o deleteMany, pois o SERIALIZABLE isola esta transaction.
281
- // O `deleteManyReturning` é acessado através do método dinâmico que você
282
- // mapeou pro adapter (ex.: um `@DynamicMethod()` expondo `deleteManyReturning`),
283
- // sempre passando `{ db: tx }` pra rodar dentro desta transaction.
284
- const deleted = await userRepository.deleteManyReturningByIdIn([...], { db: tx });
285
- }, {
286
- isolationLevel: TransactionIsolationLevel.SERIALIZABLE,
287
- });
288
- ```
289
-
290
- > O nível de isolamento só se aplica quando a chamada roda **dentro** daquela transaction (ou seja, com `{ db: tx }`). Quando o `deleteManyReturning` inicia a própria transaction interna (sem `options.db`), ele usa o nível de isolamento padrão.
291
-
292
- ## Logging
293
-
294
- O adapter usa o `VSLogger` (de `vsrepo`) internamente: todo método loga uma linha `DEBUG` com o arg resolvido do Prisma, além de logs de performance de início/fim (com `WARN` pra operações lentas). Configure `logLevel` na config do construtor pra controlar a verbosidade; o default é `VSLogLevel.WARN`.
295
-
296
- ## Requisitos
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
+ 🇺🇸 [Read in English](./README.md)
15
+
16
+ > 🚧 **Em desenvolvimento.** Esta é a `v2` do `@vsrepo/prisma7-adapter`, feita para o [VSRepository v3](https://github.com/VSRepository/VSRepository). É uma versão de desenvolvimento, ainda não uma release estável, então a API pode mudar. Para o VSRepository v2, use a linha 1.x (última: `1.3.2`). Por enquanto é o único adapter oficialmente suportado — adapters para outros ORMs estão planejados, mas ainda não foram publicados (veja [Status dos adapters](https://github.com/VSRepository/VSRepository/blob/main/README.pt-BR.md#status-dos-adapters) no README do VSRepository).
17
+
18
+ Implementação de `VSRepoAdapter` para o [VSRepository v3](https://github.com/VSRepository/VSRepository) usando [Prisma 7](https://www.prisma.io/). Traduz toda operação do `VSRepository` em chamadas do Prisma Client, resolvendo `VSRepoWhere`, `Ordering`, `select`/`relations` através de parsers dedicados e — quando uma config de `relations` é fornecida — resolvendo campos de relação em `create`/`update`/`upsert`/`save`.
19
+
20
+ ---
21
+
22
+ ## Sumário
23
+
24
+ - [Instalação](#instalação)
25
+ - [Uso básico](#uso-básico)
26
+ - [Config do construtor](#config-do-construtor)
27
+ - [Relations](#relations)
28
+ - [Os dois `relations`](#os-dois-relations)
29
+ - [`relations` no construtor (escrita)](#relations-no-construtor-escrita)
30
+ - [`mode`](#mode)
31
+ - [`restriction`](#restriction)
32
+ - [`pk` e `nullable`](#pk-e-nullable)
33
+ - [Como cada método de escrita resolve relations](#como-cada-método-de-escrita-resolve-relations)
34
+ - [`relations` nas options (leitura)](#relations-nas-options-leitura)
35
+ - [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação)
36
+ - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-não-suportam-nested-writes)
37
+ - [Limitações por provider](#limitações-por-provider)
38
+ - [Transactions](#transactions)
39
+ - [Logging](#logging)
40
+ - [Requisitos](#requisitos)
41
+
42
+ ---
43
+
44
+ ## Instalação
45
+
46
+ ```bash
47
+ npm install vsrepo@dev @prisma/client @vsrepo/prisma7-adapter@dev
48
+ ```
49
+
50
+ Enquanto a `2.0.0` está em desenvolvimento, instale o `vsrepo` e o `@vsrepo/prisma7-adapter` com a tag `dev`.
51
+
52
+ ## Uso básico
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 } }); // pela primary key
81
+ ```
82
+
83
+ Aqui, o `relations` passado no `options` do método — com a forma `{ campo: true }` — é transformado num `include` do Prisma pelo adapter (ver [`relations` nas options (leitura)](#relations-nas-options-leitura)). Se você fornecer `select`, o `relations`/`include` é ignorado. Não confunda com o `relations` da config do construtor (que descreve como campos de relação são resolvidos em escritas) — a diferença é explicada em [Os dois `relations`](#os-dois-relations).
84
+
85
+ `Prisma7OrmTypes<DB>` amarra os tipos de retorno de `getDbClient()`/`transaction()` do `VSRepository` aos seus tipos reais e gerados do Prisma — ver [Transactions](#transactions).
86
+
87
+ ## Config do construtor
88
+
89
+ ```typescript
90
+ new Prisma7Adapter(prisma, {
91
+ modelName: "user", // obrigatório — nome do model/delegate do Prisma Client, como em `prisma.user`
92
+ pkName: "id", // obrigatório — nome do campo de primary key da entidade
93
+ relations: { ... }, // opcional — ver "relations no construtor (escrita)" abaixo
94
+ provider: "postgresql", // opcional — detectado a partir do Prisma Client quando omitido
95
+ logLevel: VSLogLevel.WARN, // opcional — default: VSLogLevel.WARN
96
+ });
97
+ ```
98
+
99
+ Na construção, o adapter confere o Prisma Client e o model, e resolve o provider do datasource:
100
+
101
+ - um client `null`/`undefined` lança um `VSRepoAdapterError` (code `MISSING_DB_CLIENT`);
102
+ - um `modelName` sem delegate correspondente no client (`prisma.<modelName>`) lança `MODEL_NOT_FOUND`;
103
+ - o provider vem do `provider`, quando informado; senão é detectado a partir do client gerado (`_engineConfig.activeProvider`, com fallback pro `provider` do driver adapter). Se não for detectável, a construção lança `NOT_SUPPORTED` — nesse caso, informe o `provider` explicitamente (um de `postgresql`, `cockroachdb`, `mysql`, `sqlite`, `sqlserver`).
104
+
105
+ Os outros campos (`pkName`, `relations`, `logLevel`) são checados só pelo TypeScript, não em runtime.
106
+
107
+ ## Relations
108
+
109
+ ### Os dois `relations`
110
+
111
+ O nome `relations` aparece em **dois lugares diferentes** da API, com **formas e propósitos diferentes** — fácil de confundir. Em resumo:
112
+
113
+ | | `relations` no **construtor** | `relations` nas **options** |
114
+ | --- | --- | --- |
115
+ | Onde você define | `new Prisma7Adapter(prisma, { relations: ... })` | `repository.get(where, { relations: ... })` — e demais métodos |
116
+ | Formato | Um objeto de **configuração** por campo: `{ mode, restriction, pk, nullable? }` | Um objeto por campo **só com `true` ou sub-objeto**: `{ posts: true }` |
117
+ | Propósito | **Escrita** — quando um payload de `create`/`update`/`upsert`/`save` tem campo de relação, diz como transformá-lo num nested write do Prisma (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) | **Leitura** — eager loading: quais relations trazer junto no resultado (vira um `include` do Prisma) |
118
+ | Quem consome | `parsePrismaWriteData` (resolver de escrita) | `parsePrismaInclude` (via `resolveReadArg`) |
119
+ | Precisa da outra? | Não — só afeta escritas | Não — funciona mesmo sem `relations` no construtor |
120
+
121
+ Nenhum depende do outro: o `relations` das options faz eager loading de leitura mesmo quando o construtor não tem `relations`; e o `relations` do construtor governa o comportamento de escrita mesmo que você nunca passe `relations` nas options. As duas subseções abaixo detalham cada um.
122
+
123
+ ### `relations` no construtor (escrita)
124
+
125
+ A config `relations` do construtor descreve, para cada campo de relação da entidade, **como o adapter deve resolver esse campo quando ele aparecer nos payloads de escrita** (`create`/`update`/`upsert`/`save`).
126
+
127
+ Cada relation é configurada pelo nome do campo:
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
+ Sem `relations`, todo campo — incluindo campos de relação — é repassado direto pro `where`/`data`/`create`/`update` do Prisma, como está. Isso funciona bem pra campos escalares, mas o Prisma espera um formato bem específico de nested write (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) pra campos de relação — então, se sua entidade tem relations, normalmente você vai querer configurá-las.
138
+
139
+ #### `mode`
140
+
141
+ Cardinalidade da relation, do ponto de vista da entidade dona do campo:
142
+
143
+ | `mode` | Significado | Formato do campo que você envia |
144
+ | --- | --- | --- |
145
+ | `oto` | one-to-one | um único objeto, ou `null` |
146
+ | `mto` | many-to-one | um único objeto, ou `null` |
147
+ | `otm` | one-to-many | um array de objetos |
148
+ | `mtm` | many-to-many | um array de objetos |
149
+
150
+ #### `restriction`
151
+
152
+ Controla como `save`/`update`/`upsert` tratam itens de relação que já existem (casados pela `pk`) e, pra relations to-many, itens que **não** vieram no payload:
153
+
154
+ - **`"add"`** — só cria/conecta/faz upsert dos itens enviados. Itens já existentes que não estão no payload permanecem intocados.
155
+ - **`"set"`** — igual a `"add"`, mas também remove o que não foi enviado: em `otm` roda um `deleteMany` nos itens que faltam no array; em `mtm` reseta o vínculo (`set: []`) antes de reconectar; em `oto`/`mto` com `nullable: true`, enviar `null` apaga/desconecta a relation (ver abaixo).
156
+
157
+ #### `pk` e `nullable`
158
+
159
+ - **`pk`** — o campo usado pra identificar um registro relacionado já existente. Um item **com** `pk` no payload vira um `connectOrCreate`/`upsert`; um item **sem** `pk` vira um `create` simples.
160
+ - **`nullable`** — relevante pras relações to-one `oto`/`mto`. Quando `true`, enviar `null` no campo resolve pra `disconnect` no update (`delete` no caso de `oto` com `restriction: "set"` — um lado `oto` não dá só pra "desconectar", a linha possuída é removida). Quando omitido/`false`, enviar `null` numa relação to-one lança um `VSRepoAdapterError` (code `INVALID_DATA`) avisando que a relation não é nullable.
161
+
162
+ #### Como cada método de escrita resolve relations
163
+
164
+ | Método | Relations |
165
+ | --- | --- |
166
+ | `create` | Só `create`/`connectOrCreate` (sem upsert — ainda não existe nada pra atualizar) |
167
+ | `update` / `upsert` (metade do `update`) / `save` (branch de upsert) | Resolução completa: `create`/`connectOrCreate`/`upsert`/`disconnect`/`delete`/`deleteMany`/`set`, conforme `mode`/`restriction` |
168
+ | `createMany` / `createManyReturning` / `updateMany` / `updateManyReturning` | Não suportado — lança um `VSRepoAdapterError` apontando o campo problemático se o payload tiver uma relation configurada |
169
+
170
+ ### `relations` nas options (leitura)
171
+
172
+ Esse é o `relations` que você passa no `options` de um método do `VSRepository` (`get`, `find`, `findOne`, etc.). A forma é bem mais simples: um objeto onde cada campo de relação aceita:
173
+
174
+ - `true` — carrega a relation completa;
175
+ - ou outro objeto `relations` — para eager loading aninhado (relations da relation).
176
+
177
+ ```typescript
178
+ const user = await userRepository.get(
179
+ "...", // primary key
180
+ {
181
+ relations: {
182
+ posts: true, // carrega os posts junto
183
+ author: { profile: true }, // e, dentro do author, o profile (eager loading aninhado)
184
+ },
185
+ }
186
+ );
187
+ ```
188
+
189
+ Esse objeto é transformado num `include` do Prisma pelo adapter (`parsePrismaInclude`). Ele **não** usa a config `relations` do construtor: é puramente uma opção de leitura e funciona mesmo sem `relations` na config.
190
+
191
+ O `select` e o `relations` que você passa nas options são transformados num `select` do Prisma (`parsePrismaSelect`) e num `include` (`parsePrismaInclude`), respectivamente. Quando o `select` é fornecido, o `include` derivado do `relations` é descartado (fica `undefined`), porque o Prisma não permite combinar `include` e `select` na mesma query. Ou seja, se você já estiver usando `select`, o objeto `relations` vira redundante — por isso ele é facultativo. Essa é a lógica resolvida (de `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
+ ## Métodos atômicos e de agregação
201
+
202
+ O adapter implementa os 12 métodos abstratos pros quais `increment`/`decrement`/`multiply`/`divide`, suas versões `*Many` e `sum`/`average`/`min`/`max` do `VSRepository` delegam: `incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `incrementMany`, `decrementMany`, `multiplyMany`, `divideMany`, `sum`, `average`, `min`, `max`.
203
+
204
+ - `incrementOne`/`decrementOne`/`multiplyOne`/`divideOne` traduzem direto pra escrita atômica nativa de campo único do Prisma — `data: { [field]: { increment: value } }` (e `decrement`/`multiply`/`divide`) — então a operação é avaliada **server-side** contra o valor *atual* do registro (`UPDATE ... SET field = field + value`), e não como um fetch-then-save no cliente. O `update()` do Prisma já retorna a linha refletindo o estado *após* a escrita, então o adapter nunca precisa fazer uma leitura extra.
205
+ - `incrementMany`/`decrementMany`/`multiplyMany`/`divideMany` aplicam essa mesma escrita nativa em **todo** registro que casa com `where`, através do `updateMany` do Prisma, também avaliada server-side. Eles retornam um `{ count }` (`CountResult`) em vez dos registros.
206
+ - `sum`/`average`/`min`/`max` traduzem pro `aggregate()` do Prisma, com `_sum`/`_avg`/`_min`/`_max: { [field]: true }`. O resultado bruto (`number`, `bigint`, uma instância de `Decimal`, ou `null`) é normalizado pra `number | null` — `null` é repassado como está (espelhando o `SUM()`/`AVG()`/`MIN()`/`MAX()` do SQL, que retornam `NULL`, não `0`, sobre um conjunto vazio), e um valor `Decimal`/`DecimalLike` é convertido via seu `.toNumber()`.
207
+
208
+ ```typescript
209
+ // Atômico — avaliado server-side, sem 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 }); // ex: um reajuste de 10% no preço
213
+ await productRepository.divide("price", 2, { id: productId });
214
+
215
+ // O mesmo, pra todo registro que casa com `where` — retorna `{ count }`:
216
+ await productRepository.incrementMany("stock", 10, { category: "books" });
217
+
218
+ // Agregação — entre todos os registros que casam com um `where` opcional (todos se omitido):
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
+ **Campos Decimal** (`Prisma.Decimal`) são totalmente suportados como o `field` de todos esses métodos — o `value` dos métodos atômicos é repassado direto pro Prisma (que aceita `number`, `string`, ou `Decimal`/`DecimalJsLike`), e os métodos de agregação sempre normalizam o resultado pra `number`, nunca uma instância de `Decimal`.
226
+
227
+ ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes
228
+
229
+ `createMany`, `createManyReturning`, `updateMany` e `updateManyReturning` só aceitam campos escalares no `data`. Se seu payload incluir um campo configurado em `relations` (independente do valor), o adapter lança um `VSRepoAdapterError` apontando o campo problemático. Pra um nested write completo, use `create`/`update`/`save` registro por registro, ou envolva várias chamadas de `save` num `saveMany`/`transaction`.
230
+
231
+ > Nota sobre a ordem de retorno: `createManyReturning` e `updateManyReturning` não garantem que os registros devolvidos seguem a ordem do payload de entrada (`objs`). Sem `select`/`relations` eles são uma única chamada `createManyAndReturn`/`updateManyAndReturn`; com eles, o resultado vem de um segundo `findMany` (re-buscando as linhas inseridas/atualizadas pela primary key, dentro de uma transaction). Em qualquer caso, não conte com a ordem batendo com a da entrada.
232
+
233
+ ## Limitações por provider
234
+
235
+ Alguns recursos que esse adapter usa não estão disponíveis em todo banco. O adapter checa todos os recursos abaixo antecipadamente e lança um `VSRepoAdapterError` (code `NOT_SUPPORTED`) antes de tocar no banco.
236
+
237
+ | Recurso | Providers com suporte | Sem suporte |
238
+ | --- | --- | --- |
239
+ | `ignoreCase` (traduzido pro `mode: "insensitive"` do Prisma; usado pelos métodos dinâmicos `findByXIgnoreCase`) | PostgreSQL, CockroachDB | MySQL, SQLite, SQL Server — MySQL e SQL Server já são case-insensitive por padrão (conforme a collation), então não precisam de `mode`; SQLite só é case-insensitive pra caracteres ASCII |
240
+ | `createManyReturning` / `updateManyReturning` (baseados no `createManyAndReturn`/`updateManyAndReturn` do Prisma) | PostgreSQL, CockroachDB, SQLite | MySQL, SQL Server |
241
+ | `ignoreConflicts: true` no `createMany` / `createManyReturning` (o `skipDuplicates` do Prisma) | PostgreSQL, MySQL, CockroachDB | SQL Server, SQLite |
242
+
243
+ O operador `like` não é suportado em nenhum provider — o Prisma não tem equivalente, então o adapter lança `NOT_SUPPORTED`. Use `contains`, `startsWith` ou `endsWith`.
244
+
245
+ > Essas limitações vêm do próprio Prisma, não do adapter — veja a [doc de case sensitivity](https://www.prisma.io/docs/orm/v7/reference/prisma-client-reference#mode) e a [referência de CRUD](https://www.prisma.io/docs/orm/v7/prisma-client/queries/crud) do Prisma pra mais detalhes. O adapter é coberto por testes de integração em PostgreSQL, MySQL, SQLite, SQL Server e CockroachDB.
246
+
247
+ ## Transactions
248
+
249
+ **Todos** os métodos aceitam `options.db` e executam a operação no client/transaction passado — a diferença está em **como** cada um o trata:
250
+
251
+ - A maioria (operações de uma única chamada Prisma) roda direto em `options?.db`: você passa um transaction client e a chamada participa da transaction, sem iniciar nada novo.
252
+ - `saveMany` e `deleteManyReturning` — e `updateManyReturning`/`createManyReturning` quando você passa `select` ou `relations` — precisam rodar **mais de uma operação** do Prisma atomicamente (`saveMany` salva cada registro individualmente; `updateManyReturning`/`createManyReturning` fazem um `updateManyAndReturn`/`createManyAndReturn` + `findMany`; `deleteManyReturning` faz um `findMany` + `deleteMany`), então passam por `runTransactional`. Sem `select`/`relations`, `updateManyReturning`/`createManyReturning` são uma única chamada do Prisma. Se `options.db` já for um transaction client ativo, ele é reaproveitado — nenhuma transaction aninhada é criada; caso contrário, uma nova transaction é criada (em `options.db`, se fornecido, ou no client raiz caso contrário).
253
+
254
+ Um transaction client é diferenciado do `PrismaClient` raiz pelo método `$on`: o client raiz do Prisma o expõe (pra event listeners), o transaction client interativo não. É isso que `Prisma7ClientLike`/`Prisma7OrmTypes` codificam nos seus tipos.
255
+
256
+ ```typescript
257
+ await userRepository.transaction(async tx => {
258
+ // Toda chamada de repository feita com `{ db: tx }` dentro desse
259
+ // callback compartilha a mesma transaction — incluindo chamadas a
260
+ // `saveMany`/`updateManyReturning`/`createManyReturning`/`deleteManyReturning`,
261
+ // que vão detectar que `tx` já é uma transaction e rodar direto nele, sem
262
+ // aninhar uma nova.
263
+ await userRepository.save(user, { db: tx });
264
+ await userRepository.save(outrosUsuarios, { db: tx }); // um array salva vários registros
265
+ });
266
+ ```
267
+
268
+ ### Concorrência em `deleteManyReturning`
269
+
270
+ O `deleteManyReturning` roda um `findMany` no `where` informado (pra capturar os registros que vai devolver) e depois deleta **esses mesmos registros pela primary key** num `deleteMany`, tudo dentro de uma transaction. Como o Prisma não tem um `deleteManyAndReturn` nativo, ele precisa dessas duas etapas. Se o seu `select` deixar a primary key de fora, o adapter a adiciona internamente na query e a remove dos registros devolvidos.
271
+
272
+ Como o delete é guiado pelas primary keys que de fato foram buscadas, uma linha **inserida depois** do `findMany` nunca é deletada por engano. Ainda assim, uma alteração concorrente entre as duas etapas pode fazer os dois divergirem:
273
+
274
+ - uma linha devolvida que foi **deletada por outra pessoa** nesse meio-tempo continua sendo devolvida, mesmo não tendo sido deletada por essa chamada;
275
+ - uma linha devolvida que **deixa de bater** com o `where` antes do `deleteMany` rodar ainda é deletada, já que o `where` não é re-aplicado.
276
+
277
+ Se você quiser garantir que não haverá problemas de concorrência, rode esse método **dentro** de um `repository.transaction()` num nível de isolamento mais alto — ex.: `SERIALIZABLE`:
278
+
279
+ ```typescript
280
+ import { VSRepoIsolationLevel } from "vsrepo";
281
+
282
+ await userRepository.transaction(async tx => {
283
+ // Atômico: nenhum insert/update concorrente consegue entrar entre o
284
+ // findMany e o deleteMany, pois o SERIALIZABLE isola esta transaction.
285
+ // Sempre passe `{ db: tx }` pra chamada rodar dentro desta transaction.
286
+ const deleted = await userRepository.deleteManyReturning({ id: { in: [...] } }, { db: tx });
287
+ }, {
288
+ isolationLevel: VSRepoIsolationLevel.SERIALIZABLE,
289
+ });
290
+ ```
291
+
292
+ > O nível de isolamento só se aplica quando a chamada roda **dentro** daquela transaction (ou seja, com `{ db: tx }`). Quando o `deleteManyReturning` inicia a própria transaction interna (sem `options.db`), ele usa o nível de isolamento padrão.
293
+
294
+ ## Logging
295
+
296
+ O adapter usa o `VSLogger` (de `vsrepo`) internamente: todo método loga uma linha `DEBUG` com o arg resolvido do Prisma, além de logs de performance de início/fim. Configure `logLevel` na config do construtor pra controlar a verbosidade; o default é `VSLogLevel.WARN`.
297
+
298
+ ## Requisitos
299
+
300
+ - `vsrepo` ^3.0.0-dev.2
301
+ - `@prisma/client` ^7.10.0
302
+ - Node.js `>=22`
303
+ - Um banco PostgreSQL, CockroachDB, MySQL, SQLite ou SQL Server (todos cobertos pelos testes de integração)