vsrepo 1.0.7 → 1.0.8

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,983 +1,1041 @@
1
- # VSRepository
2
-
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
-
6
- Biblioteca de repository pattern para projetos que usam **Prisma**, com suporte completo a **TypeScript** e **type inference** automático.
7
-
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
16
-
17
- ---
18
-
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
- ---
45
-
46
- ## Instalação
47
-
48
- ```bash
49
- npm i vsrepo @prisma/client
50
- ```
51
-
52
- Gere o Prisma Client:
53
-
54
- ```bash
55
- npx prisma generate
56
- ```
57
-
58
- ---
59
-
60
- ## Gerando os tipos
61
-
62
- O VSRepository precisa conhecer o caminho real do seu Prisma Client para gerar as tipagens corretamente.
63
-
64
- ```bash
65
- npx vsrepo generate
66
- ```
67
-
68
- Equivale a:
69
-
70
- ```bash
71
- npx vsrepo generate \
72
- --output src/generated/vsrepo \
73
- --prisma src/generated/prisma
74
- ```
75
-
76
- **Flags disponíveis:**
77
-
78
- | Flag | Alias | Padrão |
79
- | ---------- | ----- | ---------------------- |
80
- | `--output` | `-o` | `src/generated/vsrepo` |
81
- | `--prisma` | `-p` | `src/generated/prisma` |
82
-
83
- **Arquivos gerados:**
84
-
85
- ```
86
- src/generated/vsrepo/
87
- ├── VSRepoError.ts
88
- ├── VSRepoError.types.d.ts
89
- ├── VSRepository.ts
90
- ├── VSRepository.types.d.ts
91
- └── index.ts
92
- ```
93
-
94
- Após gerar, importe sempre a partir da pasta gerada:
95
-
96
- ```ts
97
- import { setupVSRepo } from "../generated/vsrepo";
98
- // ou
99
- import { setupVSRepo, type SelectModels, type WhereModel } from "../generated/vsrepo";
100
- ```
101
-
102
- ---
103
-
104
- ## Uso básico
105
-
106
- ### Configurando o Prisma Client
107
-
108
- ```ts
109
- // src/configs/db.ts
110
- import { PrismaClient } from '../generated/prisma/client';
111
- import { PrismaPg } from '@prisma/adapter-pg';
112
- import 'dotenv/config';
113
-
114
- const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
115
- const prisma = new PrismaClient({ adapter });
116
-
117
- export default prisma;
118
- ```
119
-
120
- ### Criando um repository
121
-
122
- ```ts
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";
127
-
128
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
129
- tableName: "usuario",
130
- pkName: "id",
131
-
132
- selectModels: {
133
- public: { id: true, nome: true, email: true },
134
- minimal: { id: true },
135
- },
136
- defaultSelectModel: "public",
137
-
138
- requiredWhere: { ativo: true },
139
-
140
- methods: {
141
- findByEmail: { map: true, fbMode: "one" },
142
- findManyPaginated: { map: true },
143
- updateById: { map: true },
144
- deleteManyByIdIn: { map: true, whereType: "overwrite" },
145
- count: { map: true },
146
- },
147
- }).build(prisma);
148
-
149
- export default usuarioRepository;
150
- ```
151
-
152
- > `selectModels` e `requiredWhere` podem ser declarados fora do `setupVSRepo` se você precisar exportá-los para uso em outros arquivos.
153
-
154
- ### Usando o repository
155
-
156
- ```ts
157
- import usuarioRepository from "./repositories/usuarioRepository";
158
-
159
- const usuario = await usuarioRepository.save({
160
- nome: "Joao",
161
- email: "joao@email.com",
162
- senha: "password",
163
- });
164
-
165
- const encontrado = await usuarioRepository.get(usuario.id);
166
- const porEmail = await usuarioRepository.findByEmail("joao@email.com");
167
-
168
- await usuarioRepository.updateById(usuario.id, { nome: "Joao Pedro" });
169
- await usuarioRepository.remove(usuario.id);
170
- ```
171
-
172
- ---
173
-
174
- ## Integração com NestJS
175
-
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
179
-
180
- ```ts
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
- };
237
- ```
238
-
239
- ### Registrando o provider no módulo
240
-
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
- ```
256
-
257
- ### Utilizando o repository em um serviço
258
-
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
290
-
291
- ```ts
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
- ---
323
-
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
342
-
343
- baseMethods: {
344
- get: {
345
- active: true,
346
- defaultSelect: "public",
347
- },
348
- remove: {
349
- active: true,
350
- defaultSelect: "minimal",
351
- },
352
- save: {
353
- active: true,
354
- ignoreRequiredWhere: true, // Não aplica requiredWhere no upsert
355
- },
356
- },
357
- });
358
- ```
359
-
360
- ---
361
-
362
- ## Select Models
363
-
364
- `selectModels` define projeções de dados nomeadas e reutilizáveis.
365
-
366
- ```ts
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",
373
- ```
374
-
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:**
378
-
379
- ```ts
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 });
387
- ```
388
-
389
- ---
390
-
391
- ## requiredWhere
392
-
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.
416
-
417
- ```ts
418
- methods: {
419
- findByEmail: { map: true, fbMode: "one" },
420
- findManyPaginated: { map: true },
421
- updateById: { map: true },
422
- deleteManyByIdIn: { map: true, whereType: "overwrite" },
423
- }
424
- ```
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
-
506
- Exemplo:
507
-
508
- ```ts
509
- methods: {
510
- findByIdAndEmail: { map: true, fbMode: "one" },
511
- findByNomeOrEmail: { map: true },
512
- findUniqueByIdOrEmailAndNome: { map: true }
513
- }
514
-
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
- ```
520
-
521
- ---
522
-
523
- ### Filtros de relação
524
-
525
- Permitem filtrar por campos de modelos relacionados.
526
-
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 |
538
-
539
- Exemplos:
540
-
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 } }
548
- }
549
- ```
550
-
551
- ---
552
-
553
- ### Sufixos de paginação e ordenação
554
-
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.
556
-
557
- | Sufixo | Argumentos adicionais |
558
- | --------------------- | ----------------------------- |
559
- | `Paginated` | `(pagination)` |
560
- | `Ordered` | `(order)` |
561
- | `OrderedAndPaginated` | `(order, pagination)` |
562
- | `PaginatedAndOrdered` | `(pagination, order)` |
563
-
564
- Para `createMany` e `createManyAndReturn`, o sufixo `SkipDuplicates` está disponível:
565
-
566
- | Sufixo | Efeito |
567
- | ----------------- | ---------------------------------------- |
568
- | `SkipDuplicates` | Ignora registros duplicados na inserção |
569
-
570
- Exemplos completos:
571
-
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 }
587
- );
588
- ```
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
-
607
- ---
608
-
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
- ```
647
-
648
- ---
649
-
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`:
653
-
654
- ```ts
655
- import type { Prisma } from "../generated/prisma/client";
656
-
657
- type Usuario = Prisma.usuarioGetPayload<{
658
- include: { perfil: true; postagens: true };
659
- }>;
660
-
661
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
662
- tableName: "usuario",
663
- pkName: "id",
664
-
665
- relations: {
666
- perfil: {
667
- pk: "id",
668
- mode: "oto",
669
- restriction: "set",
670
- },
671
- postagens: {
672
- pk: "id",
673
- mode: "otm",
674
- restriction: "set",
675
- },
676
- },
677
- }).build(prisma);
678
- ```
679
-
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:**
683
-
684
- | Modo | Relação |
685
- | ----- | ------------ |
686
- | `oto` | one-to-one |
687
- | `otm` | one-to-many |
688
- | `mto` | many-to-one |
689
- | `mtm` | many-to-many |
690
-
691
- **Restrições:**
692
-
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 |
697
-
698
- ---
699
-
700
- ## Transações
701
-
702
- Todos os métodos aceitam `options.db` para participar de uma transação:
703
-
704
- ```ts
705
- await prisma.$transaction(async (tx) => {
706
- const usuario = await usuarioRepository.save(
707
- { nome: "Maria", email: "maria@email.com", senha: "password" },
708
- { db: tx }
709
- );
710
-
711
- await usuarioRepository.updateById(
712
- usuario.id,
713
- { ativo: true },
714
- { db: tx }
715
- );
716
- });
717
- ```
718
-
719
- ---
720
-
721
- ## Extendendo um repository
722
-
723
- ```ts
724
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
725
- tableName: "usuario",
726
- pkName: "id",
727
- methods: {
728
- findByEmailEndsWith: { map: true, fbMode: "one" },
729
- },
730
- })
731
- .build(prisma)
732
- .extend((repo) => ({
733
- buscarAtivosPorDominio: async (dominio: string) => {
734
- return repo.findByEmailEndsWith(`@${dominio}`);
735
- },
736
-
737
- ativarMultiplos: async (ids: string[]) => {
738
- return repo.updateManyByIdIn(ids, { ativo: true });
739
- },
740
- }));
741
- ```
742
-
743
- ---
744
-
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";
751
-
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
- }
759
- ```
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
-
770
- ---
771
-
772
- ## Tipos utilitários
773
-
774
- O VSRepository exporta os seguintes tipos para uso nas suas aplicações:
775
-
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">;
906
- ```
907
-
908
- ---
909
-
910
- ## API Reference
911
-
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)
954
- - Prisma
955
- - TypeScript (opcional, mas fortemente recomendado)
956
- - `"moduleResolution": "bundler"` ou `"nodenext"` no tsconfig
957
-
958
- `tsconfig.json` recomendado:
959
-
960
- ```json
961
- {
962
- "compilerOptions": {
963
- "target": "ES2020",
964
- "module": "NodeNext",
965
- "moduleResolution": "NodeNext",
966
- "strict": true,
967
- "skipLibCheck": true,
968
- "lib": ["ES2020"]
969
- }
970
- }
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
-
1
+ # VSRepository
2
+
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
+
6
+ Biblioteca de repository pattern para projetos que usam **Prisma**, com suporte completo a **TypeScript** e **type inference** automático.
7
+
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
16
+
17
+ ---
18
+
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
+ ---
45
+
46
+ ## Instalação
47
+
48
+ ```bash
49
+ npm i vsrepo @prisma/client
50
+ ```
51
+
52
+ Gere o Prisma Client:
53
+
54
+ ```bash
55
+ npx prisma generate
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Gerando os tipos
61
+
62
+ O VSRepository precisa conhecer o caminho real do seu Prisma Client para gerar as tipagens corretamente.
63
+
64
+ ```bash
65
+ npx vsrepo generate
66
+ ```
67
+
68
+ Equivale a:
69
+
70
+ ```bash
71
+ npx vsrepo generate \
72
+ --output src/generated/vsrepo \
73
+ --prisma src/generated/prisma
74
+ ```
75
+
76
+ **Flags disponíveis:**
77
+
78
+ | Flag | Alias | Padrão |
79
+ | ---------- | ----- | ---------------------- |
80
+ | `--output` | `-o` | `src/generated/vsrepo` |
81
+ | `--prisma` | `-p` | `src/generated/prisma` |
82
+
83
+ **Arquivos gerados:**
84
+
85
+ ```
86
+ src/generated/vsrepo/
87
+ ├── VSRepoError.ts
88
+ ├── VSRepoError.types.d.ts
89
+ ├── VSRepository.ts
90
+ ├── VSRepository.types.d.ts
91
+ └── index.ts
92
+ ```
93
+
94
+ Após gerar, importe sempre a partir da pasta gerada:
95
+
96
+ ```ts
97
+ import { setupVSRepo } from "../generated/vsrepo";
98
+ // ou
99
+ import { setupVSRepo, type SelectModels, type WhereModel } from "../generated/vsrepo";
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Uso básico
105
+
106
+ ### Configurando o Prisma Client
107
+
108
+ ```ts
109
+ // src/configs/db.ts
110
+ import { PrismaClient } from '../generated/prisma/client';
111
+ import { PrismaPg } from '@prisma/adapter-pg';
112
+ import 'dotenv/config';
113
+
114
+ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
115
+ const prisma = new PrismaClient({ adapter });
116
+
117
+ export default prisma;
118
+ ```
119
+
120
+ ### Criando um repository
121
+
122
+ ```ts
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";
127
+
128
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
129
+ tableName: "usuario",
130
+ pkName: "id",
131
+
132
+ selectModels: {
133
+ public: { id: true, nome: true, email: true },
134
+ minimal: { id: true },
135
+ },
136
+ defaultSelectModel: "public",
137
+
138
+ requiredWhere: { ativo: true },
139
+
140
+ methods: {
141
+ findByEmail: { map: true, fbMode: "one" },
142
+ findManyPaginated: { map: true },
143
+ updateById: { map: true },
144
+ deleteManyByIdIn: { map: true, whereType: "overwrite" },
145
+ count: { map: true },
146
+ },
147
+ }).build(prisma);
148
+
149
+ export default usuarioRepository;
150
+ ```
151
+
152
+ > `selectModels` e `requiredWhere` podem ser declarados fora do `setupVSRepo` se você precisar exportá-los para uso em outros arquivos.
153
+
154
+ ### Usando o repository
155
+
156
+ ```ts
157
+ import usuarioRepository from "./repositories/usuarioRepository";
158
+
159
+ const usuario = await usuarioRepository.save({
160
+ nome: "Joao",
161
+ email: "joao@email.com",
162
+ senha: "password",
163
+ });
164
+
165
+ const encontrado = await usuarioRepository.get(usuario.id);
166
+ const porEmail = await usuarioRepository.findByEmail("joao@email.com");
167
+
168
+ await usuarioRepository.updateById(usuario.id, { nome: "Joao Pedro" });
169
+ await usuarioRepository.remove(usuario.id);
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Integração com NestJS
175
+
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
179
+
180
+ ```ts
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
+ };
237
+ ```
238
+
239
+ ### Registrando o provider no módulo
240
+
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
+ ```
256
+
257
+ ### Utilizando o repository em um serviço
258
+
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
290
+
291
+ ```ts
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
+ ---
323
+
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 do VSRepository no console, ótimo para debugar as queries criadas e os objetos passados para o prisma
342
+
343
+ baseMethods: {
344
+ get: {
345
+ active: true,
346
+ defaultSelect: "public",
347
+ },
348
+ remove: {
349
+ active: true,
350
+ defaultSelect: "minimal",
351
+ },
352
+ save: {
353
+ active: true,
354
+ ignoreRequiredWhere: true, // Não aplica requiredWhere no upsert
355
+ },
356
+ },
357
+ });
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Select Models
363
+
364
+ `selectModels` define projeções de dados nomeadas e reutilizáveis.
365
+
366
+ ```ts
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",
373
+ ```
374
+
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:**
378
+
379
+ ```ts
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 });
387
+ ```
388
+
389
+ ---
390
+
391
+ ## requiredWhere
392
+
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.
416
+
417
+ ```ts
418
+ methods: {
419
+ findByEmail: { map: true, fbMode: "one" },
420
+ findManyPaginated: { map: true },
421
+ updateById: { map: true },
422
+ deleteManyByIdIn: { map: true, whereType: "overwrite" },
423
+ }
424
+ ```
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 (pode ser passado como undefined no parâmetro), 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
+ | `AND` | separa bloco final em `AND` | `findByEmailOrNameANDActiveStatus` |
506
+
507
+ `AND` (em capslock) tem uma regra específica:
508
+
509
+ - Só pode existir **um** `AND` por método.
510
+ - Todos os campos (conectados por `And`) **depois** de `AND` são injetados dentro de `AND: []`.
511
+ - Depois de um `AND` não pode ter `Or`.
512
+
513
+ Exemplo:
514
+
515
+ ```ts
516
+ methods: {
517
+ findByIdAndEmail: { map: true, fbMode: "one" },
518
+ findByNomeOrEmail: { map: true },
519
+ findUniqueByIdOrEmailAndNome: { map: true },
520
+ findByEmailOrNameANDActiveStatusAndIdadeGreaterThan: { map: true }
521
+ }
522
+
523
+ // Uso
524
+ await usuarioRepository.findByIdAndEmail(1, "joao@email.com");
525
+ await usuarioRepository.findByNomeOrEmail("Joao", "joao@email.com");
526
+ await usuarioRepository.findUniqueByIdOrEmailAndNome(1, "joao@email.com", "Joao");
527
+ await usuarioRepository.findByEmailOrNameANDActiveStatusAndIdadeGreaterThan("joao@email.com", "Joao", true, 17)
528
+ ```
529
+
530
+ Gera (`findByIdAndEmail`):
531
+
532
+ ```ts
533
+ {
534
+ id: 1,
535
+ email: "joao@email.com"
536
+ }
537
+ ```
538
+
539
+ Gera (`findByNomeOrEmail`):
540
+
541
+ ```ts
542
+ {
543
+ OR: [
544
+ { nome: "Joao" },
545
+ { email: "joao@email.com" }
546
+ ]
547
+ }
548
+ ```
549
+
550
+ Gera (`findUniqueByIdOrEmailAndNome`):
551
+
552
+ ```ts
553
+ {
554
+ OR: [
555
+ { id: 1 },
556
+ {
557
+ email: "joao@email.com",
558
+ nome: "Joao"
559
+ }
560
+ ]
561
+ }
562
+ ```
563
+
564
+ Gera (`findByEmailOrNameANDActiveStatusAndIdadeGreaterThan`):
565
+
566
+ ```ts
567
+ {
568
+ OR: [
569
+ { email: "joao@email.com" },
570
+ { name: "Joao" }
571
+ ],
572
+ AND: [
573
+ { activeStatus: true },
574
+ { idade: { gt: 17 } }
575
+ ]
576
+ }
577
+ ```
578
+
579
+ ---
580
+
581
+ ### Filtros de relação
582
+
583
+ Permitem filtrar por campos de modelos relacionados.
584
+
585
+ | Sufixo de relação | Operador Prisma | Observação |
586
+ | ---------------------- | --------------- | -------------------------------------------------- |
587
+ | `Some` | `some: {}` | Relação tem *algum* registro |
588
+ | `SomeField` | `some.field` | Filtra dentro dos registros da relação |
589
+ | `EveryField` | `every.field` | Filtra dentro dos registros da relação |
590
+ | `None` | `none: {}` | Relação não tem *nenhum* registro |
591
+ | `NoneField` | `none.field` | Filtra dentro dos registros da relação |
592
+ | `With` | `is: {}` | Relação existe (não é null) |
593
+ | `WithField` | `is.field` | Filtra campo dentro da relação |
594
+ | `Without` | `isNot: {}` | Relação não existe (é null) |
595
+ | `WithoutField` | `isNot.field` | Filtra campo dentro da relação com negação |
596
+
597
+ Exemplos:
598
+
599
+ ```ts
600
+ methods: {
601
+ findByPostagensSomeTituloContains: { map: true }, // postagens: { some: { titulo: { contains: valor } } }
602
+ findByPerfilWith: { map: true }, // perfil: { is: {} }
603
+ findByPerfilWithout: { map: true }, // perfil: { isNot: {} }
604
+ findByPostagensSome: { map: true }, // postagens: { some: {} }
605
+ findByPostagensEveryAtivoIsTrue: { map: true }, // postagens: { every: { ativo: true } }
606
+ }
607
+ ```
608
+
609
+ ---
610
+
611
+ ### Sufixos de paginação e ordenação
612
+
613
+ 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.
614
+
615
+ | Sufixo | Argumentos adicionais |
616
+ | --------------------- | ----------------------------- |
617
+ | `Paginated` | `(pagination)` |
618
+ | `Ordered` | `(order)` |
619
+ | `OrderedAndPaginated` | `(order, pagination)` |
620
+ | `PaginatedAndOrdered` | `(pagination, order)` |
621
+
622
+ Para `createMany` e `createManyAndReturn`, o sufixo `SkipDuplicates` está disponível:
623
+
624
+ | Sufixo | Efeito |
625
+ | ----------------- | ---------------------------------------- |
626
+ | `SkipDuplicates` | Ignora registros duplicados na inserção |
627
+
628
+ Exemplos completos:
629
+
630
+ ```ts
631
+ methods: {
632
+ findManyPaginated: { map: true },
633
+ findManyByAtivoOrderedAndPaginated: { map: true },
634
+ findByEmailOrderedAndPaginated: { map: true },
635
+ createManyAndReturnSkipDuplicates: { map: true },
636
+ }
637
+
638
+ // Uso
639
+ await usuarioRepository.findManyPaginated({ skip: 0, take: 10 });
640
+
641
+ await usuarioRepository.findManyByAtivoOrderedAndPaginated(
642
+ true,
643
+ { dataCriacao: "desc" },
644
+ { skip: 0, take: 10 }
645
+ );
646
+ ```
647
+
648
+ `PaginationOptions`:
649
+
650
+ ```ts
651
+ type PaginationOptions<TCursor = unknown> = {
652
+ skip?: number;
653
+ take?: number;
654
+ cursor?: TCursor;
655
+ };
656
+ ```
657
+
658
+ `OrderOptions`:
659
+
660
+ ```ts
661
+ type OrderOptions = OrderPattern | OrderPattern[];
662
+ // Exemplo: { dataCriacao: "desc" } ou [{ dataCriacao: "desc" }, { nome: "asc" }]
663
+ ```
664
+
665
+ ---
666
+
667
+ ### Configuração de métodos
668
+
669
+ Cada entrada em `methods` aceita as seguintes opções:
670
+
671
+ | Opção | Tipo | Padrão | Descrição |
672
+ | ------------------- | ------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------ |
673
+ | `map` | `boolean` | — | **Obrigatório.** Define se o método será exposto no repository. |
674
+ | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combina com `requiredWhere`. `overwrite` ignora o `requiredWhere`. |
675
+ | `selectModel` | `keyof SelectModels \| false` | — | Sobrescreve o `defaultSelectModel` para este método. |
676
+ | `fbMode` | `'one'` \| `'list'` | `'list'` | Somente para `findBy`. `'one'` retorna `T \| null`; `'list'` retorna `T[]`. |
677
+ | `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. |
678
+ | `pushWhere` | `WhereModel<M>` | — | Where extra adicionado à query além do `requiredWhere`. |
679
+ | `injectOrdenation` | `OrdenationModel<M>` | — | Ordenação fixa injetada automaticamente na query. |
680
+ | `injectPagination` | `PaginationModel<M>` | — | Paginação fixa injetada automaticamente na query. |
681
+
682
+ Exemplos:
683
+
684
+ ```ts
685
+ methods: {
686
+ // Retorna um único resultado em vez de array
687
+ findByEmail: { map: true, fbMode: "one" },
688
+
689
+ // Ignora o requiredWhere
690
+ deleteManyByIdIn: { map: true, whereType: "overwrite" },
691
+
692
+ // Usa um select model específico neste método
693
+ findManyByAtivo: { map: true, selectModel: "minimal" },
694
+
695
+ // Adiciona um where extra além do requiredWhere
696
+ findManyByPerfil: { map: true, pushWhere: { deletedAt: null } },
697
+
698
+ // Ordenação fixa sem precisar passar como argumento
699
+ findManyPaginado: { map: true, injectOrdenation: { dataCriacao: "desc" } },
700
+
701
+ // Nome personalizado precisa de proxyTo
702
+ buscarPorEmailEPerfil: { map: true, proxyTo: "findByEmailAndPerfil" },
703
+ }
704
+ ```
705
+
706
+ ---
707
+
708
+ ## Relações no save
709
+
710
+ 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`:
711
+
712
+ ```ts
713
+ import type { Prisma } from "../generated/prisma/client";
714
+
715
+ type Usuario = Prisma.usuarioGetPayload<{
716
+ include: { perfil: true; postagens: true };
717
+ }>;
718
+
719
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
720
+ tableName: "usuario",
721
+ pkName: "id",
722
+
723
+ relations: {
724
+ perfil: {
725
+ pk: "id",
726
+ mode: "oto",
727
+ restriction: "set",
728
+ },
729
+ postagens: {
730
+ pk: "id",
731
+ mode: "otm",
732
+ restriction: "set",
733
+ },
734
+ },
735
+ }).build(prisma);
736
+ ```
737
+
738
+ > **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.
739
+
740
+ **Modos de relação:**
741
+
742
+ | Modo | Relação |
743
+ | ----- | ------------ |
744
+ | `oto` | one-to-one |
745
+ | `otm` | one-to-many |
746
+ | `mto` | many-to-one |
747
+ | `mtm` | many-to-many |
748
+
749
+ **Restrições:**
750
+
751
+ | Restrição | Comportamento no update |
752
+ | --------- | ----------------------------------------------------------- |
753
+ | `set` | Substitui completamente (remove os que não foram enviados) |
754
+ | `add` | Adiciona/atualiza sem remover os existentes |
755
+
756
+ ---
757
+
758
+ ## Transações
759
+
760
+ Todos os métodos aceitam `options.db` para participar de uma transação:
761
+
762
+ ```ts
763
+ await prisma.$transaction(async (tx) => {
764
+ const usuario = await usuarioRepository.save(
765
+ { nome: "Maria", email: "maria@email.com", senha: "password" },
766
+ { db: tx }
767
+ );
768
+
769
+ await usuarioRepository.updateById(
770
+ usuario.id,
771
+ { ativo: true },
772
+ { db: tx }
773
+ );
774
+ });
775
+ ```
776
+
777
+ ---
778
+
779
+ ## Extendendo um repository
780
+
781
+ ```ts
782
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
783
+ tableName: "usuario",
784
+ pkName: "id",
785
+ methods: {
786
+ findByEmailEndsWith: { map: true, fbMode: "one" },
787
+ },
788
+ })
789
+ .build(prisma)
790
+ .extend((repo) => ({
791
+ buscarAtivosPorDominio: async (dominio: string) => {
792
+ return repo.findByEmailEndsWith(`@${dominio}`);
793
+ },
794
+
795
+ ativarMultiplos: async (ids: string[]) => {
796
+ return repo.updateManyByIdIn(ids, { ativo: true });
797
+ },
798
+ }));
799
+ ```
800
+
801
+ ---
802
+
803
+ ## Tratamento de erros
804
+
805
+ O VSRepository lança `VSRepoError` e suas subclasses em situações específicas (OBS: Erros do Prisma não são sobrescritos como `VSRepoError`):
806
+
807
+ ```ts
808
+ import { VSRepoError } from "../generated/vsrepo";
809
+
810
+ try {
811
+ const usuario = await usuarioRepository.get();
812
+ } catch (error) {
813
+ if (error instanceof VSRepoError) {
814
+ console.error("Erro no repository:", error.message);
815
+ }
816
+ }
817
+ ```
818
+
819
+ **Subclasses disponíveis:**
820
+
821
+ | Classe | Quando é lançada |
822
+ | -------------------- | ------------------------------------------------------- |
823
+ | `VSRepoConfigError` | Configuração inválida em `setupVSRepo` ou `build` |
824
+ | `VSRepoBuildError` | Nome de método inválido ou desconhecido no `build` |
825
+ | `VSRepoExtendError` | Argumento inválido em `extend` |
826
+ | `VSRepoRuntimeError` | Erro em tempo de execução durante uma operação |
827
+
828
+ ---
829
+
830
+ ## Tipos utilitários
831
+
832
+ O VSRepository exporta os seguintes tipos para uso nas suas aplicações:
833
+
834
+ ### Tipos de cliente
835
+
836
+ ```ts
837
+ import type { DbClient, DbTransaction, ClientOrTransaction } from "../generated/vsrepo";
838
+
839
+ type DbClient = PrismaClient;
840
+ type DbTransaction = Prisma.TransactionClient;
841
+ type ClientOrTransaction = DbClient | DbTransaction;
842
+ ```
843
+
844
+ ### Tipos derivados do modelo Prisma
845
+
846
+ ```ts
847
+ import type {
848
+ SelectModel,
849
+ SelectModels,
850
+ WhereModel,
851
+ OrdenationModel,
852
+ PaginationModel,
853
+ ModelUpsertInput,
854
+ } from "../generated/vsrepo";
855
+
856
+ // Select de um campo específico
857
+ type UsuarioSelect = SelectModel<"usuario">;
858
+
859
+ // Mapa de selects nomeados
860
+ type UsuarioSelectModels = SelectModels<"usuario">;
861
+
862
+ // Where clause do modelo
863
+ type UsuarioWhere = WhereModel<"usuario">;
864
+
865
+ // OrderBy do modelo
866
+ type UsuarioOrder = OrdenationModel<"usuario">;
867
+
868
+ // Opções de paginação com cursor tipado
869
+ type UsuarioPagination = PaginationModel<"usuario">;
870
+
871
+ // Payload de criação do modelo (para upsert)
872
+ type UsuarioUpsertInput = ModelUpsertInput<"usuario">;
873
+ ```
874
+
875
+ ### Todos os inputs do modelo
876
+
877
+ ```ts
878
+ import type { PrismaModelInputs } from "../generated/vsrepo";
879
+
880
+ type UsuarioInputs = PrismaModelInputs<"usuario">;
881
+ // Contém:
882
+ // select, createInput, createManyInput, updateInput, updateManyInput,
883
+ // whereInput, orderByInput, cursorInput, upsertCreateInput, upsertUpdateInput
884
+ ```
885
+
886
+ ### Tipos de opções de método
887
+
888
+ ```ts
889
+ import type { MethodOptions, MethodOptionsModel } from "../generated/vsrepo";
890
+
891
+ // MethodOptions<S> — opções passadas nos métodos do repository
892
+ // S = chave do select model ou false
893
+ type Opts = MethodOptions<"public" | "minimal">;
894
+
895
+ // MethodOptionsModel<T> — versão derivada do tipo do repository
896
+ type OptsModel = MethodOptionsModel<typeof usuarioSelectModels>;
897
+ ```
898
+
899
+ ### Tipos de configuração
900
+
901
+ ```ts
902
+ import type {
903
+ MethodConfig,
904
+ RepoConfig,
905
+ BuildConfig,
906
+ RepositoryRelations,
907
+ ExtractRelationConfig,
908
+ UpsertWithRelations,
909
+ } from "../generated/vsrepo";
910
+
911
+ // Configuração de um método dinâmico
912
+ type MeuMethodConfig = MethodConfig<"usuario", typeof meuSelectModels>;
913
+
914
+ // Configuração completa do repository
915
+ type MeuRepoConfig = RepoConfig<Usuario, "usuario">;
916
+
917
+ // Configuração do build
918
+ type MeuBuildConfig = BuildConfig<"public" | "minimal">;
919
+
920
+ // Tipo de relações inferidas automaticamente
921
+ type UsuarioRelations = RepositoryRelations<Usuario>;
922
+
923
+ // Configuração de relação inferida a partir de um campo
924
+ type PerfilRelationConfig = ExtractRelationConfig<Usuario["perfil"]>;
925
+
926
+ // Payload do save com relações
927
+ type UsuarioComRelacoes = UpsertWithRelations<Usuario, "usuario", typeof relations>;
928
+ ```
929
+
930
+ ### Tipo do repository construído
931
+
932
+ ```ts
933
+ import type { BuiltRepository, RepositoryOf } from "../generated/vsrepo";
934
+
935
+ // Tipo completo de um repository construído
936
+ type MeuRepo = BuiltRepository<Usuario, "usuario", typeof config, typeof buildConfig>;
937
+
938
+ // Inferência a partir de uma instância VSRepository (útil para injeção de dependência)
939
+ const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({ ... });
940
+ type UsuarioRepository = RepositoryOf<typeof usuarioVSRepo>;
941
+ ```
942
+
943
+ `RepositoryOf` aceita três parâmetros:
944
+
945
+ ```ts
946
+ type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
947
+ // ^ BuildConfig (opcional) ^ tipo do extend (opcional)
948
+ ```
949
+
950
+ Exemplo com extend tipado:
951
+
952
+ ```ts
953
+ const extension = { buscarPorDominio: (dominio: string) => Promise<Usuario[]> };
954
+ type UsuarioRepositoryExtended = RepositoryOf<typeof usuarioVSRepo, undefined, typeof extension>;
955
+ ```
956
+
957
+ ### Tipo auxiliar
958
+
959
+ ```ts
960
+ import type { DistributiveOmit } from "../generated/vsrepo";
961
+
962
+ // Omit distributivo — preserva unions ao omitir propriedades
963
+ type SemEmail = DistributiveOmit<Usuario | Perfil, "email">;
964
+ ```
965
+
966
+ ---
967
+
968
+ ## API Reference
969
+
970
+ ### `setupVSRepo<T, M>()(config)`
971
+
972
+ ```ts
973
+ setupVSRepo<TPayload, TTableName>()({
974
+ tableName: Uncapitalize<M>; // Nome da tabela no Prisma
975
+ pkName: keyof T; // Nome da primary key
976
+ selectModels?: SelectModels<M>; // Projeções de dados nomeadas
977
+ defaultSelectModel?: keyof SM; // Select aplicado por padrão
978
+ requiredWhere?: WhereModel<M>; // Filtros sempre aplicados
979
+ relations?: RepositoryRelations<T>; // Configuração de relações
980
+ methods?: Record<string, MethodConfig<M, SM>>; // Métodos dinâmicos
981
+ });
982
+ ```
983
+
984
+ ### `.build(prisma, config?)`
985
+
986
+ ```ts
987
+ vsRepo.build(prisma, {
988
+ freeze?: boolean; // Congela o objeto (padrão: true)
989
+ showWorking?: boolean; // Exibe logs internos no console
990
+
991
+ baseMethods?: {
992
+ get?: { active?: boolean; defaultSelect?: string };
993
+ remove?: { active?: boolean; defaultSelect?: string };
994
+ save?: { active?: boolean; ignoreRequiredWhere?: boolean };
995
+ };
996
+ });
997
+ ```
998
+
999
+ ### `.extend(fn)`
1000
+
1001
+ ```ts
1002
+ repo.extend((repo) => ({
1003
+ meuMetodo() { ... }
1004
+ }));
1005
+ ```
1006
+
1007
+ ---
1008
+
1009
+ ## Requisitos
1010
+
1011
+ - Node.js 16+ (ESM)
1012
+ - Prisma
1013
+ - TypeScript (opcional, mas fortemente recomendado)
1014
+ - `"moduleResolution": "bundler"` ou `"nodenext"` no tsconfig
1015
+
1016
+ `tsconfig.json` recomendado:
1017
+
1018
+ ```json
1019
+ {
1020
+ "compilerOptions": {
1021
+ "target": "ES2020",
1022
+ "module": "NodeNext",
1023
+ "moduleResolution": "NodeNext",
1024
+ "strict": true,
1025
+ "skipLibCheck": true,
1026
+ "lib": ["ES2020"]
1027
+ }
1028
+ }
1029
+ ```
1030
+
1031
+ ---
1032
+
1033
+ ## Troubleshooting
1034
+
1035
+ **Tipos genéricos não inferidos** — Verifique se `strict: true` e `moduleResolution: "bundler"` ou `"nodenext"` estão no `tsconfig.json`.
1036
+
1037
+ **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`.
1038
+
1039
+ **`proxyTo` obrigatório** — Nomes fora dos moldes (ex.: `buscarPorEmail`) não são parseados diretamente. Use `proxyTo: "findByEmail"` nesses casos.
1040
+
983
1041
  **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.