vsrepo 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +310 -441
  2. package/{VSRepository → dist}/VSRepoError.d.ts +1 -1
  3. package/dist/VSRepoError.js +17 -0
  4. package/{VSRepository → dist}/VSRepository.d.ts +937 -887
  5. package/dist/VSRepository.js +204 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +17 -0
  8. package/dist/internal/errors/types/vs-repo-error-type.type.js +2 -0
  9. package/dist/internal/errors/vs-repo.error.js +27 -0
  10. package/dist/internal/resolvers/base-methods.resolve.js +535 -0
  11. package/dist/internal/resolvers/create-update-payloads-with-relations.resolve.js +143 -0
  12. package/dist/internal/resolvers/data-payload-with-relations.resolve.js +60 -0
  13. package/dist/internal/resolvers/dbAndPrismaArgs.resolve.js +46 -0
  14. package/dist/internal/resolvers/dinamic-method-customization.resolve.js +45 -0
  15. package/dist/internal/resolvers/dinamic-method-info.resolve.js +265 -0
  16. package/dist/internal/resolvers/merge-wheres.resolve.js +22 -0
  17. package/dist/internal/resolvers/pretty-wheres.resolve.js +87 -0
  18. package/dist/internal/resolvers/select.resolve.js +7 -0
  19. package/dist/internal/resolvers/specific-where.resolve.js +77 -0
  20. package/dist/internal/resolvers/types/base-method-function.type.js +2 -0
  21. package/dist/internal/resolvers/types/dinamic-method-customization.type.js +2 -0
  22. package/dist/internal/resolvers/types/dinamic-method-info.type.js +2 -0
  23. package/dist/internal/resolvers/types/dinamic-method-where-ops.type.js +2 -0
  24. package/dist/internal/resolvers/types/pretty-where.type.js +2 -0
  25. package/dist/internal/resolvers/types/prisma-args.type.js +2 -0
  26. package/dist/internal/resolvers/types/repository-build-instance.type.js +2 -0
  27. package/dist/internal/resolvers/types/resolve-db-and-prisma-args-data.type.js +2 -0
  28. package/dist/internal/resolvers/types/ugly-where.type.js +2 -0
  29. package/dist/internal/resolvers/ugly-where.resolve.js +178 -0
  30. package/dist/internal/utils/logger.util.js +21 -0
  31. package/dist/internal/utils/schemas.util.js +10 -0
  32. package/dist/internal/utils/uncapitalize.util.js +6 -0
  33. package/dist/internal/validation/build-config.validate.js +84 -0
  34. package/dist/internal/validation/constructor-config.validate.js +68 -0
  35. package/dist/internal/validation/extension.validate.js +15 -0
  36. package/dist/internal/validation/is-object.validate.js +6 -0
  37. package/dist/internal/validation/method-options.validate.js +26 -0
  38. package/dist/internal/validation/obj-with-relations.validate.js +37 -0
  39. package/dist/internal/validation/prisma-client.validate.js +10 -0
  40. package/dist/internal/validation/types/base-methods.type.js +2 -0
  41. package/dist/internal/validation/types/build-config.type.js +2 -0
  42. package/dist/internal/validation/types/constructor-config.type.js +2 -0
  43. package/dist/internal/validation/types/method-options.type.js +2 -0
  44. package/dist/internal/validation/types/method.type.js +2 -0
  45. package/dist/internal/validation/types/pagination.type.js +2 -0
  46. package/dist/internal/validation/types/relation.type.js +2 -0
  47. package/dist/internal/validation/types/see-mode.type.js +2 -0
  48. package/package.json +72 -70
  49. package/scripts/configure-prisma-import.mjs +9 -8
  50. package/scripts/copy-types.mjs +24 -0
  51. package/VSRepository/VSRepoError.js +0 -33
  52. package/VSRepository/VSRepoUtils.js +0 -150
  53. package/VSRepository/VSRepository.js +0 -1394
  54. package/VSRepository/index.d.ts +0 -2
  55. package/VSRepository/index.js +0 -2
package/README.md CHANGED
@@ -2,17 +2,18 @@
2
2
 
3
3
  ![npm](https://img.shields.io/npm/v/vsrepo?style=flat-square)
4
4
  ![NPM License](https://img.shields.io/npm/l/vsrepo)
5
- ![NPM Downloads](https://img.shields.io/npm/d18m/vsrepo.svg)
5
+ ![NPM Downloads](https://img.shields.io/npm/dt/vsrepo?style=flat-square)
6
6
 
7
7
  Biblioteca de repository pattern para projetos que usam **Prisma**, com suporte completo a **TypeScript** e **type inference** automático.
8
8
 
9
9
  O VSRepository permite criar repositories fortemente tipados com:
10
10
 
11
- - **Métodos base** automáticos: `get`, `getOrThrow`, `save`, `remove`, `removeList`, `getAll`, `total`, `has`
11
+ - **Métodos base** automáticos: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `patchList`, `merge`, `getAll`, `total`, `has`
12
+ - **Soft-delete nativo**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
12
13
  - **Métodos dinâmicos** inferidos pelo nome: `findByEmail`, `findManyPaginated`, `updateById`, `deleteManyByIdIn`
13
14
  - **Select models** reutilizáveis para diferentes projeções de dados
14
15
  - **Type safety** em 100% das operações
15
- - **Transações** nativas do Prisma
16
+ - **Transações** nativas do Prisma (automáticas em `saveList` e `patchList`)
16
17
  - **Extensibilidade** com métodos personalizados
17
18
 
18
19
  ---
@@ -24,8 +25,13 @@ O VSRepository permite criar repositories fortemente tipados com:
24
25
  - [Uso básico](#uso-básico)
25
26
  - [Integração com NestJS](#integração-com-nestjs)
26
27
  - [Métodos base](#métodos-base)
28
+ - [Soft-delete](#soft-delete)
29
+ - [Operações em lote](#operações-em-lote)
30
+ - [Merge](#merge)
31
+ - [Configurando os métodos base](#configurando-os-métodos-base)
27
32
  - [Select Models](#select-models)
28
33
  - [Required Where](#requiredwhere)
34
+ - [Opção `see`](#opção-see)
29
35
  - [Métodos dinâmicos](#métodos-dinâmicos)
30
36
  - [Prefixos disponíveis](#prefixos-disponíveis)
31
37
  - [Filtros de campo](#filtros-de-campo)
@@ -130,7 +136,7 @@ import prisma from "../configs/db";
130
136
  import { setupVSRepo } from "../../generated/vsrepo";
131
137
  import type { Usuario } from "../../generated/prisma/client";
132
138
 
133
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
139
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
134
140
  tableName: "usuario",
135
141
  pkName: "id",
136
142
  selectModels: {
@@ -183,7 +189,7 @@ import { setupVSRepo } from "../../../generated/vsrepo";
183
189
  const userVSRepo = setupVSRepo<
184
190
  UserGetPayload<{ include: { profile: true } }>,
185
191
  "User"
186
- >()({
192
+ >()(({
187
193
  tableName: "user",
188
194
  pkName: "id",
189
195
  selectModels: {
@@ -291,31 +297,6 @@ export class UserService {
291
297
  }
292
298
  ```
293
299
 
294
- ### Usando em um controller
295
-
296
- ```ts
297
- // src/resources/user/user.controller.ts
298
- import { Controller, Get, Post, Body, Param, Patch, Delete } from "@nestjs/common";
299
- import { UserService } from "./user.service";
300
-
301
- @Controller("users")
302
- export class UserController {
303
- constructor(private readonly userService: UserService) {}
304
-
305
- @Get(":id")
306
- async getUser(@Param("id") id: string) {
307
- return this.userService.getUserById(id);
308
- }
309
-
310
- @Post()
311
- async createUser(
312
- @Body() data: { email: string; password: string; name: string }
313
- ) {
314
- return this.userService.createUser(data);
315
- }
316
- }
317
- ```
318
-
319
300
  **Benefícios desta abordagem:**
320
301
 
321
302
  - ✅ Type-safe repositories com injeção de dependência
@@ -330,26 +311,99 @@ export class UserController {
330
311
 
331
312
  Ao chamar `.build(prisma)` os métodos base abaixo são automaticamente disponibilizados:
332
313
 
333
- | Método | Descrição |
334
- | ------------ | ------------------------------------------ |
335
- | `get(pk)` | Busca um registro pela primary key |
336
- | `getOrThrow(pk)` | Busca um registro pela primary key e lança erro se não encontrar |
337
- | `save(obj)` | Cria ou atualiza (se o objeto passado tiver a `pk` faz `upsert`, se não faz `create`) |
338
- | `patch(pk, obj)` | Atualiza parcialmente um registro pela primary key (equivalente ao `update` com payload parcial) |
339
- | `remove(pk)` | Remove um registro pela primary key |
340
- | `removeList(pks)` | Remove vários registros pela lista de primary keys — retorna `{ count }` |
341
- | `getAll()` | Retorna todos os registros (aceita `pagination` e `order` no `options`) |
342
- | `total()` | Retorna o total de registros |
343
- | `has(pk)` | Verifica existência de um registro pela primary key — retorna `boolean` |
314
+ | Método | Descrição |
315
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
316
+ | `get(pk)` | Busca um registro pela primary key |
317
+ | `getOrThrow(pk)` | Busca um registro pela primary key; lança `VSRepoRuntimeError` (code `"20727"`) se não encontrado |
318
+ | `getList(pks)` | Busca múltiplos registros por uma lista de primary keys |
319
+ | `save(obj)` | Cria ou atualiza — se o objeto tiver a `pk` faz `upsert`, caso contrário faz `create` |
320
+ | `saveList(objs)` | Salva um array de objetos em uma única transação automática |
321
+ | `patch(pk, obj)` | Atualiza parcialmente um registro pela primary key |
322
+ | `patchList(tuples)` | Atualiza parcialmente múltiplos registros via array de tuplas `[pk, obj]` em transação automática |
323
+ | `merge(pk, obj)` | Busca um registro e faz um deep merge em memória — **não persiste**, retorna o objeto mesclado |
324
+ | `remove(pk)` | Remove um registro pela primary key |
325
+ | `removeList(pks)` | Remove vários registros pela lista de primary keys — retorna `{ count }` |
326
+ | `getAll()` | Retorna todos os registros (aceita `pagination` e `order` no `options`) |
327
+ | `total()` | Retorna o total de registros |
328
+ | `has(pk)` | Verifica existência de um registro pela primary key — retorna `boolean` |
344
329
 
345
330
  Todos aceitam `options` como último argumento.
346
331
 
332
+ ### Soft-delete
333
+
334
+ Quando `softRemovekName` está configurado no repository, os seguintes métodos adicionais ficam disponíveis:
335
+
336
+ | Método | Descrição |
337
+ | -------------------------- | --------------------------------------------------------------------------------- |
338
+ | `softRemove(pk)` | Marca um registro como removido preenchendo `softRemovekName` com a data atual |
339
+ | `softRemoveList(pks)` | Marca múltiplos registros como removidos em lote — retorna `{ count }` |
340
+ | `restore(pk)` | Restaura um registro soft-deletado, limpando o campo `softRemovekName` |
341
+ | `restoreList(pks)` | Restaura múltiplos registros soft-deletados em lote — retorna `{ count }` |
342
+
343
+ ```ts
344
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
345
+ tableName: "usuario",
346
+ pkName: "id",
347
+ softRemovekName: "deletedAt", // deve ser um campo DateTime no schema do Prisma
348
+ }).build(prisma);
349
+
350
+ await usuarioRepository.softRemove(1);
351
+ await usuarioRepository.restore(1);
352
+ ```
353
+
354
+ > O campo informado em `softRemovekName` **deve** ser do tipo `DateTime` no schema do Prisma. O VSRepository valida isso no momento do `build` e lança `VSRepoBuildError` se o tipo for incorreto.
355
+
356
+ ### Operações em lote
357
+
358
+ `saveList` e `patchList` executam todas as operações automaticamente dentro de uma única transação do Prisma. Se alguma falhar, todas as anteriores são revertidas.
359
+
360
+ ```ts
361
+ // saveList — cria ou atualiza múltiplos objetos em transação automática
362
+ const usuarios = await usuarioRepository.saveList([
363
+ { nome: "Maria", email: "maria@email.com" },
364
+ { id: 2, nome: "João Atualizado" },
365
+ ]);
366
+
367
+ // patchList — atualiza parcialmente múltiplos registros via tuplas [pk, obj]
368
+ const atualizados = await usuarioRepository.patchList([
369
+ [1, { ativo: false }],
370
+ [2, { nome: "Novo Nome" }],
371
+ ]);
372
+ ```
373
+
374
+ Quando você já está dentro de uma transação existente, passe-a em `options.db`. Nesse caso, o `db` deve ser um `DbTransaction` (não o cliente principal), pois o método não cria uma transação própria:
375
+
376
+ ```ts
377
+ await prisma.$transaction(async (tx) => {
378
+ await usuarioRepository.saveList([{ nome: "Maria" }], { db: tx });
379
+ await usuarioRepository.patchList([[1, { ativo: false }]], { db: tx });
380
+ });
381
+ ```
382
+
383
+ ### Merge
384
+
385
+ O método `merge` busca um registro pela PK e mescla profundamente (`deepmerge`) o objeto fornecido com os dados existentes **em memória**. Ele **não persiste** as alterações — retorna o resultado mesclado para que você decida o que fazer com ele.
386
+
387
+ ```ts
388
+ const existente = await usuarioRepository.get(1);
389
+ // existente: { id: 1, nome: "Maria", perfil: { bio: "Olá", idade: 25 } }
390
+
391
+ const mesclado = await usuarioRepository.merge(1, {
392
+ perfil: { bio: "Bio atualizada" },
393
+ });
394
+ // mesclado: { id: 1, nome: "Maria", perfil: { bio: "Bio atualizada", idade: 25 } }
395
+
396
+ // Para persistir, passe para save ou patch:
397
+ await usuarioRepository.save(mesclado);
398
+ ```
399
+
400
+ Retorna `null` se o registro não for encontrado.
401
+
347
402
  ### Configurando os métodos base
348
403
 
349
404
  ```ts
350
405
  usuarioVSRepo.build(prisma, {
351
- freeze: true, // Congela o objeto (padrão = true)
352
- showWorking: true, // Exibe logs do VSRepository no console, ótimo para debugar as queries criadas e os objetos passados para o prisma
406
+ showWorking: true, // Exibe logs do VSRepository no console, ótimo para debugar
353
407
 
354
408
  baseMethods: {
355
409
  get: {
@@ -359,17 +413,21 @@ usuarioVSRepo.build(prisma, {
359
413
  remove: {
360
414
  active: true,
361
415
  defaultSelect: "minimal",
362
- ignoreRequiredWhere: false, // Aplica o requiredWhere no remove (padrão = false)
416
+ ignoreRequiredWhere: false,
363
417
  },
364
418
  save: {
365
- ignoreRequiredWhere: true, // Não aplica requiredWhere no save
419
+ ignoreRequiredWhere: true,
366
420
  },
367
421
  patch: {
368
422
  defaultSelect: "minimal",
369
423
  },
370
424
  has: {
371
425
  active: false, // Desativa o 'has' (padrão = true)
372
- }
426
+ },
427
+ softRemove: {
428
+ active: true,
429
+ defaultSelect: "minimal",
430
+ },
373
431
  },
374
432
  });
375
433
  ```
@@ -423,7 +481,36 @@ const usuarios = await usuarioRepository.findMany();
423
481
  const usuario = await usuarioRepository.findByEmail("joao@email.com");
424
482
  ```
425
483
 
426
- Útil para soft-deletes, multi-tenancy e filtros globais de qualquer natureza.
484
+ Útil para soft-deletes manuais, multi-tenancy e filtros globais de qualquer natureza.
485
+
486
+ ---
487
+
488
+ ## Opção `see`
489
+
490
+ Quando `softRemovekName` está configurado, todos os métodos base aceitam a opção `see` para controlar a visibilidade de registros soft-deletados:
491
+
492
+ | Valor | Comportamento |
493
+ | ----------- | ------------------------------------------------------------- |
494
+ | `"active"` | Retorna apenas registros **não** removidos (padrão) |
495
+ | `"removed"` | Retorna apenas registros removidos |
496
+ | `"all"` | Retorna todos os registros, independentemente do status |
497
+
498
+ ```ts
499
+ // Retorna apenas usuários ativos (padrão)
500
+ const ativos = await usuarioRepository.getAll();
501
+
502
+ // Retorna apenas usuários removidos
503
+ const removidos = await usuarioRepository.getAll({ see: "removed" });
504
+
505
+ // Retorna todos
506
+ const todos = await usuarioRepository.getAll({ see: "all" });
507
+
508
+ // Funciona em qualquer método base
509
+ const usuario = await usuarioRepository.get(id, { see: "all" });
510
+ const existe = await usuarioRepository.has(id, { see: "removed" });
511
+ ```
512
+
513
+ > A opção `see` funciona independentemente do `requiredWhere` — ela é aplicada em cima do filtro de soft-delete, não o substitui.
427
514
 
428
515
  ---
429
516
 
@@ -433,10 +520,10 @@ Métodos dinâmicos são definidos na propriedade `methods` e têm seus comporta
433
520
 
434
521
  ```ts
435
522
  methods: {
436
- findByEmail: { map: true, fbMode: "one" },
437
- findManyPaginated: { map: true },
438
- updateById: { map: true },
439
- deleteManyByIdIn: { map: true, whereType: "overwrite" },
523
+ findOneByEmail: { map: true },
524
+ findManyPaginated: { map: true },
525
+ updateById: { map: true },
526
+ deleteManyByIdIn: { map: true },
440
527
  }
441
528
  ```
442
529
 
@@ -446,34 +533,40 @@ methods: {
446
533
 
447
534
  O prefixo do nome do método determina qual operação Prisma será chamada e quais argumentos serão esperados.
448
535
 
449
- | Prefixo | Operação Prisma | Retorno | Observações |
450
- | ------------------------- | ------------------------ | ---------------------- | -------------------------------------------------------- |
451
- | `findBy` | `findMany` / `findFirst` | `T[]` ou `T \| null` | Padrão é lista; use `fbMode: "one"` para retorno único |
452
- | `findUniqueBy` | `findUnique` | `T \| null` | |
453
- | `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Lança erro se não encontrar |
454
- | `findFirstBy` | `findFirst` | `T \| null` | Aceita campos como filtro |
455
- | `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Aceita campos como filtro; lança erro se não encontrar |
456
- | `findFirst` | `findFirst` | `T \| null` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
457
- | `findFirstOrThrow` | `findFirstOrThrow` | `T` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere`; lança erro se não encontrar |
458
- | `findManyBy` | `findMany` | `T[]` | Aceita campos como filtro |
459
- | `findMany` | `findMany` | `T[]` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
460
- | `findWhere` | `findFirst` | `T \| null` | Recebe um objeto `where` explícito como argumento |
461
- | `findListWhere` | `findMany` | `T[]` | Recebe um objeto `where` explícito como argumento |
462
- | `existsBy` | `findFirst` | `boolean` | Retorna `true` se encontrar, `false` caso contrário |
463
- | `countBy` | `count` | `number` | Aceita campos como filtro |
464
- | `countWhere` | `count` | `number` | Recebe um objeto `where` explícito como argumento |
465
- | `count` | `count` | `number` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
466
- | `create` | `create` | `T` | Recebe `data` como argumento |
467
- | `createMany` | `createMany` | `{ count: number }` | Recebe `data` como argumento; suporta `SkipDuplicates` |
468
- | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Recebe `data` como argumento; suporta `SkipDuplicates` |
469
- | `updateBy` | `update` | `T` | Recebe `data` como argumento |
470
- | `updateManyBy` | `updateMany` | `{ count: number }` | Recebe `data` como argumento |
471
- | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Recebe `data` como argumento |
472
- | `upsertBy` | `upsert` | `T` | Recebe `update` e `create` como argumentos |
473
- | `deleteBy` | `delete` | `T` | |
474
- | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
475
- | `aggregate` | `aggregate` | `Dinâmico` | Nome deve ser exato; recebe args nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
476
- | `groupBy` | `groupBy` | `Dinâmico[]` | Nome deve ser exato; recebe args nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
536
+ | Prefixo | Operação Prisma | Retorno | Observações |
537
+ | -------------------------- | ------------------------- | ---------------------- | ------------------------------------------------------------------------ |
538
+ | `findOneBy` | `findFirst` | `T \| null` | Retorno único. |
539
+ | `findBy` | `findMany` / `findFirst` | `T[]` ou `T \| null` | Padrão é lista; use `fbMode: "one"` para retorno único (**obsoleto**, use `findOneBy`) |
540
+ | `findUniqueBy` | `findUnique` | `T \| null` | |
541
+ | `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Lança erro se não encontrar |
542
+ | `findFirstBy` | `findFirst` | `T \| null` | Aceita campos como filtro |
543
+ | `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Aceita campos como filtro; lança erro se não encontrar |
544
+ | `findFirst` | `findFirst` | `T \| null` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
545
+ | `findFirstOrThrow` | `findFirstOrThrow` | `T` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere`; lança erro se não encontrar |
546
+ | `findManyBy` | `findMany` | `T[]` | Aceita campos como filtro |
547
+ | `findMany` | `findMany` | `T[]` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
548
+ | `findOneWhere` | `findFirst` | `T \| null` | Recebe um objeto `where` explícito como argumento |
549
+ | `findWhere` | `findFirst` | `T \| null` | (**Obsoleto, use `findOneWhere`**) Recebe um objeto `where` explícito |
550
+ | `findListWhere` | `findMany` | `T[]` | Recebe um objeto `where` explícito como argumento |
551
+ | `existsBy` | `findFirst` | `boolean` | Retorna `true` se encontrar, `false` caso contrário |
552
+ | `existsWhere` | `findFirst` | `boolean` | Recebe um objeto `where` explícito e retorna se existe |
553
+ | `countBy` | `count` | `number` | Aceita campos como filtro |
554
+ | `countWhere` | `count` | `number` | Recebe um objeto `where` explícito como argumento |
555
+ | `count` | `count` | `number` | Sem filtros de campo; aplica só `requiredWhere` e `pushWhere` |
556
+ | `create` | `create` | `T` | Recebe `data` como argumento |
557
+ | `createMany` | `createMany` | `{ count: number }` | Recebe `data` como argumento; suporta `SkipDuplicates` |
558
+ | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Recebe `data` como argumento; suporta `SkipDuplicates` |
559
+ | `updateBy` | `update` | `T` | Recebe `data` como argumento |
560
+ | `updateManyBy` | `updateMany` | `{ count: number }` | Recebe `data` como argumento |
561
+ | `updateManyWhere` | `updateMany` | `{ count: number }` | Recebe um objeto `where` e um objeto `data` como argumentos |
562
+ | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Recebe `data` como argumento |
563
+ | `updateManyAndReturnWhere` | `updateManyAndReturn` | `T[]` | Recebe um objeto `where` e um objeto `data` como argumentos |
564
+ | `upsertBy` | `upsert` | `T` | Recebe `update` e `create` como argumentos |
565
+ | `deleteBy` | `delete` | `T` | |
566
+ | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
567
+ | `deleteManyWhere` | `deleteMany` | `{ count: number }` | Recebe um objeto `where` explícito como argumento |
568
+ | `aggregate` | `aggregate` | `Dinâmico` | Nome deve ser exato; recebe args nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
569
+ | `groupBy` | `groupBy` | `Dinâmico[]` | Nome deve ser exato; recebe args nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
477
570
 
478
571
  ---
479
572
 
@@ -481,29 +574,29 @@ O prefixo do nome do método determina qual operação Prisma será chamada e qu
481
574
 
482
575
  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`).
483
576
 
484
- | Sufixo | Operador Prisma | Argumento necessário |
485
- | ------------------ | --------------------- | -------------------- |
486
- | *(sem sufixo)* | igualdade (`=`) | sim |
487
- | `Not` | `not` | sim |
488
- | `In` | `in` | sim (array) |
489
- | `NotIn` | `notIn` | sim (array) |
490
- | `Contains` | `contains` | sim |
491
- | `NotContains` | `not.contains` | sim |
492
- | `StartsWith` | `startsWith` | sim |
493
- | `NotStartsWith` | `not.startsWith` | sim |
494
- | `EndsWith` | `endsWith` | sim |
495
- | `NotEndsWith` | `not.endsWith` | sim |
496
- | `GreaterThan` | `gt` | sim |
497
- | `GreaterThanEqual` | `gte` | sim |
498
- | `LessThan` | `lt` | sim |
499
- | `LessThanEqual` | `lte` | sim |
500
- | `Between` | `gte` + `lte` | sim (tupla `[min, max]`) |
501
- | `NotBetween` | `not.gte` + `not.lte` | sim (tupla `[min, max]`) |
502
- | `IsNull` | `null` | não |
503
- | `IsNotNull` | `not: null` | não |
504
- | `IsTrue` | `true` | não |
505
- | `IsFalse` | `false` | não |
506
- | `Insensitive` | `mode: 'insensitive'` | combinador |
577
+ | Sufixo | Operador Prisma | Argumento necessário |
578
+ | ------------------ | --------------------- | ------------------------- |
579
+ | *(sem sufixo)* | igualdade (`=`) | sim |
580
+ | `Not` | `not` | sim |
581
+ | `In` | `in` | sim (array) |
582
+ | `NotIn` | `notIn` | sim (array) |
583
+ | `Contains` | `contains` | sim |
584
+ | `NotContains` | `not.contains` | sim |
585
+ | `StartsWith` | `startsWith` | sim |
586
+ | `NotStartsWith` | `not.startsWith` | sim |
587
+ | `EndsWith` | `endsWith` | sim |
588
+ | `NotEndsWith` | `not.endsWith` | sim |
589
+ | `GreaterThan` | `gt` | sim |
590
+ | `GreaterThanEqual` | `gte` | sim |
591
+ | `LessThan` | `lt` | sim |
592
+ | `LessThanEqual` | `lte` | sim |
593
+ | `Between` | `gte` + `lte` | sim (tupla `[min, max]`) |
594
+ | `NotBetween` | `not.gte` + `not.lte` | sim (tupla `[min, max]`) |
595
+ | `IsNull` | `null` | não |
596
+ | `IsNotNull` | `not: null` | não |
597
+ | `IsTrue` | `true` | não |
598
+ | `IsFalse` | `false` | não |
599
+ | `Insensitive` | `mode: 'insensitive'` | combinador |
507
600
 
508
601
  `Insensitive` é um combinador e pode ser usado junto com outro filtro de texto:
509
602
 
@@ -513,41 +606,24 @@ findByEmailStartsWithInsensitive // { email: { startsWith: valor, mode: 'insensi
513
606
  findByNomeInsensitive // { nome: { equals: valor, mode: 'insensitive' } }
514
607
  ```
515
608
 
516
- `Between` e `NotBetween` recebem uma **tupla `[minValue, maxValue]`** e são ideais para filtros de intervalo em números, datas ou qualquer campo comparável:
609
+ `Between` e `NotBetween` recebem uma **tupla `[minValue, maxValue]`**:
517
610
 
518
611
  ```ts
519
612
  methods: {
520
- findManyByIdadeBetween: { map: true },
521
- findManyBySalarioNotBetween: { map: true },
522
- findManyByCriadoEmBetween: { map: true },
613
+ findManyByIdadeBetween: { map: true },
614
+ findManyBySalarioNotBetween: { map: true },
615
+ findManyByCriadoEmBetween: { map: true },
523
616
  }
524
617
 
525
- // Uso
526
618
  await usuarioRepository.findManyByIdadeBetween([18, 65]);
527
619
  await usuarioRepository.findManyBySalarioNotBetween([1000, 5000]);
528
620
  await usuarioRepository.findManyByCriadoEmBetween([new Date("2024-01-01"), new Date("2024-12-31")]);
529
621
  ```
530
622
 
531
- Gera (`findManyByIdadeBetween`):
532
-
533
- ```ts
534
- {
535
- idade: { gte: 18, lte: 65 }
536
- }
537
- ```
538
-
539
- Gera (`findManyBySalarioNotBetween`):
540
-
541
- ```ts
542
- {
543
- salario: { not: { gte: 1000, lte: 5000 } }
544
- }
545
- ```
546
-
547
623
  O sufixo `Optional` pode ser adicionado a qualquer campo para tornar o argumento opcional:
548
624
 
549
625
  ```ts
550
- findByNomeOptionalAndEmail // nome é opcional (pode ser passado como undefined no parâmetro), email é obrigatório
626
+ findByNomeOptionalAndEmail // nome é opcional, email é obrigatório
551
627
  ```
552
628
 
553
629
  ---
@@ -556,67 +632,32 @@ findByNomeOptionalAndEmail // nome é opcional (pode ser passado como undefined
556
632
 
557
633
  | Operador | Uso no nome | Exemplo |
558
634
  | --------- | ---------------------------- | -------------------------------- |
559
- | `And` | entre dois campos | `findByIdAndEmail` |
635
+ | `And` | entre dois campos | `findOneByIdAndEmail` |
560
636
  | `Or` | entre dois campos | `findByNomeOrEmail` |
561
637
  | `AND` | separa bloco final em `AND` | `findByEmailOrNameANDActiveStatus` |
562
638
 
563
639
  `AND` (em capslock) tem uma regra específica:
564
640
 
565
641
  - Só pode existir **um** `AND` por método.
566
- - Todos os campos (conectados por `And`) **depois** de `AND` são injetados dentro de `AND: []`.
642
+ - Todos os campos depois de `AND` são injetados dentro de `AND: []`.
567
643
  - Depois de um `AND` não pode ter `Or`.
568
644
 
569
645
  Exemplo:
570
646
 
571
647
  ```ts
572
648
  methods: {
573
- findByIdAndEmail: { map: true, fbMode: "one" },
649
+ findOneByIdAndEmail: { map: true },
574
650
  findByNomeOrEmail: { map: true },
575
651
  findUniqueByIdOrEmailAndNome: { map: true },
576
652
  findByEmailOrNameANDActiveStatusAndIdadeGreaterThan: { map: true }
577
653
  }
578
654
 
579
- // Uso
580
- await usuarioRepository.findByIdAndEmail(1, "joao@email.com");
655
+ await usuarioRepository.findOneByIdAndEmail(1, "joao@email.com");
581
656
  await usuarioRepository.findByNomeOrEmail("Joao", "joao@email.com");
582
657
  await usuarioRepository.findUniqueByIdOrEmailAndNome(1, "joao@email.com", "Joao");
583
658
  await usuarioRepository.findByEmailOrNameANDActiveStatusAndIdadeGreaterThan("joao@email.com", "Joao", true, 17)
584
659
  ```
585
660
 
586
- Gera (`findByIdAndEmail`):
587
-
588
- ```ts
589
- {
590
- id: 1,
591
- email: "joao@email.com"
592
- }
593
- ```
594
-
595
- Gera (`findByNomeOrEmail`):
596
-
597
- ```ts
598
- {
599
- OR: [
600
- { nome: "Joao" },
601
- { email: "joao@email.com" }
602
- ]
603
- }
604
- ```
605
-
606
- Gera (`findUniqueByIdOrEmailAndNome`):
607
-
608
- ```ts
609
- {
610
- OR: [
611
- { id: 1 },
612
- {
613
- email: "joao@email.com",
614
- nome: "Joao"
615
- }
616
- ]
617
- }
618
- ```
619
-
620
661
  Gera (`findByEmailOrNameANDActiveStatusAndIdadeGreaterThan`):
621
662
 
622
663
  ```ts
@@ -638,6 +679,12 @@ Gera (`findByEmailOrNameANDActiveStatusAndIdadeGreaterThan`):
638
679
 
639
680
  Permitem filtrar por campos de modelos relacionados.
640
681
 
682
+ > [!IMPORTANT]
683
+ > - **Tipagem de relação**: Para que o TypeScript reconheça os tipos dos campos de relação nos métodos dinâmicos, o tipo genérico da entidade passado no `setupVSRepo` deve incluir as relações estruturadas (ex: usando `UsuarioGetPayload<{ include: { perfil: true, postagens: true } }>` do Prisma).
684
+ > - **Compatibilidade de sufixos**:
685
+ > - Os sufixos `Some`, `Every` e `None` só funcionam para relações **to-many** (`many-to-many` e `one-to-many`).
686
+ > - Os sufixos `With` e `Without` só funcionam para relações **to-one** (`one-to-one` e `many-to-one`).
687
+
641
688
  | Sufixo de relação | Operador Prisma | Observação |
642
689
  | ---------------------- | --------------- | -------------------------------------------------- |
643
690
  | `Some` | `some: {}` | Relação tem *algum* registro |
@@ -650,27 +697,11 @@ Permitem filtrar por campos de modelos relacionados.
650
697
  | `Without` | `isNot: {}` | Relação não existe (é null) |
651
698
  | `WithoutField` | `isNot.field` | Filtra campo dentro da relação com negação |
652
699
 
653
- Exemplos:
654
-
655
- ```ts
656
- methods: {
657
- findByPostagensSomeTituloContains: { map: true }, // postagens: { some: { titulo: { contains: valor } } } >> (Busca usuários que o título de alguma postagem contém um valor)
658
- findByPerfilWithDescricaoIsNotNull: { map: true }, // perfil: { is: { descricao: { not: null } } } >> (Busca usuários em que a descrição do perfil não é nula)
659
- findByPerfilWithout: { map: true }, // perfil: { isNot: {} } >> (Busca usuários sem perfil)
660
- findByPostagensSome: { map: true }, // postagens: { some: {} } >> (Busca usuários com alguma postagem)
661
- findByPostagensEveryAtivoIsTrue: { map: true }, // postagens: { every: { ativo: true } } >> (Busca usuários que todas as postagens estão ativas)
662
- findByPostagensNone: { map: true }, // postagens: { none: {} } >> (Busca usuários sem postagens)
663
- }
664
- ```
665
-
666
- > [!NOTE]
667
- > Teoricamente você pode usar `Every` sem `Field` (ele geraria { every: {} }), porém isso não produz um filtro efetivo. A condição é considerada verdadeira para qualquer relação, inclusive quando não existem registros relacionados, tornando o resultado equivalente a não aplicar filtro algum.
668
-
669
700
  ---
670
701
 
671
702
  ### Sufixos de paginação e ordenação
672
703
 
673
- 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.
704
+ Aplicados ao **final** do nome do método, eles injetam automaticamente os argumentos de paginação e ordenação.
674
705
 
675
706
  | Sufixo | Argumentos adicionais |
676
707
  | --------------------- | ----------------------------- |
@@ -685,43 +716,6 @@ Para `createMany` e `createManyAndReturn`, o sufixo `SkipDuplicates` está dispo
685
716
  | ----------------- | ---------------------------------------- |
686
717
  | `SkipDuplicates` | Ignora registros duplicados na inserção |
687
718
 
688
- Exemplos completos:
689
-
690
- ```ts
691
- methods: {
692
- findManyPaginated: { map: true },
693
- findManyByAtivoOrderedAndPaginated: { map: true },
694
- findByEmailOrderedAndPaginated: { map: true },
695
- createManyAndReturnSkipDuplicates: { map: true },
696
- }
697
-
698
- // Uso
699
- await usuarioRepository.findManyPaginated({ skip: 0, take: 10 });
700
-
701
- await usuarioRepository.findManyByAtivoOrderedAndPaginated(
702
- true,
703
- { dataCriacao: "desc" },
704
- { skip: 0, take: 10 }
705
- );
706
- ```
707
-
708
- `PaginationOptions`:
709
-
710
- ```ts
711
- type PaginationOptions<TCursor = unknown> = {
712
- skip?: number;
713
- take?: number;
714
- cursor?: TCursor;
715
- };
716
- ```
717
-
718
- `OrderOptions`:
719
-
720
- ```ts
721
- type OrderOptions = OrderPattern | OrderPattern[];
722
- // Exemplo: { dataCriacao: "desc" } ou [{ dataCriacao: "desc" }, { nome: "asc" }]
723
- ```
724
-
725
719
  ---
726
720
 
727
721
  ### Configuração de métodos
@@ -731,46 +725,20 @@ Cada entrada em `methods` aceita as seguintes opções:
731
725
  | Opção | Tipo | Padrão | Descrição |
732
726
  | ------------------- | ------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------ |
733
727
  | `map` | `boolean` | — | **Obrigatório.** Define se o método será exposto no repository. |
734
- | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combina com `requiredWhere`. `overwrite` ignora o `requiredWhere`. |
728
+ | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combina com `requiredWhere`. `overwrite` ignora o `requiredWhere`. |
735
729
  | `selectModel` | `keyof SelectModels \| false` | — | Sobrescreve o `defaultSelectModel` para este método. |
736
- | `fbMode` | `'one'` \| `'list'` | `'list'` | Somente para `findBy`. `'one'` retorna `T \| null`; `'list'` retorna `T[]`. |
737
- | `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. |
730
+ | `fbMode` | `'one'` \| `'list'` | `'list'` | (**Obsoleto. Use `findOneBy`**) Somente para `findBy`. `'one'` retorna `T \| null`; `'list'` retorna `T[]`. |
731
+ | `proxyTo` | `Padrão de método válido` | — | Delega a lógica para outro padrão de método válido. |
738
732
  | `pushWhere` | `WhereModel<M>` | — | Where extra adicionado à query além do `requiredWhere`. |
739
733
  | `injectOrdenation` | `OrdenationModel<M>` | — | Ordenação fixa injetada automaticamente na query. |
740
734
  | `injectPagination` | `PaginationModel<M>` | — | Paginação fixa injetada automaticamente na query. |
741
735
 
742
- Exemplos:
743
-
744
- ```ts
745
- methods: {
746
- // Retorna um único resultado em vez de array
747
- findByEmail: { map: true, fbMode: "one" },
748
-
749
- // Ignora o requiredWhere
750
- deleteManyByIdIn: { map: true, whereType: "overwrite" },
751
-
752
- // Usa um select model específico neste método
753
- findManyByAtivo: { map: true, selectModel: "minimal" },
754
-
755
- // Adiciona um where extra além do requiredWhere
756
- findManyByPerfil: { map: true, pushWhere: { deletedAt: null } },
757
-
758
- // Ordenação fixa sem precisar passar como argumento
759
- findManyPaginado: { map: true, injectOrdenation: { dataCriacao: "desc" } },
760
-
761
- // Nome personalizado precisa de proxyTo
762
- buscarPorEmailEPerfil: { map: true, proxyTo: "findByEmailAndPerfil" },
763
- }
764
- ```
765
-
766
736
  ---
767
737
 
768
738
  ### Aggregate e GroupBy
769
739
 
770
- Para utilizar operações de agrupamento e agregação do Prisma (`aggregate` e `groupBy`), você deve expô-las explicitamente na configuração dos métodos dinâmicos (`methods`):
771
-
772
740
  ```ts
773
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
741
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
774
742
  tableName: "usuario",
775
743
  pkName: "id",
776
744
  methods: {
@@ -784,65 +752,11 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
784
752
  > Estes métodos devem ter exatamente esses nomes (`aggregate` e `groupBy`).
785
753
  > Ao contrário dos outros métodos dinâmicos, eles recebem argumentos nativos do Prisma e **ignoram** as configurações de `selectModels`, `pushWhere` e `requiredWhere`.
786
754
 
787
- #### Exemplo de uso do `aggregate`
788
-
789
- O método `aggregate` permite calcular valores agregados (como média, soma, mínimo, máximo, contagem) sobre os registros:
790
-
791
- ```ts
792
- const resultado = await usuarioRepository.aggregate({
793
- _count: {
794
- _all: true,
795
- },
796
- _avg: {
797
- idade: true,
798
- },
799
- _sum: {
800
- saldo: true,
801
- },
802
- where: {
803
- ativo: true,
804
- },
805
- });
806
-
807
- console.log(resultado._count._all); // Total de usuários ativos
808
- console.log(resultado._avg.idade); // Média de idade dos usuários ativos
809
- console.log(resultado._sum.saldo); // Soma dos saldos dos usuários ativos
810
- ```
811
-
812
- #### Exemplo de uso do `groupBy`
813
-
814
- O método `groupBy` permite agrupar registros por um ou mais campos para realizar operações de agregação em cada grupo:
815
-
816
- ```ts
817
- const grupos = await usuarioRepository.groupBy({
818
- by: ["status"],
819
- _count: {
820
- status: true,
821
- },
822
- _avg: {
823
- idade: true,
824
- },
825
- having: {
826
- idade: {
827
- _avg: {
828
- gt: 18,
829
- },
830
- },
831
- },
832
- });
833
-
834
- for (const grupo of grupos) {
835
- console.log(`Status: ${grupo.status}`);
836
- console.log(`Quantidade: ${grupo._count.status}`);
837
- console.log(`Média de Idade: ${grupo._avg.idade}`);
838
- }
839
- ```
840
-
841
755
  ---
842
756
 
843
757
  ## Relações no save
844
758
 
845
- Configure relações para que o `save` as gerencie automaticamente. Essas mesmas relações também são usadas no `patch`. Para que as relações apareçam no autocomplete, o tipo genérico deve incluir as relações usando `GetPayload`:
759
+ Configure relações para que o `save` e o `patch` as gerenciem automaticamente (`saveList` e `pacthList` também gerenciam as relations automaticamente).
846
760
 
847
761
  ```ts
848
762
  import type { Prisma } from "../../generated/prisma/client";
@@ -851,7 +765,7 @@ type Usuario = Prisma.usuarioGetPayload<{
851
765
  include: { perfil: true; postagens: true };
852
766
  }>;
853
767
 
854
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
768
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
855
769
  tableName: "usuario",
856
770
  pkName: "id",
857
771
 
@@ -870,35 +784,6 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
870
784
  }).build(prisma);
871
785
  ```
872
786
 
873
- Exemplos de uso com relações:
874
-
875
- ```ts
876
- await usuarioRepository.save({
877
- nome: "Maria",
878
- email: "maria@email.com",
879
- senha: "password",
880
- perfil: {
881
- id: 1,
882
- bio: "Bio da Maria",
883
- },
884
- postagens: [
885
- { titulo: "Primeiro post", conteudo: "Olá!" },
886
- ],
887
- });
888
-
889
- await usuarioRepository.patch(1, {
890
- perfil: {
891
- id: 1,
892
- bio: "Bio atualizada",
893
- },
894
- postagens: [
895
- { id: 10, titulo: "Post revisado", conteudo: "Conteúdo ajustado" },
896
- ],
897
- });
898
- ```
899
-
900
- > **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.
901
-
902
787
  **Modos de relação:**
903
788
 
904
789
  | Modo | Relação |
@@ -915,6 +800,23 @@ await usuarioRepository.patch(1, {
915
800
  | `set` | Substitui completamente (remove os que não foram enviados) |
916
801
  | `add` | Adiciona/atualiza sem remover os existentes |
917
802
 
803
+ **Relação `mto` com nullable:**
804
+
805
+ Use `nullable` (letra minúscula) para permitir a desvinculação de uma relação many-to-one:
806
+
807
+ ```ts
808
+ relations: {
809
+ categoria: {
810
+ pk: "id",
811
+ mode: "mto",
812
+ restriction: "set",
813
+ nullable: true, // permite passar null para desvincular
814
+ },
815
+ }
816
+ ```
817
+
818
+ > **Nota:** `nullAble` (com A maiúsculo) ainda é aceito por compatibilidade, mas está **obsoleto**. Prefira `nullable`.
819
+
918
820
  ---
919
821
 
920
822
  ## Transações
@@ -935,28 +837,38 @@ await usuarioRepository.prisma.$transaction(async (tx) => {
935
837
  });
936
838
  ```
937
839
 
938
- Observe que é possível diferentes repositories participarem da mesma transação.
840
+ Para `saveList` e `patchList`, o campo `db` deve ser um `DbTransaction` (não o cliente principal):
841
+
842
+ ```ts
843
+ await prisma.$transaction(async (tx) => {
844
+ // CORRETO: tx é uma DbTransaction
845
+ await usuarioRepository.saveList([{ nome: "Maria" }], { db: tx });
846
+
847
+ // ERRADO: não passe o prisma diretamente
848
+ // await usuarioRepository.saveList([{ nome: "Maria" }], { db: prisma });
849
+ });
850
+ ```
939
851
 
940
852
  ---
941
853
 
942
854
  ## Estendendo um repository
943
855
 
944
856
  ```ts
945
- const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
857
+ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
946
858
  tableName: "usuario",
947
859
  pkName: "id",
948
860
  methods: {
949
- findByEmailEndsWith: { map: true, fbMode: "one" },
861
+ findOneByEmailEndsWith: { map: true },
950
862
  },
951
863
  })
952
864
  .build(prisma)
953
865
  .extend((repo) => ({
954
866
  buscarAtivosPorDominio: async (dominio: string) => {
955
- return repo.findByEmailEndsWith(`@${dominio}`);
867
+ return repo.findOneByEmailEndsWith(`@${dominio}`);
956
868
  },
957
869
 
958
870
  ativarMultiplos: async (ids: string[]) => {
959
- return repo.updateManyByIdIn(ids, { ativo: true });
871
+ return repo.patchList(ids.map(id => [id, { ativo: true }]));
960
872
  },
961
873
  }));
962
874
  ```
@@ -965,15 +877,17 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()({
965
877
 
966
878
  ## Tratamento de erros
967
879
 
968
- O VSRepository lança `VSRepoError` e suas subclasses em situações específicas (OBS: Erros do Prisma não são sobrescritos como `VSRepoError`):
880
+ O VSRepository lança `VSRepoError` e suas subclasses em situações específicas (erros do Prisma não são sobrescritos):
969
881
 
970
882
  ```ts
971
- import { VSRepoError } from "../../generated/vsrepo";
883
+ import { VSRepoError, VSRepoRuntimeError } from "../../generated/vsrepo";
972
884
 
973
885
  try {
974
- const usuario = await usuarioRepository.get();
886
+ const usuario = await usuarioRepository.getOrThrow(id);
975
887
  } catch (error) {
976
- if (error instanceof VSRepoError) {
888
+ if (error instanceof VSRepoRuntimeError && error.code === "20727") {
889
+ // Registro não encontrado pelo getOrThrow
890
+ } else if (error instanceof VSRepoError) {
977
891
  console.error("Erro no repository:", error.message);
978
892
  }
979
893
  }
@@ -981,29 +895,37 @@ try {
981
895
 
982
896
  **Subclasses disponíveis:**
983
897
 
984
- | Classe | Quando é lançada |
985
- | -------------------- | ------------------------------------------------------- |
986
- | `VSRepoConfigError` | Configuração inválida em `setupVSRepo` |
987
- | `VSRepoBuildError` | Nome de método inválido ou desconhecido no `build` |
988
- | `VSRepoExtendError` | Argumento inválido em `extend` |
989
- | `VSRepoRuntimeError` | Erro em tempo de execução durante uma operação |
898
+ | Classe | Quando é lançada |
899
+ | -------------------- | ----------------------------------------------------------------- |
900
+ | `VSRepoConfigError` | Configuração inválida em `setupVSRepo` |
901
+ | `VSRepoBuildError` | Nome de método, tipo de campo ou configuração inválida no `build` |
902
+ | `VSRepoExtendError` | Argumento inválido em `extend` |
903
+ | `VSRepoRuntimeError` | Erro em tempo de execução durante uma operação |
904
+
905
+ `VSRepoRuntimeError` possui a propriedade `code` para identificação programática. O código `"20727"` é lançado pelo `getOrThrow` quando o registro não é encontrado.
990
906
 
991
907
  ---
992
908
 
993
909
  ## Tipos utilitários
994
910
 
995
- O VSRepository exporta os seguintes tipos para uso nas suas aplicações:
996
-
997
911
  ### Tipos de cliente
998
912
 
999
913
  ```ts
1000
914
  import type { DbClient, DbTransaction, ClientOrTransaction } from "../../generated/vsrepo";
1001
915
 
1002
- type DbClient = PrismaClient;
1003
- type DbTransaction = Prisma.TransactionClient;
916
+ type DbClient = PrismaClient;
917
+ type DbTransaction = Prisma.TransactionClient;
1004
918
  type ClientOrTransaction = DbClient | DbTransaction;
1005
919
  ```
1006
920
 
921
+ ### Tipo de visibilidade soft-delete
922
+
923
+ ```ts
924
+ import type { SeeMode } from "../../generated/vsrepo";
925
+
926
+ type SeeMode = "active" | "removed" | "all";
927
+ ```
928
+
1007
929
  ### Tipos derivados do modelo Prisma
1008
930
 
1009
931
  ```ts
@@ -1014,36 +936,8 @@ import type {
1014
936
  OrdenationModel,
1015
937
  PaginationModel,
1016
938
  ModelUpsertInput,
939
+ PrismaModelInputs,
1017
940
  } from "../../generated/vsrepo";
1018
-
1019
- // Select de um campo específico
1020
- type UsuarioSelect = SelectModel<"usuario">;
1021
-
1022
- // Mapa de selects nomeados
1023
- type UsuarioSelectModels = SelectModels<"usuario">;
1024
-
1025
- // Where clause do modelo
1026
- type UsuarioWhere = WhereModel<"usuario">;
1027
-
1028
- // OrderBy do modelo
1029
- type UsuarioOrder = OrdenationModel<"usuario">;
1030
-
1031
- // Opções de paginação com cursor tipado
1032
- type UsuarioPagination = PaginationModel<"usuario">;
1033
-
1034
- // Payload de criação do modelo (para upsert)
1035
- type UsuarioUpsertInput = ModelUpsertInput<"usuario">;
1036
- ```
1037
-
1038
- ### Todos os inputs do modelo
1039
-
1040
- ```ts
1041
- import type { PrismaModelInputs } from "../../generated/vsrepo";
1042
-
1043
- type UsuarioInputs = PrismaModelInputs<"usuario">;
1044
- // Contém:
1045
- // select, createInput, createManyInput, updateInput, updateManyInput,
1046
- // whereInput, orderByInput, cursorInput, upsertCreateInput, upsertUpdateInput
1047
941
  ```
1048
942
 
1049
943
  ### Tipos de opções de método
@@ -1052,10 +946,9 @@ type UsuarioInputs = PrismaModelInputs<"usuario">;
1052
946
  import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
1053
947
 
1054
948
  // MethodOptions<S> — opções passadas nos métodos do repository
1055
- // S = chave do select model ou false
1056
949
  type Opts = MethodOptions<"public" | "minimal">;
1057
950
 
1058
- // MethodOptionsModel<TRepo> — derivado diretamente de uma instância VSRepository configurada
951
+ // MethodOptionsModel<TRepo> — derivado de uma instância VSRepository configurada
1059
952
  const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()(config);
1060
953
  type OptsModel = MethodOptionsModel<typeof usuarioVSRepo>;
1061
954
  ```
@@ -1070,21 +963,6 @@ import type {
1070
963
  RepositoryRelations,
1071
964
  ExtractRelationConfig,
1072
965
  } from "../../generated/vsrepo";
1073
-
1074
- // Configuração de um método dinâmico
1075
- type MeuMethodConfig = MethodConfig<"usuario", typeof meuSelectModels>;
1076
-
1077
- // Configuração completa do repository
1078
- type MeuRepoConfig = RepoConfig<Usuario, "usuario">;
1079
-
1080
- // Configuração do build
1081
- type MeuBuildConfig = BuildConfig<"public" | "minimal">;
1082
-
1083
- // Tipo de relações inferidas automaticamente
1084
- type UsuarioRelations = RepositoryRelations<Usuario>;
1085
-
1086
- // Configuração de relação inferida a partir de um campo
1087
- type PerfilRelationConfig = ExtractRelationConfig<Usuario["perfil"]>;
1088
966
  ```
1089
967
 
1090
968
  ### Tipo do repository construído
@@ -1092,7 +970,6 @@ type PerfilRelationConfig = ExtractRelationConfig<Usuario["perfil"]>;
1092
970
  ```ts
1093
971
  import type { RepositoryOf } from "../../generated/vsrepo";
1094
972
 
1095
- // Inferência a partir de uma instância VSRepository (útil para injeção de dependência)
1096
973
  const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({ ... });
1097
974
  type UsuarioRepository = RepositoryOf<typeof usuarioVSRepo>;
1098
975
  ```
@@ -1101,42 +978,25 @@ type UsuarioRepository = RepositoryOf<typeof usuarioVSRepo>;
1101
978
 
1102
979
  ```ts
1103
980
  type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
1104
- // ^ BuildConfig (opcional) ^ tipo do extend (opcional)
1105
981
  ```
1106
982
 
1107
- Exemplo com extend tipado:
1108
-
1109
- ```ts
1110
- const extension = { buscarPorDominio: (dominio: string) => Promise<Usuario[]> };
1111
- type UsuarioRepositoryExtended = RepositoryOf<typeof usuarioVSRepo, undefined, typeof extension>;
1112
- ```
1113
-
1114
- ### Tipos do payload dos métodos `save` e `patch`
1115
-
1116
- Use `SaveObject` e `PatchObject` para extrair o tipo do payload esperado pelos métodos `save` e `patch` diretamente a partir de uma instância `VSRepository` configurada. Esses tipos combinam o input base do Prisma com as relações configuradas no repository.
983
+ ### Tipos do payload de `save` e `patch`
1117
984
 
1118
985
  ```ts
1119
986
  import type { SaveObject, PatchObject } from "../../generated/vsrepo";
1120
- import type { Prisma } from "../../generated/prisma/client";
1121
987
 
1122
- const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()({
988
+ const usuarioVSRepo = setupVSRepo<Usuario, "usuario">()(({
1123
989
  tableName: "usuario",
1124
990
  pkName: "id",
1125
991
  relations: {
1126
992
  perfil: { pk: "id", mode: "oto", restriction: "set" },
1127
- postagens: { pk: "id", mode: "otm", restriction: "add" },
1128
993
  },
1129
994
  });
1130
995
 
1131
- // Tipo do objeto aceito pelo .save()
1132
- type UsuarioSavePayload = SaveObject<Prisma.UsuarioCreateInput, typeof usuarioVSRepo>;
1133
-
1134
- // Tipo do objeto aceito pelo .patch()
996
+ type UsuarioSavePayload = SaveObject<Prisma.UsuarioCreateInput, typeof usuarioVSRepo>;
1135
997
  type UsuarioPatchPayload = PatchObject<Prisma.UsuarioUpdateInput, typeof usuarioVSRepo>;
1136
998
  ```
1137
999
 
1138
- > Útil para tipar DTOs, funções auxiliares ou serviços que chamam `save`/`patch` e precisam do tipo correto do payload sem referenciar diretamente os tipos internos do Prisma.
1139
-
1140
1000
  ---
1141
1001
 
1142
1002
  ## API Reference
@@ -1145,12 +1005,13 @@ type UsuarioPatchPayload = PatchObject<Prisma.UsuarioUpdateInput, typeof usuario
1145
1005
 
1146
1006
  ```ts
1147
1007
  setupVSRepo<TPayload, TTableName>()({
1148
- tableName: Uncapitalize<M>; // Nome da tabela no Prisma
1149
- pkName: keyof T; // Nome da primary key
1150
- selectModels?: SelectModels<M>; // Projeções de dados nomeadas
1151
- defaultSelectModel?: keyof SM; // Select aplicado por padrão
1152
- requiredWhere?: WhereModel<M>; // Filtros sempre aplicados
1153
- relations?: RepositoryRelations<T>; // Configuração de relações
1008
+ tableName: Uncapitalize<M>; // Nome da tabela no Prisma
1009
+ pkName: keyof T; // Nome da primary key
1010
+ softRemovekName?: keyof T & string; // Campo DateTime para soft-delete (opcional)
1011
+ selectModels?: SelectModels<M>; // Projeções de dados nomeadas
1012
+ defaultSelectModel?: keyof SM; // Select aplicado por padrão
1013
+ requiredWhere?: WhereModel<M>; // Filtros sempre aplicados
1014
+ relations?: RepositoryRelations<T>; // Configuração de relações
1154
1015
  methods?: Record<string, MethodConfig<M, SM>>; // Métodos dinâmicos
1155
1016
  });
1156
1017
  ```
@@ -1159,25 +1020,29 @@ setupVSRepo<TPayload, TTableName>()({
1159
1020
 
1160
1021
  ```ts
1161
1022
  vsRepo.build(prisma, {
1162
- freeze?: boolean; // Congela o objeto (default = true)
1163
1023
  showWorking?: boolean; // Exibe logs internos no console (default = false)
1164
1024
 
1165
- // `active` serve para definir se o método vai existir depois do build (default = true)
1166
- // `defaultSelect` serve para definir qual `selectModel` esse método vai usar por default (default = `defaultSelectModel` definido no `setupVSRepo`)
1167
- // `ignoreRequiredWhere` serve para definir se o método vai ignorar o `requiredWhere` definido no `setupVSRepo` (default = false)
1168
1025
  baseMethods?: {
1169
- // Métodos que podem utilizar um `defaultSelect`
1170
- get?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1171
- getOrThrow?:{ active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1172
- remove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1173
- save?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1174
- patch?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1175
- getAll?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1176
-
1177
- // Métodos que NÃO aceitam `defaultSelect`
1178
- removeList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1179
- total?: { active?: boolean; ignoreRequiredWhere?: boolean };
1180
- has?: { active?: boolean; ignoreRequiredWhere?: boolean };
1026
+ // Métodos que podem utilizar um defaultSelect
1027
+ get?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1028
+ getOrThrow?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1029
+ getList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1030
+ remove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1031
+ save?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1032
+ saveList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1033
+ patch?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1034
+ patchList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1035
+ merge?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1036
+ getAll?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1037
+ softRemove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1038
+ restore?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1039
+
1040
+ // Métodos que NÃO aceitam defaultSelect
1041
+ removeList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1042
+ softRemoveList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1043
+ restoreList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1044
+ total?: { active?: boolean; ignoreRequiredWhere?: boolean };
1045
+ has?: { active?: boolean; ignoreRequiredWhere?: boolean };
1181
1046
  };
1182
1047
  });
1183
1048
  ```
@@ -1207,7 +1072,7 @@ Para reportar problemas ou sugerir novas funcionalidades, abra uma **Issue**.
1207
1072
 
1208
1073
  ## Requisitos
1209
1074
 
1210
- - Node.js 16+ (ESM)
1075
+ - Node.js 18+ (ESM)
1211
1076
  - Prisma
1212
1077
  - TypeScript (opcional, mas fortemente recomendado)
1213
1078
  - `"moduleResolution": "bundler"` ou `"nodenext"` no tsconfig
@@ -1238,3 +1103,7 @@ Para reportar problemas ou sugerir novas funcionalidades, abra uma **Issue**.
1238
1103
  **`proxyTo` obrigatório** — Nomes fora dos moldes (ex.: `buscarPorEmail`) não são parseados diretamente. Use `proxyTo: "findByEmail"` nesses casos.
1239
1104
 
1240
1105
  **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.
1106
+
1107
+ **`softRemovekName` lança erro no build** — O campo informado deve ser do tipo `DateTime` no schema do Prisma. Tipos como `Boolean` ou `String` não são aceitos.
1108
+
1109
+ **`saveList`/`patchList` com `db` inválido** — O campo `db` nestes métodos aceita apenas `DbTransaction` (retorno de `prisma.$transaction`), não o cliente principal. Passar o `PrismaClient` diretamente causará comportamento inesperado.