vsrepo 1.0.4 → 1.0.6

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,60 +1,88 @@
1
- # VSRepo
1
+ # VSRepository
2
2
 
3
- Biblioteca de repository para projetos que usam Prisma.
4
- O VSRepo permite criar repositories tipados com métodos base (`get`, `save` e `remove`) e métodos dinâmicos inferidos pelo nome, como:
3
+ ![npm](https://img.shields.io/npm/v/vsrepo?style=flat-square)
4
+ ![license](https://img.shields.io/github/license/jaobrabo123/vsrepository?style=flat-square)
5
5
 
6
- - `findByEmail`
7
- - `findManyPaginated`
8
- - `updateById`
9
- - `deleteManyByIdIn`
6
+ Biblioteca de repository pattern para projetos que usam **Prisma**, com suporte completo a **TypeScript** e **type inference** automático.
10
7
 
11
- Tudo com inferência automática baseada no Prisma Client do próprio projeto.
8
+ O VSRepository permite criar repositories fortemente tipados com:
9
+
10
+ - **Métodos base** automáticos: `get`, `save`, `remove`
11
+ - **Métodos dinâmicos** inferidos pelo nome: `findByEmail`, `findManyPaginated`, `updateById`, `deleteManyByIdIn`
12
+ - **Select models** reutilizáveis para diferentes projeções de dados
13
+ - **Type safety** em 100% das operações
14
+ - **Transações** nativas do Prisma
15
+ - **Extensibilidade** com métodos personalizados
12
16
 
13
17
  ---
14
18
 
15
- # Instalação
19
+ ## Sumário
20
+
21
+ - [Instalação](#instalação)
22
+ - [Gerando os tipos](#gerando-os-tipos)
23
+ - [Uso básico](#uso-básico)
24
+ - [Integração com NestJS](#integração-com-nestjs)
25
+ - [Métodos base](#métodos-base)
26
+ - [Select Models](#select-models)
27
+ - [requiredWhere](#requiredwhere)
28
+ - [Métodos dinâmicos](#métodos-dinâmicos)
29
+ - [Prefixos disponíveis](#prefixos-disponíveis)
30
+ - [Filtros de campo](#filtros-de-campo)
31
+ - [Operadores lógicos](#operadores-lógicos)
32
+ - [Filtros de relação](#filtros-de-relação)
33
+ - [Sufixos de paginação e ordenação](#sufixos-de-paginação-e-ordenação)
34
+ - [Configuração de métodos](#configuração-de-métodos)
35
+ - [Relações no save](#relações-no-save)
36
+ - [Transações](#transações)
37
+ - [Extendendo um repository](#extendendo-um-repository)
38
+ - [Tratamento de erros](#tratamento-de-erros)
39
+ - [Tipos utilitários](#tipos-utilitários)
40
+ - [API Reference](#api-reference)
41
+ - [Requisitos](#requisitos)
42
+ - [Troubleshooting](#troubleshooting)
43
+
44
+ ---
16
45
 
17
- Instale o VSRepo e o Prisma Client:
46
+ ## Instalação
18
47
 
19
48
  ```bash
20
- pnpm add vsrepo @prisma/client
49
+ npm i vsrepo @prisma/client
21
50
  ```
22
51
 
23
52
  Gere o Prisma Client:
24
53
 
25
54
  ```bash
26
- pnpm prisma generate
55
+ npx prisma generate
27
56
  ```
28
57
 
29
58
  ---
30
59
 
31
- # Gerando os tipos do VSRepo
60
+ ## Gerando os tipos
32
61
 
33
- O VSRepo precisa conhecer o caminho real do seu Prisma Client para gerar as tipagens corretamente.
34
-
35
- Execute:
62
+ O VSRepository precisa conhecer o caminho real do seu Prisma Client para gerar as tipagens corretamente.
36
63
 
37
64
  ```bash
38
- pnpm vsrepo generate --output src/generated/vsrepo --prisma src/generated/prisma/client
65
+ npx vsrepo generate
39
66
  ```
40
67
 
41
- ou:
68
+ Equivale a:
42
69
 
43
70
  ```bash
44
- pnpm vsrepo generate -o src/generated/vsrepo -p src/generated/prisma/client
71
+ npx vsrepo generate \
72
+ --output src/generated/vsrepo \
73
+ --prisma src/generated/prisma
45
74
  ```
46
75
 
47
- ## O que esse comando faz
76
+ **Flags disponíveis:**
48
77
 
49
- O script:
78
+ | Flag | Alias | Padrão |
79
+ | ---------- | ----- | ---------------------- |
80
+ | `--output` | `-o` | `src/generated/vsrepo` |
81
+ | `--prisma` | `-p` | `src/generated/prisma` |
50
82
 
51
- 1. Lê os arquivos de tipagem internos do VSRepo
52
- 2. Substitui o import do Prisma pelo caminho informado em `--prisma`
53
- 3. Gera uma façade tipada dentro da pasta definida em `--output`
83
+ **Arquivos gerados:**
54
84
 
55
- Arquivos gerados:
56
-
57
- ```txt
85
+ ```
58
86
  src/generated/vsrepo/
59
87
  ├── VSRepoError.ts
60
88
  ├── VSRepoError.types.d.ts
@@ -63,125 +91,67 @@ src/generated/vsrepo/
63
91
  └── index.ts
64
92
  ```
65
93
 
66
- ---
67
-
68
- # Como importar depois
69
-
70
- Após gerar os arquivos:
94
+ Após gerar, importe sempre a partir da pasta gerada:
71
95
 
72
96
  ```ts
73
97
  import { setupVSRepo } from "../generated/vsrepo";
98
+ // ou
99
+ import { setupVSRepo, type SelectModels, type WhereModel } from "../generated/vsrepo";
74
100
  ```
75
101
 
76
- ou:
77
-
78
- ```ts
79
- import {
80
- setupVSRepo,
81
- type SelectModels,
82
- type WhereModel,
83
- } from "../generated/vsrepo";
84
- ```
85
-
86
- Você NÃO deve mais importar diretamente de `vsrepo`.
87
-
88
102
  ---
89
103
 
90
- # Uso básico
104
+ ## Uso básico
91
105
 
92
- ## Criando o Prisma Client
106
+ ### Configurando o Prisma Client
93
107
 
94
108
  ```ts
95
- // src/db.ts
96
- import { PrismaClient } from "@prisma/client";
109
+ // src/configs/db.ts
110
+ import { PrismaClient } from '../generated/prisma/client';
111
+ import { PrismaPg } from '@prisma/adapter-pg';
112
+ import 'dotenv/config';
97
113
 
98
- const prisma = new PrismaClient();
114
+ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
115
+ const prisma = new PrismaClient({ adapter });
99
116
 
100
117
  export default prisma;
101
118
  ```
102
119
 
103
- ---
104
-
105
- # Criando um repository
120
+ ### Criando um repository
106
121
 
107
122
  ```ts
108
123
  // src/repositories/usuarioRepository.ts
124
+ import prisma from "../configs/db";
125
+ import { setupVSRepo } from "../generated/vsrepo";
126
+ import type { Usuario } from "../generated/prisma/client";
109
127
 
110
- import prisma from "../db";
111
-
112
- import {
113
- setupVSRepo,
114
- type SelectModels,
115
- type WhereModel,
116
- } from "../generated/vsrepo";
117
-
118
- import type { Prisma } from "@prisma/client";
119
-
120
- type Usuario = Prisma.usuarioGetPayload<{
121
- include: {
122
- perfil: true;
123
- postagens: true;
124
- };
125
- }>;
126
-
127
- const usuarioSelectModels = {
128
- public: {
129
- id: true,
130
- nome: true,
131
- email: true,
132
- },
133
-
134
- minimal: {
135
- id: true,
136
- },
137
- } satisfies SelectModels<"usuario">;
138
-
139
- const usuarioRequiredWhere = {
140
- ativo: true,
141
- } satisfies WhereModel<"usuario">;
142
-
143
- const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({
128
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
144
129
  tableName: "usuario",
145
130
  pkName: "id",
146
131
 
147
- selectModels: usuarioSelectModels,
132
+ selectModels: {
133
+ public: { id: true, nome: true, email: true },
134
+ minimal: { id: true },
135
+ },
148
136
  defaultSelectModel: "public",
149
137
 
150
- requiredWhere: usuarioRequiredWhere,
138
+ requiredWhere: { ativo: true },
151
139
 
152
140
  methods: {
153
- findByEmail: {
154
- map: true,
155
- fbMode: "one",
156
- },
157
-
158
- findManyPaginated: {
159
- map: true,
160
- },
161
-
162
- updateById: {
163
- map: true,
164
- },
165
-
166
- deleteManyByIdIn: {
167
- map: true,
168
- whereType: "overwrite",
169
- },
170
-
171
- count: {
172
- map: true,
173
- },
141
+ findByEmail: { map: true, fbMode: "one" },
142
+ findManyPaginated: { map: true },
143
+ updateById: { map: true },
144
+ deleteManyByIdIn: { map: true, whereType: "overwrite" },
145
+ count: { map: true },
174
146
  },
175
- });
176
-
177
- const usuarioRepository = usuarioVSRepo.build(prisma);
147
+ }).build(prisma);
178
148
 
179
149
  export default usuarioRepository;
180
150
  ```
181
151
 
182
- ---
152
+ > `selectModels` e `requiredWhere` podem ser declarados fora do `setupVSRepo` se você precisar exportá-los para uso em outros arquivos.
183
153
 
184
- # Usando o repository
154
+ ### Usando o repository
185
155
 
186
156
  ```ts
187
157
  import usuarioRepository from "./repositories/usuarioRepository";
@@ -192,57 +162,196 @@ const usuario = await usuarioRepository.save({
192
162
  senha: "password",
193
163
  });
194
164
 
195
- const usuarioEncontrado = await usuarioRepository.get(usuario.id);
196
-
165
+ const encontrado = await usuarioRepository.get(usuario.id);
197
166
  const porEmail = await usuarioRepository.findByEmail("joao@email.com");
198
167
 
199
- await usuarioRepository.updateById(usuario.id, {
200
- nome: "Joao Pedro",
201
- });
202
-
168
+ await usuarioRepository.updateById(usuario.id, { nome: "Joao Pedro" });
203
169
  await usuarioRepository.remove(usuario.id);
204
170
  ```
205
171
 
206
172
  ---
207
173
 
208
- # Métodos base
174
+ ## Integração com NestJS
209
175
 
210
- Ao executar:
176
+ 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.
177
+
178
+ ### Configurando o repository como provider
211
179
 
212
180
  ```ts
213
- .build(prisma)
181
+ // src/repositories/user.repository.ts
182
+ import { Provider } from "@nestjs/common";
183
+ import { PrismaService } from "../../database/prisma.service";
184
+ import { UserGetPayload } from "../../generated/prisma/models";
185
+ import { RepositoryOf, setupVSRepo } from "../../generated/vsrepo";
186
+
187
+ const userVSRepo = setupVSRepo<
188
+ UserGetPayload<{ include: { profile: true } }>,
189
+ "User"
190
+ >()({
191
+ tableName: "user",
192
+ pkName: "id",
193
+ selectModels: {
194
+ public: {
195
+ id: true,
196
+ email: true,
197
+ createdAt: true,
198
+ updatedAt: true,
199
+ },
200
+ auth: {
201
+ id: true,
202
+ email: true,
203
+ password: true,
204
+ },
205
+ },
206
+ defaultSelectModel: "public",
207
+ requiredWhere: {
208
+ deletedAt: null,
209
+ },
210
+ relations: {
211
+ profile: {
212
+ mode: "oto",
213
+ pk: "id",
214
+ restriction: "add",
215
+ },
216
+ },
217
+ methods: {
218
+ findAuthByEmail: {
219
+ map: true,
220
+ proxyTo: "findUniqueByEmail",
221
+ selectModel: "auth",
222
+ },
223
+ },
224
+ });
225
+
226
+ export type UserRepository = RepositoryOf<typeof userVSRepo>;
227
+
228
+ export const USER_REPOSITORY = Symbol("USER_REPOSITORY");
229
+
230
+ export const UserRepositoryProvider: Provider = {
231
+ provide: USER_REPOSITORY,
232
+ inject: [PrismaService],
233
+ useFactory: (prisma: PrismaService) => {
234
+ return userVSRepo.build(prisma);
235
+ },
236
+ };
214
237
  ```
215
238
 
216
- o VSRepo cria automaticamente:
239
+ ### Registrando o provider no módulo
217
240
 
218
- | Método | Descrição |
219
- | ------------ | ----------------------- |
220
- | `get(pk)` | Busca pela primary key |
221
- | `save(obj)` | Cria ou atualiza |
222
- | `remove(pk)` | Remove pela primary key |
241
+ ```ts
242
+ // src/modules/user/user.module.ts
243
+ import { Module } from "@nestjs/common";
244
+ import { UserRepositoryProvider } from "../../repositories/user.repository";
245
+ import { UserService } from "./user.service";
246
+ import { UserController } from "./user.controller";
247
+
248
+ @Module({
249
+ imports: [DatabaseModule],
250
+ providers: [UserRepositoryProvider, UserService],
251
+ controllers: [UserController],
252
+ exports: [UserService],
253
+ })
254
+ export class UserModule {}
255
+ ```
223
256
 
224
- ---
257
+ ### Utilizando o repository em um serviço
225
258
 
226
- # Configurando métodos base
259
+ ```ts
260
+ // src/modules/user/user.service.ts
261
+ import { Injectable, Inject } from "@nestjs/common";
262
+ import { USER_REPOSITORY, UserRepository } from "../../repositories/user.repository";
263
+
264
+ @Injectable()
265
+ export class UserService {
266
+ constructor(
267
+ @Inject(USER_REPOSITORY)
268
+ private readonly userRepository: UserRepository,
269
+ ) {}
270
+
271
+ async getUserById(id: string) {
272
+ return this.userRepository.get(id);
273
+ }
274
+
275
+ async getUserAuthByEmail(email: string) {
276
+ return this.userRepository.findAuthByEmail(email);
277
+ }
278
+
279
+ async createUser(data: { email: string; password: string; name: string }) {
280
+ return this.userRepository.save({
281
+ email: data.email,
282
+ password: data.password,
283
+ name: data.name,
284
+ });
285
+ }
286
+ }
287
+ ```
288
+
289
+ ### Usando em um controller
227
290
 
228
291
  ```ts
229
- const usuarioRepository = usuarioVSRepo.build(prisma, {
230
- freeze: true,
292
+ // src/modules/user/user.controller.ts
293
+ import { Controller, Get, Post, Body, Param, Patch, Delete } from "@nestjs/common";
294
+ import { UserService } from "./user.service";
295
+
296
+ @Controller("users")
297
+ export class UserController {
298
+ constructor(private readonly userService: UserService) {}
299
+
300
+ @Get(":id")
301
+ async getUser(@Param("id") id: string) {
302
+ return this.userService.getUserById(id);
303
+ }
304
+
305
+ @Post()
306
+ async createUser(
307
+ @Body() data: { email: string; password: string; name: string }
308
+ ) {
309
+ return this.userService.createUser(data);
310
+ }
311
+ }
312
+ ```
313
+
314
+ **Benefícios desta abordagem:**
315
+
316
+ - ✅ Type-safe repositories com injeção de dependência
317
+ - ✅ Fácil de testar (mock do `USER_REPOSITORY`)
318
+ - ✅ Isolamento da lógica de persistência
319
+ - ✅ Reutilização do repository em múltiplos serviços
320
+ - ✅ Suporte a transações via `PrismaService`
321
+
322
+ ---
231
323
 
232
- showWorking: false,
324
+ ## Métodos base
325
+
326
+ Ao chamar `.build(prisma)`, três métodos são automaticamente disponibilizados:
327
+
328
+ | Método | Descrição |
329
+ | ------------ | ------------------------------------------ |
330
+ | `get(pk)` | Busca um registro pela primary key |
331
+ | `save(obj)` | Cria ou atualiza (upsert pela primary key) |
332
+ | `remove(pk)` | Remove um registro pela primary key |
333
+
334
+ Todos aceitam `options?: { selectModel?, db? }` como último argumento.
335
+
336
+ ### Configurando os métodos base
337
+
338
+ ```ts
339
+ usuarioVSRepo.build(prisma, {
340
+ freeze: true, // Congela o objeto (padrão: true)
341
+ showWorking: false, // Exibe logs internos no console
233
342
 
234
343
  baseMethods: {
235
344
  get: {
236
345
  active: true,
237
346
  defaultSelect: "public",
238
347
  },
239
-
240
348
  remove: {
349
+ active: true,
241
350
  defaultSelect: "minimal",
242
351
  },
243
-
244
352
  save: {
245
353
  active: true,
354
+ ignoreRequiredWhere: true, // Não aplica requiredWhere no upsert
246
355
  },
247
356
  },
248
357
  });
@@ -250,135 +359,307 @@ const usuarioRepository = usuarioVSRepo.build(prisma, {
250
359
 
251
360
  ---
252
361
 
253
- # Select models
362
+ ## Select Models
254
363
 
255
- `selectModels` cria selects reutilizáveis.
364
+ `selectModels` define projeções de dados nomeadas e reutilizáveis.
256
365
 
257
366
  ```ts
258
- const usuario = await usuarioRepository.get(id, {
259
- selectModel: "minimal",
260
- });
367
+ selectModels: {
368
+ public: { id: true, nome: true, email: true },
369
+ internal: { id: true, nome: true, email: true, senha: true },
370
+ minimal: { id: true },
371
+ },
372
+ defaultSelectModel: "public",
261
373
  ```
262
374
 
263
- Para retornar o payload padrão do Prisma:
375
+ `defaultSelectModel` define qual select é usado automaticamente quando nenhum é especificado na chamada. É recomendado sempre definí-lo junto com `selectModels`.
376
+
377
+ **Usando um select específico na chamada:**
264
378
 
265
379
  ```ts
266
- const usuarioCompleto = await usuarioRepository.get(id, {
267
- selectModel: false,
268
- });
380
+ const usuario = await usuarioRepository.get(id, { selectModel: "minimal" });
381
+ ```
382
+
383
+ **Retornando o payload padrão do Prisma (sem select):**
384
+
385
+ ```ts
386
+ const usuarioCompleto = await usuarioRepository.get(id, { selectModel: false });
269
387
  ```
270
388
 
271
389
  ---
272
390
 
273
- # Métodos dinâmicos
391
+ ## requiredWhere
274
392
 
275
- Métodos dinâmicos são definidos em:
393
+ `requiredWhere` define filtros aplicados automaticamente em todas as queries do repository.
394
+
395
+ ```ts
396
+ requiredWhere: { ativo: true },
397
+ ```
398
+
399
+ Agora toda query incluirá `ativo: true` automaticamente:
400
+
401
+ ```ts
402
+ // Internamente: WHERE ativo = true
403
+ const usuarios = await usuarioRepository.findMany();
404
+
405
+ // Internamente: WHERE email = 'joao@email.com' AND ativo = true
406
+ const usuario = await usuarioRepository.findByEmail("joao@email.com");
407
+ ```
408
+
409
+ Útil para soft-deletes, multi-tenancy e filtros globais de qualquer natureza.
410
+
411
+ ---
412
+
413
+ ## Métodos dinâmicos
414
+
415
+ Métodos dinâmicos são definidos na propriedade `methods` e têm seus comportamentos inferidos a partir do nome.
276
416
 
277
417
  ```ts
278
418
  methods: {
419
+ findByEmail: { map: true, fbMode: "one" },
420
+ findManyPaginated: { map: true },
421
+ updateById: { map: true },
422
+ deleteManyByIdIn: { map: true, whereType: "overwrite" },
279
423
  }
280
424
  ```
281
425
 
426
+ ---
427
+
428
+ ### Prefixos disponíveis
429
+
430
+ O prefixo do nome do método determina qual operação Prisma será chamada e quais argumentos serão esperados.
431
+
432
+ | Prefixo | Operação Prisma | Retorno | Observações |
433
+ | ------------------------- | ------------------------ | ---------------------- | -------------------------------------------------------- |
434
+ | `findBy` | `findMany` / `findFirst` | `T[]` ou `T \| null` | Padrão é lista; use `fbMode: "one"` para retorno único |
435
+ | `findUniqueBy` | `findUnique` | `T \| null` | |
436
+ | `findFirstBy` | `findFirst` | `T \| null` | Aceita campos como filtro |
437
+ | `findFirst` | `findFirst` | `T \| null` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
438
+ | `findManyBy` | `findMany` | `T[]` | Aceita campos como filtro |
439
+ | `findMany` | `findMany` | `T[]` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
440
+ | `findWhere` | `findFirst` | `T \| null` | Recebe um objeto `where` explícito como argumento |
441
+ | `findListWhere` | `findMany` | `T[]` | Recebe um objeto `where` explícito como argumento |
442
+ | `existsBy` | `findFirst` | `boolean` | Retorna `true` se encontrar, `false` caso contrário |
443
+ | `countBy` | `count` | `number` | Aceita campos como filtro |
444
+ | `count` | `count` | `number` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
445
+ | `create` | `create` | `T` | Recebe `data` como argumento |
446
+ | `createMany` | `createMany` | `{ count: number }` | Recebe `data` como argumento; suporta `SkipDuplicates` |
447
+ | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Recebe `data` como argumento; suporta `SkipDuplicates` |
448
+ | `updateBy` | `update` | `T` | Recebe `data` como argumento |
449
+ | `updateManyBy` | `updateMany` | `{ count: number }` | Recebe `data` como argumento |
450
+ | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Recebe `data` como argumento |
451
+ | `upsertBy` | `upsert` | `T` | Recebe `update` e `create` como argumentos |
452
+ | `deleteBy` | `delete` | `T` | |
453
+ | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
454
+
455
+ ---
456
+
457
+ ### Filtros de campo
458
+
459
+ 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`).
460
+
461
+ | Sufixo | Operador Prisma | Argumento necessário |
462
+ | ------------------ | --------------------- | -------------------- |
463
+ | *(sem sufixo)* | igualdade (`=`) | sim |
464
+ | `Not` | `not` | sim |
465
+ | `In` | `in` | sim (array) |
466
+ | `NotIn` | `notIn` | sim (array) |
467
+ | `Contains` | `contains` | sim |
468
+ | `NotContains` | `not.contains` | sim |
469
+ | `StartsWith` | `startsWith` | sim |
470
+ | `NotStartsWith` | `not.startsWith` | sim |
471
+ | `EndsWith` | `endsWith` | sim |
472
+ | `NotEndsWith` | `not.endsWith` | sim |
473
+ | `GreaterThan` | `gt` | sim |
474
+ | `GreaterThanEqual` | `gte` | sim |
475
+ | `LessThan` | `lt` | sim |
476
+ | `LessThanEqual` | `lte` | sim |
477
+ | `IsNull` | `null` | não (zero-arg) |
478
+ | `IsNotNull` | `not: null` | não (zero-arg) |
479
+ | `IsTrue` | `true` | não (zero-arg) |
480
+ | `IsFalse` | `false` | não (zero-arg) |
481
+ | `Insensitive` | `mode: 'insensitive'` | combinador |
482
+
483
+ `Insensitive` é um combinador e pode ser usado junto com outro filtro de texto:
484
+
485
+ ```ts
486
+ findByNomeContainsInsensitive // { nome: { contains: valor, mode: 'insensitive' } }
487
+ findByEmailStartsWithInsensitive // { email: { startsWith: valor, mode: 'insensitive' } }
488
+ findByNomeInsensitive // { nome: { equals: valor, mode: 'insensitive' } }
489
+ ```
490
+
491
+ O sufixo `Optional` pode ser adicionado a qualquer campo para tornar o argumento opcional:
492
+
493
+ ```ts
494
+ findByNomeOptionalAndEmail // nome é opcional, email é obrigatório
495
+ ```
496
+
497
+ ---
498
+
499
+ ### Operadores lógicos
500
+
501
+ | Operador | Uso no nome | Exemplo |
502
+ | --------- | ---------------------------- | -------------------------------- |
503
+ | `And` | entre dois campos | `findByIdAndEmail` |
504
+ | `Or` | entre dois campos | `findByNomeOrEmail` |
505
+
282
506
  Exemplo:
283
507
 
284
508
  ```ts
285
509
  methods: {
286
- findUniqueByIdAndEmail: { map: true },
510
+ findByIdAndEmail: { map: true, fbMode: "one" },
511
+ findByNomeOrEmail: { map: true },
512
+ findUniqueByIdOrEmailAndNome: { map: true }
513
+ }
287
514
 
288
- findByNomeContainsInsensitive: {
289
- map: true,
290
- },
515
+ // Uso
516
+ await usuarioRepository.findByIdAndEmail(1, "joao@email.com");
517
+ await usuarioRepository.findByNomeOrEmail("Joao", "joao@email.com");
518
+ await usuarioRepository.findUniqueByIdOrEmailAndNome(1, "joao@email.com", "Joao") // { OR: [ { id: 1 }, { email: "joao@email.com", nome: "Joao" } ] }
519
+ ```
291
520
 
292
- findManyPaginated: {
293
- map: true,
294
- },
521
+ ---
295
522
 
296
- findListWhereOrderedAndPaginated: {
297
- map: true,
298
- whereType: 'overwrite',
299
- },
523
+ ### Filtros de relação
300
524
 
301
- createManyAndReturn: {
302
- map: true,
303
- },
525
+ Permitem filtrar por campos de modelos relacionados.
304
526
 
305
- updateById: {
306
- map: true,
307
- },
527
+ | Sufixo de relação | Operador Prisma | Observação |
528
+ | ---------------------- | --------------- | -------------------------------------------------- |
529
+ | `Some` | `some: {}` | Relação tem *algum* registro |
530
+ | `SomeField` | `some.field` | Filtra dentro dos registros da relação |
531
+ | `EveryField` | `every.field` | Filtra dentro dos registros da relação |
532
+ | `None` | `none: {}` | Relação não tem *nenhum* registro |
533
+ | `NoneField` | `none.field` | Filtra dentro dos registros da relação |
534
+ | `With` | `is: {}` | Relação existe (não é null) |
535
+ | `WithField` | `is.field` | Filtra campo dentro da relação |
536
+ | `Without` | `isNot: {}` | Relação não existe (é null) |
537
+ | `WithoutField` | `isNot.field` | Filtra campo dentro da relação com negação |
308
538
 
309
- deleteManyByIdIn: {
310
- map: true,
311
- whereType: 'overwrite',
312
- },
539
+ Exemplos:
313
540
 
314
- existsByEmail: {
315
- map: true,
316
- },
317
-
318
- countByPerfil: {
319
- map: true,
320
- },
541
+ ```ts
542
+ methods: {
543
+ findByPostagensSomeTituloContains: { map: true }, // postagens: { some: { titulo: { contains: valor } } }
544
+ findByPerfilWith: { map: true }, // perfil: { is: {} }
545
+ findByPerfilWithout: { map: true }, // perfil: { isNot: {} }
546
+ findByPostagensSome: { map: true }, // postagens: { some: {} }
547
+ findByPostagensEveryAtivoIsTrue: { map: true }, // postagens: { every: { ativo: true } }
321
548
  }
322
549
  ```
323
550
 
324
551
  ---
325
552
 
326
- # Exemplos de uso
553
+ ### Sufixos de paginação e ordenação
327
554
 
328
- ```ts
329
- const usuario = await usuarioRepository.findUniqueByIdAndEmail(id, email);
555
+ Aplicados ao **final** do nome do método (após os filtros de campo), eles injetam automaticamente os argumentos de paginação e ordenação.
330
556
 
331
- const usuarios = await usuarioRepository.findByNomeContainsInsensitive("joao");
557
+ | Sufixo | Argumentos adicionais |
558
+ | --------------------- | ----------------------------- |
559
+ | `Paginated` | `(pagination)` |
560
+ | `Ordered` | `(order)` |
561
+ | `OrderedAndPaginated` | `(order, pagination)` |
562
+ | `PaginatedAndOrdered` | `(pagination, order)` |
332
563
 
333
- const pagina = await usuarioRepository.findManyPaginated({
334
- take: 10,
335
- skip: 0,
336
- });
564
+ Para `createMany` e `createManyAndReturn`, o sufixo `SkipDuplicates` está disponível:
337
565
 
338
- const ordenado = await usuarioRepository.findListWhereOrderedAndPaginated(
339
- {
340
- nome: {
341
- startsWith: "A",
342
- },
343
- },
566
+ | Sufixo | Efeito |
567
+ | ----------------- | ---------------------------------------- |
568
+ | `SkipDuplicates` | Ignora registros duplicados na inserção |
344
569
 
345
- {
346
- dataCriacao: "desc",
347
- },
570
+ Exemplos completos:
348
571
 
349
- {
350
- take: 10,
351
- },
572
+ ```ts
573
+ methods: {
574
+ findManyPaginated: { map: true },
575
+ findManyByAtivoOrderedAndPaginated: { map: true },
576
+ findByEmailOrderedAndPaginated: { map: true },
577
+ createManyAndReturnSkipDuplicates: { map: true },
578
+ }
579
+
580
+ // Uso
581
+ await usuarioRepository.findManyPaginated({ skip: 0, take: 10 });
582
+
583
+ await usuarioRepository.findManyByAtivoOrderedAndPaginated(
584
+ true,
585
+ { dataCriacao: "desc" },
586
+ { skip: 0, take: 10 }
352
587
  );
353
588
  ```
354
589
 
590
+ `PaginationOptions`:
591
+
592
+ ```ts
593
+ type PaginationOptions<TCursor = unknown> = {
594
+ skip?: number;
595
+ take?: number;
596
+ cursor?: TCursor;
597
+ };
598
+ ```
599
+
600
+ `OrderOptions`:
601
+
602
+ ```ts
603
+ type OrderOptions = OrderPattern | OrderPattern[];
604
+ // Exemplo: { dataCriacao: "desc" } ou [{ dataCriacao: "desc" }, { nome: "asc" }]
605
+ ```
606
+
355
607
  ---
356
608
 
357
- # Filtros suportados
358
-
359
- | Padrão | Exemplo |
360
- | ------------------ | ----------------------------------- |
361
- | `And` / `Or` | `findByIdAndEmail` |
362
- | `In` / `NotIn` | `deleteManyByIdIn` |
363
- | `Contains` | `findByNomeContains` |
364
- | `StartsWith` | `findByNomeStartsWith` |
365
- | `EndsWith` | `findByEmailEndsWith` |
366
- | `Insensitive` | `findByNomeContainsInsensitive` |
367
- | `GreaterThan` | `findByIdadeGreaterThan` |
368
- | `LessThanEqual` | `findByIdadeLessThanEqual` |
369
- | `IsNull` | `findByPerfilIsNull` |
370
- | `IsTrue` | `findByAtivoIsTrue` |
371
- | `Some` | `findByPostagensSomeTituloContains` |
372
- | `With` / `Without` | `findByPerfilWith` |
609
+ ### Configuração de métodos
610
+
611
+ Cada entrada em `methods` aceita as seguintes opções:
612
+
613
+ | Opção | Tipo | Padrão | Descrição |
614
+ | ------------------- | ------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------ |
615
+ | `map` | `boolean` | — | **Obrigatório.** Define se o método será exposto no repository. |
616
+ | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combina com `requiredWhere`. `overwrite` ignora o `requiredWhere`. |
617
+ | `selectModel` | `keyof SelectModels \| false` | — | Sobrescreve o `defaultSelectModel` para este método. |
618
+ | `fbMode` | `'one'` \| `'list'` | `'list'` | Somente para `findBy`. `'one'` retorna `T \| null`; `'list'` retorna `T[]`. |
619
+ | `proxyTo` | `Padrão de método válido` | — | Delega a lógica para outro padrão de método válido. Útil para methods com nomes personalizados. |
620
+ | `pushWhere` | `WhereModel<M>` | — | Where extra adicionado à query além do `requiredWhere`. |
621
+ | `injectOrdenation` | `OrdenationModel<M>` | — | Ordenação fixa injetada automaticamente na query. |
622
+ | `injectPagination` | `PaginationModel<M>` | — | Paginação fixa injetada automaticamente na query. |
623
+
624
+ Exemplos:
625
+
626
+ ```ts
627
+ methods: {
628
+ // Retorna um único resultado em vez de array
629
+ findByEmail: { map: true, fbMode: "one" },
630
+
631
+ // Ignora o requiredWhere
632
+ deleteManyByIdIn: { map: true, whereType: "overwrite" },
633
+
634
+ // Usa um select model específico neste método
635
+ findManyByAtivo: { map: true, selectModel: "minimal" },
636
+
637
+ // Adiciona um where extra além do requiredWhere
638
+ findManyByPerfil: { map: true, pushWhere: { deletedAt: null } },
639
+
640
+ // Ordenação fixa sem precisar passar como argumento
641
+ findManyPaginado: { map: true, injectOrdenation: { dataCriacao: "desc" } },
642
+
643
+ // Nome personalizado precisa de proxyTo
644
+ buscarPorEmailEPerfil: { map: true, proxyTo: "findByEmailAndPerfil" },
645
+ }
646
+ ```
373
647
 
374
648
  ---
375
649
 
376
- # Relações no save
650
+ ## Relações no save
651
+
652
+ Configure relações para que o `save` as gerencie automaticamente. Para que as relações apareçam no autocomplete, o tipo genérico deve incluir as relações usando `GetPayload`:
377
653
 
378
654
  ```ts
379
- const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({
380
- tableName: "usuario",
655
+ import type { Prisma } from "../generated/prisma/client";
656
+
657
+ type Usuario = Prisma.usuarioGetPayload<{
658
+ include: { perfil: true; postagens: true };
659
+ }>;
381
660
 
661
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
662
+ tableName: "usuario",
382
663
  pkName: "id",
383
664
 
384
665
  relations: {
@@ -387,17 +668,18 @@ const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({
387
668
  mode: "oto",
388
669
  restriction: "set",
389
670
  },
390
-
391
671
  postagens: {
392
672
  pk: "id",
393
673
  mode: "otm",
394
674
  restriction: "set",
395
675
  },
396
676
  },
397
- });
677
+ }).build(prisma);
398
678
  ```
399
679
 
400
- ## Modos disponíveis
680
+ > **Dica:** Se o tipo genérico não incluir `include: { relação: true }`, o VSRepository ainda funcionará, mas o autocomplete não sugerirá as relações no `save`. O tipo recomendado é sempre `GetPayload<{ include: { /* suas relações */ } }>` para melhor experiência de desenvolvimento.
681
+
682
+ **Modos de relação:**
401
683
 
402
684
  | Modo | Relação |
403
685
  | ----- | ------------ |
@@ -406,103 +688,296 @@ const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({
406
688
  | `mto` | many-to-one |
407
689
  | `mtm` | many-to-many |
408
690
 
409
- ---
691
+ **Restrições:**
410
692
 
411
- # Transações
693
+ | Restrição | Comportamento no update |
694
+ | --------- | ----------------------------------------------------------- |
695
+ | `set` | Substitui completamente (remove os que não foram enviados) |
696
+ | `add` | Adiciona/atualiza sem remover os existentes |
412
697
 
413
- Todos os métodos aceitam:
698
+ ---
414
699
 
415
- ```ts
416
- options.db;
417
- ```
700
+ ## Transações
418
701
 
419
- Exemplo:
702
+ Todos os métodos aceitam `options.db` para participar de uma transação:
420
703
 
421
704
  ```ts
422
705
  await prisma.$transaction(async (tx) => {
423
706
  const usuario = await usuarioRepository.save(
424
- {
425
- nome: "Maria",
426
- email: "maria@email.com",
427
- senha: "password",
428
- },
429
-
430
- {
431
- db: tx,
432
- },
707
+ { nome: "Maria", email: "maria@email.com", senha: "password" },
708
+ { db: tx }
433
709
  );
434
710
 
435
711
  await usuarioRepository.updateById(
436
712
  usuario.id,
437
-
438
- {
439
- ativo: true,
440
- },
441
-
442
- {
443
- db: tx,
444
- },
713
+ { ativo: true },
714
+ { db: tx }
445
715
  );
446
716
  });
447
717
  ```
448
718
 
449
719
  ---
450
720
 
451
- # Extendendo um repository
721
+ ## Extendendo um repository
452
722
 
453
723
  ```ts
454
- const repository = usuarioVSRepo
724
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
725
+ tableName: "usuario",
726
+ pkName: "id",
727
+ methods: {
728
+ findByEmailEndsWith: { map: true, fbMode: "one" },
729
+ },
730
+ })
455
731
  .build(prisma)
456
-
457
732
  .extend((repo) => ({
458
- async buscarAtivosPorDominio(dominio: string) {
733
+ buscarAtivosPorDominio: async (dominio: string) => {
459
734
  return repo.findByEmailEndsWith(`@${dominio}`);
460
735
  },
736
+
737
+ ativarMultiplos: async (ids: string[]) => {
738
+ return repo.updateManyByIdIn(ids, { ativo: true });
739
+ },
461
740
  }));
462
741
  ```
463
742
 
464
743
  ---
465
744
 
466
- # Estrutura recomendada
745
+ ## Tratamento de erros
746
+
747
+ O VSRepository lança `VSRepoError` e suas subclasses em situações específicas (OBS: Erros do Prisma não são sobrescritos como `VSRepoError`):
748
+
749
+ ```ts
750
+ import { VSRepoError } from "../generated/vsrepo";
467
751
 
468
- ```txt
469
- src/
470
- ├── db.ts
471
- ├── generated/
472
- │ ├── prisma/
473
- │ └── vsrepo/
474
- ├── repositories/
475
- └── services/
752
+ try {
753
+ const usuario = await usuarioRepository.get();
754
+ } catch (error) {
755
+ if (error instanceof VSRepoError) {
756
+ console.error("Erro no repository:", error.message);
757
+ }
758
+ }
476
759
  ```
477
760
 
761
+ **Subclasses disponíveis:**
762
+
763
+ | Classe | Quando é lançada |
764
+ | -------------------- | ------------------------------------------------------- |
765
+ | `VSRepoConfigError` | Configuração inválida em `setupVSRepo` ou `build` |
766
+ | `VSRepoBuildError` | Nome de método inválido ou desconhecido no `build` |
767
+ | `VSRepoExtendError` | Argumento inválido em `extend` |
768
+ | `VSRepoRuntimeError` | Erro em tempo de execução durante uma operação |
769
+
478
770
  ---
479
771
 
480
- # Scripts úteis
772
+ ## Tipos utilitários
481
773
 
482
- ```bash
483
- pnpm prisma generate
774
+ O VSRepository exporta os seguintes tipos para uso nas suas aplicações:
484
775
 
485
- pnpm vsrepo generate \
486
- --output src/generated/vsrepo \
487
- --prisma src/generated/prisma/client
776
+ ### Tipos de cliente
777
+
778
+ ```ts
779
+ import type { DbClient, DbTransaction, ClientOrTransaction } from "../generated/vsrepo";
780
+
781
+ type DbClient = PrismaClient;
782
+ type DbTransaction = Prisma.TransactionClient;
783
+ type ClientOrTransaction = DbClient | DbTransaction;
784
+ ```
785
+
786
+ ### Tipos derivados do modelo Prisma
787
+
788
+ ```ts
789
+ import type {
790
+ SelectModel,
791
+ SelectModels,
792
+ WhereModel,
793
+ OrdenationModel,
794
+ PaginationModel,
795
+ ModelUpsertInput,
796
+ } from "../generated/vsrepo";
797
+
798
+ // Select de um campo específico
799
+ type UsuarioSelect = SelectModel<"usuario">;
800
+
801
+ // Mapa de selects nomeados
802
+ type UsuarioSelectModels = SelectModels<"usuario">;
803
+
804
+ // Where clause do modelo
805
+ type UsuarioWhere = WhereModel<"usuario">;
806
+
807
+ // OrderBy do modelo
808
+ type UsuarioOrder = OrdenationModel<"usuario">;
809
+
810
+ // Opções de paginação com cursor tipado
811
+ type UsuarioPagination = PaginationModel<"usuario">;
812
+
813
+ // Payload de criação do modelo (para upsert)
814
+ type UsuarioUpsertInput = ModelUpsertInput<"usuario">;
815
+ ```
816
+
817
+ ### Todos os inputs do modelo
818
+
819
+ ```ts
820
+ import type { PrismaModelInputs } from "../generated/vsrepo";
821
+
822
+ type UsuarioInputs = PrismaModelInputs<"usuario">;
823
+ // Contém:
824
+ // select, createInput, createManyInput, updateInput, updateManyInput,
825
+ // whereInput, orderByInput, cursorInput, upsertCreateInput, upsertUpdateInput
826
+ ```
827
+
828
+ ### Tipos de opções de método
829
+
830
+ ```ts
831
+ import type { MethodOptions, MethodOptionsModel } from "../generated/vsrepo";
832
+
833
+ // MethodOptions<S> — opções passadas nos métodos do repository
834
+ // S = chave do select model ou false
835
+ type Opts = MethodOptions<"public" | "minimal">;
836
+
837
+ // MethodOptionsModel<T> — versão derivada do tipo do repository
838
+ type OptsModel = MethodOptionsModel<typeof usuarioSelectModels>;
839
+ ```
840
+
841
+ ### Tipos de configuração
842
+
843
+ ```ts
844
+ import type {
845
+ MethodConfig,
846
+ RepoConfig,
847
+ BuildConfig,
848
+ RepositoryRelations,
849
+ ExtractRelationConfig,
850
+ UpsertWithRelations,
851
+ } from "../generated/vsrepo";
852
+
853
+ // Configuração de um método dinâmico
854
+ type MeuMethodConfig = MethodConfig<"usuario", typeof meuSelectModels>;
855
+
856
+ // Configuração completa do repository
857
+ type MeuRepoConfig = RepoConfig<Usuario, "usuario">;
858
+
859
+ // Configuração do build
860
+ type MeuBuildConfig = BuildConfig<"public" | "minimal">;
861
+
862
+ // Tipo de relações inferidas automaticamente
863
+ type UsuarioRelations = RepositoryRelations<Usuario>;
864
+
865
+ // Configuração de relação inferida a partir de um campo
866
+ type PerfilRelationConfig = ExtractRelationConfig<Usuario["perfil"]>;
867
+
868
+ // Payload do save com relações
869
+ type UsuarioComRelacoes = UpsertWithRelations<Usuario, "usuario", typeof relations>;
870
+ ```
871
+
872
+ ### Tipo do repository construído
873
+
874
+ ```ts
875
+ import type { BuiltRepository, RepositoryOf } from "../generated/vsrepo";
876
+
877
+ // Tipo completo de um repository construído
878
+ type MeuRepo = BuiltRepository<Usuario, "usuario", typeof config, typeof buildConfig>;
879
+
880
+ // Inferência a partir de uma instância VSRepository (útil para injeção de dependência)
881
+ const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({ ... });
882
+ type UsuarioRepository = RepositoryOf<typeof usuarioVSRepo>;
883
+ ```
884
+
885
+ `RepositoryOf` aceita três parâmetros:
886
+
887
+ ```ts
888
+ type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
889
+ // ^ BuildConfig (opcional) ^ tipo do extend (opcional)
890
+ ```
891
+
892
+ Exemplo com extend tipado:
893
+
894
+ ```ts
895
+ const extension = { buscarPorDominio: (dominio: string) => Promise<Usuario[]> };
896
+ type UsuarioRepositoryExtended = RepositoryOf<typeof usuarioVSRepo, undefined, typeof extension>;
897
+ ```
898
+
899
+ ### Tipo auxiliar
900
+
901
+ ```ts
902
+ import type { DistributiveOmit } from "../generated/vsrepo";
903
+
904
+ // Omit distributivo — preserva unions ao omitir propriedades
905
+ type SemEmail = DistributiveOmit<Usuario | Perfil, "email">;
488
906
  ```
489
907
 
490
908
  ---
491
909
 
492
- # Requisitos
910
+ ## API Reference
493
911
 
494
- - TypeScript
912
+ ### `setupVSRepo<T, M>()(config)`
913
+
914
+ ```ts
915
+ setupVSRepo<TPayload, TTableName>()({
916
+ tableName: Uncapitalize<M>; // Nome da tabela no Prisma
917
+ pkName: keyof T; // Nome da primary key
918
+ selectModels?: SelectModels<M>; // Projeções de dados nomeadas
919
+ defaultSelectModel?: keyof SM; // Select aplicado por padrão
920
+ requiredWhere?: WhereModel<M>; // Filtros sempre aplicados
921
+ relations?: RepositoryRelations<T>; // Configuração de relações
922
+ methods?: Record<string, MethodConfig<M, SM>>; // Métodos dinâmicos
923
+ });
924
+ ```
925
+
926
+ ### `.build(prisma, config?)`
927
+
928
+ ```ts
929
+ vsRepo.build(prisma, {
930
+ freeze?: boolean; // Congela o objeto (padrão: true)
931
+ showWorking?: boolean; // Exibe logs internos no console
932
+
933
+ baseMethods?: {
934
+ get?: { active?: boolean; defaultSelect?: string };
935
+ remove?: { active?: boolean; defaultSelect?: string };
936
+ save?: { active?: boolean; ignoreRequiredWhere?: boolean };
937
+ };
938
+ });
939
+ ```
940
+
941
+ ### `.extend(fn)`
942
+
943
+ ```ts
944
+ repo.extend((repo) => ({
945
+ meuMetodo() { ... }
946
+ }));
947
+ ```
948
+
949
+ ---
950
+
951
+ ## Requisitos
952
+
953
+ - Node.js 16+ (ESM)
495
954
  - Prisma
496
- - Node.js ESM
497
- - `"moduleResolution": "bundler"` ou `"nodenext"`
955
+ - TypeScript (opcional, mas fortemente recomendado)
956
+ - `"moduleResolution": "bundler"` ou `"nodenext"` no tsconfig
498
957
 
499
- Exemplo:
958
+ `tsconfig.json` recomendado:
500
959
 
501
960
  ```json
502
961
  {
503
962
  "compilerOptions": {
963
+ "target": "ES2020",
504
964
  "module": "NodeNext",
505
- "moduleResolution": "NodeNext"
965
+ "moduleResolution": "NodeNext",
966
+ "strict": true,
967
+ "skipLibCheck": true,
968
+ "lib": ["ES2020"]
506
969
  }
507
970
  }
508
971
  ```
972
+
973
+ ---
974
+
975
+ ## Troubleshooting
976
+
977
+ **Tipos genéricos não inferidos** — Verifique se `strict: true` e `moduleResolution: "bundler"` ou `"nodenext"` estão no `tsconfig.json`.
978
+
979
+ **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`.
980
+
981
+ **`proxyTo` obrigatório** — Nomes fora dos moldes (ex.: `buscarPorEmail`) não são parseados diretamente. Use `proxyTo: "findByEmail"` nesses casos.
982
+
983
+ **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.