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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +535 -0
  2. package/LICENSE +20 -20
  3. package/README.md +394 -299
  4. package/README.pt-BR.md +394 -299
  5. package/dist/constants/provider-capabilities.constant.d.ts +17 -0
  6. package/dist/constants/provider-capabilities.constant.js +27 -0
  7. package/dist/index.d.ts +9 -8
  8. package/dist/index.js +1 -7
  9. package/dist/parsers/data.parser.d.ts +3 -33
  10. package/dist/parsers/data.parser.js +70 -69
  11. package/dist/parsers/include.parser.d.ts +1 -1
  12. package/dist/parsers/include.parser.js +3 -7
  13. package/dist/parsers/order-by.parser.d.ts +1 -10
  14. package/dist/parsers/order-by.parser.js +3 -16
  15. package/dist/parsers/select.parser.d.ts +1 -1
  16. package/dist/parsers/select.parser.js +3 -7
  17. package/dist/parsers/where.parser.d.ts +4 -29
  18. package/dist/parsers/where.parser.js +49 -148
  19. package/dist/prisma7.adapter.d.ts +116 -81
  20. package/dist/prisma7.adapter.js +345 -362
  21. package/dist/resolvers/map-prisma-error.resolver.d.ts +0 -11
  22. package/dist/resolvers/map-prisma-error.resolver.js +48 -72
  23. package/dist/resolvers/merge-entities.resolver.d.ts +11 -15
  24. package/dist/resolvers/merge-entities.resolver.js +29 -38
  25. package/dist/resolvers/resolve-provider.resolver.d.ts +2 -22
  26. package/dist/resolvers/resolve-provider.resolver.js +10 -33
  27. package/dist/types/adapter-config.type.d.ts +21 -10
  28. package/dist/types/adapter-config.type.js +1 -3
  29. package/dist/types/adapter-relations.type.d.ts +14 -8
  30. package/dist/types/adapter-relations.type.js +1 -3
  31. package/dist/types/plain-object.type.js +1 -3
  32. package/dist/types/prisma-arg-like.type.js +1 -3
  33. package/dist/types/prisma-client-models.type.d.ts +4 -4
  34. package/dist/types/prisma-client-models.type.js +1 -3
  35. package/dist/types/prisma-provider.type.d.ts +1 -1
  36. package/dist/types/prisma-provider.type.js +1 -3
  37. package/dist/types/prisma-repository-like.type.d.ts +1 -1
  38. package/dist/types/prisma-repository-like.type.js +1 -3
  39. package/dist/types/prisma7-client-like.type.d.ts +1 -8
  40. package/dist/types/prisma7-client-like.type.js +1 -3
  41. package/dist/types/prisma7-global-config.type.d.ts +17 -0
  42. package/dist/types/prisma7-global-config.type.js +1 -0
  43. package/dist/types/prisma7-orm-types.type.d.ts +2 -2
  44. package/dist/types/prisma7-orm-types.type.js +1 -3
  45. package/dist/types/relation.type.d.ts +119 -25
  46. package/dist/types/relation.type.js +1 -3
  47. package/dist/validators/is-decimal.validator.d.ts +2 -5
  48. package/dist/validators/is-decimal.validator.js +3 -6
  49. package/dist/validators/is-plain-object.validator.d.ts +1 -1
  50. package/dist/validators/is-plain-object.validator.js +3 -7
  51. package/dist/validators/validate-prisma-client.validator.d.ts +1 -14
  52. package/dist/validators/validate-prisma-client.validator.js +7 -24
  53. package/package.json +108 -91
  54. package/dist/index.js.map +0 -1
  55. package/dist/parsers/data.parser.js.map +0 -1
  56. package/dist/parsers/include.parser.js.map +0 -1
  57. package/dist/parsers/order-by.parser.js.map +0 -1
  58. package/dist/parsers/select.parser.js.map +0 -1
  59. package/dist/parsers/where.parser.js.map +0 -1
  60. package/dist/prisma7.adapter.js.map +0 -1
  61. package/dist/resolvers/map-prisma-error.resolver.js.map +0 -1
  62. package/dist/resolvers/merge-entities.resolver.js.map +0 -1
  63. package/dist/resolvers/resolve-provider.resolver.js.map +0 -1
  64. package/dist/types/adapter-config.type.js.map +0 -1
  65. package/dist/types/adapter-relations.type.js.map +0 -1
  66. package/dist/types/plain-object.type.js.map +0 -1
  67. package/dist/types/prisma-arg-like.type.js.map +0 -1
  68. package/dist/types/prisma-client-models.type.js.map +0 -1
  69. package/dist/types/prisma-provider.type.js.map +0 -1
  70. package/dist/types/prisma-repository-like.type.js.map +0 -1
  71. package/dist/types/prisma7-client-like.type.js.map +0 -1
  72. package/dist/types/prisma7-orm-types.type.js.map +0 -1
  73. package/dist/types/relation.type.js.map +0 -1
  74. package/dist/validators/is-decimal.validator.js.map +0 -1
  75. package/dist/validators/is-plain-object.validator.js.map +0 -1
  76. package/dist/validators/validate-adapter-config.validator.d.ts +0 -8
  77. package/dist/validators/validate-adapter-config.validator.js +0 -78
  78. package/dist/validators/validate-adapter-config.validator.js.map +0 -1
  79. package/dist/validators/validate-prisma-client.validator.js.map +0 -1
package/README.pt-BR.md CHANGED
@@ -1,299 +1,394 @@
1
- <div align="center">
2
- <img src="https://res.cloudinary.com/ddbfifdxd/image/upload/w_200,q_auto,f_auto/v1786386427/VS_logo_TextoAbaixo_yev4tq.png" alt="VSRepository Logo" width="200"/>
3
-
4
- <p style="margin-top: 12px;">
5
- <img src="https://img.shields.io/npm/v/@vsrepo/prisma7-adapter?style=flat-square" alt="npm version"/>
6
- <img src="https://img.shields.io/npm/l/@vsrepo/prisma7-adapter?style=flat-square" alt="npm license"/>
7
- <img src="https://img.shields.io/npm/dt/@vsrepo/prisma7-adapter?style=flat-square" alt="npm downloads"/>
8
- <img src="https://img.shields.io/badge/inspired%20by-JpaRepository-E73121?style=flat-square" alt="inspired by JpaRepository"/>
9
- </p>
10
- </div>
11
-
12
- # VSRepoPrisma7Adapter
13
-
14
- 🇺🇸 [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
+ - [Config global](#config-global)
28
+ - [Relations](#relations)
29
+ - [Os dois `relations`](#os-dois-relations)
30
+ - [`relations` no construtor (escrita)](#relations-no-construtor-escrita)
31
+ - [`mode`](#mode)
32
+ - [`pk`](#pk)
33
+ - [`onUpdate`](#onupdate)
34
+ - [`onDisconnect`](#ondisconnect)
35
+ - [Ignorando uma relation (`"ignore"`)](#ignorando-uma-relation-ignore)
36
+ - [`fkHere` e `fkThere`](#fkhere-e-fkthere)
37
+ - [`allowOmitPk`](#allowomitpk)
38
+ - [Como cada método de escrita resolve relations](#como-cada-método-de-escrita-resolve-relations)
39
+ - [`relations` nas options (leitura)](#relations-nas-options-leitura)
40
+ - [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação)
41
+ - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-não-suportam-nested-writes)
42
+ - [Limitações por provider](#limitações-por-provider)
43
+ - [Transactions](#transactions)
44
+ - [Logging](#logging)
45
+ - [Requisitos](#requisitos)
46
+
47
+ ---
48
+
49
+ ## Instalação
50
+
51
+ ```bash
52
+ npm install vsrepo@dev @prisma/client @vsrepo/prisma7-adapter@dev
53
+ ```
54
+
55
+ Enquanto a `2.0.0` está em desenvolvimento, instale o `vsrepo` e o `@vsrepo/prisma7-adapter` com a tag `dev`.
56
+
57
+ ## Uso básico
58
+
59
+ ```typescript
60
+ import { VSRepository, VSLogLevel } from "vsrepo";
61
+ import { Prisma7Adapter, Prisma7OrmTypes } from "@vsrepo/prisma7-adapter";
62
+ import { Prisma, PrismaClient } from "./generated/prisma/client";
63
+ import { prisma } from "./prisma";
64
+
65
+ type User = Prisma.UserGetPayload<{ include: { posts: true } }>;
66
+ type MyOrmTypes = Prisma7OrmTypes<PrismaClient>;
67
+
68
+ class UserRepository extends VSRepository<User, string, MyOrmTypes> {
69
+ constructor() {
70
+ super({
71
+ adapter: new Prisma7Adapter(prisma, {
72
+ modelName: "user",
73
+ pkName: "id",
74
+ relations: {
75
+ posts: { mode: "otm", pk: "id" },
76
+ },
77
+ logLevel: VSLogLevel.WARN,
78
+ }),
79
+ });
80
+ }
81
+ }
82
+
83
+ const userRepository = new UserRepository();
84
+
85
+ const user = await userRepository.get("...", { relations: { posts: true } }); // pela primary key
86
+ ```
87
+
88
+ 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).
89
+
90
+ `Prisma7OrmTypes<DB>` amarra os tipos de retorno de `getDbClient()`/`transaction()` do `VSRepository` aos seus tipos reais e gerados do Prisma — ver [Transactions](#transactions).
91
+
92
+ ## Config do construtor
93
+
94
+ ```typescript
95
+ new Prisma7Adapter(prisma, {
96
+ modelName: "user", // obrigatório — nome do model/delegate do Prisma Client, como em `prisma.user`
97
+ pkName: "id", // obrigatório — nome do campo de primary key da entidade
98
+ relations: { ... }, // opcional — ver "relations no construtor (escrita)" abaixo
99
+ provider: "postgresql", // opcional — detectado a partir do Prisma Client quando omitido
100
+ logLevel: VSLogLevel.WARN, // opcional — default: VSLogLevel.WARN
101
+ allowOmitPk: false, // opcional — default: false. Leia "`allowOmitPk`" antes de ativar
102
+ });
103
+ ```
104
+
105
+ Na construção, o adapter confere o Prisma Client e o model, e resolve o provider do datasource:
106
+
107
+ - um client `null`/`undefined` lança um `VSRepoAdapterError` (code `MISSING_DB_CLIENT`);
108
+ - um `modelName` sem delegate correspondente no client (`prisma.<modelName>`) lança `MODEL_NOT_FOUND`;
109
+ - 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`).
110
+
111
+ Os outros campos (`pkName`, `relations`, `logLevel`, `allowOmitPk`) são checados só pelo TypeScript, não em runtime.
112
+
113
+ ## Config global
114
+
115
+ `provider`, `logLevel` e `allowOmitPk` podem ser definidos uma vez pra todos os adapters, pelo `setGlobalConfig` estático:
116
+
117
+ ```typescript
118
+ import { VSLogLevel } from "vsrepo";
119
+ import { Prisma7Adapter } from "@vsrepo/prisma7-adapter";
120
+
121
+ Prisma7Adapter.setGlobalConfig({ logLevel: VSLogLevel.WARN });
122
+ ```
123
+
124
+ - Só afeta adapters criados **depois** da chamada, então chame antes de instanciar seus repositories.
125
+ - Cada chamada **substitui** a config global anterior — não faz merge com ela.
126
+ - Um valor definido na config do próprio adapter sempre vence o global.
127
+
128
+ ## Relations
129
+
130
+ ### Os dois `relations`
131
+
132
+ O nome `relations` aparece em **dois lugares diferentes** da API, com **formas e propósitos diferentes** — fácil de confundir. Em resumo:
133
+
134
+ | | `relations` no **construtor** | `relations` nas **options** |
135
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
136
+ | Onde você define | `new Prisma7Adapter(prisma, { relations: ... })` | `repository.get(where, { relations: ... })` — e demais métodos |
137
+ | Formato | Um objeto de **configuração** por campo (`{ mode, pk, onUpdate?, onDisconnect?, fkHere?, fkThere? }`), ou `"ignore"` | Um objeto por campo **só com `true` ou sub-objeto**: `{ posts: true }` |
138
+ | 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`/`connect`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) | **Leitura** — eager loading: quais relations trazer junto no resultado (vira um `include` do Prisma) |
139
+ | Quem consome | `parsePrismaWriteData` (resolver de escrita) | `parsePrismaInclude` (via `resolveReadArg`) |
140
+ | Precisa da outra? | Não — só afeta escritas | Não — funciona mesmo sem `relations` no construtor |
141
+
142
+ 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.
143
+
144
+ ### `relations` no construtor (escrita)
145
+
146
+ 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`).
147
+
148
+ Cada relation é configurada pelo nome do campo:
149
+
150
+ ```typescript
151
+ relations: {
152
+ posts: { mode: "otm", pk: "id", onUpdate: "set" },
153
+ address: { mode: "oto", pk: "id", onUpdate: "upsert", onDisconnect: "delete" },
154
+ author: { mode: "mto", pk: "id" }, // toda opção tem default
155
+ audit: "ignore", // nunca é escrita por este repository
156
+ }
157
+ ```
158
+
159
+ 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.
160
+
161
+ #### `mode`
162
+
163
+ Cardinalidade da relation, do ponto de vista da entidade dona do campo:
164
+
165
+ | `mode` | Significado | Formato do campo que você envia | Opções |
166
+ | ------ | ------------ | ------------------------------- | ------------------------------------------------------------------------------------------ |
167
+ | `oto` | one-to-one | um único objeto, ou `null` | `pk`, `onUpdate` (to-one), `onDisconnect` (`"disconnect"`/`"delete"`), `fkHere`, `fkThere` |
168
+ | `mto` | many-to-one | um único objeto, ou `null` | `pk`, `onUpdate` (to-one), `onDisconnect` (`"disconnect"`/`"delete"`), `fkHere`, `fkThere` |
169
+ | `otm` | one-to-many | um array de objetos | `pk`, `onUpdate` (to-many), `fkThere` |
170
+ | `mtm` | many-to-many | um array de objetos | `pk`, `onUpdate` (to-many), `fkThere` |
171
+
172
+ #### `pk`
173
+
174
+ O campo usado pra identificar um registro relacionado já existente. Um item **com** `pk` no payload é conectado (ou recebe upsert) ao registro existente; um item **sem** ele é criado.
175
+
176
+ #### `onUpdate`
177
+
178
+ Como `save`/`update`/`upsert` escrevem a relation no update. Os valores aceitos dependem do `mode`.
179
+
180
+ **To-one (`oto`/`mto`)**, quando o objeto relacionado tem a `pk`:
181
+
182
+ - **`"connect"`** nunca cria nem sobrescreve o registro relacionado: só aponta a relation pra ele.
183
+ - **`"connectOrCreate"`** cria o relacionado quando ele não existe.
184
+ - **`"upsert"`** é a que **sobrescreve** o relacionado com os campos enviados.
185
+ - Quando o próprio registro dono está sendo criado (`create`, ou o ramo `create` do `save`), um relacionado com `pk` sempre vira `connectOrCreate`.
186
+
187
+ **To-many (`otm`/`mtm`)**, pros itens que já existem (casados por `pk`) e pros que **não** foram enviados:
188
+
189
+ - **`"add"`** (default) — só cria/conecta/faz upsert dos itens enviados. Itens já existentes que não estão no payload permanecem intocados.
190
+ - **`"set"`** — igual ao `"add"`, mas também remove o que não foi enviado: `otm` roda um `deleteMany` nos itens ausentes do array, e `mtm` zera o join (`set: []`) antes de reconectar.
191
+
192
+ #### `onDisconnect`
193
+
194
+ O que enviar `null` pra uma relation to-one faz no update:
195
+
196
+ - **`"disconnect"`** (default) — desanexa o registro relacionado e o mantém. O Prisma rejeita quando a relation é obrigatória.
197
+ - **`"delete"`** — apaga o registro relacionado. Existe pra `oto` e `mto`, e é sempre opt-in. Cuidado num `mto`: o relacionado é compartilhado com outras linhas, então apagar afeta todas elas, e o Prisma rejeita a escrita enquanto outras linhas ainda o referenciam (a menos que a FK tenha uma ação referencial como `onDelete: Cascade`).
198
+
199
+ #### Ignorando uma relation (`"ignore"`)
200
+
201
+ Definir uma relation como `"ignore"` deixa o campo fora de toda escrita (`create`, `update`, `save`...): o objeto (ou array) é simplesmente descartado. Serve pra uma relation que você **carrega** mas não quer **persistir** por essa entidade.
202
+
203
+ ```typescript
204
+ // AddressRepository
205
+ relations: {
206
+ user: "ignore";
207
+ }
208
+ ```
209
+
210
+ Como o objeto de relation é descartado, uma FK escalar no payload (`userId`) passa como está, junto da primary key. Isso faz dela a forma mais simples de contornar o [problema do `allowOmitPk`](#allowomitpk) quando você só quer manter a FK: `address.save(addressCarregadoComUser)` atualiza o address e nunca toca no user.
211
+
212
+ #### `fkHere` e `fkThere`
213
+
214
+ Numa relation one-to-one ou many-to-one, só um dos lados guarda a coluna da foreign key. Em `Address.user`, é a `Address.userId`. Informe ao adapter em qual lado cada entidade está:
215
+
216
+ | Entidade | Campo da relation | Guarda a FK? | Config |
217
+ | --------------------- | ----------------- | --------------------------------------------------- | ------------------- |
218
+ | `Address` (lado dono) | `user` | **sim** — `userId` é uma coluna de `Address` | `fkHere: "userId"` |
219
+ | `User` (lado inverso) | `address` | não — a FK fica na entidade relacionada (`Address`) | `fkThere: "userId"` |
220
+
221
+ ```typescript
222
+ // UserRepository
223
+ relations: { address: { mode: "oto", pk: "id", fkThere: "userId" } }
224
+
225
+ // AddressRepository
226
+ relations: { user: { mode: "oto", pk: "id", fkHere: "userId" } }
227
+ ```
228
+
229
+ - **`fkThere`** — nome da FK que fica na entidade **relacionada**. Um objeto relacionado carregado com `get`/`find` normalmente a traz (`address: { id, street, userId }`), mas a relation já define esse valor, e o Prisma não aceita ela dentro do nested write. O adapter remove a FK do(s) payload(s) relacionado(s) antes de montar a escrita.
230
+ - **`fkHere`** — nome da FK que fica **nesta** entidade. O Prisma não aceita a FK escalar junto do objeto de relation, então, quando o campo da relation está no payload, o adapter remove esse campo do `create` e do `update`. Um payload que traz só a FK escalar (sem o objeto de relation) fica como está.
231
+
232
+ O `fkHere` tem mais uma consequência, pra primary key — leia a próxima seção.
233
+
234
+ #### `allowOmitPk`
235
+
236
+ `allowOmitPk` é uma opção do adapter (também dá pra definir na [config global](#config-global)), mas só importa pra relations configuradas com `fkHere`.
237
+
238
+ **O problema.** Pegue um `Address` carregado junto com o `user` (`relations: { user: true }`) e enviado de volta pro `save`. O payload tem a primary key (`id`) **e** uma relation que guarda a FK na própria entidade. Com o objeto de relation presente, o Prisma não aceita nem a FK escalar nem a primary key (`Unknown argument 'id'`), então o adapter não consegue enviar tudo.
239
+
240
+ **O que o adapter faz:**
241
+
242
+ - **`allowOmitPk: false`** (default) — lança um `VSRepoAdapterError` (`INVALID_DATA`) **antes** de tocar no banco, explicando o que fazer. Payloads sem primary key (um `create` simples, por exemplo) ou sem o objeto de relation não são afetados.
243
+ - **`allowOmitPk: true`** — remove a primary key do `create` e do `update`. Registros que já existem continuam sendo achados pelo `where: { id }`, então **os updates funcionam normalmente**. O porém é o ramo `create` do `save`: se o registro ainda não existe, ele é criado com uma **primary key gerada pelo banco, e não a que você enviou** — e nada te avisa disso.
244
+
245
+ **Quando ativar.** Só se for realmente necessário, e só se não for a sua aplicação que gerencia os ids — ou seja, o banco os gera (`autoincrement`, defaults `uuid()`/`cuid()`, sequences...). Se a aplicação atribui os ids (UUIDs gerados no client, ids importados de outro sistema, ids que outros registros já referenciam), mantenha **desativado**: um registro que não existe seria gravado, sem aviso, com outro id.
246
+
247
+ **Formas de contornar, sem `allowOmitPk`:**
248
+
249
+ 1. **Ignorar a relation** quando você só quer manter a FK: `relations: { user: "ignore" }` (veja [Ignorando uma relation](#ignorando-uma-relation-ignore)). O payload mantém `id` e `userId`, e a entidade relacionada não é escrita.
250
+ 2. **Enviar a FK escalar no lugar do objeto de relation.** `address.save({ id, city, userId })` mantém `id` e `userId`, já que não há objeto de relation. Persista a entidade relacionada por conta própria (pelo repository dela) quando ela também mudou — de preferência numa só `transaction`.
251
+ 3. **Enviar só o que mudou.** `address.update({ id }, { city })` não carrega nem a primary key nem a relation.
252
+ 4. **Salvar pelo outro lado.** `user.save({ ...user, address })` mantém as primary keys (configure o `fkThere` lá): a FK fica em `Address`, então `User` não tem FK própria pra conflitar.
253
+ 5. **Deixar o banco gerar a key dos registros novos.** Não envie `id` ao criar: `address.create({ city, user })`.
254
+
255
+ #### Como cada método de escrita resolve relations
256
+
257
+ | Método | Relations |
258
+ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | `create` | Só `create`/`connectOrCreate` (sem upsert — ainda não existe nada pra atualizar) |
260
+ | `update` / `upsert` (metade do `update`) / `save` (branch de upsert) | Resolução completa: `create`/`connect`/`connectOrCreate`/`upsert`/`disconnect`/`delete`/`deleteMany`/`set`, conforme `mode`/`onUpdate`/`onDisconnect` |
261
+ | `createMany` / `createManyReturning` / `updateMany` / `updateManyReturning` | Não suportado — lança um `VSRepoAdapterError` apontando o campo problemático se o payload tiver uma relation configurada |
262
+
263
+ ### `relations` nas options (leitura)
264
+
265
+ 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:
266
+
267
+ - `true` — carrega a relation completa;
268
+ - ou outro objeto `relations` — para eager loading aninhado (relations da relation).
269
+
270
+ ```typescript
271
+ const user = await userRepository.get(
272
+ "...", // primary key
273
+ {
274
+ relations: {
275
+ posts: true, // carrega os posts junto
276
+ author: { profile: true }, // e, dentro do author, o profile (eager loading aninhado)
277
+ },
278
+ },
279
+ );
280
+ ```
281
+
282
+ 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.
283
+
284
+ 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`):
285
+
286
+ ```ts
287
+ const prismaSelect = options.select && parsePrismaSelect(options.select);
288
+ const prismaInclude = prismaSelect ? undefined : options.relations && parsePrismaInclude(options.relations);
289
+ ```
290
+
291
+ ## Métodos atômicos e de agregação
292
+
293
+ 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`.
294
+
295
+ - `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.
296
+ - `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.
297
+ - `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()`.
298
+
299
+ ```typescript
300
+ // Atômico — avaliado server-side, sem read-modify-write:
301
+ await productRepository.increment("stock", 10, { id: productId });
302
+ await accountRepository.decrement("balance", 50, { id: accountId });
303
+ await productRepository.multiply("price", 1.1, { id: productId }); // ex: um reajuste de 10% no preço
304
+ await productRepository.divide("price", 2, { id: productId });
305
+
306
+ // O mesmo, pra todo registro que casa com `where` — retorna `{ count }`:
307
+ await productRepository.incrementMany("stock", 10, { category: "books" });
308
+
309
+ // Agregação — entre todos os registros que casam com um `where` opcional (todos se omitido):
310
+ const total = await productRepository.sum("price"); // number | null
311
+ const avgPrice = await productRepository.average("price", { active: true });
312
+ const cheapest = await productRepository.min("price");
313
+ const mostExpensive = await productRepository.max("price");
314
+ ```
315
+
316
+ **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`.
317
+
318
+ ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes
319
+
320
+ `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`.
321
+
322
+ > 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.
323
+
324
+ ## Limitações por provider
325
+
326
+ 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.
327
+
328
+ | Recurso | Providers com suporte | Sem suporte |
329
+ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
330
+ | `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 |
331
+ | `createManyReturning` / `updateManyReturning` (baseados no `createManyAndReturn`/`updateManyAndReturn` do Prisma) | PostgreSQL, CockroachDB, SQLite | MySQL, SQL Server |
332
+ | `ignoreConflicts: true` no `createMany` / `createManyReturning` (o `skipDuplicates` do Prisma) | PostgreSQL, MySQL, CockroachDB | SQL Server, SQLite |
333
+
334
+ 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`.
335
+
336
+ > 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.
337
+
338
+ ## Transactions
339
+
340
+ **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:
341
+
342
+ - 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.
343
+ - `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).
344
+
345
+ 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.
346
+
347
+ ```typescript
348
+ await userRepository.transaction(async tx => {
349
+ // Toda chamada de repository feita com `{ db: tx }` dentro desse
350
+ // callback compartilha a mesma transaction — incluindo chamadas a
351
+ // `saveMany`/`updateManyReturning`/`createManyReturning`/`deleteManyReturning`,
352
+ // que vão detectar que `tx` já é uma transaction e rodar direto nele, sem
353
+ // aninhar uma nova.
354
+ await userRepository.save(user, { db: tx });
355
+ await userRepository.save(outrosUsuarios, { db: tx }); // um array salva vários registros
356
+ });
357
+ ```
358
+
359
+ ### Concorrência em `deleteManyReturning`
360
+
361
+ 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.
362
+
363
+ 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:
364
+
365
+ - uma linha devolvida que foi **deletada por outra pessoa** nesse meio-tempo continua sendo devolvida, mesmo não tendo sido deletada por essa chamada;
366
+ - uma linha devolvida que **deixa de bater** com o `where` antes do `deleteMany` rodar ainda é deletada, já que o `where` não é re-aplicado.
367
+
368
+ 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`:
369
+
370
+ ```typescript
371
+ import { VSRepoIsolationLevel } from "vsrepo";
372
+
373
+ await userRepository.transaction(async tx => {
374
+ // Atômico: nenhum insert/update concorrente consegue entrar entre o
375
+ // findMany e o deleteMany, pois o SERIALIZABLE isola esta transaction.
376
+ // Sempre passe `{ db: tx }` pra chamada rodar dentro desta transaction.
377
+ const deleted = await userRepository.deleteManyReturning({ id: { in: [...] } }, { db: tx });
378
+ }, {
379
+ isolationLevel: VSRepoIsolationLevel.SERIALIZABLE,
380
+ });
381
+ ```
382
+
383
+ > 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.
384
+
385
+ ## Logging
386
+
387
+ 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`.
388
+
389
+ ## Requisitos
390
+
391
+ - `vsrepo` ^3.0.0-dev.3
392
+ - `@prisma/client` ^7.10.1
393
+ - Node.js `>=22`
394
+ - Um banco PostgreSQL, CockroachDB, MySQL, SQLite ou SQL Server (todos cobertos pelos testes de integração)