vsrepo 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,66 +1,70 @@
1
1
  # VSRepository
2
2
 
3
3
  ![npm](https://img.shields.io/npm/v/vsrepo?style=flat-square)
4
- ![NPM License](https://img.shields.io/npm/l/vsrepo)
4
+ ![NPM License](https://img.shields.io/npm/l/vsrepo?style=flat-square)
5
5
  ![NPM Downloads](https://img.shields.io/npm/dt/vsrepo?style=flat-square)
6
6
 
7
- Biblioteca de repository pattern para projetos que usam **Prisma**, com suporte completo a **TypeScript** e **type inference** automático.
7
+ Repository pattern library for projects using **Prisma**, with full **TypeScript** support and automatic **type inference**.
8
8
 
9
- O VSRepository permite criar repositories fortemente tipados com:
9
+ VSRepository lets you create strongly-typed repositories with:
10
10
 
11
- - **Métodos base** automáticos: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `patchList`, `merge`, `getAll`, `total`, `has`
12
- - **Soft-delete nativo**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
13
- - **Métodos dinâmicos** inferidos pelo nome: `findByEmail`, `findManyPaginated`, `updateById`, `deleteManyByIdIn`
14
- - **Select models** reutilizáveis para diferentes projeções de dados
15
- - **Type safety** em 100% das operações
16
- - **Transações** nativas do Prisma (automáticas em `saveList` e `patchList`)
17
- - **Extensibilidade** com métodos personalizados
11
+ - Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `patchList`, `merge`, `getAll`, `total`, `has`
12
+ - **Native soft-delete**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
13
+ - **Dynamic methods** inferred from their name: `findOneByEmail`, `findManyPaginated`, `updateById`, `deleteManyByNameStartsWith`
14
+ - Reusable **select models** for different data projections
15
+ - **Type safety** across 100% of operations
16
+ - Native Prisma **transactions** (automatic in `saveList` and `patchList`)
17
+ - **Extensibility** with custom methods
18
+
19
+ > 💡 Want to see all of this in practice? The repository's [`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples) folder has commented, runnable examples for every feature — see the [Practical examples](#practical-examples) section below.
18
20
 
19
21
  ---
20
22
 
21
- ## Sumário
23
+ ## Table of contents
22
24
 
23
- - [Instalação](#instalação)
24
- - [Gerando os tipos](#gerando-os-tipos)
25
- - [Uso básico](#uso-básico)
26
- - [Integração com NestJS](#integração-com-nestjs)
27
- - [Métodos base](#métodos-base)
25
+ - [Installation](#installation)
26
+ - [Generating the types](#generating-the-types)
27
+ - [Basic usage](#basic-usage)
28
+ - [NestJS integration](#nestjs-integration)
29
+ - [Base methods](#base-methods)
28
30
  - [Soft-delete](#soft-delete)
29
- - [Operações em lote](#operações-em-lote)
31
+ - [Batch operations](#batch-operations)
30
32
  - [Merge](#merge)
31
- - [Configurando os métodos base](#configurando-os-métodos-base)
33
+ - [Configuring the base methods](#configuring-the-base-methods)
32
34
  - [Select Models](#select-models)
33
35
  - [Include Models](#include-models)
34
- - [Required Where](#requiredwhere)
36
+ - [Required Where](#required-where)
35
37
  - [Default Ordenation](#default-ordenation)
36
- - [Opção `see`](#opção-see)
37
- - [Métodos dinâmicos](#métodos-dinâmicos)
38
- - [Prefixos disponíveis](#prefixos-disponíveis)
39
- - [Filtros de campo](#filtros-de-campo)
40
- - [Operadores lógicos](#operadores-lógicos)
41
- - [Filtros de relação](#filtros-de-relação)
42
- - [Sufixos de paginação e ordenação](#sufixos-de-paginação-e-ordenação)
43
- - [Configuração de métodos](#configuração-de-métodos)
44
- - [Aggregate e GroupBy](#aggregate-e-groupby)
45
- - [Relações no save](#relações-no-save)
46
- - [Transações](#transações)
47
- - [Estendendo um repository](#estendendo-um-repository)
48
- - [Tratamento de erros](#tratamento-de-erros)
49
- - [Tipos utilitários](#tipos-utilitários)
38
+ - [`see` option](#see-option)
39
+ - [Dynamic methods](#dynamic-methods)
40
+ - [Available prefixes](#available-prefixes)
41
+ - [Field filters](#field-filters)
42
+ - [Logical operators](#logical-operators)
43
+ - [Relation filters](#relation-filters)
44
+ - [Pagination and ordering suffixes](#pagination-and-ordering-suffixes)
45
+ - [Distinct](#distinct)
46
+ - [Method configuration](#method-configuration)
47
+ - [Aggregate and GroupBy](#aggregate-and-groupby)
48
+ - [Relations in save](#relations-in-save)
49
+ - [Transactions](#transactions)
50
+ - [Extending a repository](#extending-a-repository)
51
+ - [Error handling](#error-handling)
52
+ - [Utility types](#utility-types)
50
53
  - [API Reference](#api-reference)
51
- - [Contribuindo](#contribuindo)
52
- - [Requisitos](#requisitos)
54
+ - [Practical examples](#practical-examples)
55
+ - [Contributing](#contributing)
56
+ - [Requirements](#requirements)
53
57
  - [Troubleshooting](#troubleshooting)
54
58
 
55
59
  ---
56
60
 
57
- ## Instalação
61
+ ## Installation
58
62
 
59
63
  ```bash
60
64
  npm i vsrepo @prisma/client
61
65
  ```
62
66
 
63
- Gere o Prisma Client:
67
+ Generate the Prisma Client:
64
68
 
65
69
  ```bash
66
70
  npx prisma generate
@@ -68,15 +72,15 @@ npx prisma generate
68
72
 
69
73
  ---
70
74
 
71
- ## Gerando os tipos
75
+ ## Generating the types
72
76
 
73
- O VSRepository precisa conhecer o caminho real do seu Prisma Client para gerar as tipagens corretamente.
77
+ VSRepository needs to know the real path of your Prisma Client to generate the typings correctly.
74
78
 
75
79
  ```bash
76
80
  npx vsrepo generate
77
81
  ```
78
82
 
79
- Equivale a:
83
+ Equivalent to:
80
84
 
81
85
  ```bash
82
86
  npx vsrepo generate \
@@ -84,14 +88,14 @@ npx vsrepo generate \
84
88
  --prisma generated/prisma
85
89
  ```
86
90
 
87
- **Flags disponíveis:**
91
+ **Available flags:**
88
92
 
89
- | Flag | Alias | Padrão |
90
- | ---------- | ----- | ---------------------- |
91
- | `--output` | `-o` | `generated/vsrepo` |
92
- | `--prisma` | `-p` | `generated/prisma` |
93
+ | Flag | Alias | Default |
94
+ | ---------- | ----- | -------------------- |
95
+ | `--output` | `-o` | `generated/vsrepo` |
96
+ | `--prisma` | `-p` | `generated/prisma` |
93
97
 
94
- **Arquivos gerados:**
98
+ **Generated files:**
95
99
 
96
100
  ```
97
101
  generated/vsrepo/
@@ -102,21 +106,21 @@ generated/vsrepo/
102
106
  └── index.ts
103
107
  ```
104
108
 
105
- Após gerar, importe sempre a partir da pasta gerada:
109
+ After generating, always import from the generated folder:
106
110
 
107
111
  ```ts
108
- // CORRETO ✅
112
+ // CORRECT ✅
109
113
  import { setupVSRepo } from "../../generated/vsrepo";
110
114
 
111
- // ERRADO ❌
115
+ // WRONG ❌
112
116
  import { setupVSRepo } from "vsrepo";
113
117
  ```
114
118
 
115
119
  ---
116
120
 
117
- ## Uso básico
121
+ ## Basic usage
118
122
 
119
- ### Configurando o Prisma Client
123
+ ### Configuring the Prisma Client
120
124
 
121
125
  ```ts
122
126
  // src/configs/db.ts
@@ -130,59 +134,56 @@ const prisma = new PrismaClient({ adapter });
130
134
  export default prisma;
131
135
  ```
132
136
 
133
- ### Criando um repository
137
+ ### Creating a repository
134
138
 
135
139
  ```ts
136
- // src/repositories/usuarioRepository.ts
140
+ // src/repositories/userRepository.ts
137
141
  import prisma from "../configs/db";
138
142
  import { setupVSRepo } from "../../generated/vsrepo";
139
- import type { Usuario } from "../../generated/prisma/client";
143
+ import type { User } from "../../generated/prisma/client";
140
144
 
141
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
142
- tableName: "usuario",
145
+ const userRepository = setupVSRepo<User, "User">()(({
146
+ tableName: "user",
143
147
  pkName: "id",
144
148
  selectModels: {
145
- public: { id: true, nome: true, email: true },
149
+ public: { id: true, name: true, email: true },
146
150
  },
147
151
  defaultSelectModel: "public",
148
- requiredWhere: { ativo: true },
149
152
  }).build(prisma);
150
153
 
151
- export default usuarioRepository;
154
+ export default userRepository;
152
155
  ```
153
156
 
154
- > `selectModels` e `requiredWhere` podem ser declarados fora do `setupVSRepo` se você precisar exportá-los para uso em outros arquivos.
155
-
156
- ### Usando o repository
157
+ ### Using the repository
157
158
 
158
159
  ```ts
159
- import usuarioRepository from "./repositories/usuarioRepository";
160
+ import userRepository from "./repositories/userRepository";
160
161
 
161
- const usuario = await usuarioRepository.save({
162
- nome: "Joao",
163
- email: "joao@email.com",
164
- senha: "password",
162
+ const user = await userRepository.save({
163
+ name: "John",
164
+ email: "john@email.com",
165
+ password: "password",
165
166
  });
166
167
 
167
- const encontrado = await usuarioRepository.get(usuario.id);
168
- const todos = await usuarioRepository.getAll();
168
+ const found = await userRepository.get(user.id);
169
+ const all = await userRepository.getAll();
169
170
 
170
- usuario.nome = "Joao Pedro";
171
+ user.name = "John Smith";
171
172
 
172
- await usuarioRepository.save(usuario);
173
- await usuarioRepository.remove(usuario.id);
173
+ await userRepository.save(user);
174
+ await userRepository.remove(user.id);
174
175
  ```
175
176
 
176
177
  ---
177
178
 
178
- ## Integração com NestJS
179
+ ## NestJS integration
179
180
 
180
- O VSRepository pode ser facilmente integrado em projetos NestJS através de providers. Abaixo está um exemplo completo usando o padrão de injeção de dependência do NestJS.
181
+ VSRepository can be easily integrated into NestJS projects through providers. Below is a complete example using NestJS's dependency injection pattern.
181
182
 
182
- ### Configurando o repository como provider
183
+ ### Configuring the repository as a provider
183
184
 
184
185
  ```ts
185
- // src/resources/user/user.repository.ts
186
+ // src/modules/user/user.repository.ts
186
187
  import { Provider } from "@nestjs/common";
187
188
  import { PrismaService } from "../../database/prisma.service";
188
189
  import { UserGetPayload } from "../../../generated/prisma/models";
@@ -231,14 +232,18 @@ const userVSRepo = setupVSRepo<
231
232
  });
232
233
 
233
234
  const setupUserRepository = (prisma: PrismaService) => {
234
- return userVSRepo.build(prisma).extend((repo) => ({
235
- buscarPorDominio: async (dominio: string) => {
236
- return repo.findByEmailEndsWith(`@${dominio}`);
237
- },
238
- }));
235
+ return userVSRepo.build(prisma);
239
236
  };
240
237
 
241
238
  export type UserRepository = ReturnType<typeof setupUserRepository>;
239
+ /*
240
+ The type can also be inferred using VSRepository's `RepositoryOf`, passing the `userVSRepo` type:
241
+
242
+ export type UserRepository = RepositoryOf<typeof userVSRepo>;
243
+
244
+ NOTE: If you use `.extend` to extend the repository or configure the base methods,
245
+ using `ReturnType` is recommended since it's simpler to infer the type
246
+ */
242
247
 
243
248
  export const USER_REPOSITORY = Symbol("USER_REPOSITORY");
244
249
 
@@ -249,10 +254,10 @@ export const UserRepositoryProvider: Provider = {
249
254
  };
250
255
  ```
251
256
 
252
- ### Registrando o provider no módulo
257
+ ### Registering the provider in the module
253
258
 
254
259
  ```ts
255
- // src/resources/user/user.module.ts
260
+ // src/modules/user/user.module.ts
256
261
  import { Module } from "@nestjs/common";
257
262
  import { UserRepositoryProvider } from "./user.repository";
258
263
  import { UserService } from "./user.service";
@@ -267,12 +272,12 @@ import { UserController } from "./user.controller";
267
272
  export class UserModule {}
268
273
  ```
269
274
 
270
- ### Utilizando o repository em um serviço
275
+ ### Using the repository in a service
271
276
 
272
277
  ```ts
273
- // src/resources/user/user.service.ts
278
+ // src/modules/user/user.service.ts
274
279
  import { Injectable, Inject } from "@nestjs/common";
275
- import { USER_REPOSITORY, UserRepository } from "./user.repository";
280
+ import { USER_REPOSITORY, type UserRepository } from "./user.repository";
276
281
 
277
282
  @Injectable()
278
283
  export class UserService {
@@ -299,134 +304,182 @@ export class UserService {
299
304
  }
300
305
  ```
301
306
 
302
- **Benefícios desta abordagem:**
307
+ **Benefits of this approach:**
303
308
 
304
- - ✅ Type-safe repositories com injeção de dependência
305
- - ✅ Fácil de testar (mock do `USER_REPOSITORY`)
306
- - ✅ Isolamento da lógica de persistência
307
- - ✅ Reutilização do repository em múltiplos serviços
308
- - ✅ Suporte a transações via `PrismaService`
309
+ - ✅ Type-safe repositories with dependency injection
310
+ - ✅ Easy to test (mock the `USER_REPOSITORY`)
311
+ - ✅ Isolation of persistence logic
312
+ - ✅ Repository reuse across multiple services
313
+ - ✅ Transaction support via `PrismaService`
309
314
 
310
315
  ---
311
316
 
312
- ## Métodos base
317
+ ## Base methods
313
318
 
314
- Ao chamar `.build(prisma)` os métodos base abaixo são automaticamente disponibilizados:
319
+ When calling `.build(prisma)`, the base methods below are automatically made available:
315
320
 
316
- | Método | Descrição |
317
- | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
318
- | `get(pk)` | Busca um registro pela primary key |
319
- | `getOrThrow(pk)` | Busca um registro pela primary key; lança `VSRepoRuntimeError` (code `"20727"`) se não encontrado |
320
- | `getList(pks)` | Busca múltiplos registros por uma lista de primary keys |
321
- | `save(obj)` | Cria ou atualiza — se o objeto tiver a `pk` faz `upsert`, caso contrário faz `create` |
322
- | `saveList(objs)` | Salva um array de objetos em uma única transação automática |
323
- | `patch(pk, obj)` | Atualiza parcialmente um registro pela primary key |
324
- | `patchList(tuples)` | Atualiza parcialmente múltiplos registros via array de tuplas `[pk, obj]` em transação automática |
325
- | `merge(pk, obj)` | Busca um registro e faz um deep merge em memória — **não persiste**, retorna o objeto mesclado |
326
- | `remove(pk)` | Remove um registro pela primary key |
327
- | `removeList(pks)` | Remove vários registros pela lista de primary keys — retorna `{ count }` |
328
- | `getAll()` | Retorna todos os registros (aceita `pagination` e `order` no `options`) |
329
- | `total()` | Retorna o total de registros |
330
- | `has(pk)` | Verifica existência de um registro pela primary key — retorna `boolean` |
321
+ | Method | Description |
322
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------|
323
+ | `get(pk)` | Fetches a record by its primary key |
324
+ | `getOrThrow(pk)` | Fetches a record by its primary key; throws `VSRepoRuntimeError` (code `"20727"`) if not found |
325
+ | `getList(pks)` | Fetches multiple records from a list of primary keys |
326
+ | `save(obj)` | Creates or updates — if the object has a `pk` it performs an `upsert`, otherwise a `create` |
327
+ | `saveList(objs)` | Saves an array of objects in a single automatic transaction |
328
+ | `patch(pk, obj)` | Partially updates a record by its primary key |
329
+ | `patchList(tuples)` | Partially updates multiple records via an array of `[pk, obj]` tuples in an automatic transaction |
330
+ | `merge(pk, obj)` | Fetches a record and deep merges it in memory — **does not persist**, returns the merged object |
331
+ | `remove(pk)` | Removes a record by its primary key |
332
+ | `removeList(pks)` | Removes several records by a list of primary keys — returns `{ count }` |
333
+ | `getAll()` | Returns all records (accepts `pagination` and `order` in `options`) |
334
+ | `total()` | Returns the total number of records |
335
+ | `has(pk)` | Checks whether a record exists by its primary key — returns `boolean` |
331
336
 
332
- Todos aceitam `options` como último argumento.
337
+ All of them accept `options` as the last argument.
333
338
 
334
339
  ### Soft-delete
335
340
 
336
- Quando `softRemovekName` está configurado no repository, os seguintes métodos adicionais ficam disponíveis:
341
+ When `softRemovekName` is configured on the repository, the following additional methods become available:
337
342
 
338
- | Método | Descrição |
339
- | -------------------------- | --------------------------------------------------------------------------------- |
340
- | `softRemove(pk)` | Marca um registro como removido preenchendo `softRemovekName` com a data atual |
341
- | `softRemoveList(pks)` | Marca múltiplos registros como removidos em lote — retorna `{ count }` |
342
- | `restore(pk)` | Restaura um registro soft-deletado, limpando o campo `softRemovekName` |
343
- | `restoreList(pks)` | Restaura múltiplos registros soft-deletados em lote — retorna `{ count }` |
343
+ | Method | Description |
344
+ | -------------------------- | ------------------------------------------------------------------------------------|
345
+ | `softRemove(pk)` | Marks a record as removed by filling `softRemovekName` with the current date |
346
+ | `softRemoveList(pks)` | Marks multiple records as removed in batch — returns `{ count }` |
347
+ | `restore(pk)` | Restores a soft-deleted record, clearing the `softRemovekName` field |
348
+ | `restoreList(pks)` | Restores multiple soft-deleted records in batch — returns `{ count }` |
344
349
 
345
350
  ```ts
346
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
347
- tableName: "usuario",
351
+ const userRepository = setupVSRepo<User, "user">()(({
352
+ tableName: "user",
348
353
  pkName: "id",
349
- softRemovekName: "deletedAt", // deve ser um campo DateTime no schema do Prisma
354
+ softRemovekName: "deletedAt", // must be a DateTime field in the Prisma schema
350
355
  }).build(prisma);
351
356
 
352
- await usuarioRepository.softRemove(1);
353
- await usuarioRepository.restore(1);
357
+ await userRepository.softRemove(1);
358
+ await userRepository.restore(1);
354
359
  ```
355
360
 
356
- > O campo informado em `softRemovekName` **deve** ser do tipo `DateTime` no schema do Prisma. O VSRepository valida isso no momento do `build` e lança `VSRepoBuildError` se o tipo for incorreto.
361
+ > The field provided in `softRemovekName` **must** be of type `DateTime` in the Prisma schema. VSRepository validates this at `build` time and throws `VSRepoBuildError` if the type is incorrect.
357
362
 
358
- ### Operações em lote
363
+ ### Batch operations
359
364
 
360
- `saveList` e `patchList` executam todas as operações automaticamente dentro de uma única transação do Prisma. Se alguma falhar, todas as anteriores são revertidas.
365
+ `saveList` and `patchList` automatically run all operations inside a single Prisma transaction. If any operation fails, all previous ones are rolled back.
361
366
 
362
367
  ```ts
363
- // saveList — cria ou atualiza múltiplos objetos em transação automática
364
- const usuarios = await usuarioRepository.saveList([
365
- { nome: "Maria", email: "maria@email.com" },
366
- { id: 2, nome: "João Atualizado" },
368
+ // saveList — creates or updates multiple objects in an automatic transaction
369
+ const users = await userRepository.saveList([
370
+ { name: "Mary", email: "mary@email.com" },
371
+ { id: 2, name: "John Updated", email: "john@email.com" },
367
372
  ]);
368
373
 
369
- // patchList — atualiza parcialmente múltiplos registros via tuplas [pk, obj]
370
- const atualizados = await usuarioRepository.patchList([
371
- [1, { ativo: false }],
372
- [2, { nome: "Novo Nome" }],
374
+ // patchList — partially updates multiple records via [pk, obj] tuples
375
+ const updated = await userRepository.patchList([
376
+ [1, { active: false }],
377
+ [2, { name: "New Name" }],
373
378
  ]);
374
379
  ```
375
380
 
376
- Quando você já está dentro de uma transação existente, passe-a em `options.db`. Nesse caso, o `db` deve ser um `DbTransaction` (não o cliente principal), pois o método não cria uma transação própria:
381
+ When you're already inside an existing transaction, pass it in `options.db`. In this case, `db` must be a `DbTransaction` (not the main client):
377
382
 
378
383
  ```ts
379
384
  await prisma.$transaction(async (tx) => {
380
- await usuarioRepository.saveList([{ nome: "Maria" }], { db: tx });
381
- await usuarioRepository.patchList([[1, { ativo: false }]], { db: tx });
385
+ await userRepository.saveList([{ name: "Mary" }, { name: "Gus" }], { db: tx });
386
+ await userRepository.patchList([[1, { active: false }], [2, { active: true }]], { db: tx });
382
387
  });
383
388
  ```
384
389
 
385
390
  ### Merge
386
391
 
387
- O método `merge` busca um registro pela PK e mescla profundamente (`deepmerge`) o objeto fornecido com os dados existentes **em memória**. Ele **não persiste** as alterações — retorna o resultado mesclado para que você decida o que fazer com ele.
392
+ The `merge` method fetches a record by its PK and deeply merges (`deepmerge`) the provided object with the existing data **in memory**. It **does not persist** the changes — it returns the merged result so you can decide what to do with it.
388
393
 
389
394
  ```ts
390
- const existente = await usuarioRepository.get(1);
391
- // existente: { id: 1, nome: "Maria", perfil: { bio: "Olá", idade: 25 } }
395
+ const existing = await userRepository.get(1);
396
+ // existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
392
397
 
393
- const mesclado = await usuarioRepository.merge(1, {
394
- perfil: { bio: "Bio atualizada" },
398
+ const merged = await userRepository.merge(1, {
399
+ profile: { bio: "Updated bio" },
395
400
  });
396
- // mesclado: { id: 1, nome: "Maria", perfil: { bio: "Bio atualizada", idade: 25 } }
401
+ // merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
402
+
403
+ // To persist, pass it to save or patch:
404
+ await userRepository.save(merged);
405
+ ```
397
406
 
398
- // Para persistir, passe para save ou patch:
399
- await usuarioRepository.save(mesclado);
407
+ Returns `null` if the record is not found.
408
+
409
+ **Merging to-many relations (`otm`/`mtm`) is done by PK, not by simple concatenation.** For to-one relations (`oto`/`mto`), `merge` performs a regular deep merge of the object. For to-many relations, each item in the sent array is matched against the existing item that has the same PK (defined in `relations[key].pk`): if the PK matches, the two objects are merged together; if it doesn't match (a new item with no counterpart), it's simply added to the list. Existing items that don't appear in the sent array are kept.
410
+
411
+ ```ts
412
+ const existing = await userRepository.get(1);
413
+ // existing: {
414
+ // id: 1,
415
+ // posts: [
416
+ // { id: 10, title: "Post A", published: false },
417
+ // { id: 11, title: "Post B", published: true },
418
+ // ],
419
+ // }
420
+
421
+ const merged = await userRepository.merge(1, {
422
+ posts: [
423
+ { id: 10, published: true }, // same PK (id: 10) → merges with the existing item
424
+ { title: "Post C" }, // no PK → added as a new item
425
+ ],
426
+ });
427
+ // merged: {
428
+ // id: 1,
429
+ // posts: [
430
+ // { id: 10, title: "Post A", published: true }, // merged
431
+ // { id: 11, title: "Post B", published: true }, // kept, wasn't in the sent array
432
+ // { title: "Post C" }, // added
433
+ // ],
434
+ // }
400
435
  ```
401
436
 
402
- Retorna `null` se o registro não for encontrado.
437
+ > Note that `merge` never removes items from a to-many relation — it only merges the ones that match by PK and adds the ones that don't. To remove items from a relation, use `save`/`patch` with `restriction: "set"` in the relation configuration.
438
+
439
+ ### Configuring the base methods
403
440
 
404
- ### Configurando os métodos base
441
+ The second argument of `.build(prisma, config)` lets you adjust the repository's global behavior and customize each base method individually through `baseMethods`.
405
442
 
406
443
  ```ts
407
- usuarioVSRepo.build(prisma, {
408
- showWorking: true, // Exibe logs do VSRepository no console, ótimo para debugar
444
+ userVSRepo.build(prisma, {
445
+ // Shows VSRepository's internal logs on the console (built queries, detected prefix,
446
+ // applied filters, etc). Great for debugging dynamic methods. Default = false.
447
+ showWorking: true,
409
448
 
410
449
  baseMethods: {
411
450
  get: {
451
+ // Enables/disables the method on the final repository. If `false`, the method
452
+ // doesn't even appear in the repository's type (it's not just a runtime error). Default = true.
412
453
  active: true,
454
+
455
+ // Select model applied by default when the method is called without `options.selectModel`.
456
+ // Overrides the `defaultSelectModel` from setupVSRepo for this method only.
413
457
  defaultSelect: "public",
414
458
  },
415
459
  remove: {
416
460
  active: true,
417
461
  defaultSelect: "minimal",
462
+
463
+ // When `true`, ignores the `requiredWhere` configured in setupVSRepo for
464
+ // this specific method — useful when a method needs to "punch through" a
465
+ // global filter (e.g. multi-tenancy) in a specific case. Default = false.
418
466
  ignoreRequiredWhere: false,
419
467
  },
420
468
  save: {
469
+ // Here only `ignoreRequiredWhere` is set — `active` and `defaultSelect`
470
+ // keep their defaults (true and the global `defaultSelectModel`).
421
471
  ignoreRequiredWhere: true,
422
472
  },
423
473
  patch: {
474
+ // Only the select is overridden; the method stays active normally.
424
475
  defaultSelect: "minimal",
425
476
  },
426
477
  has: {
427
- active: false, // Desativa o 'has' (padrão = true)
478
+ active: false, // Disables 'has' (default = true) — the method disappears from the repository
428
479
  },
429
480
  softRemove: {
481
+ // Soft-delete methods follow the same options (`active`, `defaultSelect`,
482
+ // `ignoreRequiredWhere`). They're only available if `softRemovekName` is configured.
430
483
  active: true,
431
484
  defaultSelect: "minimal",
432
485
  },
@@ -434,176 +487,176 @@ usuarioVSRepo.build(prisma, {
434
487
  });
435
488
  ```
436
489
 
490
+ > Batch/aggregate methods like `removeList`, `softRemoveList`, `restoreList`, `total`, and `has` **do not** accept `defaultSelect` (they don't return a selectable record — they return `{ count }` or `boolean`). In these cases `BaseMethodConfig` is restricted to `active` and `ignoreRequiredWhere`.
491
+
437
492
  ---
438
493
 
439
494
  ## Select Models
440
495
 
441
- `selectModels` define projeções de dados nomeadas e reutilizáveis.
496
+ `selectModels` defines named, reusable data projections.
442
497
 
443
498
  ```ts
444
499
  selectModels: {
445
- public: { id: true, nome: true, email: true },
446
- internal: { id: true, nome: true, email: true, senha: true },
500
+ public: { id: true, name: true, email: true },
501
+ internal: { id: true, name: true, email: true, password: true },
447
502
  minimal: { id: true },
448
503
  },
449
504
  defaultSelectModel: "public",
450
505
  ```
451
506
 
452
- `defaultSelectModel` define qual select é usado automaticamente quando nenhum é especificado na chamada. É recomendado sempre definí-lo junto com `selectModels`.
507
+ `defaultSelectModel` defines which select is used automatically when none is specified in the call. It's recommended to always define it together with `selectModels`.
453
508
 
454
- **Usando um select específico na chamada:**
509
+ **Using a specific select in the call:**
455
510
 
456
511
  ```ts
457
- const usuario = await usuarioRepository.get(id, { selectModel: "minimal" });
512
+ const user = await userRepository.get(id, { selectModel: "minimal" });
458
513
  ```
459
514
 
460
- **Retornando o payload padrão do Prisma (sem select):**
515
+ **Returning Prisma's default payload (without select):**
461
516
 
462
517
  ```ts
463
- const usuarioCompleto = await usuarioRepository.get(id, { selectModel: false });
518
+ const fullUser = await userRepository.get(id, { selectModel: false });
464
519
  ```
465
520
 
466
521
  ---
467
522
 
468
523
  ## Include Models
469
524
 
470
- `includeModels` funciona de forma parecida com o `selectModels`, mas em vez de receber um `select`, ele recebe um `include` válido do Prisma.
525
+ `includeModels` works similarly to `selectModels`, but instead of receiving a `select`, it receives a valid Prisma `include`.
471
526
 
472
527
  ```ts
473
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
474
- tableName: "usuario",
528
+ const userRepository = setupVSRepo<User, "user">()(({
529
+ tableName: "user",
475
530
  pkName: "id",
476
531
  selectModels: {
477
- public: { id: true, nome: true, email: true },
532
+ public: { id: true, name: true, email: true },
478
533
  },
479
534
  defaultSelectModel: "public",
480
535
  includeModels: {
481
- comPosts: { posts: true },
482
- comPostsEPerfil: { posts: true, perfil: true },
536
+ withPosts: { posts: true },
537
+ withPostsAndProfile: { posts: true, profile: true },
483
538
  },
484
539
  }).build(prisma);
485
540
  ```
486
541
 
487
- **Usando um `includeModel` na chamada:**
542
+ **Using an `includeModel` in the call:**
488
543
 
489
544
  ```ts
490
- const usuario = await usuarioRepository.get(id, { includeModel: "comPosts" });
545
+ const user = await userRepository.get(id, { includeModel: "withPosts" });
491
546
  ```
492
547
 
493
- Nesse caso, o `select` padrão (`selectModels`/`defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
548
+ In this case, the default `select` (`selectModels`/`defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
494
549
 
495
- ### Diferenças em relação ao `selectModels`
550
+ ### Differences from `selectModels`
496
551
 
497
- - **Só pode ser passado na chamada do método**, via `options.includeModel`. Não existe `defaultIncludeModel` nem `defaultInclude` — não há como configurar um `includeModel` padrão no repository, diferente do que ocorre com `defaultSelectModel`.
498
- - **`includeModel` e `selectModel` não podem ser passados juntos** na mesma chamada. Se um `includeModel` for informado, qualquer `selectModel` (incluindo o padrão) é ignorado.
552
+ - **Can only be passed in the method call**, via `options.includeModel`. There's no `defaultIncludeModel` or `defaultInclude` — there's no way to configure a default `includeModel` on the repository, unlike what happens with `defaultSelectModel`.
553
+ - **`includeModel` and `selectModel` cannot be passed together** in the same call. If an `includeModel` is provided, any `selectModel` (including the default one) is ignored.
499
554
 
500
555
  ```ts
501
- // CORRETO ✅ — apenas includeModel
502
- await usuarioRepository.get(id, { includeModel: "comPosts" });
556
+ // CORRECT ✅ — includeModel only
557
+ await userRepository.get(id, { includeModel: "withPosts" });
503
558
 
504
- // CORRETO ✅ — apenas selectModel
505
- await usuarioRepository.get(id, { selectModel: "public" });
559
+ // CORRECT ✅ — selectModel only
560
+ await userRepository.get(id, { selectModel: "public" });
506
561
 
507
- // ERRADO ❌ — não é permitido combinar os dois
508
- await usuarioRepository.get(id, { selectModel: "public", includeModel: "comPosts" });
562
+ // WRONG ❌ — combining both is not allowed
563
+ await userRepository.get(id, { selectModel: "public", includeModel: "withPosts" });
509
564
  ```
510
565
 
511
566
  ---
512
567
 
513
568
  ## Required Where
514
569
 
515
- `requiredWhere` define filtros aplicados automaticamente em todas as queries do repository.
570
+ `requiredWhere` defines filters that are automatically applied to every query on the repository.
516
571
 
517
572
  ```ts
518
- requiredWhere: { ativo: true },
573
+ requiredWhere: { active: true },
519
574
  ```
520
575
 
521
- Agora toda query incluirá `ativo: true` automaticamente:
576
+ Now every query will automatically include `active: true`:
522
577
 
523
578
  ```ts
524
- // Internamente: WHERE ativo = true
525
- const usuarios = await usuarioRepository.findMany();
579
+ // Internally: WHERE active = true
580
+ const users = await userRepository.findMany();
526
581
 
527
- // Internamente: WHERE email = 'joao@email.com' AND ativo = true
528
- const usuario = await usuarioRepository.findByEmail("joao@email.com");
582
+ // Internally: WHERE email = 'john@email.com' AND active = true
583
+ const user = await userRepository.findByEmail("john@email.com");
529
584
  ```
530
585
 
531
- Útil para soft-deletes manuais, multi-tenancy e filtros globais de qualquer natureza.
586
+ Useful for manual soft-deletes, multi-tenancy, and global filters of any kind.
532
587
 
533
588
  ---
534
589
 
535
590
  ## Default Ordenation
536
591
 
537
- `defaultOrdenation` define uma ordenação padrão aplicada automaticamente em todas as queries que aceitam `orderBy`, sem precisar repetir o argumento `order` em cada chamada.
592
+ `defaultOrdenation` defines a default ordering that's automatically applied to every query that accepts `orderBy`, without needing to repeat the `order` argument on every call.
538
593
 
539
594
  ```ts
540
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
541
- tableName: "usuario",
595
+ const userRepository = setupVSRepo<User, "user">()(({
596
+ tableName: "user",
542
597
  pkName: "id",
543
- defaultOrdenation: { criadoEm: "desc" },
598
+ defaultOrdenation: { createdAt: "desc" },
544
599
  }).build(prisma);
545
600
  ```
546
601
 
547
- Com isso, toda query de listagem já virá ordenada por `criadoEm` decrescente:
602
+ With this, every listing query will already come ordered by `createdAt` descending:
548
603
 
549
604
  ```ts
550
- // Internamente: ORDER BY criadoEm DESC
551
- const usuarios = await usuarioRepository.getAll();
605
+ // Internally: ORDER BY createdAt DESC
606
+ const users = await userRepository.getAll();
552
607
 
553
- // Também aplica ao getAll com pagination
554
- const paginados = await usuarioRepository.getAll({ pagination: { take: 10 } });
608
+ // Also applies to getAll with pagination
609
+ const paginated = await userRepository.getAll({ pagination: { take: 10 } });
555
610
  ```
556
611
 
557
- **A `defaultOrdenation` é ignorada quando:**
612
+ **`defaultOrdenation` is ignored when:**
558
613
 
559
- - O método usa o sufixo `Ordered`, `OrderedAndPaginated` ou `PaginatedAndOrdered` — nesses casos o argumento `order` passado na chamada tem prioridade.
560
- - O método dinâmico tem `injectOrdenation` configurado — a ordenação fixa do método prevalece.
614
+ - The method uses the `Ordered`, `OrderedAndPaginated`, or `PaginatedAndOrdered` suffix — in these cases the `order` argument passed in the call takes priority.
615
+ - The dynamic method has `injectOrdenation` configured — the method's fixed ordering takes precedence.
561
616
 
562
617
  ```ts
563
618
  methods: {
564
- findManyPaginatedAndOrdered: { map: true }, // ordem vem do argumento → defaultOrdenation ignorada
565
- findManyByAtivo: { map: true }, // sem Ordered → defaultOrdenation aplicada
619
+ findManyPaginatedAndOrdered: { map: true }, // order comes from the argument → defaultOrdenation ignored
620
+ findManyByActive: { map: true }, // no Ordered → defaultOrdenation applied
566
621
  findManyByStatus: {
567
622
  map: true,
568
- injectOrdenation: { nome: "asc" }, // injectOrdenation → defaultOrdenation ignorada
623
+ injectOrdenation: { name: "asc" }, // injectOrdenation → defaultOrdenation ignored
569
624
  },
570
625
  }
571
626
  ```
572
627
 
573
- > `defaultOrdenation` aceita o mesmo tipo que o `orderBy` nativo do Prisma para o modelo — incluindo arrays de ordenações encadeadas.
628
+ > `defaultOrdenation` accepts the same type as Prisma's native `orderBy` for the model — including arrays of chained orderings.
574
629
 
575
630
  ---
576
631
 
577
- Quando `softRemovekName` está configurado, todos os métodos base aceitam a opção `see` para controlar a visibilidade de registros soft-deletados:
632
+ ## `see` option
578
633
 
579
- | Valor | Comportamento |
580
- | ----------- | ------------------------------------------------------------- |
581
- | `"active"` | Retorna apenas registros **não** removidos (padrão) |
582
- | `"removed"` | Retorna apenas registros removidos |
583
- | `"all"` | Retorna todos os registros, independentemente do status |
634
+ When `softRemovekName` is configured, every method accepts the `see` option to control the visibility of soft-deleted records:
584
635
 
585
- ```ts
586
- // Retorna apenas usuários ativos (padrão)
587
- const ativos = await usuarioRepository.getAll();
636
+ | Value | Behavior |
637
+ | ----------- | --------------------------------------------------------------|
638
+ | `"active"` | Returns only records that are **not** removed (default) |
639
+ | `"removed"` | Returns only removed records |
640
+ | `"all"` | Returns all records, regardless of status |
588
641
 
589
- // Retorna apenas usuários removidos
590
- const removidos = await usuarioRepository.getAll({ see: "removed" });
642
+ ```ts
643
+ // Returns only active users (default)
644
+ const active = await userRepository.getAll();
591
645
 
592
- // Retorna todos
593
- const todos = await usuarioRepository.getAll({ see: "all" });
646
+ // Returns only removed users
647
+ const removed = await userRepository.getAll({ see: "removed" });
594
648
 
595
- // Funciona em qualquer método base
596
- const usuario = await usuarioRepository.get(id, { see: "all" });
597
- const existe = await usuarioRepository.has(id, { see: "removed" });
649
+ // Returns all
650
+ const all = await userRepository.getAll({ see: "all" });
598
651
  ```
599
652
 
600
- > A opção `see` funciona independentemente do `requiredWhere` — ela é aplicada em cima do filtro de soft-delete, não o substitui.
653
+ > The `see` option works independently of `requiredWhere` — it's applied on top of the soft-delete filter, not as a replacement for it.
601
654
 
602
655
  ---
603
656
 
604
- ## Métodos dinâmicos
657
+ ## Dynamic methods
605
658
 
606
- Métodos dinâmicos são definidos na propriedade `methods` e têm seus comportamentos inferidos a partir do nome.
659
+ Dynamic methods are defined in the `methods` property and have their behavior inferred from their name.
607
660
 
608
661
  ```ts
609
662
  methods: {
@@ -616,217 +669,358 @@ methods: {
616
669
 
617
670
  ---
618
671
 
619
- ### Prefixos disponíveis
620
-
621
- O prefixo do nome do método determina qual operação Prisma será chamada e quais argumentos serão esperados.
622
-
623
- | Prefixo | Operação Prisma | Retorno | Observações |
624
- | -------------------------- | ------------------------- | ---------------------- | ------------------------------------------------------------------------ |
625
- | `findOneBy` | `findFirst` | `T \| null` | Retorno único. |
626
- | `findBy` | `findMany` / `findFirst` | `T[]` ou `T \| null` | Padrão é lista; use `fbMode: "one"` para retorno único (**obsoleto**, use `findOneBy`) |
627
- | `findUniqueBy` | `findUnique` | `T \| null` | |
628
- | `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Lança erro se não encontrar |
629
- | `findFirstBy` | `findFirst` | `T \| null` | Aceita campos como filtro |
630
- | `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Aceita campos como filtro; lança erro se não encontrar |
631
- | `findFirst` | `findFirst` | `T \| null` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
632
- | `findFirstOrThrow` | `findFirstOrThrow` | `T` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere`; lança erro se não encontrar |
633
- | `findManyBy` | `findMany` | `T[]` | Aceita campos como filtro |
634
- | `findMany` | `findMany` | `T[]` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
635
- | `findOneWhere` | `findFirst` | `T \| null` | Recebe um objeto `where` explícito como argumento |
636
- | `findWhere` | `findFirst` | `T \| null` | (**Obsoleto, use `findOneWhere`**) Recebe um objeto `where` explícito |
637
- | `findListWhere` | `findMany` | `T[]` | Recebe um objeto `where` explícito como argumento |
638
- | `existsBy` | `findFirst` | `boolean` | Retorna `true` se encontrar, `false` caso contrário |
639
- | `existsWhere` | `findFirst` | `boolean` | Recebe um objeto `where` explícito e retorna se existe |
640
- | `countBy` | `count` | `number` | Aceita campos como filtro |
641
- | `countWhere` | `count` | `number` | Recebe um objeto `where` explícito como argumento |
642
- | `count` | `count` | `number` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
643
- | `create` | `create` | `T` | Recebe `data` como argumento |
644
- | `createMany` | `createMany` | `{ count: number }` | Recebe `data` como argumento; suporta `SkipDuplicates` |
645
- | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Recebe `data` como argumento; suporta `SkipDuplicates` |
646
- | `updateBy` | `update` | `T` | Recebe `data` como argumento |
647
- | `updateManyBy` | `updateMany` | `{ count: number }` | Recebe `data` como argumento |
648
- | `updateManyWhere` | `updateMany` | `{ count: number }` | Recebe um objeto `where` e um objeto `data` como argumentos |
649
- | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Recebe `data` como argumento |
650
- | `updateManyAndReturnWhere` | `updateManyAndReturn` | `T[]` | Recebe um objeto `where` e um objeto `data` como argumentos |
651
- | `upsertBy` | `upsert` | `T` | Recebe `update` e `create` como argumentos |
652
- | `deleteBy` | `delete` | `T` | |
653
- | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
654
- | `deleteManyWhere` | `deleteMany` | `{ count: number }` | Recebe um objeto `where` explícito como argumento |
655
- | `aggregate` | `aggregate` | `Dinâmico` | Nome deve ser exato; recebe args nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
656
- | `groupBy` | `groupBy` | `Dinâmico[]` | Nome deve ser exato; recebe args nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
672
+ ### Available prefixes
673
+
674
+ The method name's prefix determines which Prisma operation will be called and which arguments are expected.
675
+
676
+ | Prefix | Prisma operation | Return | Notes |
677
+ | ---------------------------- | -------------------------- | ------------------------ | ---------------------------------------------------------------------------|
678
+ | `findOneBy` | `findFirst` | `T \| null` | Single return. |
679
+ | `findBy` | `findMany` / `findFirst` | `T[]` or `T \| null` | Default is list; use `fbMode: "one"` for a single return (**deprecated**, use `findOneBy`) |
680
+ | `findUniqueBy` | `findUnique` | `T \| null` | |
681
+ | `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Throws an error if not found |
682
+ | `findFirstBy` | `findFirst` | `T \| null` | Accepts fields as filter |
683
+ | `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Accepts fields as filter; throws an error if not found |
684
+ | `findFirst` | `findFirst` | `T \| null` | No field filters; applies only `requiredWhere` and `pushWhere` |
685
+ | `findFirstOrThrow` | `findFirstOrThrow` | `T` | No field filters; applies only `requiredWhere` and `pushWhere`; throws an error if not found |
686
+ | `findManyBy` | `findMany` | `T[]` | Accepts fields as filter |
687
+ | `findMany` | `findMany` | `T[]` | No field filters; applies only `requiredWhere` and `pushWhere` |
688
+ | `findOneWhere` | `findFirst` | `T \| null` | Receives an explicit `where` object as argument |
689
+ | `findListWhere` | `findMany` | `T[]` | Receives an explicit `where` object as argument |
690
+ | `existsBy` | `findFirst` | `boolean` | Returns `true` if found, `false` otherwise |
691
+ | `existsWhere` | `findFirst` | `boolean` | Receives an explicit `where` object and returns whether it exists |
692
+ | `countBy` | `count` | `number` | Accepts fields as filter |
693
+ | `countWhere` | `count` | `number` | Receives an explicit `where` object as argument |
694
+ | `count` | `count` | `number` | No field filters; applies only `requiredWhere` and `pushWhere` |
695
+ | `create` | `create` | `T` | Receives `data` as argument |
696
+ | `createMany` | `createMany` | `{ count: number }` | Receives `data` as argument; supports `SkipDuplicates` |
697
+ | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Receives `data` as argument; supports `SkipDuplicates` |
698
+ | `updateBy` | `update` | `T` | Receives `data` as argument |
699
+ | `updateManyBy` | `updateMany` | `{ count: number }` | Receives `data` as argument |
700
+ | `updateManyWhere` | `updateMany` | `{ count: number }` | Receives a `where` object and a `data` object as arguments |
701
+ | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Receives `data` as argument |
702
+ | `updateManyAndReturnWhere` | `updateManyAndReturn` | `T[]` | Receives a `where` object and a `data` object as arguments |
703
+ | `upsertBy` | `upsert` | `T` | Receives `update` and `create` as arguments |
704
+ | `deleteBy` | `delete` | `T` | |
705
+ | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
706
+ | `deleteManyWhere` | `deleteMany` | `{ count: number }` | Receives an explicit `where` object as argument |
707
+ | `aggregate` | `aggregate` | `Dynamic` | Name must be exact; receives native Prisma args; ignores `selectModels`, `pushWhere`, and `requiredWhere` |
708
+ | `groupBy` | `groupBy` | `Dynamic[]` | Name must be exact; receives native Prisma args; ignores `selectModels`, `pushWhere`, and `requiredWhere` |
657
709
 
658
710
  ---
659
711
 
660
- ### Filtros de campo
661
-
662
- Os filtros são sufixos aplicados ao nome do campo dentro do método. O campo em si vem capitalizado logo após o prefixo (ou após `By`).
663
-
664
- | Sufixo | Operador Prisma | Argumento necessário |
665
- | ------------------ | --------------------- | ------------------------- |
666
- | *(sem sufixo)* | igualdade (`=`) | sim |
667
- | `Not` | `not` | sim |
668
- | `In` | `in` | sim (array) |
669
- | `NotIn` | `notIn` | sim (array) |
670
- | `Contains` | `contains` | sim |
671
- | `NotContains` | `not.contains` | sim |
672
- | `StartsWith` | `startsWith` | sim |
673
- | `NotStartsWith` | `not.startsWith` | sim |
674
- | `EndsWith` | `endsWith` | sim |
675
- | `NotEndsWith` | `not.endsWith` | sim |
676
- | `GreaterThan` | `gt` | sim |
677
- | `GreaterThanEqual` | `gte` | sim |
678
- | `LessThan` | `lt` | sim |
679
- | `LessThanEqual` | `lte` | sim |
680
- | `Between` | `gte` + `lte` | sim (tupla `[min, max]`) |
681
- | `NotBetween` | `not.gte` + `not.lte` | sim (tupla `[min, max]`) |
682
- | `IsNull` | `null` | não |
683
- | `IsNotNull` | `not: null` | não |
684
- | `IsTrue` | `true` | não |
685
- | `IsFalse` | `false` | não |
686
- | `Insensitive` | `mode: 'insensitive'` | combinador |
687
-
688
- `Insensitive` é um combinador e pode ser usado junto com outro filtro de texto:
712
+ ### Field filters
713
+
714
+ Filters are suffixes applied to the field name inside the method. The field itself comes capitalized right after the prefix (or after `By`).
715
+
716
+ | Suffix | Prisma operator | Argument required |
717
+ | -------------------- | ---------------------- | ---------------------------|
718
+ | *(no suffix)* | equality (`=`) | yes |
719
+ | `Not` | `not` | yes |
720
+ | `In` | `in` | yes (array) |
721
+ | `NotIn` | `notIn` | yes (array) |
722
+ | `Contains` | `contains` | yes |
723
+ | `NotContains` | `not.contains` | yes |
724
+ | `StartsWith` | `startsWith` | yes |
725
+ | `NotStartsWith` | `not.startsWith` | yes |
726
+ | `EndsWith` | `endsWith` | yes |
727
+ | `NotEndsWith` | `not.endsWith` | yes |
728
+ | `GreaterThan` | `gt` | yes |
729
+ | `GreaterThanEqual` | `gte` | yes |
730
+ | `LessThan` | `lt` | yes |
731
+ | `LessThanEqual` | `lte` | yes |
732
+ | `Between` | `gte` + `lte` | yes (tuple `[min, max]`) |
733
+ | `NotBetween` | `not.gte` + `not.lte` | yes (tuple `[min, max]`) |
734
+ | `IsNull` | `null` | no |
735
+ | `IsNotNull` | `not: null` | no |
736
+ | `IsTrue` | `true` | no |
737
+ | `IsFalse` | `false` | no |
738
+ | `Insensitive` | `mode: 'insensitive'` | combinator |
739
+
740
+ `Insensitive` is a combinator and can be used together with another text filter:
689
741
 
690
742
  ```ts
691
- findByNomeContainsInsensitive // { nome: { contains: valor, mode: 'insensitive' } }
692
- findByEmailStartsWithInsensitive // { email: { startsWith: valor, mode: 'insensitive' } }
693
- findByNomeInsensitive // { nome: { equals: valor, mode: 'insensitive' } }
743
+ findByNameContainsInsensitive // { name: { contains: value, mode: 'insensitive' } }
744
+ findByEmailStartsWithInsensitive // { email: { startsWith: value, mode: 'insensitive' } }
745
+ findByNameInsensitive // { name: { equals: value, mode: 'insensitive' } }
694
746
  ```
695
747
 
696
- `Between` e `NotBetween` recebem uma **tupla `[minValue, maxValue]`**:
748
+ `Between` and `NotBetween` receive a **tuple `[minValue, maxValue]`**:
697
749
 
698
750
  ```ts
699
751
  methods: {
700
- findManyByIdadeBetween: { map: true },
701
- findManyBySalarioNotBetween: { map: true },
702
- findManyByCriadoEmBetween: { map: true },
752
+ findManyByAgeBetween: { map: true },
753
+ findManyBySalaryNotBetween: { map: true },
754
+ findManyByCreatedAtBetween: { map: true },
703
755
  }
704
756
 
705
- await usuarioRepository.findManyByIdadeBetween([18, 65]);
706
- await usuarioRepository.findManyBySalarioNotBetween([1000, 5000]);
707
- await usuarioRepository.findManyByCriadoEmBetween([new Date("2024-01-01"), new Date("2024-12-31")]);
757
+ await userRepository.findManyByAgeBetween([18, 65]);
758
+ await userRepository.findManyBySalaryNotBetween([1000, 5000]);
759
+ await userRepository.findManyByCreatedAtBetween([new Date("2024-01-01"), new Date("2024-12-31")]);
708
760
  ```
709
761
 
710
- O sufixo `Optional` pode ser adicionado a qualquer campo para tornar o argumento opcional:
762
+ The `Optional` suffix can be added to any field to make the argument optional:
711
763
 
712
764
  ```ts
713
- findByNomeOptionalAndEmail // nome é opcional, email é obrigatório
765
+ findByNameOptionalAndEmail // name is optional, email is required
714
766
  ```
715
767
 
716
768
  ---
717
769
 
718
- ### Operadores lógicos
770
+ ### Logical operators
719
771
 
720
- | Operador | Uso no nome | Exemplo |
721
- | --------- | ---------------------------- | -------------------------------- |
722
- | `And` | entre dois campos | `findOneByIdAndEmail` |
723
- | `Or` | entre dois campos | `findByNomeOrEmail` |
724
- | `AND` | separa bloco final em `AND` | `findByEmailOrNameANDActiveStatus` |
772
+ | Operator | Usage in the name | Example |
773
+ | --------- | ------------------------------ | -----------------------------------|
774
+ | `And` | between two fields | `findOneByIdAndEmail` |
775
+ | `Or` | between two fields | `findByNameOrEmail` |
776
+ | `AND` | separates a final `AND` block | `findByEmailOrNameANDActiveStatus` |
725
777
 
726
- `AND` (em capslock) tem uma regra específica:
778
+ `AND` (in caps) has a specific rule:
727
779
 
728
- - Só pode existir **um** `AND` por método.
729
- - Todos os campos depois de `AND` são injetados dentro de `AND: []`.
730
- - Depois de um `AND` não pode ter `Or`.
780
+ - Only **one** `AND` can exist per method.
781
+ - All fields after `AND` are injected inside `AND: []`.
782
+ - After an `AND`, there can't be an `Or`.
731
783
 
732
- Exemplo:
784
+ Example:
733
785
 
734
786
  ```ts
735
787
  methods: {
736
788
  findOneByIdAndEmail: { map: true },
737
- findByNomeOrEmail: { map: true },
738
- findUniqueByIdOrEmailAndNome: { map: true },
739
- findByEmailOrNameANDActiveStatusAndIdadeGreaterThan: { map: true }
789
+ findByNameOrEmail: { map: true },
790
+ findFirstByIdOrEmailAndName: { map: true },
791
+ findByEmailOrNameANDActiveStatusAndAgeGreaterThan: { map: true }
740
792
  }
741
793
 
742
- await usuarioRepository.findOneByIdAndEmail(1, "joao@email.com");
743
- await usuarioRepository.findByNomeOrEmail("Joao", "joao@email.com");
744
- await usuarioRepository.findUniqueByIdOrEmailAndNome(1, "joao@email.com", "Joao");
745
- await usuarioRepository.findByEmailOrNameANDActiveStatusAndIdadeGreaterThan("joao@email.com", "Joao", true, 17)
794
+ await userRepository.findOneByIdAndEmail(1, "john@email.com");
795
+ await userRepository.findByNameOrEmail("John", "john@email.com");
796
+ await userRepository.findFirstByIdOrEmailAndName(1, "john@email.com", "John");
797
+ await userRepository.findByEmailOrNameANDActiveStatusAndAgeGreaterThan("john@email.com", "John", true, 17)
798
+ ```
799
+
800
+ Generates (`findOneByIdAndEmail`):
801
+
802
+ ```ts
803
+ {
804
+ id: 1,
805
+ email: "john@email.com"
806
+ }
746
807
  ```
747
808
 
748
- Gera (`findByEmailOrNameANDActiveStatusAndIdadeGreaterThan`):
809
+ Generates (`findByNameOrEmail`):
749
810
 
750
811
  ```ts
751
812
  {
752
813
  OR: [
753
- { email: "joao@email.com" },
754
- { name: "Joao" }
814
+ { name: "John" },
815
+ { email: "john@email.com" }
816
+ ]
817
+ }
818
+ ```
819
+
820
+ Generates (`findFirstByIdOrEmailAndName`):
821
+
822
+ ```ts
823
+ {
824
+ OR: [
825
+ { id: 1 },
826
+ {
827
+ email: "john@email.com",
828
+ name: "John"
829
+ }
830
+ ]
831
+ }
832
+ ```
833
+
834
+ Generates (`findByEmailOrNameANDActiveStatusAndAgeGreaterThan`):
835
+
836
+ ```ts
837
+ {
838
+ OR: [
839
+ { email: "john@email.com" },
840
+ { name: "John" }
755
841
  ],
756
842
  AND: [
757
843
  { activeStatus: true },
758
- { idade: { gt: 17 } }
844
+ { age: { gt: 17 } }
759
845
  ]
760
846
  }
761
847
  ```
762
848
 
763
849
  ---
764
850
 
765
- ### Filtros de relação
851
+ ### Relation filters
766
852
 
767
- Permitem filtrar por campos de modelos relacionados.
853
+ Allow filtering by fields of related models.
768
854
 
769
855
  > [!IMPORTANT]
770
- > - **Tipagem de relação**: Para que o TypeScript reconheça os tipos dos campos de relação nos métodos dinâmicos, o tipo genérico da entidade passado no `setupVSRepo` deve incluir as relações estruturadas (ex: usando `UsuarioGetPayload<{ include: { perfil: true, postagens: true } }>` do Prisma).
771
- > - **Compatibilidade de sufixos**:
772
- > - Os sufixos `Some`, `Every` e `None` só funcionam para relações **to-many** (`many-to-many` e `one-to-many`).
773
- > - Os sufixos `With` e `Without` só funcionam para relações **to-one** (`one-to-one` e `many-to-one`).
774
-
775
- | Sufixo de relação | Operador Prisma | Observação |
776
- | ---------------------- | --------------- | -------------------------------------------------- |
777
- | `Some` | `some: {}` | Relação tem *algum* registro |
778
- | `SomeField` | `some.field` | Filtra dentro dos registros da relação |
779
- | `EveryField` | `every.field` | Filtra dentro dos registros da relação |
780
- | `None` | `none: {}` | Relação não tem *nenhum* registro |
781
- | `NoneField` | `none.field` | Filtra dentro dos registros da relação |
782
- | `With` | `is: {}` | Relação existe (não é null) |
783
- | `WithField` | `is.field` | Filtra campo dentro da relação |
784
- | `Without` | `isNot: {}` | Relação não existe (é null) |
785
- | `WithoutField` | `isNot.field` | Filtra campo dentro da relação com negação |
856
+ > - **Relation typing**: For TypeScript to recognize the types of relation fields in dynamic methods, the generic entity type passed to `setupVSRepo` must include the structured relations (e.g. using Prisma's `UserGetPayload<{ include: { profile: true, posts: true } }>`).
857
+ > - **Suffix compatibility**:
858
+ > - The `Some`, `Every`, and `None` suffixes only work for **to-many** relations (`many-to-many` and `one-to-many`).
859
+ > - The `With` and `Without` suffixes only work for **to-one** relations (`one-to-one` and `many-to-one`).
860
+
861
+ | Relation suffix | Prisma operator | Note |
862
+ | ------------------------ | ----------------- | -------------------------------------------------------|
863
+ | `Some` | `some: {}` | Relation has *some* record |
864
+ | `SomeField` | `some.field` | Filters within the relation's records |
865
+ | `EveryField` | `every.field` | Filters within the relation's records |
866
+ | `None` | `none: {}` | Relation has *no* records |
867
+ | `NoneField` | `none.field` | Filters within the relation's records |
868
+ | `With` | `is: {}` | Relation exists (not null) |
869
+ | `WithField` | `is.field` | Filters a field within the relation |
870
+ | `Without` | `isNot: {}` | Relation doesn't exist (is null) |
871
+ | `WithoutField` | `isNot.field` | Filters a field within the relation with negation |
872
+
873
+ Considering `user` with a to-one relation `profile` and a to-many relation `posts`:
874
+
875
+ ```ts
876
+ methods: {
877
+ // to-many (posts)
878
+ findByPostsSome: { map: true }, // has at least one post
879
+ findByPostsSomeTitle: { map: true }, // has at least one post with that title
880
+ findByPostsEveryPublishedIsTrue:{ map: true }, // all posts are published
881
+ findByPostsNone: { map: true }, // has no posts
882
+ findByPostsNoneTitle: { map: true }, // no post has that title
883
+
884
+ // to-one (profile)
885
+ findByProfileWith: { map: true }, // has a profile (not null)
886
+ findByProfileWithBio: { map: true }, // has a profile with that bio
887
+ findByProfileWithout: { map: true }, // has no profile (is null)
888
+ findByProfileWithoutBio: { map: true }, // has a profile, but with a different bio than the one provided
889
+ }
890
+
891
+ await userRepository.findByPostsSome();
892
+ await userRepository.findByPostsSomeTitle("My first post");
893
+ await userRepository.findByPostsEveryPublishedIsTrue();
894
+ await userRepository.findByPostsNone();
895
+ await userRepository.findByPostsNoneTitle("Draft");
896
+
897
+ await userRepository.findByProfileWith();
898
+ await userRepository.findByProfileWithBio("Hello, world!");
899
+ await userRepository.findByProfileWithout();
900
+ await userRepository.findByProfileWithoutBio("Old bio");
901
+ ```
902
+
903
+ Generates (`findByPostsSomeTitle`):
904
+
905
+ ```ts
906
+ {
907
+ posts: {
908
+ some: { title: "My first post" }
909
+ }
910
+ }
911
+ ```
912
+
913
+ Generates (`findByPostsEveryPublishedIsTrue`):
914
+
915
+ ```ts
916
+ {
917
+ posts: {
918
+ every: { published: true }
919
+ }
920
+ }
921
+ ```
922
+
923
+ Generates (`findByProfileWithBio`):
924
+
925
+ ```ts
926
+ {
927
+ profile: {
928
+ is: { bio: "Hello, world!" }
929
+ }
930
+ }
931
+ ```
932
+
933
+ Generates (`findByProfileWithout`):
934
+
935
+ ```ts
936
+ {
937
+ profile: {
938
+ isNot: {}
939
+ }
940
+ }
941
+ ```
942
+
943
+ > `Some`, `None`, `With`, and `Without` (without a field) don't receive an argument — the whole relation is tested for the existence of records (`some`/`none`) or for being `null`/not `null` (`is`/`isNot`). The `SomeField`, `EveryField`, `NoneField`, `WithField`, and `WithoutField` variants receive the filtered field's value as an argument.
944
+
945
+ ---
946
+
947
+ ### Pagination and ordering suffixes
948
+
949
+ Applied at the **end** of the method name, they automatically inject the pagination and ordering arguments.
950
+
951
+ | Suffix | Additional arguments |
952
+ | ------------------------ | -------------------------------|
953
+ | `Paginated` | `(pagination)` |
954
+ | `Ordered` | `(order)` |
955
+ | `OrderedAndPaginated` | `(order, pagination)` |
956
+ | `PaginatedAndOrdered` | `(pagination, order)` |
957
+
958
+ For `createMany` and `createManyAndReturn`, the `SkipDuplicates` suffix is available:
959
+
960
+ | Suffix | Effect |
961
+ | --------------------- | ---------------------------------------------|
962
+ | `SkipDuplicates` | Skips duplicate records during insertion |
786
963
 
787
964
  ---
788
965
 
789
- ### Sufixos de paginação e ordenação
966
+ ### Distinct
967
+
968
+ The `Distinct` suffix lets you get only unique records based on one or more fields, equivalent to Prisma's `distinct` option.
969
+
970
+ To use it, put `Distinct` in the method name (after the field filters, if any) followed by the desired fields separated by `And`. The first character of each field must be uppercase, just like in regular field filters.
971
+
972
+ ```ts
973
+ methods: {
974
+ // Returns unique users combining "age" and "role" (no field filter)
975
+ findManyDistinctAgeAndRole: { map: true },
976
+
977
+ // Distinct combined with the Paginated suffix
978
+ findManyDistinctNamePaginated: { map: true },
790
979
 
791
- Aplicados ao **final** do nome do método, eles injetam automaticamente os argumentos de paginação e ordenação.
980
+ // Distinct combined with a field filter (name) — filters by name and then applies distinct on role
981
+ findManyByNameDistinctRole: { map: true },
982
+ },
983
+ ```
792
984
 
793
- | Sufixo | Argumentos adicionais |
794
- | --------------------- | ----------------------------- |
795
- | `Paginated` | `(pagination)` |
796
- | `Ordered` | `(order)` |
797
- | `OrderedAndPaginated` | `(order, pagination)` |
798
- | `PaginatedAndOrdered` | `(pagination, order)` |
985
+ ```ts
986
+ // No arguments: the distinct fields are already fixed in the method name
987
+ await userRepository.findManyDistinctAgeAndRole();
988
+
989
+ // The pagination argument still works normally
990
+ await userRepository.findManyDistinctNamePaginated({ take: 10, skip: 0 });
991
+
992
+ // The "name" field filter is still passed normally as an argument
993
+ await userRepository.findManyByNameDistinctRole("John");
994
+ ```
799
995
 
800
- Para `createMany` e `createManyAndReturn`, o sufixo `SkipDuplicates` está disponível:
996
+ > The fields specified after `Distinct` are resolved from the method name at build time — they **don't** become runtime arguments, unlike regular field filters.
801
997
 
802
- | Sufixo | Efeito |
803
- | ----------------- | ---------------------------------------- |
804
- | `SkipDuplicates` | Ignora registros duplicados na inserção |
998
+ `Distinct` is available on prefixes that read multiple or single records: `findMany`, `findManyBy`, `findFirst`, `findFirstBy`, `findFirstOrThrow`, `findFirstOrThrowBy`, `findBy`, `findOneBy`, `findWhere`, `findOneWhere`, `findListWhere`, `existsBy`, and `existsWhere`.
805
999
 
806
1000
  ---
807
1001
 
808
- ### Configuração de métodos
1002
+ ### Method configuration
809
1003
 
810
- Cada entrada em `methods` aceita as seguintes opções:
1004
+ Each entry in `methods` accepts the following options:
811
1005
 
812
- | Opção | Tipo | Padrão | Descrição |
813
- | ------------------- | ------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------ |
814
- | `map` | `boolean` | — | **Obrigatório.** Define se o método será exposto no repository. |
815
- | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combina com `requiredWhere`. `overwrite` ignora o `requiredWhere`. |
816
- | `selectModel` | `keyof SelectModels \| false` | — | Sobrescreve o `defaultSelectModel` para este método. |
817
- | `fbMode` | `'one'` \| `'list'` | `'list'` | (**Obsoleto. Use `findOneBy`**) Somente para `findBy`. `'one'` retorna `T \| null`; `'list'` retorna `T[]`. |
818
- | `proxyTo` | `Padrão de método válido` | — | Delega a lógica para outro padrão de método válido. |
819
- | `pushWhere` | `WhereModel<M>` | — | Where extra adicionado à query além do `requiredWhere`. |
820
- | `injectOrdenation` | `OrdenationModel<M>` | — | Ordenação fixa injetada automaticamente na query. |
821
- | `injectPagination` | `PaginationModel<M>` | — | Paginação fixa injetada automaticamente na query. |
1006
+ | Option | Type | Default | Description |
1007
+ | --------------------- | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------|
1008
+ | `map` | `boolean` | — | **Required.** Defines whether the method will be exposed on the repository. |
1009
+ | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combines with `requiredWhere`. `overwrite` ignores `requiredWhere`. |
1010
+ | `selectModel` | `keyof SelectModels \| false` | — | Overrides `defaultSelectModel` for this method. |
1011
+ | `fbMode` | `'one'` \| `'list'` | `'list'` | (**Deprecated. Use `findOneBy`**) Only for `findBy`. `'one'` returns `T \| null`; `'list'` returns `T[]`. |
1012
+ | `proxyTo` | `Valid method pattern` | — | Delegates the logic to another valid method pattern. |
1013
+ | `pushWhere` | `WhereModel<M>` | — | Extra `where` added to the query in addition to `requiredWhere`. |
1014
+ | `injectOrdenation` | `OrdenationModel<M>` | — | Fixed ordering automatically injected into the query. |
1015
+ | `injectPagination` | `PaginationModel<M>` | — | Fixed pagination automatically injected into the query. |
822
1016
 
823
1017
  ---
824
1018
 
825
- ### Aggregate e GroupBy
1019
+ ### Aggregate and GroupBy
826
1020
 
827
1021
  ```ts
828
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
829
- tableName: "usuario",
1022
+ const userRepository = setupVSRepo<User, "user">()(({
1023
+ tableName: "user",
830
1024
  pkName: "id",
831
1025
  methods: {
832
1026
  aggregate: { map: true },
@@ -836,33 +1030,33 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
836
1030
  ```
837
1031
 
838
1032
  > [!NOTE]
839
- > Estes métodos devem ter exatamente esses nomes (`aggregate` e `groupBy`).
840
- > Ao contrário dos outros métodos dinâmicos, eles recebem argumentos nativos do Prisma e **ignoram** as configurações de `selectModels`, `pushWhere` e `requiredWhere`.
1033
+ > These methods must have exactly these names (`aggregate` and `groupBy`).
1034
+ > Unlike the other dynamic methods, they receive native Prisma arguments and **ignore** the `selectModels`, `pushWhere`, and `requiredWhere` configurations.
841
1035
 
842
1036
  ---
843
1037
 
844
- ## Relações no save
1038
+ ## Relations in save
845
1039
 
846
- Configure relações para que o `save` e o `patch` as gerenciem automaticamente (`saveList` e `pacthList` também gerenciam as relations automaticamente).
1040
+ Configure relations so that `save` and `patch` manage them automatically (`saveList` and `patchList` also manage relations automatically).
847
1041
 
848
1042
  ```ts
849
1043
  import type { Prisma } from "../../generated/prisma/client";
850
1044
 
851
- type Usuario = Prisma.usuarioGetPayload<{
852
- include: { perfil: true; postagens: true };
1045
+ type User = Prisma.userGetPayload<{
1046
+ include: { profile: true; posts: true };
853
1047
  }>;
854
1048
 
855
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
856
- tableName: "usuario",
1049
+ const userRepository = setupVSRepo<User, "user">()(({
1050
+ tableName: "user",
857
1051
  pkName: "id",
858
1052
 
859
1053
  relations: {
860
- perfil: {
1054
+ profile: {
861
1055
  pk: "id",
862
1056
  mode: "oto",
863
1057
  restriction: "set",
864
1058
  },
865
- postagens: {
1059
+ posts: {
866
1060
  pk: "id",
867
1061
  mode: "otm",
868
1062
  restriction: "add",
@@ -871,78 +1065,92 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
871
1065
  }).build(prisma);
872
1066
  ```
873
1067
 
874
- **Modos de relação:**
1068
+ **Relation modes:**
875
1069
 
876
- | Modo | Relação |
877
- | ----- | ------------ |
878
- | `oto` | one-to-one |
879
- | `otm` | one-to-many |
880
- | `mto` | many-to-one |
881
- | `mtm` | many-to-many |
1070
+ | Mode | Relation |
1071
+ | ----- | -------------- |
1072
+ | `oto` | one-to-one |
1073
+ | `otm` | one-to-many |
1074
+ | `mto` | many-to-one |
1075
+ | `mtm` | many-to-many |
882
1076
 
883
- **Restrições:**
1077
+ **Restrictions:**
884
1078
 
885
- | Restrição | Comportamento no update |
886
- | --------- | ----------------------------------------------------------- |
887
- | `set` | Substitui completamente (remove os que não foram enviados) |
888
- | `add` | Adiciona/atualiza sem remover os existentes |
1079
+ | Restriction | Behavior on update |
1080
+ | ------------ | ----------------------------------------------------------------|
1081
+ | `set` | Fully replaces (removes the ones that weren't sent) |
1082
+ | `add` | Adds/updates without removing existing ones |
889
1083
 
890
- **Relação `mto` com nullable:**
1084
+ > [!WARNING]
1085
+ > **`set` means different things depending on the relation's `mode` — and this can cause data loss if you're not careful.**
1086
+ >
1087
+ > In relations where the related record **belongs** to the parent record (`oto` and `otm`), "removing the ones that weren't sent" means **deleting the record from the database** (`delete`/`deleteMany`). In relations where the related record is **independent** (`mto` and `mtm`), "removing" just means **unlinking** (`disconnect`/`set: []`) — the related record continues to exist in the database, it just stops pointing to the parent (or being in the join table).
1088
+ >
1089
+ > | Mode | `restriction: "set"` when an item is omitted | Does the item continue to exist in the database? |
1090
+ > | ----- | ------------------------------------------------ | -----------------------------------------------------|
1091
+ > | `oto` | Passing `null` in the field → **deletes** the related record (`delete: true`) | No |
1092
+ > | `otm` | Items outside the sent list → **deleted** (`deleteMany` with `notIn`) | No |
1093
+ > | `mto` | Passing `null` in the field (with `nullable: true`) → **unlinks** (`disconnect: true`) | Yes |
1094
+ > | `mtm` | Items outside the sent list → **unlinked** from the join table (`set: []`) | Yes |
1095
+ >
1096
+ > Practical example: if `posts` is `otm` with `restriction: "set"`, a `save`/`patch` that sends the user with only 2 of the 5 existing posts will **delete the other 3 posts from the database**, not just unlink them from the user. If the expected behavior is just to unlink without deleting, use `restriction: "add"` (which never removes anything) and handle removal manually.
891
1097
 
892
- Use `nullable` (letra minúscula) para permitir a desvinculação de uma relação many-to-one:
1098
+ **`mto` relation with nullable:**
1099
+
1100
+ Use `nullable` (lowercase) to allow unlinking a many-to-one relation:
893
1101
 
894
1102
  ```ts
895
1103
  relations: {
896
- categoria: {
1104
+ category: {
897
1105
  pk: "id",
898
1106
  mode: "mto",
899
1107
  restriction: "set",
900
- nullable: true, // permite passar null para desvincular
1108
+ nullable: true, // allows passing null to unlink
901
1109
  },
902
1110
  }
903
1111
  ```
904
1112
 
905
- > **Nota:** `nullAble` (com A maiúsculo) ainda é aceito por compatibilidade, mas está **obsoleto**. Prefira `nullable`.
906
-
907
1113
  ---
908
1114
 
909
- ## Transações
1115
+ ## Transactions
910
1116
 
911
- Todos os métodos aceitam `options.db` para participar de uma transação:
1117
+ All methods accept `options.db` to participate in a transaction:
912
1118
 
913
1119
  ```ts
914
- await usuarioRepository.prisma.$transaction(async (tx) => {
915
- const usuario = await usuarioRepository.save(
916
- { nome: "Maria", email: "maria@email.com", senha: "password" },
1120
+ await userRepository.prisma.$transaction(async (tx) => {
1121
+ const user = await userRepository.save(
1122
+ { name: "Mary", email: "mary@email.com", password: "password" },
917
1123
  { db: tx }
918
1124
  );
919
1125
 
920
- await usuarioLogsRepository.save(
921
- { acao: "Cadastro de usuário", data: { usuarioCadastrado: usuario.id } },
1126
+ await userLogsRepository.save(
1127
+ { action: "User registration", data: { registeredUser: user.id } },
922
1128
  { db: tx }
923
1129
  );
924
1130
  });
925
1131
  ```
926
1132
 
927
- Para `saveList` e `patchList`, o campo `db` deve ser um `DbTransaction` (não o cliente principal):
1133
+ For `saveList` and `patchList`, the `db` field must be a `DbTransaction`:
928
1134
 
929
1135
  ```ts
930
1136
  await prisma.$transaction(async (tx) => {
931
- // CORRETO: tx é uma DbTransaction
932
- await usuarioRepository.saveList([{ nome: "Maria" }], { db: tx });
1137
+ // CORRECT: tx is a DbTransaction
1138
+ const registeredUsers = await userRepository.saveList([{ name: "Mary" }, { name: "Lucas" }], { db: tx });
933
1139
 
934
- // ERRADO: não passe o prisma diretamente
935
- // await usuarioRepository.saveList([{ nome: "Maria" }], { db: prisma });
1140
+ await userLogsRepository.save(
1141
+ { action: "User registration", data: { registeredUsers: registeredUsers.map(u => u.id) } },
1142
+ { db: tx }
1143
+ );
936
1144
  });
937
1145
  ```
938
1146
 
939
1147
  ---
940
1148
 
941
- ## Estendendo um repository
1149
+ ## Extending a repository
942
1150
 
943
1151
  ```ts
944
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
945
- tableName: "usuario",
1152
+ const userRepository = setupVSRepo<User, "user">()(({
1153
+ tableName: "user",
946
1154
  pkName: "id",
947
1155
  methods: {
948
1156
  findOneByEmailEndsWith: { map: true },
@@ -950,52 +1158,54 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
950
1158
  })
951
1159
  .build(prisma)
952
1160
  .extend((repo) => ({
953
- buscarAtivosPorDominio: async (dominio: string) => {
954
- return repo.findOneByEmailEndsWith(`@${dominio}`);
1161
+ findActiveByDomain: async (domain: string) => {
1162
+ return repo.findOneByEmailEndsWith(`@${domain}`);
955
1163
  },
956
1164
 
957
- ativarMultiplos: async (ids: string[]) => {
958
- return repo.patchList(ids.map(id => [id, { ativo: true }]));
1165
+ activateMultiple: async (ids: string[]) => {
1166
+ return repo.patchList(ids.map(id => [id, { active: true }]));
959
1167
  },
960
1168
  }));
961
1169
  ```
962
1170
 
963
1171
  ---
964
1172
 
965
- ## Tratamento de erros
1173
+ ## Error handling
966
1174
 
967
- O VSRepository lança `VSRepoError` e suas subclasses em situações específicas (erros do Prisma não são sobrescritos):
1175
+ VSRepository throws `VSRepoError` and its subclasses in specific situations (Prisma errors are not overridden):
968
1176
 
969
1177
  ```ts
970
1178
  import { VSRepoError, VSRepoRuntimeError } from "../../generated/vsrepo";
971
1179
 
972
1180
  try {
973
- const usuario = await usuarioRepository.getOrThrow(id);
1181
+ const user = await userRepository.getOrThrow("id-that-does-not-exist");
974
1182
  } catch (error) {
975
1183
  if (error instanceof VSRepoRuntimeError && error.code === "20727") {
976
- // Registro não encontrado pelo getOrThrow
1184
+ console.error("Record not found");
977
1185
  } else if (error instanceof VSRepoError) {
978
- console.error("Erro no repository:", error.message);
1186
+ console.error("Repository error:", error.message);
1187
+ } else {
1188
+ console.error("Error:", error.message)
979
1189
  }
980
1190
  }
981
1191
  ```
982
1192
 
983
- **Subclasses disponíveis:**
1193
+ **Available subclasses:**
984
1194
 
985
- | Classe | Quando é lançada |
986
- | -------------------- | ----------------------------------------------------------------- |
987
- | `VSRepoConfigError` | Configuração inválida em `setupVSRepo` |
988
- | `VSRepoBuildError` | Nome de método, tipo de campo ou configuração inválida no `build` |
989
- | `VSRepoExtendError` | Argumento inválido em `extend` |
990
- | `VSRepoRuntimeError` | Erro em tempo de execução durante uma operação |
1195
+ | Class | When it's thrown |
1196
+ | ---------------------- | ------------------------------------------------------------------------|
1197
+ | `VSRepoConfigError` | Invalid configuration in `setupVSRepo` |
1198
+ | `VSRepoBuildError` | Invalid method name, field type, or configuration in `build` |
1199
+ | `VSRepoExtendError` | Invalid argument in `extend` |
1200
+ | `VSRepoRuntimeError` | Runtime error during an operation |
991
1201
 
992
- `VSRepoRuntimeError` possui a propriedade `code` para identificação programática. O código `"20727"` é lançado pelo `getOrThrow` quando o registro não é encontrado.
1202
+ `VSRepoRuntimeError` has a `code` property for programmatic identification. Code `"20727"` is thrown by `getOrThrow` when the record is not found, for example.
993
1203
 
994
1204
  ---
995
1205
 
996
- ## Tipos utilitários
1206
+ ## Utility types
997
1207
 
998
- ### Tipos de cliente
1208
+ ### Client types
999
1209
 
1000
1210
  ```ts
1001
1211
  import type { DbClient, DbTransaction, ClientOrTransaction } from "../../generated/vsrepo";
@@ -1005,7 +1215,7 @@ type DbTransaction = Prisma.TransactionClient;
1005
1215
  type ClientOrTransaction = DbClient | DbTransaction;
1006
1216
  ```
1007
1217
 
1008
- ### Tipo de visibilidade soft-delete
1218
+ ### Soft-delete visibility type
1009
1219
 
1010
1220
  ```ts
1011
1221
  import type { SeeMode } from "../../generated/vsrepo";
@@ -1013,7 +1223,7 @@ import type { SeeMode } from "../../generated/vsrepo";
1013
1223
  type SeeMode = "active" | "removed" | "all";
1014
1224
  ```
1015
1225
 
1016
- ### Tipos derivados do modelo Prisma
1226
+ ### Types derived from the Prisma model
1017
1227
 
1018
1228
  ```ts
1019
1229
  import type {
@@ -1029,22 +1239,22 @@ import type {
1029
1239
  } from "../../generated/vsrepo";
1030
1240
  ```
1031
1241
 
1032
- ### Tipos de opções de método
1242
+ ### Method options types
1033
1243
 
1034
1244
  ```ts
1035
1245
  import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
1036
1246
 
1037
- // MethodOptions<S, IM> — opções passadas nos métodos do repository
1038
- type Opts = MethodOptions<"public" | "minimal", "comPosts">;
1247
+ // MethodOptions<S, IM> — options passed into the repository's methods
1248
+ type Opts = MethodOptions<"public" | "minimal", "withPosts">;
1039
1249
 
1040
- // MethodOptionsModel<TRepo> — derivado de uma instância VSRepository configurada
1041
- const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()(config);
1042
- type OptsModel = MethodOptionsModel<typeof usuarioVSRepo>;
1250
+ // MethodOptionsModel<TRepo> — derived from a configured VSRepository instance
1251
+ const userVSRepo = setupVSRepo<User, "user">()(config);
1252
+ type OptsModel = MethodOptionsModel<typeof userVSRepo>;
1043
1253
  ```
1044
1254
 
1045
- > O segundo parâmetro de `MethodOptions` (`IM`) representa as chaves válidas de `includeModels`. Quando informado, `selectModel` e `includeModel` tornam-se mutuamente exclusivos no tipo — não é possível passar os dois na mesma chamada.
1255
+ > The second parameter of `MethodOptions` (`IM`) represents the valid keys of `includeModels`. When provided, `selectModel` and `includeModel` become mutually exclusive in the type — it's not possible to pass both in the same call.
1046
1256
 
1047
- ### Tipos de configuração
1257
+ ### Configuration types
1048
1258
 
1049
1259
  ```ts
1050
1260
  import type {
@@ -1056,36 +1266,36 @@ import type {
1056
1266
  } from "../../generated/vsrepo";
1057
1267
  ```
1058
1268
 
1059
- ### Tipo do repository construído
1269
+ ### Built repository type
1060
1270
 
1061
1271
  ```ts
1062
1272
  import type { RepositoryOf } from "../../generated/vsrepo";
1063
1273
 
1064
- const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({ ... });
1065
- type UsuarioRepository = RepositoryOf<typeof usuarioVSRepo>;
1274
+ const userVSRepo = setupVSRepo<User, "user">()({ ... });
1275
+ type UserRepository = RepositoryOf<typeof userVSRepo>;
1066
1276
  ```
1067
1277
 
1068
- `RepositoryOf` aceita três parâmetros:
1278
+ `RepositoryOf` accepts three parameters:
1069
1279
 
1070
1280
  ```ts
1071
1281
  type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
1072
1282
  ```
1073
1283
 
1074
- ### Tipos do payload de `save` e `patch`
1284
+ ### `save` and `patch` payload types
1075
1285
 
1076
1286
  ```ts
1077
1287
  import type { SaveObject, PatchObject } from "../../generated/vsrepo";
1078
1288
 
1079
- const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()(({
1080
- tableName: "usuario",
1289
+ const userVSRepo = setupVSRepo<User, "user">()(({
1290
+ tableName: "user",
1081
1291
  pkName: "id",
1082
1292
  relations: {
1083
- perfil: { pk: "id", mode: "oto", restriction: "set" },
1293
+ profile: { pk: "id", mode: "oto", restriction: "set" },
1084
1294
  },
1085
1295
  });
1086
1296
 
1087
- type UsuarioSavePayload = SaveObject<Prisma.UsuarioCreateInput, typeof usuarioVSRepo>;
1088
- type UsuarioPatchPayload = PatchObject<Prisma.UsuarioUpdateInput, typeof usuarioVSRepo>;
1297
+ type UserSavePayload = SaveObject<Prisma.UserCreateInput, typeof userVSRepo>;
1298
+ type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
1089
1299
  ```
1090
1300
 
1091
1301
  ---
@@ -1096,16 +1306,16 @@ type UsuarioPatchPayload = PatchObject<Prisma.UsuarioUpdateInput, typeof usuario
1096
1306
 
1097
1307
  ```ts
1098
1308
  setupVSRepo<TPayload, TTableName>()({
1099
- tableName: Uncapitalize<M>; // Nome da tabela no Prisma
1100
- pkName: keyof T; // Nome da primary key
1101
- softRemovekName?: keyof T & string; // Campo DateTime para soft-delete (opcional)
1102
- selectModels?: SelectModels<M>; // Projeções de dados nomeadas (select)
1103
- defaultSelectModel?: keyof SM; // Select aplicado por padrão
1104
- includeModels?: IncludeModels<M>; // Projeções de dados nomeadas (include) — sem default, só na chamada
1105
- requiredWhere?: WhereModel<M>; // Filtros sempre aplicados
1106
- defaultOrdenation?: OrdenationModel<M>; // Ordenação padrão para queries sem Ordered/injectOrdenation
1107
- relations?: RepositoryRelations<T>; // Configuração de relações
1108
- methods?: Record<string, MethodConfig<M, SM>>; // Métodos dinâmicos
1309
+ tableName: Uncapitalize<M>; // Table name in Prisma
1310
+ pkName: keyof T; // Primary key name
1311
+ softRemovekName?: keyof T & string; // DateTime field for soft-delete
1312
+ selectModels?: SelectModels<M>; // Named data projections (select)
1313
+ defaultSelectModel?: keyof SM; // Select applied by default
1314
+ includeModels?: IncludeModels<M>; // Named data projections (include) — no default, only in the call
1315
+ requiredWhere?: WhereModel<M>; // Always-applied filters
1316
+ defaultOrdenation?: OrdenationModel<M>; // Default ordering for queries without Ordered/injectOrdenation
1317
+ relations?: RepositoryRelations<T>; // Relation configuration
1318
+ methods?: Record<string, MethodConfig<M, SM>>; // Dynamic methods
1109
1319
  });
1110
1320
  ```
1111
1321
 
@@ -1113,10 +1323,10 @@ setupVSRepo<TPayload, TTableName>()({
1113
1323
 
1114
1324
  ```ts
1115
1325
  vsRepo.build(prisma, {
1116
- showWorking?: boolean; // Exibe logs internos no console (default = false)
1326
+ showWorking?: boolean; // Shows internal logs on the console (default = false)
1117
1327
 
1118
1328
  baseMethods?: {
1119
- // Métodos que podem utilizar um defaultSelect
1329
+ // Methods that can use a defaultSelect
1120
1330
  get?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1121
1331
  getOrThrow?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1122
1332
  getList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
@@ -1130,7 +1340,7 @@ vsRepo.build(prisma, {
1130
1340
  softRemove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1131
1341
  restore?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1132
1342
 
1133
- // Métodos que NÃO aceitam defaultSelect
1343
+ // Methods that do NOT accept defaultSelect
1134
1344
  removeList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1135
1345
  softRemoveList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1136
1346
  restoreList?: { active?: boolean; ignoreRequiredWhere?: boolean };
@@ -1144,33 +1354,55 @@ vsRepo.build(prisma, {
1144
1354
 
1145
1355
  ```ts
1146
1356
  repo.extend((repo) => ({
1147
- meuMetodo: () => { ... }
1357
+ myMethod: () => { ... }
1148
1358
  }));
1149
1359
  ```
1150
1360
 
1151
1361
  ---
1152
1362
 
1153
- ## Contribuindo
1363
+ ## Practical examples
1154
1364
 
1155
- Contribuições são bem-vindas! Se você encontrou um bug, tem uma ideia de melhoria ou quer ajudar com a documentação, sinta-se à vontade para participar (**[Repositório do GitHub](https://github.com/jaobrabo123/VSRepository)**):
1365
+ Besides this README, the repository has an **[`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples)** folder with practical, commented, ready-to-run examples — it's the best place to see VSRepository being used in real scenarios.
1156
1366
 
1157
- 1. Faça um **Fork** do projeto.
1158
- 2. Crie uma nova branch com a sua alteração: `git checkout -b corrigindo-bug`.
1159
- 3. Faça o push para a sua branch: `git push origin corrigindo-bug`.
1160
- 4. Abra um **Pull Request**.
1367
+ ```
1368
+ examples/
1369
+ ├── prisma.ts # PrismaClient instance used by the examples
1370
+ ├── repositories.ts # Repository configuration (User, Address, Product) with setupVSRepo
1371
+ └── tests/
1372
+ ├── base-methods.test.ts # Base methods: get, save, patch, remove, getAll, total, has...
1373
+ ├── relations.test.ts # How to configure and use relations in save/patch and in filters
1374
+ ├── required-where.test.ts # How requiredWhere is automatically applied to queries
1375
+ ├── dynamic-methods.test.ts # Prefixes, field filters, logical operators, and pagination/ordering
1376
+ ├── transactions.test.ts # Transactions with options.db and instance access via repository.prisma
1377
+ ├── soft-delete.test.ts # Soft-delete: softRemove, softRemoveList, restore, restoreList and SeeMode
1378
+ └── batch-methods.test.ts # Batch operations: getList, saveList, patchList and merge
1379
+ ```
1161
1380
 
1162
- Para reportar problemas ou sugerir novas funcionalidades, abra uma **Issue**.
1381
+ Each file in `tests/` is an independent, runnable script (via `tsx`) that demonstrates a specific set of features, with `console.log` at each step so you can follow the result in the terminal. The folder itself has a [README](https://github.com/jaobrabo123/VSRepository/blob/main/examples/README.md) explaining the suggested reading order, how to set up the environment, and how to run the tests.
1163
1382
 
1164
1383
  ---
1165
1384
 
1166
- ## Requisitos
1385
+ ## Contributing
1386
+
1387
+ Contributions are welcome! If you found a bug, have an improvement idea, or want to help with the documentation, feel free to get involved (**[GitHub Repository](https://github.com/jaobrabo123/VSRepository)**):
1388
+
1389
+ 1. **Fork** the project.
1390
+ 2. Create a new branch with your change: `git checkout -b fixing-bug`.
1391
+ 3. Push to your branch: `git push origin fixing-bug`.
1392
+ 4. Open a **Pull Request**.
1393
+
1394
+ To report issues or suggest new features, open an **Issue**.
1395
+
1396
+ ---
1397
+
1398
+ ## Requirements
1167
1399
 
1168
1400
  - Node.js 18+ (ESM)
1169
1401
  - Prisma
1170
- - TypeScript (opcional, mas fortemente recomendado)
1171
- - `"moduleResolution": "bundler"` ou `"nodenext"` no tsconfig
1402
+ - TypeScript (optional, but strongly recommended)
1403
+ - `"moduleResolution": "bundler"` or `"nodenext"` in tsconfig
1172
1404
 
1173
- `tsconfig.json` recomendado:
1405
+ Recommended `tsconfig.json`:
1174
1406
 
1175
1407
  ```json
1176
1408
  {
@@ -1189,20 +1421,22 @@ Para reportar problemas ou sugerir novas funcionalidades, abra uma **Issue**.
1189
1421
 
1190
1422
  ## Troubleshooting
1191
1423
 
1192
- **Tipos genéricos não inferidos** — Verifique se `strict: true` e `moduleResolution: "bundler"` ou `"nodenext"` estão no `tsconfig.json`.
1424
+ **Generic types not inferred** — Check that `strict: true` and `moduleResolution: "bundler"` or `"nodenext"` are set in `tsconfig.json`.
1425
+
1426
+ **Dynamic method doesn't exist at runtime** — The field referenced in the method name must exist in the Prisma model. E.g.: `findByEmail` requires the model to have an `email` field.
1193
1427
 
1194
- **Método dinâmico não existe em runtime** — O campo referenciado no nome do método deve existir no modelo Prisma. Ex.: `findByEmail` exige que o modelo tenha um campo `email`.
1428
+ **`proxyTo` required** — Names outside the standard patterns (e.g. `searchByEmail`) aren't parsed directly. Use `proxyTo: "findByEmail"` in these cases.
1195
1429
 
1196
- **`proxyTo` obrigatório** — Nomes fora dos moldes (ex.: `buscarPorEmail`) não são parseados diretamente. Use `proxyTo: "findByEmail"` nesses casos.
1430
+ **Select model returns unexpected fields** — Check that the select model defines exactly the fields your TypeScript type expects.
1197
1431
 
1198
- **Select model retorna campos inesperados** — Verifique se o select model define exatamente os campos que o seu tipo TypeScript espera. Campos com `false` não serão retornados pelo Prisma.
1432
+ **`selectModel` and `includeModel` together in the same call** — Not allowed. Choose one or the other: if `includeModel` is provided, the `select` (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
1199
1433
 
1200
- **`selectModel` e `includeModel` juntos na mesma chamada** — Não é permitido. Escolha um ou outro: se `includeModel` for informado, o `select` (incluindo o `defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
1434
+ **`includeModel` doesn't appear as a default repository option** — This is expected. Unlike `defaultSelectModel`, there's no `defaultIncludeModel`/`defaultInclude`. An `includeModel` can only be set in the method call, via `options.includeModel`.
1201
1435
 
1202
- **`includeModel` não aparece como opção padrão do repository** — Isso é esperado. Diferente de `defaultSelectModel`, não existe `defaultIncludeModel`/`defaultInclude`. Um `includeModel` só pode ser definido na chamada do método, via `options.includeModel`.
1436
+ **`softRemovekName` throws an error at build** — The provided field must be of type `DateTime` in the Prisma schema. Types like `Boolean` or `String` are not accepted.
1203
1437
 
1204
- **`softRemovekName` lança erro no build** — O campo informado deve ser do tipo `DateTime` no schema do Prisma. Tipos como `Boolean` ou `String` não são aceitos.
1438
+ **`defaultOrdenation` isn't being applied** — Check whether the method uses the `Ordered`, `OrderedAndPaginated`, or `PaginatedAndOrdered` suffix, and whether it has `injectOrdenation` configured. Both take priority over the default ordering.
1205
1439
 
1206
- **`defaultOrdenation` não está sendo aplicada** — Verifique se o método não usa o sufixo `Ordered`, `OrderedAndPaginated` ou `PaginatedAndOrdered`, e se não possui `injectOrdenation` configurado. Ambos têm prioridade sobre a ordenação padrão.
1440
+ **`Distinct` suffix not recognized** — `Distinct` is only resolved on read prefixes (`findMany`, `findFirst`, `findBy`, `existsBy`, etc). In methods like `count`, `createMany`, `updateMany`, or `deleteMany` the suffix is ignored.
1207
1441
 
1208
- **`saveList`/`patchList` com `db` inválido** — O campo `db` nestes métodos aceita apenas `DbTransaction` (retorno de `prisma.$transaction`), não o cliente principal. Passar o `PrismaClient` diretamente causará comportamento inesperado.
1442
+ **`saveList`/`patchList` with invalid `db`** — The `db` field in these methods only accepts a `DbTransaction` (the return of `prisma.$transaction`), not the main client. Passing the `PrismaClient` directly will cause unexpected behavior.