vsrepo 1.4.2 → 2.1.0

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 (151) hide show
  1. package/README.md +827 -1322
  2. package/README.pt-BR.md +833 -1325
  3. package/dist/VSRepoAdapter.d.ts +109 -0
  4. package/dist/VSRepoAdapter.js +18 -0
  5. package/dist/VSRepository.d.ts +166 -1201
  6. package/dist/VSRepository.js +327 -237
  7. package/dist/decorators/dynamic-method.decorator.d.ts +25 -0
  8. package/dist/decorators/dynamic-method.decorator.js +38 -0
  9. package/dist/decorators/query-method.decorator.d.ts +27 -0
  10. package/dist/decorators/query-method.decorator.js +45 -0
  11. package/dist/errors/VSRepoAdapterError.d.ts +22 -0
  12. package/dist/errors/VSRepoAdapterError.js +31 -0
  13. package/dist/errors/VSRepoError.d.ts +15 -0
  14. package/dist/errors/VSRepoError.js +21 -0
  15. package/dist/index.d.ts +38 -1
  16. package/dist/index.js +28 -15
  17. package/dist/internal/constants/debug-arg-symbol.constant.d.ts +1 -0
  18. package/dist/internal/constants/debug-arg-symbol.constant.js +4 -0
  19. package/dist/internal/constants/dynamic-methods-key.constant.d.ts +1 -0
  20. package/dist/internal/constants/query-methods-key.constant.d.ts +1 -0
  21. package/dist/internal/constants/query-methods-key.constant.js +4 -0
  22. package/dist/internal/enums/adapter-error-code.enum.d.ts +125 -0
  23. package/dist/internal/enums/adapter-error-code.enum.js +129 -0
  24. package/dist/internal/enums/transaction-isolation-level.enum.d.ts +16 -0
  25. package/dist/internal/enums/transaction-isolation-level.enum.js +20 -0
  26. package/dist/internal/enums/vs-log-level.enum.d.ts +18 -0
  27. package/dist/internal/enums/vs-log-level.enum.js +22 -0
  28. package/dist/internal/enums/vsrepo-error-type.enum.d.ts +19 -0
  29. package/dist/internal/enums/vsrepo-error-type.enum.js +23 -0
  30. package/dist/internal/resolvers/dynamic-methods.resolver.d.ts +23 -0
  31. package/dist/internal/resolvers/dynamic-methods.resolver.js +910 -0
  32. package/dist/internal/resolvers/merge-wheres.resolver.d.ts +7 -0
  33. package/dist/internal/resolvers/merge-wheres.resolver.js +26 -0
  34. package/dist/internal/utils/uncapitalize.util.d.ts +1 -0
  35. package/dist/internal/utils/vs-logger.util.d.ts +25 -0
  36. package/dist/internal/utils/vs-logger.util.js +139 -0
  37. package/dist/internal/validators/decorators.validator.d.ts +8 -0
  38. package/dist/internal/validators/decorators.validator.js +75 -0
  39. package/dist/internal/validators/schemas/ordering.schema.d.ts +3 -0
  40. package/dist/internal/validators/schemas/ordering.schema.js +38 -0
  41. package/dist/internal/validators/schemas/pagination.schema.d.ts +6 -0
  42. package/dist/internal/validators/schemas/pagination.schema.js +40 -0
  43. package/dist/internal/validators/schemas/where.schema.d.ts +7 -0
  44. package/dist/internal/validators/schemas/where.schema.js +42 -0
  45. package/dist/internal/validators/vsrepo.validator.d.ts +40 -0
  46. package/dist/internal/validators/vsrepo.validator.js +182 -0
  47. package/dist/types/adapter/adapter-method-options.type.d.ts +24 -0
  48. package/dist/types/adapter/adapter-query-options.type.d.ts +5 -0
  49. package/dist/types/decorators/dynamic-method-options.type.d.ts +14 -0
  50. package/dist/types/decorators/query-method-options.type.d.ts +15 -0
  51. package/dist/types/dynamic-methods/dynamic-method-customization.type.d.ts +7 -0
  52. package/dist/types/dynamic-methods/dynamic-method-info.type.d.ts +18 -0
  53. package/dist/types/dynamic-methods/dynamic-method-where-ops.type.d.ts +6 -0
  54. package/dist/types/utils/count-result.type.d.ts +9 -0
  55. package/dist/types/utils/decimal-like.type.d.ts +23 -0
  56. package/dist/types/utils/deep-partial.type.d.ts +14 -0
  57. package/dist/types/utils/keys-of-type.type.d.ts +20 -0
  58. package/dist/types/utils/methods-options.type.d.ts +23 -0
  59. package/dist/types/utils/numeric-keys.type.d.ts +24 -0
  60. package/dist/types/utils/numeric-like.type.d.ts +10 -0
  61. package/dist/types/utils/ordering.type.d.ts +39 -0
  62. package/dist/types/utils/pagination.type.d.ts +11 -0
  63. package/dist/types/utils/perform-data.type.d.ts +4 -0
  64. package/dist/types/utils/primitive.type.d.ts +7 -0
  65. package/dist/types/utils/query-method-arg.type.d.ts +27 -0
  66. package/dist/types/utils/restrict-method-options.type.d.ts +14 -0
  67. package/dist/types/utils/see-mode.type.d.ts +12 -0
  68. package/dist/types/vsrepo/vsrepo-args.type.d.ts +9 -0
  69. package/dist/types/vsrepo/vsrepo-method.type.d.ts +4 -0
  70. package/dist/types/vsrepo/vsrepo-method.type.js +2 -0
  71. package/dist/types/vsrepo/vsrepo-options.type.d.ts +34 -0
  72. package/dist/types/vsrepo/vsrepo-options.type.js +2 -0
  73. package/dist/types/vsrepo/vsrepo-orm-types.type.d.ts +17 -0
  74. package/dist/types/vsrepo/vsrepo-orm-types.type.js +2 -0
  75. package/dist/types/vsrepo/vsrepo-pretty-where.type.d.ts +7 -0
  76. package/dist/types/vsrepo/vsrepo-pretty-where.type.js +2 -0
  77. package/dist/types/vsrepo/vsrepo-query-options.type.d.ts +17 -0
  78. package/dist/types/vsrepo/vsrepo-query-options.type.js +2 -0
  79. package/dist/types/vsrepo/vsrepo-query.type.d.ts +5 -0
  80. package/dist/types/vsrepo/vsrepo-query.type.js +2 -0
  81. package/dist/types/vsrepo/vsrepo-relations.type.d.ts +26 -0
  82. package/dist/types/vsrepo/vsrepo-relations.type.js +2 -0
  83. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.d.ts +19 -0
  84. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.js +2 -0
  85. package/dist/types/vsrepo/vsrepo-select.type.d.ts +15 -0
  86. package/dist/types/vsrepo/vsrepo-select.type.js +2 -0
  87. package/dist/types/vsrepo/vsrepo-transaction-options.type.d.ts +12 -0
  88. package/dist/types/vsrepo/vsrepo-transaction-options.type.js +2 -0
  89. package/dist/types/vsrepo/vsrepo-ugly-where.type.d.ts +9 -0
  90. package/dist/types/vsrepo/vsrepo-ugly-where.type.js +2 -0
  91. package/dist/types/vsrepo/vsrepo-where.type.d.ts +99 -0
  92. package/dist/types/vsrepo/vsrepo-where.type.js +2 -0
  93. package/package.json +16 -37
  94. package/README-DynamicRepo.md +0 -625
  95. package/README-DynamicRepo.pt-BR.md +0 -625
  96. package/dist/DynamicRepository.d.ts +0 -497
  97. package/dist/DynamicRepository.js +0 -26
  98. package/dist/VSRepoError.d.ts +0 -83
  99. package/dist/VSRepoError.js +0 -17
  100. package/dist/internal/decorators/dynamic-method.decorator.js +0 -14
  101. package/dist/internal/decorators/query-method.decorator.js +0 -20
  102. package/dist/internal/entities/dynamic-method-metadata.entity.js +0 -26
  103. package/dist/internal/errors/vs-repo.error.js +0 -31
  104. package/dist/internal/resolvers/base-methods.resolve.js +0 -541
  105. package/dist/internal/resolvers/create-update-payloads-with-relations.resolve.js +0 -143
  106. package/dist/internal/resolvers/data-payload-with-relations.resolve.js +0 -60
  107. package/dist/internal/resolvers/dbAndPrismaArgs.resolve.js +0 -63
  108. package/dist/internal/resolvers/dynamic-method-customization.resolve.js +0 -57
  109. package/dist/internal/resolvers/dynamic-method-info.resolve.js +0 -279
  110. package/dist/internal/resolvers/dynamic-methods-metadata.resolve.js +0 -15
  111. package/dist/internal/resolvers/merge-wheres.resolve.js +0 -22
  112. package/dist/internal/resolvers/pretty-wheres.resolve.js +0 -87
  113. package/dist/internal/resolvers/select.resolve.js +0 -7
  114. package/dist/internal/resolvers/specific-where.resolve.js +0 -84
  115. package/dist/internal/resolvers/ugly-where.resolve.js +0 -178
  116. package/dist/internal/utils/logger.util.js +0 -21
  117. package/dist/internal/utils/schemas.util.js +0 -31
  118. package/dist/internal/validation/build-config.validate.js +0 -84
  119. package/dist/internal/validation/constructor-config.validate.js +0 -64
  120. package/dist/internal/validation/dynamic-method-config.validate.js +0 -19
  121. package/dist/internal/validation/extension.validate.js +0 -15
  122. package/dist/internal/validation/is-object.validate.js +0 -6
  123. package/dist/internal/validation/method-options.validate.js +0 -42
  124. package/dist/internal/validation/obj-with-relations.validate.js +0 -37
  125. package/dist/internal/validation/prisma-client.validate.js +0 -10
  126. package/dist/internal/validation/query-method-arg.validate.js +0 -22
  127. package/dist/internal/validation/query-method-options.validate.js +0 -24
  128. package/scripts/configure-prisma-import.mjs +0 -283
  129. package/scripts/copy-types.mjs +0 -24
  130. /package/dist/{internal/decorators/types/dynamic-method-config.type.js → types/adapter/adapter-method-options.type.js} +0 -0
  131. /package/dist/{internal/errors/types/vs-repo-error-type.type.js → types/adapter/adapter-query-options.type.js} +0 -0
  132. /package/dist/{internal/errors/types/vs-repo-runtime-error-code.type.js → types/decorators/dynamic-method-options.type.js} +0 -0
  133. /package/dist/{internal/validation/types → types/decorators}/query-method-options.type.js +0 -0
  134. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-customization.type.js +0 -0
  135. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-info.type.js +0 -0
  136. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-where-ops.type.js +0 -0
  137. /package/dist/{internal/resolvers/types/base-method-function.type.js → types/utils/count-result.type.js} +0 -0
  138. /package/dist/{internal/resolvers/types/pretty-where.type.js → types/utils/decimal-like.type.js} +0 -0
  139. /package/dist/{internal/resolvers/types/prisma-args.type.js → types/utils/deep-partial.type.js} +0 -0
  140. /package/dist/{internal/resolvers/types/repository-build-instance.type.js → types/utils/keys-of-type.type.js} +0 -0
  141. /package/dist/{internal/resolvers/types/resolve-db-and-prisma-args-data.type.js → types/utils/methods-options.type.js} +0 -0
  142. /package/dist/{internal/resolvers/types/ugly-where.type.js → types/utils/numeric-keys.type.js} +0 -0
  143. /package/dist/{internal/validation/types/base-methods.type.js → types/utils/numeric-like.type.js} +0 -0
  144. /package/dist/{internal/validation/types/build-config.type.js → types/utils/ordering.type.js} +0 -0
  145. /package/dist/{internal/validation/types → types/utils}/pagination.type.js +0 -0
  146. /package/dist/{internal/validation/types/constructor-config.type.js → types/utils/perform-data.type.js} +0 -0
  147. /package/dist/{internal/validation/types/method-options.type.js → types/utils/primitive.type.js} +0 -0
  148. /package/dist/{internal/validation/types → types/utils}/query-method-arg.type.js +0 -0
  149. /package/dist/{internal/validation/types/method.type.js → types/utils/restrict-method-options.type.js} +0 -0
  150. /package/dist/{internal/validation/types → types/utils}/see-mode.type.js +0 -0
  151. /package/dist/{internal/validation/types/relation.type.js → types/vsrepo/vsrepo-args.type.js} +0 -0
package/README.pt-BR.md CHANGED
@@ -9,139 +9,127 @@
9
9
  </p>
10
10
  </div>
11
11
 
12
- # VSRepository
12
+ # VSRepository v2
13
13
 
14
14
  🇧🇷 Você está lendo a versão em português. [🇺🇸 Read in English](./README.md)
15
15
 
16
- Biblioteca de repository pattern para projetos que usam **Prisma**, com suporte completo a **TypeScript** e **inferência de tipos** automática.
16
+ > ✅ **Lançado.** O VSRepository v2.0.0 (o core agnóstico de ORM) e o [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) já foram publicados e estão prontos para uso. O Prisma 7 é o primeiro adapter totalmente suportado; outros ORMs (TypeORM, Drizzle, etc.) ainda estão em desenvolvimento — veja [Status dos adapters](#status-dos-adapters). Se você precisa da versão anterior, somente Prisma, use o código/docs da [`v1`](https://github.com/jaobrabo123/VSRepository/tree/v1).
17
17
 
18
- O VSRepository permite criar repositórios fortemente tipados com:
18
+ Biblioteca de repository pattern **agnóstica de ORM**, com suporte completo a **TypeScript** e **type inference** automático. O VSRepository v2 é uma reescrita da biblioteca [v1](https://github.com/jaobrabo123/VSRepository/tree/v1): em vez de falar diretamente com o Prisma, o núcleo agora delega toda operação a um **adapter** plugável, permitindo que a mesma API de repository funcione com Prisma, TypeORM ou qualquer outro ORM/banco que implemente o contrato de adapter.
19
19
 
20
- - **Métodos base** automáticos: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `patchList`, `merge`, `getAll`, `total`, `has`
20
+ O VSRepository permite criar repositories fortemente tipados com:
21
+
22
+ - **Métodos base** automáticos: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `merge`, `getAll`, `total`, `has`
21
23
  - **Soft-delete nativo**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
22
- - **Métodos dinâmicos** inferidos pelo nome: `findOneByEmail`, `findManyPaginated`, `updateById`, `deleteManyByNameStartsWith`
23
- - **Select models** reutilizáveis para diferentes projeções de dados
24
+ - **Métodos dinâmicos** inferidos a partir do nome de um campo `declare` via o decorador `@DynamicMethod`: `findByEmail`, `findManyByStatusPaginated`, `updateById`
25
+ - **Métodos de query SQL raw** através do novo decorador `@QueryMethod`, ignorando totalmente o engine de parsing por nome
26
+ - **`select`/`relations`** ad-hoc em cada chamada — sem mais projeções nomeadas pré-declaradas
24
27
  - **Type safety** em 100% das operações
25
- - **Transações** nativas do Prisma (automáticas em `saveList` e `patchList`)
26
- - **Extensibilidade** com métodos customizados
27
-
28
- > 💡 Quer ver tudo isso na prática? A pasta [`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples) do repositório tem exemplos comentados e executáveis para cada funcionalidade — veja a seção [Exemplos práticos](#exemplos-práticos) mais abaixo.
28
+ - **Transações** nativas do ORM, compartilhadas entre repositories
29
+ - Um **núcleo agnóstico de ORM** — a mesma classe de repository funciona com qualquer implementação de `VSRepoAdapter`
29
30
 
30
31
  ---
31
32
 
32
33
  ## Sumário
33
34
 
35
+ - [O que mudou da v1](#o-que-mudou-da-v1)
36
+ - [Status dos adapters](#status-dos-adapters)
34
37
  - [Instalação](#instalação)
35
- - [Gerando os tipos](#gerando-os-tipos)
36
38
  - [Uso básico](#uso-básico)
37
- - [Abordagem baseada em classes (DynamicRepository)](#abordagem-baseada-em-classes-dynamicrepository)
38
- - [Integração com NestJS](#integração-com-nestjs)
39
+ - [Options do construtor](#options-do-construtor)
39
40
  - [Métodos base](#métodos-base)
40
- - [Soft-delete](#soft-delete)
41
- - [Operações em lote](#operações-em-lote)
42
- - [Merge](#merge)
43
- - [Configurando os métodos base](#configurando-os-métodos-base)
44
- - [Select Models](#select-models)
45
- - [Select bruto (options.select)](#select-bruto-optionsselect)
46
- - [Include Models](#include-models)
47
- - [Include bruto (options.include)](#include-bruto-optionsinclude)
48
- - [Required Where](#required-where)
49
- - [Ordenação padrão (Default Ordering)](#ordenação-padrão-default-ordering)
50
- - [Opção `see`](#opção-see)
41
+ - [Soft-delete](#soft-delete)
42
+ - [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação)
43
+ - [Quais campos são elegíveis](#quais-campos-são-elegíveis)
44
+ - [Escrevendo um adapter](#escrevendo-um-adapter)
45
+ - [`select` e `relations`](#select-e-relations)
51
46
  - [Métodos dinâmicos](#métodos-dinâmicos)
52
- - [Prefixos disponíveis](#prefixos-disponíveis)
53
- - [Filtros de campo](#filtros-de-campo)
54
- - [Operadores lógicos](#operadores-lógicos)
55
- - [Filtros de relação](#filtros-de-relação)
56
- - [Sufixos de paginação e ordenação](#sufixos-de-paginação-e-ordenação)
57
- - [Distinct](#distinct)
58
- - [Configuração de método](#configuração-de-método)
59
- - [Aggregate e GroupBy](#aggregate-e-groupby)
60
- - [Query Methods](#query-methods)
61
- - [Relações no save](#relações-no-save)
47
+ - [Prefixos disponíveis](#prefixos-disponíveis)
48
+ - [Filtros de campo](#filtros-de-campo)
49
+ - [Operadores lógicos](#operadores-lógicos)
50
+ - [Filtros de relação](#filtros-de-relação)
51
+ - [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct)
52
+ - [Options do decorador](#options-do-decorador)
53
+ - [Query methods (SQL raw)](#query-methods-sql-raw)
54
+ - [Queries raw pontuais com `query()`](#queries-raw-pontuais-com-query)
62
55
  - [Transações](#transações)
63
- - [Estendendo um repositório](#estendendo-um-repositório)
64
- - [Tratamento de erros](#tratamento-de-erros)
65
56
  - [Tipos utilitários](#tipos-utilitários)
66
- - [Referência da API](#referência-da-api)
67
- - [Exemplos práticos](#exemplos-práticos)
68
- - [Contribuindo](#contribuindo)
57
+ - [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)
58
+ - [Tratamento de erros](#tratamento-de-erros)
59
+ - [`VSRepoAdapterError` e `AdapterErrorCode`](#vsrepoadaptererror-e-adaptererrorcode)
60
+ - [Logging](#logging)
61
+ - [Desenvolvimento](#desenvolvimento)
69
62
  - [Requisitos](#requisitos)
70
- - [Solução de problemas](#solução-de-problemas)
63
+ - [Contribuindo](#contribuindo)
71
64
 
72
65
  ---
73
66
 
74
- ## Instalação
75
-
76
- ```bash
77
- npm i vsrepo @prisma/client
78
- ```
79
-
80
- Gere o Prisma Client:
81
-
82
- ```bash
83
- npx prisma generate
84
- ```
67
+ ## O que mudou da v1
68
+
69
+ Se você vem do código/docs da [v1](https://github.com/jaobrabo123/VSRepository/tree/v1), aqui está o resumo. Veja cada seção linkada para detalhes.
70
+
71
+ | Área | v1 | v2 |
72
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | Acesso ao banco | Fala diretamente com o **Prisma**, embutido no pacote core | Fala com um **`VSRepoAdapter`**; o suporte a cada ORM é distribuído em pacotes separados (`@vsrepo/prisma7-adapter`, `@vsrepo/typeorm-adapter`, ...) em vez de vir embutido no pacote core `vsrepo` |
74
+ | Definindo um repository | `setupVSRepo<T, M>()({...}).build(prisma)` funcional, **ou** uma classe `DynamicRepository` | Uma única API **baseada em classes**: `extends VSRepository<Entity, PKType, OrmTypes>` |
75
+ | Métodos dinâmicos | Objeto de config `methods: { findByEmail: { map: true } }` | Decorador `@DynamicMethod()` em um campo `declare` |
76
+ | Projeções de dados | `selectModels` + `defaultSelectModel` nomeados e reutilizáveis | `select`/`relations` ad-hoc passados em cada chamada (sem modelos nomeados) |
77
+ | Eager loading | `include`/`includeModels` (específico do Prisma) | Option `relations` agnóstica de ORM |
78
+ | Filtros globais | `requiredWhere` (qualquer filtro arbitrário, sempre aplicado) | **Removido**; Agora aceita apenas `softRemoveKey` + `see: "active" \| "removed" \| "all"` |
79
+ | Sufixo de filtro case-insensitive | `Insensitive` | `IgnoreCase` |
80
+ | Ordenação inline no nome do método | Não suportado (`order` tinha que ser passado como argumento via `Ordered`/`Paginated`) | Cadeias `OrderBy<Campo>Asc`/`OrderBy<Campo>Desc` embutidas diretamente no nome do método |
81
+ | Tratamento de duplicatas no `createMany` | Sufixo `SkipDuplicates` | Sufixo `IgnoreConflicts` |
82
+ | `aggregate` / `groupBy` | Suportado (passthrough nativo do Prisma) | **Ainda não implementado** |
83
+ | Tipos de erro | `VSRepoError` + subclasses (`VSRepoConfigError`, `VSRepoBuildError`, `VSRepoExtendError`, `VSRepoRuntimeError`) | Uma classe base `VSRepoError` com um campo `type: VSRepoErrorType` (`DECORATOR`, `RESOLVER`, `DYNAMIC`, `VALIDATOR`, `BASE`, `ADAPTER`), além de uma subclasse `VSRepoAdapterError` que carrega um `AdapterErrorCode` e o erro original do ORM |
84
+ | Log de debug | Boolean `showWorking: true` | `logLevel: VSLogLevel` (`DEBUG`/`INFO`/`WARN`/`ERROR`) + `logSlowThresholdMs` para avisos de queries lentas |
85
+ | CLI `vsrepo generate` (etapa de geração de tipos) | Obrigatória antes de usar | Não faz parte do núcleo da v2 — os tipos vêm diretamente das suas entidades/tipos do ORM |
86
+ | Extras de CRUD | `patchList`, `options.select`/`options.include` raw | `select`/`relations` já são o padrão (sempre "raw"); `patch`/`merge` mantêm a mesma semântica. **`patchList` foi removido** — para uma atualização parcial em lote, use um dynamic method `updateManyBy`/`updateManyWhere` |
85
87
 
86
88
  ---
87
89
 
88
- ## Gerando os tipos
89
-
90
- O VSRepository precisa saber o caminho real do seu Prisma Client para gerar as tipagens corretamente.
91
-
92
- ```bash
93
- npx vsrepo generate
94
- ```
90
+ ## Status dos adapters
95
91
 
96
- Equivalente a:
92
+ O VSRepository v2 é **agnóstico de ORM por design**. O pacote core (`vsrepo`) traz apenas a classe de repository, os decoradores, o engine de parsing de nomes, o tratamento de erros e o logging — ele **não** inclui um adapter de produção. O suporte de fato a cada ORM/banco deve viver em **pacotes separados, versionados de forma independente**, um por ORM (e, quando fizer sentido, um por versão principal do ORM), por exemplo:
97
93
 
98
- ```bash
99
- npx vsrepo generate \
100
- --output generated/vsrepo \
101
- --prisma generated/prisma
102
- ```
94
+ - `@vsrepo/prisma7-adapter`
95
+ - `@vsrepo/prisma8-adapter`
96
+ - `@vsrepo/typeorm-adapter`
97
+ - `@vsrepo/drizzle-adapter`
103
98
 
104
- **Flags disponíveis:**
99
+ O adapter do Prisma 7 já foi publicado no npm como `@vsrepo/prisma7-adapter` — por enquanto é o **único** adapter publicado. Os adapters para os outros ORMs listados acima (Prisma 8, TypeORM, Drizzle) estão **planejados**; eles só ainda não foram publicados. Até que exista um pacote `@vsrepo/*-adapter` oficial para o seu ORM, você pode escrever o seu próprio para o seu projeto e, se quiser, publicá-lo e abrir um PR para ajudar a fazer o ecossistema crescer — contribuições nesse sentido são muito bem-vindas.
105
100
 
106
- | Flag | Atalho | Padrão |
107
- | ---------- | ------ | -------------------- |
108
- | `--output` | `-o` | `generated/vsrepo` |
109
- | `--prisma` | `-p` | `generated/prisma` |
101
+ | Adapter | Status |
102
+ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Lançado** — publicado no npm, implementa o contrato de `VSRepoAdapter` (CRUD, relations, transactions, `merge`, logging) com testes; veja o [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) para o código-fonte e docs. **Nota:** os métodos atômicos/de agregação (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — veja [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação)) foram adicionados ao contrato do `VSRepoAdapter` depois do último release desse adapter; confirme no changelog/versão dele se já implementam esses métodos antes de depender de `increment`/`sum`/etc. contra o Prisma 7. |
104
+ | TypeORM (`@vsrepo/typeorm-adapter`) | 🟡 **Planejado, ainda não publicado.** Só foi escrito um parser de referência da cláusula `where` (`parseVSRepoWhere`) para validar o design; é o ponto de partida planejado do futuro pacote `@vsrepo/typeorm-adapter`. Contribuições da comunidade nessa frente são bem-vindas. |
105
+ | Outros ORMs (Prisma 8, Drizzle, etc.) | 🟡 **Planejados, ainda não publicados.** Nenhum pacote oficial existe ainda — por enquanto, escreva o seu próprio adapter (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)) e considere publicá-lo/contribuir de volta com o projeto. |
106
+ | Adapters customizados | 🟢 Totalmente suportados hoje — implemente você mesmo a classe abstrata [`VSRepoAdapter`](#escrevendo-seu-próprio-adapter) para qualquer ORM/banco que precisar, no seu próprio projeto ou pacote, seguindo o mesmo formato esperado dos `@vsrepo/*-adapter`. |
110
107
 
111
- **Arquivos gerados:**
108
+ Resumindo: a classe de repository, os decoradores `@DynamicMethod`/`@QueryMethod`, o engine de parsing de nomes, o tratamento de erros e o logging já funcionam de ponta a ponta, e o suporte ao Prisma 7 agora é um adapter lançado e publicado. Adapters oficiais para os demais ORMs estão no roadmap e serão distribuídos como pacotes `@vsrepo/*-adapter` separados, e não como parte do pacote core `vsrepo` — mas você não precisa esperar por isso: escrever (e opcionalmente publicar) o seu próprio adapter enquanto isso é uma forma totalmente suportada de usar a v2 hoje e de contribuir de volta com o projeto.
112
109
 
113
- ```text
114
- generated/vsrepo/
115
- ├── DynamicRepository.ts
116
- ├── DynamicRepository.types.d.ts
117
- ├── VSRepoError.ts
118
- ├── VSRepoError.types.d.ts
119
- ├── VSRepository.ts
120
- ├── VSRepository.types.d.ts
121
- └── index.ts
122
- ```
110
+ ---
123
111
 
124
- Depois de gerar, sempre importe a partir da pasta gerada:
112
+ ## Instalação
125
113
 
126
- ```ts
127
- // CORRETO ✅
128
- import { setupVSRepo } from "../../generated/vsrepo";
114
+ A v2 é instalada como o pacote core mais um pacote de adapter para o seu ORM, por exemplo:
129
115
 
130
- // ERRADO ❌
131
- import { setupVSRepo } from "vsrepo";
116
+ ```bash
117
+ npm i vsrepo @vsrepo/prisma7-adapter
132
118
  ```
133
119
 
120
+ > O `vsrepo` v2.0.0 e o `@vsrepo/prisma7-adapter` já foram publicados no npm e estão prontos para uso. Para qualquer ORM além do Prisma 7, ainda não existe um pacote de adapter — instale o core e escreva o seu próprio (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)).
121
+
134
122
  ---
135
123
 
136
124
  ## Uso básico
137
125
 
138
- ### Configurando o Prisma Client
126
+ ### Implementando/escolhendo um adapter
139
127
 
140
- ```ts
128
+ ```typescript
141
129
  // src/configs/db.ts
142
- import { PrismaClient } from '../../generated/prisma/client';
143
- import { PrismaPg } from '@prisma/adapter-pg';
144
- import 'dotenv/config';
130
+ import { PrismaClient } from "../../generated/prisma/client";
131
+ import { PrismaPg } from "@prisma/adapter-pg";
132
+ import "dotenv/config";
145
133
 
146
134
  const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
147
135
  const prisma = new PrismaClient({ adapter });
@@ -149,1459 +137,979 @@ const prisma = new PrismaClient({ adapter });
149
137
  export default prisma;
150
138
  ```
151
139
 
152
- ### Criando um repositório
140
+ ### Criando um repository
153
141
 
154
- ```ts
155
- // src/repositories/userRepository.ts
142
+ ```typescript
143
+ // src/repositories/user.repository.ts
144
+ import { VSRepository, DynamicMethod } from "vsrepo";
145
+ import { VSRepoPrisma7Adapter } from "@vsrepo/prisma7-adapter";
156
146
  import prisma from "../configs/db";
157
- import { setupVSRepo } from "../../generated/vsrepo";
158
- import type { User } from "../../generated/prisma/client";
159
-
160
- const userRepository = setupVSRepo<User, "User">()(({
161
- tableName: "user",
162
- pkName: "id",
163
- selectModels: {
164
- public: { id: true, name: true, email: true },
165
- },
166
- defaultSelectModel: "public",
167
- }).build(prisma);
168
-
169
- export default userRepository;
170
- ```
171
-
172
- ### Usando o repositório
173
-
174
- ```ts
175
- import userRepository from "./repositories/userRepository";
176
-
177
- const user = await userRepository.save({
178
- name: "John",
179
- email: "john@email.com",
180
- password: "password",
181
- });
182
-
183
- const found = await userRepository.get(user.id);
184
- const all = await userRepository.getAll();
185
-
186
- user.name = "John Smith";
187
-
188
- await userRepository.save(user);
189
- await userRepository.remove(user.id);
190
- ```
191
-
192
- ---
193
-
194
- ## Abordagem baseada em classes (DynamicRepository)
195
-
196
- Se você prefere um estilo OOP com decorators em vez da abordagem funcional `setupVSRepo`, o VSRepository também oferece `DynamicRepository` — uma classe que você pode estender com decorators `@DynamicMethod()` para definir seus métodos dinâmicos.
197
-
198
- Veja **[README-DynamicRepo.pt-BR.md](./README-DynamicRepo.pt-BR.md)** para a documentação completa da abordagem baseada em classes, incluindo exemplos de integração com NestJS, configuração de decorators e uma comparação com o `setupVSRepo`.
199
-
200
- ---
147
+ import type { UserGetPayload } from "../../generated/prisma/models";
201
148
 
202
- ## Integração com NestJS
149
+ type User = UserGetPayload<{ include: { address: true } }>;
203
150
 
204
- O VSRepository pode ser facilmente integrado a projetos NestJS através de providers. Abaixo está um exemplo completo usando o padrão de injeção de dependência do NestJS.
205
-
206
- ### Configurando o repositório como provider
207
-
208
- ```ts
209
- // src/modules/user/user.repository.ts
210
- import { Provider } from "@nestjs/common";
211
- import { PrismaService } from "../../database/prisma.service";
212
- import { UserGetPayload } from "../../../generated/prisma/models";
213
- import { setupVSRepo } from "../../../generated/vsrepo";
214
-
215
- const userVSRepo = setupVSRepo<
216
- UserGetPayload<{ include: { profile: true } }>,
217
- "User"
218
- >()(({
219
- tableName: "user",
220
- pkName: "id",
221
- selectModels: {
222
- public: {
223
- id: true,
224
- email: true,
225
- createdAt: true,
226
- updatedAt: true,
227
- },
228
- auth: {
229
- id: true,
230
- email: true,
231
- password: true,
232
- },
233
- },
234
- defaultSelectModel: "public",
235
- requiredWhere: {
236
- deletedAt: null,
237
- },
238
- relations: {
239
- profile: {
240
- mode: "oto",
241
- pk: "id",
242
- restriction: "add",
243
- },
244
- },
245
- methods: {
246
- findAuthByEmail: {
247
- map: true,
248
- proxyTo: "findUniqueByEmail",
249
- selectModel: "auth",
250
- },
251
- findByEmailEndsWith: {
252
- map: true,
253
- }
254
- },
255
- });
256
-
257
- const setupUserRepository = (prisma: PrismaService) => {
258
- return userVSRepo.build(prisma);
259
- };
260
-
261
- export type UserRepository = ReturnType<typeof setupUserRepository>;
262
- /*
263
- O tipo também pode ser inferido usando o `RepositoryOf` do VSRepository, passando o tipo de `userVSRepo`:
264
-
265
- export type UserRepository = RepositoryOf<typeof userVSRepo>;
266
-
267
- OBS: Se você usar `.extend` para estender o repositório ou configurar os métodos base,
268
- usar `ReturnType` é recomendado por ser mais simples de inferir o tipo
269
- */
270
-
271
- export const USER_REPOSITORY = Symbol("USER_REPOSITORY");
272
-
273
- export const UserRepositoryProvider: Provider = {
274
- provide: USER_REPOSITORY,
275
- inject: [PrismaService],
276
- useFactory: setupUserRepository,
277
- };
278
- ```
279
-
280
- ### Registrando o provider no módulo
281
-
282
- ```ts
283
- // src/modules/user/user.module.ts
284
- import { Module } from "@nestjs/common";
285
- import { UserRepositoryProvider } from "./user.repository";
286
- import { UserService } from "./user.service";
287
- import { UserController } from "./user.controller";
288
-
289
- @Module({
290
- imports: [DatabaseModule],
291
- providers: [UserRepositoryProvider, UserService],
292
- controllers: [UserController],
293
- exports: [UserService],
294
- })
295
- export class UserModule {}
296
- ```
297
-
298
- ### Usando o repositório em um service
299
-
300
- ```ts
301
- // src/modules/user/user.service.ts
302
- import { Injectable, Inject } from "@nestjs/common";
303
- import { USER_REPOSITORY, type UserRepository } from "./user.repository";
304
-
305
- @Injectable()
306
- export class UserService {
307
- constructor(
308
- @Inject(USER_REPOSITORY)
309
- private readonly userRepository: UserRepository,
310
- ) {}
311
-
312
- async getUserById(id: string) {
313
- return this.userRepository.get(id);
314
- }
315
-
316
- async getUserAuthByEmail(email: string) {
317
- return this.userRepository.findAuthByEmail(email);
318
- }
319
-
320
- async createUser(data: { email: string; password: string; name: string }) {
321
- return this.userRepository.save({
322
- email: data.email,
323
- password: data.password,
324
- name: data.name,
151
+ class UserRepository extends VSRepository<User, string> {
152
+ constructor() {
153
+ super({
154
+ pkName: "id",
155
+ adapter: new VSRepoPrisma7Adapter<User>(prisma, { tableName: "user", pkName: "id" }),
156
+ softRemoveKey: "deletedAt",
157
+ defaultOrdering: { createdAt: "desc" },
325
158
  });
326
159
  }
327
- }
328
- ```
329
-
330
- **Benefícios dessa abordagem:**
331
-
332
- - ✅ Repositórios type-safe com injeção de dependência
333
- - ✅ Fácil de testar (mock do `USER_REPOSITORY`)
334
- - ✅ Isolamento da lógica de persistência
335
- - ✅ Reuso do repositório em múltiplos services
336
- - ✅ Suporte a transações via `PrismaService`
337
-
338
- ---
339
-
340
- ## Métodos base
341
-
342
- Ao chamar `.build(prisma)`, os métodos base abaixo ficam automaticamente disponíveis:
343
-
344
- | Método | Descrição |
345
- | ------------------------- | -------------------------------------------------------------------------------------------------------------- |
346
- | `get(pk)` | Busca um registro pela sua chave primária |
347
- | `getOrThrow(pk)` | Busca um registro pela sua chave primária; lança `VSRepoRuntimeError` (código `"20727"`) se não for encontrado |
348
- | `getList(pks)` | Busca múltiplos registros a partir de uma lista de chaves primárias |
349
- | `save(obj)` | Cria ou atualiza — se o objeto tiver uma `pk`, realiza um `upsert`; caso contrário, um `create` |
350
- | `saveList(objs)` | Salva um array de objetos em uma única transação automática |
351
- | `patch(pk, obj)` | Atualiza parcialmente um registro pela sua chave primária |
352
- | `patchList(tuples)` | Atualiza parcialmente múltiplos registros via um array de tuplas `[pk, obj]` em uma transação automática |
353
- | `merge(pk, obj)` | Busca um registro e faz um deep merge em memória — **não persiste**, retorna o objeto mesclado |
354
- | `remove(pk)` | Remove um registro pela sua chave primária |
355
- | `removeList(pks)` | Remove vários registros a partir de uma lista de chaves primárias — retorna `{ count }` |
356
- | `getAll()` | Retorna todos os registros (aceita `pagination` e `order` em `options`) |
357
- | `total()` | Retorna o número total de registros |
358
- | `has(pk)` | Verifica se um registro existe pela sua chave primária — retorna `boolean` |
359
-
360
- Todos eles aceitam `options` como último argumento.
361
-
362
- ### Soft-delete
363
-
364
- Quando `softRemovekName` está configurado no repositório, os métodos adicionais abaixo ficam disponíveis:
365
-
366
- | Método | Descrição |
367
- | --------------------------- | ---------------------------------------------------------------------------------- |
368
- | `softRemove(pk)` | Marca um registro como removido, preenchendo `softRemovekName` com a data atual |
369
- | `softRemoveList(pks)` | Marca múltiplos registros como removidos em lote — retorna `{ count }` |
370
- | `restore(pk)` | Restaura um registro com soft-delete, limpando o campo `softRemovekName` |
371
- | `restoreList(pks)` | Restaura múltiplos registros com soft-delete em lote — retorna `{ count }` |
372
-
373
- ```ts
374
- const userRepository = setupVSRepo<User, "user">()(({
375
- tableName: "user",
376
- pkName: "id",
377
- softRemovekName: "deletedAt", // deve ser um campo DateTime no schema do Prisma
378
- }).build(prisma);
379
-
380
- await userRepository.softRemove(1);
381
- await userRepository.restore(1);
382
- ```
383
-
384
- > 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 estiver incorreto.
385
160
 
386
- ### Operações em lote
161
+ @DynamicMethod()
162
+ declare findByEmail: (email: string) => Promise<User[]>;
387
163
 
388
- `saveList` e `patchList` executam automaticamente todas as operações dentro de uma única transação do Prisma. Se alguma operação falhar, todas as anteriores são desfeitas (rollback).
389
-
390
- ```ts
391
- // saveList — cria ou atualiza múltiplos objetos em uma transação automática
392
- const users = await userRepository.saveList([
393
- { name: "Mary", email: "mary@email.com" },
394
- { id: 2, name: "John Updated", email: "john@email.com" },
395
- ]);
164
+ @DynamicMethod()
165
+ declare findOneByEmail: (email: string) => Promise<User | null>;
166
+ }
396
167
 
397
- // patchList — atualiza parcialmente múltiplos registros via tuplas [pk, obj]
398
- const updated = await userRepository.patchList([
399
- [1, { active: false }],
400
- [2, { name: "New Name" }],
401
- ]);
168
+ export default new UserRepository();
402
169
  ```
403
170
 
404
- Quando você já está dentro de uma transação existente, passe-a em `options.db`. Nesse caso, `db` deve ser um `DbTransaction` (não o client principal):
405
-
406
- ```ts
407
- await prisma.$transaction(async (tx) => {
408
- await userRepository.saveList([{ name: "Mary" }, { name: "Gus" }], { db: tx });
409
- await userRepository.patchList([[1, { active: false }], [2, { active: true }]], { db: tx });
410
- });
411
- ```
171
+ > A API do core (`VSRepository`, `VSRepoAdapter`, `DynamicMethod`, `QueryMethod`, `VSRepoError`, enums e tipos) é importada do entry point único `vsrepo`. O adapter concreto vem de um pacote **separado** (`@vsrepo/*-adapter`). No Prisma 7, instale o [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) já publicado (o construtor dele recebe um objeto de config — `tableName`, `pkName`, `relations`/`logLevel` opcionais — como no exemplo acima). Adapters oficiais para outros ORMs estão planejados, mas ainda não publicados; até lá, você pode implementar o contrato `VSRepoAdapter` você mesmo (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)) — e publicá-lo para ajudar o projeto é muito bem-vindo.
412
172
 
413
- ### Merge
173
+ > **O terceiro generic (`OrmTypes`):** `VSRepository<Entity, PKType, OrmTypes>` aceita um terceiro type parameter opcional descrevendo os tipos de client/transaction do seu ORM, via `VSRepoOrmTypes` (`{ dbClient; dbTransaction }`). Ao fornecê-lo, `getDbClient()`, o callback de `transaction()` e a option `db` de todo método passam a ser tipados corretamente, em vez de `any`:
174
+ > ```typescript
175
+ > type PrismaOrmTypes = { dbClient: PrismaClient; dbTransaction: Prisma.TransactionClient };
176
+ >
177
+ > class UserRepository extends VSRepository<User, string, PrismaOrmTypes> {
178
+ > // getDbClient() agora retorna PrismaClient, e transaction(fn) tipa `tx` como Prisma.TransactionClient
179
+ > }
180
+ > ```
181
+ > Se omitido, o padrão é `VSRepoOrmTypes` (`dbClient`/`dbTransaction` como `any`).
414
182
 
415
- O método `merge` busca um registro pela PK e faz um deep merge (`deepmerge`) do objeto informado 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.
183
+ ### Usando o repository
416
184
 
417
- ```ts
418
- const existing = await userRepository.get(1);
419
- // existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
185
+ ```typescript
186
+ import userRepository from "./repositories/user.repository";
420
187
 
421
- const merged = await userRepository.merge(1, {
422
- profile: { bio: "Updated bio" },
188
+ const usuario = await userRepository.save({
189
+ name: "Joao",
190
+ email: "joao@email.com",
191
+ password: "password",
423
192
  });
424
- // merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
425
193
 
426
- // Para persistir, passe para save ou patch:
427
- await userRepository.save(merged);
428
- ```
194
+ const encontrado = await userRepository.get(usuario.id);
195
+ const todos = await userRepository.getAll();
196
+ const porEmail = await userRepository.findByEmail("joao@email.com");
429
197
 
430
- Retorna `null` se o registro não for encontrado.
431
-
432
- **O merge de relações to-many (`otm`/`mtm`) é feito por PK, não por simples concatenação.** Para relações to-one (`oto`/`mto`), o `merge` faz um deep merge comum do objeto. Para relações to-many, cada item do array enviado é comparado com o item existente que tem a mesma PK (definida em `relations[key].pk`): se a PK bater, os dois objetos são mesclados; se não bater (um item novo, sem correspondente), ele é simplesmente adicionado à lista. Itens existentes que não aparecem no array enviado são mantidos.
433
-
434
- ```ts
435
- const existing = await userRepository.get(1);
436
- // existing: {
437
- // id: 1,
438
- // posts: [
439
- // { id: 10, title: "Post A", published: false },
440
- // { id: 11, title: "Post B", published: true },
441
- // ],
442
- // }
443
-
444
- const merged = await userRepository.merge(1, {
445
- posts: [
446
- { id: 10, published: true }, // mesma PK (id: 10) → mescla com o item existente
447
- { title: "Post C" }, // sem PK → adicionado como novo item
448
- ],
449
- });
450
- // merged: {
451
- // id: 1,
452
- // posts: [
453
- // { id: 10, title: "Post A", published: true }, // mesclado
454
- // { id: 11, title: "Post B", published: true }, // mantido, não estava no array enviado
455
- // { title: "Post C" }, // adicionado
456
- // ],
457
- // }
198
+ await userRepository.patch(usuario.id, { name: "Joao Pedro" });
199
+ await userRepository.remove(usuario.id);
458
200
  ```
459
201
 
460
- > Observe que o `merge` nunca remove itens de uma relação to-many — ele apenas mescla os que batem por PK e adiciona os que não batem. Para remover itens de uma relação, use `save`/`patch` com `restriction: "set"` na configuração da relação.
461
-
462
- ### Configurando os métodos base
463
-
464
- O segundo argumento de `.build(prisma, config)` permite ajustar o comportamento global do repositório e customizar cada método base individualmente através de `baseMethods`.
465
-
466
- ```ts
467
- userVSRepo.build(prisma, {
468
- // Exibe os logs internos do VSRepository no console (queries montadas, prefixo
469
- // detectado, filtros aplicados, etc). Ótimo para debugar métodos dinâmicos. Padrão = false.
470
- showWorking: true,
471
-
472
- baseMethods: {
473
- get: {
474
- // Habilita/desabilita o método no repositório final. Se `false`, o método
475
- // nem aparece no tipo do repositório (não é apenas um erro em runtime). Padrão = true.
476
- active: true,
477
-
478
- // Select model aplicado por padrão quando o método é chamado sem `options.selectModel`.
479
- // Sobrescreve o `defaultSelectModel` de setupVSRepo apenas para este método.
480
- defaultSelect: "public",
481
- },
482
- remove: {
483
- active: true,
484
- defaultSelect: "minimal",
485
-
486
- // Quando `true`, ignora o `requiredWhere` configurado em setupVSRepo para
487
- // este método específico — útil quando um método precisa "furar" um
488
- // filtro global (ex: multi-tenancy) em um caso específico. Padrão = false.
489
- ignoreRequiredWhere: false,
490
- },
491
- save: {
492
- // Aqui apenas `ignoreRequiredWhere` é definido — `active` e `defaultSelect`
493
- // mantêm seus padrões (true e o `defaultSelectModel` global).
494
- ignoreRequiredWhere: true,
495
- },
496
- patch: {
497
- // Apenas o select é sobrescrito; o método continua ativo normalmente.
498
- defaultSelect: "minimal",
499
- },
500
- has: {
501
- active: false, // Desabilita 'has' (padrão = true) — o método desaparece do repositório
502
- },
503
- softRemove: {
504
- // Métodos de soft-delete seguem as mesmas opções (`active`, `defaultSelect`,
505
- // `ignoreRequiredWhere`). Só estão disponíveis se `softRemovekName` estiver configurado.
506
- active: true,
507
- defaultSelect: "minimal",
508
- },
509
- },
510
- });
511
- ```
512
-
513
- > Métodos de lote/agregação como `removeList`, `softRemoveList`, `restoreList`, `total` e `has` **não** aceitam `defaultSelect` (eles não retornam um registro selecionável — retornam `{ count }` ou `boolean`). Nesses casos, `BaseMethodConfig` fica restrito a `active` e `ignoreRequiredWhere`.
514
-
515
202
  ---
516
203
 
517
- ## Select Models
204
+ ## Options do construtor
518
205
 
519
- `selectModels` define projeções de dados nomeadas e reutilizáveis.
206
+ `VSRepoOptions<T, K>`, passado para o `super(...)` dentro do construtor do seu repository:
520
207
 
521
- ```ts
522
- selectModels: {
523
- public: { id: true, name: true, email: true },
524
- internal: { id: true, name: true, email: true, password: true },
525
- minimal: { id: true },
526
- },
527
- defaultSelectModel: "public",
528
- ```
208
+ | Option | Tipo | Descrição |
209
+ | -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
210
+ | `adapter` | `VSRepoAdapter<T>` | **Obrigatório.** A instância do adapter que traduz as chamadas do repository em chamadas contra o ORM/banco por trás dele. |
211
+ | `pkName` | `keyof T` | **Obrigatório.** Nome do campo que representa a primary key da entidade. |
212
+ | `softRemoveKey` | `keyof T` | Opcional. Quando definido, habilita `softRemove`, `softRemoveList`, `restore` e `restoreList`. |
213
+ | `defaultOrdering` | `Ordering<T>` | Opcional. Ordenação padrão aplicada automaticamente em queries que aceitam `order`, a menos que seja sobrescrita em uma chamada específica. |
214
+ | `logLevel` | `VSLogLevel` | Opcional. Severidade mínima impressa pelo logger interno. Padrão: `VSLogLevel.WARN`. |
215
+ | `logSlowThresholdMs` | `number` | Opcional. Duração (ms) acima da qual uma operação concluída é logada como `WARN` em vez de `DEBUG`. Padrão: 300ms. |
529
216
 
530
- `defaultSelectModel` define qual select é usado automaticamente quando nenhum é especificado na chamada. É recomendado sempre defini-lo junto com `selectModels`.
531
-
532
- **Usando um select específico na chamada:**
217
+ ---
533
218
 
534
- ```ts
535
- const user = await userRepository.get(id, { selectModel: "minimal" });
536
- ```
219
+ ## Métodos base
537
220
 
538
- **Retornando o payload padrão do Prisma (sem select):**
221
+ Disponíveis automaticamente em toda subclasse de `VSRepository`:
222
+
223
+ | Método | Descrição |
224
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
225
+ | `get(pk, options?)` | Busca um registro pela primary key. |
226
+ | `getOrThrow(pk, options?)` | Busca um registro pela primary key, lançando erro se não encontrar. |
227
+ | `getList(pks, options?)` | Busca vários registros por uma lista de primary keys. |
228
+ | `getAll(options?)` | Busca todos os registros; aceita `pagination` e `order` em `options`. |
229
+ | `save(obj, options?)` | Cria ou atualiza (upsert) um único registro. |
230
+ | `saveList(objs, options?)` | Cria ou atualiza (upsert) vários registros em uma única chamada. |
231
+ | `patch(pk, obj, options?)` | Atualiza parcialmente um registro pela primary key. |
232
+ | `merge(pk, obj, options?)` | Busca um registro e o retorna mesclado (deep-merge), em memória, com o objeto informado — **não** persiste nada. |
233
+ | `remove(pk, options?)` | Remove um registro pela primary key. |
234
+ | `removeList(pks, options?)` | Remove vários registros pela primary key, retornando `{ count }`. |
235
+ | `total(options?)` | Retorna o total de registros. |
236
+ | `has(pk, options?)` | Verifica se um registro existe, retornando `boolean`. |
237
+ | `increment(pk, field, value, options?)` | Adiciona `value` a um campo numérico de forma atômica. Veja [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação). |
238
+ | `decrement(pk, field, value, options?)` | Subtrai `value` de um campo numérico de forma atômica. |
239
+ | `multiply(pk, field, value, options?)` | Multiplica um campo numérico por `value` de forma atômica. |
240
+ | `divide(pk, field, value, options?)` | Divide um campo numérico por `value` de forma atômica. |
241
+ | `sum(field, where?, options?)` | Soma um campo numérico em todos os registros que baterem no filtro; `null` se nenhum bater. |
242
+ | `average(field, where?, options?)` | Média aritmética de um campo numérico em todos os registros que baterem no filtro; `null` se nenhum bater. |
243
+ | `min(field, where?, options?)` | Valor mínimo de um campo numérico em todos os registros que baterem no filtro; `null` se nenhum bater. |
244
+ | `max(field, where?, options?)` | Valor máximo de um campo numérico em todos os registros que baterem no filtro; `null` se nenhum bater. |
245
+ | `transaction(fn, options?)` | Executa `fn` dentro de uma transação nativa do ORM. |
246
+ | `getDbClient()` | Retorna a instância do client do ORM usada fora de transações. |
247
+ | `query<T>(query, options?)` | Executa uma instrução SQL raw diretamente contra o banco. Veja [Queries raw pontuais com `query()`](#queries-raw-pontuais-com-query). |
248
+
249
+ A maioria dos métodos acima aceita um objeto `MethodOptions<Entity, OrmTypes>` como último argumento (`select`, `relations`, `see`, `db`). Alguns — `total`, `has`, `removeList`, `sum`, `average`, `min`, `max`, e os métodos em lote de soft-delete (`softRemoveList`/`restoreList`) — não retornam/moldam uma `Entity`, então aceitam o tipo mais restrito `RestrictMethodOptions<Entity, OrmTypes>` (só `see`, `db`; sem `select`/`relations`). `transaction`, `query` e `getDbClient` recebem options próprias ou nenhuma.
539
250
 
540
- ```ts
541
- const fullUser = await userRepository.get(id, { selectModel: false });
542
- ```
251
+ ---
543
252
 
544
- ### Select bruto (`options.select`)
253
+ ## Soft-delete
545
254
 
546
- Além do `selectModel` (nomeado, pré-configurado em `selectModels`), você pode passar um `select` bruto do Prisma diretamente na chamada, sem precisar registrá-lo antecipadamente no repositório.
255
+ O soft-delete agora é um **conceito nativo de primeira classe**. Configure `softRemoveKey` uma vez no repository:
547
256
 
548
- ```ts
549
- const usuario = await usuarioRepository.get(id, {
550
- select: { id: true, nome: true },
257
+ ```typescript
258
+ super({
259
+ pkName: "id",
260
+ adapter,
261
+ softRemoveKey: "deletedAt",
551
262
  });
552
263
  ```
553
264
 
554
- `options.select` aceita qualquer `select` válido para o modelo Prisma do repositório — é totalmente tipado e oferece o mesmo autocomplete/validação de chamar `prisma.usuario.findMany({ select: ... })` diretamente, e o tipo de retorno do método é restrito exatamente aos campos selecionados.
265
+ Isso libera quatro métodos extras:
555
266
 
556
- **Regras e comportamento:**
267
+ | Método | Efeito |
268
+ | ------------------------------- | --------------------------------------- |
269
+ | `softRemove(pk, options?)` | Define `deletedAt` para a data atual. |
270
+ | `softRemoveList(pks, options?)` | O mesmo, em lote — retorna `{ count }`. |
271
+ | `restore(pk, options?)` | Volta `deletedAt` para `null`. |
272
+ | `restoreList(pks, options?)` | O mesmo, em lote — retorna `{ count }`. |
557
273
 
558
- - **Mutuamente exclusivo com `selectModel`, `includeModel` e `include`.** Apenas um dos quatro pode ser fornecido por chamada; os tipos garantem isso — passar mais de um é um erro em tempo de compilação.
559
- - **Ad hoc, não reutilizável.** Diferente do `selectModel`, não precisa ser declarado em `selectModels`. Use-o para projeções pontuais que não justificam um select model nomeado.
560
- - **Nenhum `defaultSelectModel` é aplicado.** Quando `select` é fornecido, o select padrão (`defaultSelectModel`) é ignorado e apenas o `select` bruto é enviado ao Prisma.
274
+ Todo o restante dos métodos aceita uma option `see` que controla a visibilidade de registros com soft-delete:
561
275
 
562
- ```ts
563
- // CORRETO ✅ — apenas select bruto
564
- await usuarioRepository.get(id, { select: { id: true, nome: true } });
565
-
566
- // ERRADO ❌ — combinar select com selectModel/includeModel/include não é permitido
567
- await usuarioRepository.get(id, { selectModel: "public", select: { id: true } });
568
- await usuarioRepository.get(id, { include: { posts: true }, select: { id: true } });
276
+ ```typescript
277
+ await userRepository.getAll({ see: "active" }); // padrão — apenas registros não removidos
278
+ await userRepository.getAll({ see: "removed" }); // apenas registros com soft-delete
279
+ await userRepository.getAll({ see: "all" }); // todos, ignorando o soft-delete
569
280
  ```
570
281
 
571
- > **Quando usar `selectModel` vs. `select`:** prefira `selectModel` para projeções reutilizadas em várias chamadas (definidas uma vez em `selectModels`); use `select` para projeções específicas e ocasionais que não precisam de um nome.
572
-
573
282
  ---
574
283
 
575
- ## Include Models
576
-
577
- `includeModels` funciona de forma parecida com `selectModels`, mas em vez de receber um `select`, recebe um `include` válido do Prisma.
578
-
579
- ```ts
580
- const userRepository = setupVSRepo<User, "user">()(({
581
- tableName: "user",
582
- pkName: "id",
583
- selectModels: {
584
- public: { id: true, name: true, email: true },
585
- },
586
- defaultSelectModel: "public",
587
- includeModels: {
588
- withPosts: { posts: true },
589
- withPostsAndProfile: { posts: true, profile: true },
590
- },
591
- }).build(prisma);
592
- ```
593
-
594
- **Usando um `includeModel` na chamada:**
595
-
596
- ```ts
597
- const user = await userRepository.get(id, { includeModel: "withPosts" });
598
- ```
599
-
600
- Nesse caso, o `select` padrão (`selectModels`/`defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
601
-
602
- ### Diferenças em relação a `selectModels`
284
+ ## Métodos atômicos e de agregação
603
285
 
604
- - **Só pode ser passado na chamada do método**, via `options.includeModel`. Não existe `defaultIncludeModel` ou `defaultInclude` — não há como configurar um `includeModel` padrão no repositório, ao contrário do que acontece com `defaultSelectModel`.
605
- - **`includeModel` e `selectModel` não podem ser passados juntos** na mesma chamada. Se um `includeModel` for informado, qualquer `selectModel` (incluindo o padrão) é ignorado.
286
+ Toda subclasse de `VSRepository` ganha 8 métodos extras para trabalhar com campos numéricos, divididos em dois grupos:
606
287
 
607
- ```ts
608
- // CORRETO ✅ — apenas includeModel
609
- await userRepository.get(id, { includeModel: "withPosts" });
288
+ **Updates atômicos** — avaliados no servidor contra o valor *atual* da linha (`UPDATE ... SET field = field + value`), não um read-modify-write feito no client:
610
289
 
611
- // CORRETO ✅ — apenas selectModel
612
- await userRepository.get(id, { selectModel: "public" });
613
-
614
- // ERRADO ❌ — combinar os dois não é permitido
615
- await userRepository.get(id, { selectModel: "public", includeModel: "withPosts" });
290
+ ```typescript
291
+ await userRepository.increment("user-1", "balance", 50); // balance = balance + 50
292
+ await userRepository.decrement("user-1", "balance", 50); // balance = balance - 50
293
+ await userRepository.multiply("user-1", "balance", 2); // balance = balance * 2
294
+ await userRepository.divide("user-1", "balance", 4); // balance = balance / 4
616
295
  ```
617
296
 
618
- ### Include bruto (`options.include`)
297
+ Os quatro retornam a `Entity` atualizada e aceitam o `MethodOptions<Entity, OrmTypes>` completo (`select`, `relations`, `see`, `db`) como último argumento, igual `get`/`save`/`patch`.
619
298
 
620
- Além do `includeModel` (nomeado, pré-configurado em `includeModels`), você pode passar um `include` bruto do Prisma diretamente na chamada, sem precisar registrá-lo antes no repositório.
299
+ **Agregações** — calculadas sobre todos os registros que baterem num `where` (opcional):
621
300
 
622
- ```ts
623
- const user = await userRepository.get(id, {
624
- include: { posts: true, profile: true },
625
- });
301
+ ```typescript
302
+ await userRepository.sum("balance"); // soma do saldo de todos os registros ativos
303
+ await userRepository.sum("balance", { active: true }); // ...restrito por um where
304
+ await userRepository.average("balance");
305
+ await userRepository.min("balance");
306
+ await userRepository.max("balance");
626
307
  ```
627
308
 
628
- `options.include` aceita qualquer `include` válido para o modelo Prisma do repositório — é totalmente tipado e oferece o mesmo autocomplete/validação de chamar `prisma.user.findMany({ include: ... })` diretamente.
309
+ Os quatro retornam `number | null` — `null` quando nenhum registro bate no filtro, espelhando o comportamento de `SUM()`/`AVG()`/`MIN()`/`MAX()` do SQL, que retornam `NULL` (não `0`) sobre um conjunto vazio. Diferente dos métodos atômicos, eles aceitam o tipo mais restrito `RestrictMethodOptions<Entity, OrmTypes>` (só `see`, `db` — sem `select`/`relations`, já que o resultado é um número simples, não uma `Entity` moldada).
629
310
 
630
- **Regras e comportamento:**
311
+ Os dois grupos respeitam `softRemoveKey`/`see` do mesmo jeito que todo outro método base — `sum("balance")` só soma registros não removidos por padrão; passe `{ see: "all" }` ou `{ see: "removed" }` para mudar isso.
631
312
 
632
- - **Mutuamente exclusivo com `selectModel` e `includeModel`.** Apenas um dos três pode ser informado por chamada; os tipos garantem isso — passar mais de um é um erro em tempo de compilação.
633
- - **Ad hoc, não reutilizável.** Diferente do `includeModel`, não precisa ser declarado em `includeModels`. Use para includes pontuais que não justificam um model nomeado.
634
- - **Nenhum `selectModel` padrão é aplicado.** Assim como com `includeModel`, quando `include` é informado, o select (incluindo `defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
313
+ ### Quais campos são elegíveis
635
314
 
636
- ```ts
637
- // CORRETO ✅ — apenas include bruto
638
- await userRepository.get(id, { include: { posts: true } });
315
+ `field` é restrito a `NumericKeys<Entity>` — chaves cujo valor (ignorando `null`/`undefined`) é um `number`, um `bigint`, ou um objeto `DecimalLike` (qualquer coisa que exponha `toNumber()` e `decimalPlaces()`, como o `Prisma.Decimal` do Prisma):
639
316
 
640
- // ERRADO ❌ — combinar include com selectModel/includeModel não é permitido
641
- await userRepository.get(id, { selectModel: "public", include: { posts: true } });
642
- await userRepository.get(id, { includeModel: "withPosts", include: { posts: true } });
643
- ```
644
-
645
- > **Quando usar `includeModel` vs. `include`:** prefira `includeModel` para includes reutilizados em várias chamadas (definidos uma vez em `includeModels`); use `include` para includes específicos e pontuais que não precisam de um nome.
646
-
647
- ---
648
-
649
- ## Required Where
650
-
651
- `requiredWhere` define filtros que são aplicados automaticamente em toda query do repositório.
652
-
653
- ```ts
654
- requiredWhere: { active: true },
655
- ```
656
-
657
- Agora toda query vai incluir automaticamente `active: true`:
658
-
659
- ```ts
660
- // Internamente: WHERE active = true
661
- const users = await userRepository.findMany();
317
+ ```typescript
318
+ type Product = { id: string; name: string; price: Decimal; stock: number | null };
662
319
 
663
- // Internamente: WHERE email = 'john@email.com' AND active = true
664
- const user = await userRepository.findByEmail("john@email.com");
320
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok — Decimal-like
321
+ await productRepository.increment(id, "stock", 5); // ok — campos numéricos nullable são incluídos
322
+ await productRepository.increment(id, "name", 1); // erro de compilação — "name" não é numérico
665
323
  ```
666
324
 
667
- Útil para soft-deletes manuais, multi-tenancy e filtros globais de qualquer tipo.
325
+ `value` é tipado como `NonNullable<Entity[Field]>` — precisa bater exatamente com o tipo do próprio campo. Um campo `Decimal` espera uma instância de `Decimal`, não um `number`/`string` puro:
668
326
 
669
- ---
670
-
671
- ## Ordenação padrão (Default Ordering)
672
-
673
- `defaultOrdering` define uma ordenação padrão que é aplicada automaticamente em toda query que aceita `orderBy`, sem precisar repetir o argumento `order` em cada chamada.
674
-
675
- ```ts
676
- const userRepository = setupVSRepo<User, "user">()(({
677
- tableName: "user",
678
- pkName: "id",
679
- defaultOrdering: { createdAt: "desc" },
680
- }).build(prisma);
681
- ```
682
-
683
- Com isso, toda query de listagem já virá ordenada por `createdAt` decrescente:
684
-
685
- ```ts
686
- // Internamente: ORDER BY createdAt DESC
687
- const users = await userRepository.getAll();
688
-
689
- // Também se aplica ao getAll com paginação
690
- const paginated = await userRepository.getAll({ pagination: { take: 10 } });
327
+ ```typescript
328
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok
329
+ await productRepository.increment(id, "price", 10.5); // erro de compilação — envolva: new Decimal(10.5)
691
330
  ```
692
331
 
693
- **`defaultOrdering` é ignorado quando:**
332
+ Vale notar que vários ORMs (Drizzle, MikroORM, TypeORM) representam colunas `decimal`/`numeric` como `string` pura por padrão, para evitar perda de precisão de ponto flutuante — um campo `string` **não** satisfaz `NumericKeys<Entity>` por padrão. Configure a coluna em modo numérico (ou um transformer) nesses ORMs se quiser que o campo fique disponível para esses 8 métodos.
694
333
 
695
- - O método usa o sufixo `Ordered`, `OrderedAndPaginated` ou `PaginatedAndOrdered` — nesses casos, o argumento `order` passado na chamada tem prioridade.
696
- - O método dinâmico tem `injectOrdering` configurado — a ordenação fixa do método tem precedência.
697
-
698
- ```ts
699
- methods: {
700
- findManyPaginatedAndOrdered: { map: true }, // order vem do argumento → defaultOrdering ignorado
701
- findManyByActive: { map: true }, // sem Ordered → defaultOrdering aplicado
702
- findManyByStatus: {
703
- map: true,
704
- injectOrdering: { name: "asc" }, // injectOrdering → defaultOrdering ignorado
705
- },
706
- }
707
- ```
334
+ ### Escrevendo um adapter
708
335
 
709
- > `defaultOrdering` aceita o mesmo tipo do `orderBy` nativo do Prisma para o modelo — incluindo arrays de ordenações encadeadas.
336
+ O `VSRepoAdapter` espelha as mesmas 8 operações (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)). Cada adapter traduz isso para o que o ORM/banco considera "nativo": o Prisma tem um formato de update embutido (`{ field: { increment: value } }`) e uma chamada `aggregate()`; outros ORMs em geral precisam de um `QueryBuilder`/expressão `sql` raw (ex.: `SET field = field * :value`, `SELECT SUM(field) ...`). Os métodos atômicos precisam retornar o registro refletindo o estado *depois* do write — se a API de update atômico do ORM só retorna a quantidade de linhas afetadas, faça uma leitura extra em vez de devolver uma cópia desatualizada que já estava em memória.
710
337
 
711
338
  ---
712
339
 
713
- ## Opção `see`
714
-
715
- Quando `softRemovekName` está configurado, todo método aceita a opção `see` para controlar a visibilidade de registros com soft-delete:
716
-
717
- | Valor | Comportamento |
718
- | ----------- | -------------------------------------------------------------- |
719
- | `"active"` | Retorna apenas registros que **não** foram removidos (padrão) |
720
- | `"removed"` | Retorna apenas registros removidos |
721
- | `"all"` | Retorna todos os registros, independente do status |
340
+ ## `select` e `relations`
722
341
 
723
- ```ts
724
- // Retorna apenas usuários ativos (padrão)
725
- const active = await userRepository.getAll();
342
+ Os `selectModels`/`defaultSelectModel` nomeados e reutilizáveis da v1 não existem mais. Na v2 você passa `select` e `relations` diretamente em cada chamada — não há nada para pré-registrar:
726
343
 
727
- // Retorna apenas usuários removidos
728
- const removed = await userRepository.getAll({ see: "removed" });
344
+ ```typescript
345
+ const usuario = await userRepository.get(id, {
346
+ select: { id: true, name: true, address: { city: true } },
347
+ });
729
348
 
730
- // Retorna todos
731
- const all = await userRepository.getAll({ see: "all" });
349
+ const usuarioComEndereco = await userRepository.get(id, {
350
+ relations: { address: true },
351
+ });
732
352
  ```
733
353
 
734
- > A opção `see` funciona independentemente do `requiredWhere` — ela é aplicada em cima do filtro de soft-delete, não como substituta dele.
354
+ - `select` espelha o formato da entidade: campos escalares recebem um `boolean`; campos de relação recebem um `boolean` ou um `select` aninhado.
355
+ - `relations` carrega registros relacionados; cada campo de relação recebe um `boolean` ou um objeto `relations` aninhado.
356
+ - Se `select` e `relations` podem ser combinados depende do adapter (veja abaixo).
357
+
358
+ > ⚠️ **Comportamento de `relations` depende do adapter:**
359
+ >
360
+ > O core apenas repassa `MethodOptions.select` e `MethodOptions.relations` ao adapter — cada adapter decide como traduzi-los para o ORM subjacente:
361
+ >
362
+ > - **TypeORM (`@vsrepo/typeorm-adapter`)** — `relations` é **obrigatório** para carregar qualquer relação, mesmo quando você quer apenas uma projeção aninhada via `select`. O TypeORM não fará JOIN/carregar a relação a menos que ela esteja listada em `relations`:
363
+ > ```typescript
364
+ > // TypeORM: apenas select NÃO é suficiente
365
+ > await userRepository.get(id, {
366
+ > select: { id: true, address: { city: true } },
367
+ > relations: { address: true }, // ← obrigatório no TypeORM
368
+ > });
369
+ > ```
370
+ > - **Prisma 7 (`@vsrepo/prisma7-adapter` / `VSRepoPrisma7Adapter`)** — `relations` é convertido para `include` do Prisma (`parsePrismaInclude`). **Se `select` estiver presente, `relations` é ignorado** porque o Prisma não permite `select` + `include` na mesma query:
371
+ > ```typescript
372
+ > // Prisma7: relations é ignorado quando select existe
373
+ > await userRepository.get(id, {
374
+ > select: { id: true, name: true },
375
+ > relations: { address: true }, // ← ignorado, include = undefined
376
+ > });
377
+ > ```
378
+ >
379
+ > Adapters customizados podem mapear `relations` de forma diferente — consulte a documentação do adapter para a semântica exata.
735
380
 
736
381
  ---
737
382
 
738
383
  ## Métodos dinâmicos
739
384
 
740
- Métodos dinâmicos são definidos na propriedade `methods` e têm seu comportamento inferido pelo nome.
741
-
742
- ```ts
743
- methods: {
744
- findOneByEmail: { map: true },
745
- findManyPaginated: { map: true },
746
- updateById: { map: true },
747
- deleteManyByIdIn: { map: true },
385
+ Métodos dinâmicos são declarados como um campo `declare` anotado com `@DynamicMethod()`. O comportamento deles — qual método do adapter chamar, quais filtros aplicar e como os argumentos se mapeiam para eles — é inferido inteiramente a partir do **nome** do campo, seguindo a mesma filosofia de convenção sobre configuração da v1.
386
+
387
+ ```typescript
388
+ class UserRepository extends VSRepository<User, string> {
389
+ @DynamicMethod()
390
+ declare findByEmail: (email: string) => Promise<User[]>;
391
+
392
+ @DynamicMethod()
393
+ declare findOneByEmail: (email: string) => Promise<User | null>;
394
+
395
+ @DynamicMethod()
396
+ declare updateById: (id: string, data: DeepPartial<User>) => Promise<User>;
397
+
398
+ // Baseado em where: VSRepoWhere<T> como primeiro parâmetro, pagination penúltimo, MethodOptions por último
399
+ @DynamicMethod()
400
+ declare findWherePaginated: (
401
+ where: VSRepoWhere<User>,
402
+ pagination: Pagination,
403
+ options?: MethodOptions<User>,
404
+ ) => Promise<User[]>;
405
+
406
+ // OrderedAndPaginated: filtros de campo, depois order, depois pagination, depois MethodOptions
407
+ @DynamicMethod()
408
+ declare findByNameIgnoreCaseOrAgeBetweenOrderByCreatedAtAscPaginated: (
409
+ name: string,
410
+ age: [number, number],
411
+ order: Ordering<User>,
412
+ pagination: Pagination,
413
+ options?: MethodOptions<User>,
414
+ ) => Promise<User[]>;
748
415
  }
749
416
  ```
750
417
 
751
- ---
752
-
753
418
  ### Prefixos disponíveis
754
419
 
755
- O prefixo do nome do método determina qual operação do Prisma será chamada e quais argumentos são esperados.
756
-
757
- | Prefixo | Operação do Prisma | Retorno | Observações |
758
- | ---------------------------- | --------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------- |
759
- | `findOneBy` | `findFirst` | `T \| null` | Retorno único. |
760
- | `findBy` | `findMany` / `findFirst` | `T[]` ou `T \| null` | Padrão é lista; use `fbMode: "one"` para retorno único (**deprecated**, use `findOneBy`) |
761
- | `findUniqueBy` | `findUnique` | `T \| null` | |
762
- | `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Lança um erro se não encontrado |
763
- | `findFirstBy` | `findFirst` | `T \| null` | Aceita campos como filtro |
764
- | `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Aceita campos como filtro; lança um erro se não encontrado |
765
- | `findFirst` | `findFirst` | `T \| null` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere` |
766
- | `findFirstOrThrow` | `findFirstOrThrow` | `T` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere`; lança um erro se não encontrado |
767
- | `findManyBy` | `findMany` | `T[]` | Aceita campos como filtro |
768
- | `findMany` | `findMany` | `T[]` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere` |
769
- | `findOneWhere` | `findFirst` | `T \| null` | Recebe um objeto `where` explícito como argumento |
770
- | `findListWhere` | `findMany` | `T[]` | Recebe um objeto `where` explícito como argumento |
771
- | `existsBy` | `findFirst` | `boolean` | Retorna `true` se encontrado, `false` caso contrário |
772
- | `existsWhere` | `findFirst` | `boolean` | Recebe um objeto `where` explícito e retorna se existe |
773
- | `countBy` | `count` | `number` | Aceita campos como filtro |
774
- | `countWhere` | `count` | `number` | Recebe um objeto `where` explícito como argumento |
775
- | `count` | `count` | `number` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere` |
776
- | `create` | `create` | `T` | Recebe `data` como argumento |
777
- | `createMany` | `createMany` | `{ count: number }` | Recebe `data` como argumento; suporta `SkipDuplicates` |
778
- | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Recebe `data` como argumento; suporta `SkipDuplicates` |
779
- | `updateBy` | `update` | `T` | Recebe `data` como argumento |
780
- | `updateManyBy` | `updateMany` | `{ count: number }` | Recebe `data` como argumento |
781
- | `updateManyWhere` | `updateMany` | `{ count: number }` | Recebe um objeto `where` e um objeto `data` como argumentos |
782
- | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Recebe `data` como argumento |
783
- | `updateManyAndReturnWhere` | `updateManyAndReturn` | `T[]` | Recebe um objeto `where` e um objeto `data` como argumentos |
784
- | `upsertBy` | `upsert` | `T` | Recebe `update` e `create` como argumentos |
785
- | `deleteBy` | `delete` | `T` | |
786
- | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
787
- | `deleteManyWhere` | `deleteMany` | `{ count: number }` | Recebe um objeto `where` explícito como argumento |
788
- | `aggregate` | `aggregate` | `Dynamic` | O nome deve ser exato; recebe argumentos nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
789
- | `groupBy` | `groupBy` | `Dynamic[]` | O nome deve ser exato; recebe argumentos nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
790
-
791
- ---
420
+ | Prefixo | Método do adapter | Observações |
421
+ | -------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------- |
422
+ | `findBy` | `findMany` | Filtros de campo seguem o prefixo. |
423
+ | `findOneBy` | `findOne` | Filtros de campo seguem o prefixo; resultado único. |
424
+ | `findOneOrThrowBy` | `findOneOrThrow` | Lança erro se não encontrar. |
425
+ | `findOneOrThrow` | `findOneOrThrow` | Sem filtros de campo; aplica só soft-delete/`see`. |
426
+ | `findOneOrThrowWhere` | `findOneOrThrow` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
427
+ | `findWhere` | `findMany` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
428
+ | `findOneWhere` | `findOne` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
429
+ | `findOne` | `findOne` | Sem filtros de campo; aplica só soft-delete/`see`. |
430
+ | `countBy` | `count` | Filtros de campo seguem o prefixo. |
431
+ | `countWhere` | `count` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
432
+ | `count` | `count` | Sem filtros de campo. |
433
+ | `existsBy` | `exists` | Retorna `boolean`. |
434
+ | `existsWhere` | `exists` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
435
+ | `create` | `create` | Recebe `data` como argumento. |
436
+ | `createMany` | `createMany` | Recebe `data[]` como argumento; suporta `IgnoreConflicts`. |
437
+ | `createManyReturning` | `createManyReturning` | Recebe `data[]` como argumento; suporta `IgnoreConflicts`; retorna os registros criados (`T[]`), em vez de `CountResult`. |
438
+ | `updateBy` | `update` | Filtros de campo + `data` como argumento. |
439
+ | `updateWhere` | `update` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois `data`. |
440
+ | `updateManyBy` | `updateMany` | Filtros de campo + `data`. |
441
+ | `updateManyWhere` | `updateMany` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois `data`. |
442
+ | `updateManyReturningBy` | `updateManyReturning` | Filtros de campo + `data`; retorna os registros atualizados. |
443
+ | `updateManyReturningWhere` | `updateManyReturning` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois `data`; retorna os registros atualizados. |
444
+ | `upsertBy` | `upsert` | Filtros de campo + payloads `create`/`update`. |
445
+ | `upsertWhere` | `upsert` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois os payloads `create`/`update`. |
446
+ | `deleteBy` | `delete` | Filtros de campo seguem o prefixo. |
447
+ | `deleteWhere` | `delete` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
448
+ | `deleteManyBy` | `deleteMany` | Filtros de campo seguem o prefixo. |
449
+ | `deleteManyWhere` | `deleteMany` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
450
+ | `deleteManyReturningBy` | `deleteManyReturning` | Filtros de campo seguem o prefixo; retorna os registros removidos. |
451
+ | `deleteManyReturningWhere` | `deleteManyReturning` | Recebe um `VSRepoWhere<T>` como primeiro argumento; retorna os registros removidos. |
452
+
453
+ > `aggregate` e `groupBy` **ainda não estão implementados** na v2 (existiam na v1). Está planejado, mas não disponível no momento.
792
454
 
793
455
  ### Filtros de campo
794
456
 
795
- Filtros são sufixos aplicados ao nome do campo dentro do método. O próprio campo vem capitalizado logo após o prefixo (ou após `By`).
796
-
797
- | Sufixo | Operador Prisma | Argumento obrigatório |
798
- | --------------------- | ---------------------- | --------------------------- |
799
- | *(sem sufixo)* | igualdade (`=`) | sim |
800
- | `Not` | `not` | sim |
801
- | `In` | `in` | sim (array) |
802
- | `NotIn` | `notIn` | sim (array) |
803
- | `Contains` | `contains` | sim |
804
- | `NotContains` | `not.contains` | sim |
805
- | `StartsWith` | `startsWith` | sim |
806
- | `NotStartsWith` | `not.startsWith` | sim |
807
- | `EndsWith` | `endsWith` | sim |
808
- | `NotEndsWith` | `not.endsWith` | sim |
809
- | `GreaterThan` | `gt` | sim |
810
- | `GreaterThanEqual` | `gte` | sim |
811
- | `LessThan` | `lt` | sim |
812
- | `LessThanEqual` | `lte` | sim |
813
- | `Between` | `gte` + `lte` | sim (tupla `[min, max]`) |
814
- | `NotBetween` | `not.gte` + `not.lte` | sim (tupla `[min, max]`) |
815
- | `IsNull` | `null` | não |
816
- | `IsNotNull` | `not: null` | não |
817
- | `IsTrue` | `true` | não |
818
- | `IsFalse` | `false` | não |
819
- | `Insensitive` | `mode: 'insensitive'` | combinador |
820
-
821
- `Insensitive` é um combinador e pode ser usado junto com outro filtro de texto:
822
-
823
- ```ts
824
- findByNameContainsInsensitive // { name: { contains: value, mode: 'insensitive' } }
825
- findByEmailStartsWithInsensitive // { email: { startsWith: value, mode: 'insensitive' } }
826
- findByNameInsensitive // { name: { equals: value, mode: 'insensitive' } }
827
- ```
828
-
829
- `Between` e `NotBetween` recebem uma **tupla `[valorMinimo, valorMaximo]`**:
830
-
831
- ```ts
832
- methods: {
833
- findManyByAgeBetween: { map: true },
834
- findManyBySalaryNotBetween: { map: true },
835
- findManyByCreatedAtBetween: { map: true },
836
- }
837
-
838
- await userRepository.findManyByAgeBetween([18, 65]);
839
- await userRepository.findManyBySalaryNotBetween([1000, 5000]);
840
- await userRepository.findManyByCreatedAtBetween([new Date("2024-01-01"), new Date("2024-12-31")]);
457
+ Aplicados como sufixos ao nome do campo dentro do método (mesma ideia da v1, com um sufixo renomeado):
458
+
459
+ | Sufixo | Significado | Argumento |
460
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
461
+ | _(sem sufixo)_ | igualdade (`=`) | sim |
462
+ | `Not` | negação | sim |
463
+ | `In` | está em | sim (array) |
464
+ | `NotIn` | não está em | sim (array) |
465
+ | `Contains` | contém substring | sim |
466
+ | `NotContains` | não contém substring | sim |
467
+ | `StartsWith` | começa com | sim |
468
+ | `NotStartsWith` | não começa com | sim |
469
+ | `EndsWith` | termina com | sim |
470
+ | `NotEndsWith` | não termina com | sim |
471
+ | `GreaterThan` | `>` | sim |
472
+ | `GreaterThanEqual` | `>=` | sim |
473
+ | `LessThan` | `<` | sim |
474
+ | `LessThanEqual` | `<=` | sim |
475
+ | `Between` | intervalo inclusivo | sim (tupla `[min, max]`) |
476
+ | `NotBetween` | fora de um intervalo inclusivo | sim (tupla `[min, max]`) |
477
+ | `IsNull` | campo é `null` | não |
478
+ | `IsNotNull` | campo não é `null` | não |
479
+ | `IsTrue` | campo é `true` | não |
480
+ | `IsFalse` | campo é `false` | não |
481
+ | `IgnoreCase` | combinador case-insensitive para filtros de texto | não _(renomeado do `Insensitive` da v1)_ |
482
+ | `Optional` | **explícita** o argumento do campo como opcional — ele já é opcional por padrão, então esse sufixo é facultativo e serve apenas para deixar isso explícito | — |
483
+
484
+ ```typescript
485
+ @DynamicMethod()
486
+ declare findByNameContainsIgnoreCase: (name: string) => Promise<User[]>;
487
+
488
+ @DynamicMethod()
489
+ declare findByAgeBetween: (age: [number, number]) => Promise<User[]>;
841
490
  ```
842
491
 
843
- O sufixo `Optional` pode ser adicionado a qualquer campo para tornar o argumento opcional:
844
-
845
- ```ts
846
- findByNameOptionalAndEmail // name é opcional, email é obrigatório
847
- ```
848
-
849
- ---
850
-
851
492
  ### Operadores lógicos
852
493
 
853
- | Operador | Uso no nome | Exemplo |
854
- | --------- | -------------------------------- | ---------------------------------- |
855
- | `And` | entre dois campos | `findOneByIdAndEmail` |
856
- | `Or` | entre dois campos | `findByNameOrEmail` |
857
- | `AND` | separa um bloco final de `AND` | `findByEmailOrNameANDActiveStatus` |
494
+ | Operador | Uso no nome | Exemplo |
495
+ | -------- | ------------------------------ | --------------------------------------------------- |
496
+ | `And` | entre dois campos | `findOneByIdAndEmail` |
497
+ | `Or` | entre dois campos | `findByNameOrEmail` |
498
+ | `AND` | separa um bloco final em `AND` | `findByEmailOrNameANDActiveStatusAndAgeGreaterThan` |
858
499
 
859
- `AND` (em maiúsculas) tem uma regra específica:
500
+ Regras do `AND` (em capslock), iguais às da v1: só é permitido **um** `AND` por nome de método; todo campo conectado por `And` depois dele é aninhado dentro de `AND: []`; `Or` não pode aparecer depois de um `AND`.
860
501
 
861
- - Só pode existir **um** `AND` por método.
862
- - Todos os campos depois do `AND` são injetados dentro de `AND: []`.
863
- - Depois de um `AND`, não pode haver um `Or`.
502
+ ### Filtros de relação
864
503
 
865
- Exemplo:
504
+ Filtram por campos de entidades relacionadas. Internamente, mapeiam para os operadores `_some`/`_every`/`_none`/`_with`/`_without` de `VSRepoWhere` (veja [`select` e `relations`](#select-e-relations) para o equivalente de eager loading).
866
505
 
867
- ```ts
868
- methods: {
869
- findOneByIdAndEmail: { map: true },
870
- findByNameOrEmail: { map: true },
871
- findFirstByIdOrEmailAndName: { map: true },
872
- findByEmailOrNameANDActiveStatusAndAgeGreaterThan: { map: true }
873
- }
506
+ | Sufixo | Significado | Restrição |
507
+ | -------------- | ------------------------------------------------- | ----------------------------------------------------------------------- |
508
+ | `Some` | pelo menos um registro relacionado corresponde | apenas relações to-many |
509
+ | `SomeField` | filtra dentro dos registros relacionados | apenas relações to-many |
510
+ | `Every` | todo registro relacionado corresponde | apenas relações to-many (precisa de `Field` para ser um filtro efetivo) |
511
+ | `EveryField` | filtra dentro dos registros relacionados | apenas relações to-many |
512
+ | `None` | nenhum registro relacionado corresponde | apenas relações to-many |
513
+ | `NoneField` | filtra dentro dos registros relacionados | apenas relações to-many |
514
+ | `With` | o registro relacionado existe | apenas relações to-one |
515
+ | `WithField` | filtra um campo dentro do registro relacionado | apenas relações to-one |
516
+ | `Without` | o registro relacionado não existe | apenas relações to-one |
517
+ | `WithoutField` | filtro negado em um campo do registro relacionado | apenas relações to-one |
874
518
 
875
- await userRepository.findOneByIdAndEmail(1, "john@email.com");
876
- await userRepository.findByNameOrEmail("John", "john@email.com");
877
- await userRepository.findFirstByIdOrEmailAndName(1, "john@email.com", "John");
878
- await userRepository.findByEmailOrNameANDActiveStatusAndAgeGreaterThan("john@email.com", "John", true, 17)
879
- ```
880
-
881
- Gera (`findOneByIdAndEmail`):
519
+ ```typescript
520
+ @DynamicMethod()
521
+ declare findByAddressWithCityStartsWithIgnoreCase: (city: string) => Promise<User[]>;
882
522
 
883
- ```ts
884
- {
885
- id: 1,
886
- email: "john@email.com"
887
- }
523
+ @DynamicMethod()
524
+ declare findByProductsSome: () => Promise<User[]>;
888
525
  ```
889
526
 
890
- Gera (`findByNameOrEmail`):
527
+ ### Ordenação, paginação e distinct
891
528
 
892
- ```ts
893
- {
894
- OR: [
895
- { name: "John" },
896
- { email: "john@email.com" }
897
- ]
898
- }
899
- ```
529
+ | Sufixo | Efeito |
530
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
531
+ | `Paginated` | Injeta um argumento `pagination` (`{ limit?, offset? }`) como **penúltimo** parâmetro (antes do `MethodOptions` opcional). |
532
+ | `Ordered` | Injeta um argumento `order: Ordering<T>` como **penúltimo** parâmetro (antes do `MethodOptions` opcional). |
533
+ | `OrderedAndPaginated` | Injeta `order` como antepenúltimo, depois `pagination` como penúltimo — ambos antes do `MethodOptions`. |
534
+ | `PaginatedAndOrdered` | Injeta `pagination` como antepenúltimo, depois `order` como penúltimo — ambos antes do `MethodOptions`. |
535
+ | `OrderBy<Campo>Asc` / `OrderBy<Campo>Desc` | **Novo na v2.** Embute uma ordenação fixa diretamente no nome do método — encadeie campos com `And` (ex.: `OrderByCreatedAtAscAndNameDesc`). Não precisa de argumento `order`. |
536
+ | `Distinct<Campo>And<Campo>...` | Embute campos `distinct` fixos diretamente no nome do método (só válido em métodos da família `findBy`/`findWhere`). |
537
+ | `IgnoreConflicts` | No `createMany`/`createManyReturning`, ignora registros que violariam uma constraint única, em vez de lançar erro. _(Renomeado do `SkipDuplicates` da v1.)_ |
900
538
 
901
- Gera (`findFirstByIdOrEmailAndName`):
539
+ > ⚠️ **Ordem dos parâmetros:** `pagination` e `order` sempre vêm **antes** do último argumento opcional `MethodOptions<T>`. Quando `order` e `pagination` estão presentes juntos, a ordem relativa entre eles segue o nome do sufixo (`OrderedAndPaginated` → order, pagination; `PaginatedAndOrdered` → pagination, order).
902
540
 
903
- ```ts
904
- {
905
- OR: [
906
- { id: 1 },
907
- {
908
- email: "john@email.com",
909
- name: "John"
910
- }
911
- ]
912
- }
913
- ```
541
+ ```typescript
542
+ // Paginated: pagination é o penúltimo parâmetro (antes do MethodOptions)
543
+ @DynamicMethod()
544
+ declare findByActiveOrderByCreatedAtDescPaginated:
545
+ (active: boolean, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
914
546
 
915
- Gera (`findByEmailOrNameANDActiveStatusAndAgeGreaterThan`):
547
+ // OrderedAndPaginated: order, depois pagination, depois MethodOptions
548
+ @DynamicMethod()
549
+ declare findByNameContainsIgnoreCaseOrderedAndPaginated:
550
+ (name: string, order: Ordering<User>, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
916
551
 
917
- ```ts
918
- {
919
- OR: [
920
- { email: "john@email.com" },
921
- { name: "John" }
922
- ],
923
- AND: [
924
- { activeStatus: true },
925
- { age: { gt: 17 } }
926
- ]
927
- }
928
- ```
552
+ @DynamicMethod()
553
+ declare createManyIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<{ count: number }>;
929
554
 
930
- ---
555
+ // createManyReturning: mesmo comportamento do createMany, mas retorna os registros criados
556
+ @DynamicMethod()
557
+ declare createManyReturningIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<User[]>;
931
558
 
932
- ### Filtros de relação
933
-
934
- Permitem filtrar por campos de modelos relacionados.
559
+ // findOne sem filtro (equivalente ao findOneOrThrow sem filtro, mas retorna null em vez de lançar)
560
+ @DynamicMethod()
561
+ declare findOne: (options?: MethodOptions<User>) => Promise<User | null>;
562
+ ```
935
563
 
936
- > [!IMPORTANT]
564
+ > ⚠️ **Precedência entre `Distinct` e `OrderBy`:** quando os dois são usados no mesmo nome de método, **`Distinct` deve vir antes de `OrderBy`**:
937
565
  >
938
- > - **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 para `setupVSRepo` deve incluir as relações estruturadas (ex.: usando o `UserGetPayload<{ include: { profile: true, posts: true } }>` do Prisma).
939
- > - **Compatibilidade de sufixos**:
940
- > - Os sufixos `Some`, `Every` e `None` só funcionam em relações **to-many** (`many-to-many` e `one-to-many`).
941
- > - Os sufixos `With` e `Without` só funcionam em relações **to-one** (`one-to-one` e `many-to-one`).
942
-
943
- | Sufixo de relação | Operador Prisma | Observação |
944
- | ---------------------- | ---------------- | ------------------------------------------------- |
945
- | `Some` | `some: {}` | A relação tem *algum* registro |
946
- | `SomeField` | `some.field` | Filtra dentro dos registros da relação |
947
- | `EveryField` | `every.field` | Filtra dentro dos registros da relação |
948
- | `None` | `none: {}` | A relação não tem *nenhum* registro |
949
- | `NoneField` | `none.field` | Filtra dentro dos registros da relação |
950
- | `With` | `is: {}` | A relação existe (não é nula) |
951
- | `WithField` | `is.field` | Filtra um campo dentro da relação |
952
- | `Without` | `isNot: {}` | A relação não existe (é nula) |
953
- | `WithoutField` | `isNot.field` | Filtra um campo dentro da relação com negação |
954
-
955
- Considerando `user` com uma relação to-one `profile` e uma relação to-many `posts`:
956
-
957
- ```ts
958
- methods: {
959
- // to-many (posts)
960
- findByPostsSome: { map: true }, // tem pelo menos um post
961
- findByPostsSomeTitle: { map: true }, // tem pelo menos um post com aquele título
962
- findByPostsEveryPublishedIsTrue:{ map: true }, // todos os posts estão publicados
963
- findByPostsNone: { map: true }, // não tem nenhum post
964
- findByPostsNoneTitle: { map: true }, // nenhum post tem aquele título
965
-
966
- // to-one (profile)
967
- findByProfileWith: { map: true }, // tem um profile (não é nulo)
968
- findByProfileWithBio: { map: true }, // tem um profile com aquela bio
969
- findByProfileWithout: { map: true }, // não tem profile (é nulo)
970
- findByProfileWithoutBio: { map: true }, // tem um profile, mas com uma bio diferente da informada
971
- }
566
+ > ```typescript
567
+ > @DynamicMethod()
568
+ > declare findByActiveDistinctNameOrderByCreatedAtDesc:
569
+ > (active: boolean) => Promise<User[]>;
570
+ > ```
571
+ >
572
+ > Colocar `OrderBy` antes de `Distinct` (ex.: `findByActiveOrderByCreatedAtDescDistinctName`) não é um padrão válido e não será interpretado como esperado.
972
573
 
973
- await userRepository.findByPostsSome();
974
- await userRepository.findByPostsSomeTitle("My first post");
975
- await userRepository.findByPostsEveryPublishedIsTrue();
976
- await userRepository.findByPostsNone();
977
- await userRepository.findByPostsNoneTitle("Draft");
574
+ ### Options do decorador
978
575
 
979
- await userRepository.findByProfileWith();
980
- await userRepository.findByProfileWithBio("Hello, world!");
981
- await userRepository.findByProfileWithout();
982
- await userRepository.findByProfileWithoutBio("Old bio");
983
- ```
576
+ `@DynamicMethod<T>(options?)` aceita:
984
577
 
985
- Gera (`findByPostsSomeTitle`):
578
+ | Option | Tipo | Descrição |
579
+ | ---------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
580
+ | `proxyTo` | `string` | Redireciona a lógica do método para outro padrão de método dinâmico válido — útil para nomes que não seguem a convenção de nomenclatura. |
581
+ | `injectOrdering` | `Ordering<T>` | Ordenação fixa injetada automaticamente, sobrescrevendo o `defaultOrdering` do repository. |
986
582
 
987
- ```ts
988
- {
989
- posts: {
990
- some: { title: "My first post" }
991
- }
992
- }
583
+ ```typescript
584
+ @DynamicMethod<User>({ injectOrdering: { createdAt: "desc" } })
585
+ declare findByStatus: (status: string) => Promise<User[]>;
993
586
  ```
994
587
 
995
- Gera (`findByPostsEveryPublishedIsTrue`):
996
-
997
- ```ts
998
- {
999
- posts: {
1000
- every: { published: true }
1001
- }
1002
- }
1003
- ```
588
+ ---
1004
589
 
1005
- Gera (`findByProfileWithBio`):
590
+ ## Query methods (SQL raw)
1006
591
 
1007
- ```ts
1008
- {
1009
- profile: {
1010
- is: { bio: "Hello, world!" }
1011
- }
1012
- }
1013
- ```
592
+ `@QueryMethod` ignora totalmente o engine de parsing por nome e executa uma instrução SQL raw através do método `query()` do adapter. Use placeholders `$1`, `$2`, ... — nunca interpole valores diretamente na string SQL.
1014
593
 
1015
- Gera (`findByProfileWithout`):
594
+ ```typescript
595
+ class UserRepository extends VSRepository<User, string> {
596
+ @QueryMethod('SELECT * FROM "user" WHERE email = $1')
597
+ declare findByEmailRaw: (arg: QueryMethodArg<[email: string]>) => Promise<User[]>;
1016
598
 
1017
- ```ts
1018
- {
1019
- profile: {
1020
- isNot: {}
1021
- }
599
+ @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
600
+ declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
1022
601
  }
1023
602
  ```
1024
603
 
1025
- > `Some`, `None`, `With` e `Without` (sem campo) não recebem argumento — toda a relação é testada quanto à existência de registros (`some`/`none`) ou quanto a ser `null`/não `null` (`is`/`isNot`). As variantes `SomeField`, `EveryField`, `NoneField`, `WithField` e `WithoutField` recebem como argumento o valor do campo filtrado.
1026
-
1027
- ---
1028
-
1029
- ### Sufixos de paginação e ordenação
604
+ | Option | Tipo | Padrão | Descrição |
605
+ | ----------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
606
+ | `modifying` | `boolean` | `false` | Quando `true`, executa como `INSERT`/`UPDATE`/`DELETE` e o método resolve para o número de linhas afetadas. Quando `false`, executa como query de leitura e resolve para o tipo de retorno declarado. |
1030
607
 
1031
- Aplicados no **final** do nome do método, injetam automaticamente os argumentos de paginação e ordenação.
608
+ Query methods aceitam `{ args, db? }` na chamada — `db` permite que participem de um bloco `transaction()`, assim como os métodos base e dinâmicos.
1032
609
 
1033
- | Sufixo | Argumentos adicionais |
1034
- | ------------------------ | ---------------------------- |
1035
- | `Paginated` | `(pagination)` |
1036
- | `Ordered` | `(order)` |
1037
- | `OrderedAndPaginated` | `(order, pagination)` |
1038
- | `PaginatedAndOrdered` | `(pagination, order)` |
610
+ ### Queries raw pontuais com `query()`
1039
611
 
1040
- Para `createMany` e `createManyAndReturn`, o sufixo `SkipDuplicates` está disponível:
612
+ Para SQL raw pontual que não justifica declarar um `@QueryMethod` na classe do repository, chame `query()` diretamente — ele está disponível em toda instância de `VSRepository` e passa pela mesma implementação de `query()` do adapter por baixo dos panos:
1041
613
 
1042
- | Sufixo | Efeito |
1043
- | ---------------------| ---------------------------------------------- |
1044
- | `SkipDuplicates` | Ignora registros duplicados durante a inserção |
1045
-
1046
- ---
1047
-
1048
- ### Distinct
1049
-
1050
- O sufixo `Distinct` permite obter apenas registros únicos com base em um ou mais campos, equivalente à opção `distinct` do Prisma.
1051
-
1052
- Para usá-lo, coloque `Distinct` no nome do método (depois dos filtros de campo, se houver), seguido dos campos desejados separados por `And`. A primeira letra de cada campo deve ser maiúscula, assim como nos filtros de campo comuns.
1053
-
1054
- ```ts
1055
- methods: {
1056
- // Retorna usuários únicos combinando "age" e "role" (sem filtro de campo)
1057
- findManyDistinctAgeAndRole: { map: true },
1058
-
1059
- // Distinct combinado com o sufixo Paginated
1060
- findManyDistinctNamePaginated: { map: true },
1061
-
1062
- // Distinct combinado com um filtro de campo (name) — filtra por name e depois aplica distinct em role
1063
- findManyByNameDistinctRole: { map: true },
1064
- },
614
+ ```typescript
615
+ query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
1065
616
  ```
1066
617
 
1067
- ```ts
1068
- // Sem argumentos: os campos distinct já estão fixados no nome do método
1069
- await userRepository.findManyDistinctAgeAndRole();
1070
-
1071
- // O argumento de paginação continua funcionando normalmente
1072
- await userRepository.findManyDistinctNamePaginated({ take: 10, skip: 0 });
618
+ ```typescript
619
+ const users = await userRepository.query<User[]>('SELECT * FROM "user" WHERE email = $1', {
620
+ args: ["maria@email.com"],
621
+ });
1073
622
 
1074
- // O filtro do campo "name" continua sendo passado normalmente como argumento
1075
- await userRepository.findManyByNameDistinctRole("John");
623
+ const linhasAfetadas = await userRepository.query<number>(
624
+ 'UPDATE "user" SET active = true WHERE id = $1',
625
+ { args: ["123"], modifying: true },
626
+ );
1076
627
  ```
1077
628
 
1078
- > Os campos especificados depois de `Distinct` são resolvidos a partir do nome do método em tempo de build — eles **não** se tornam argumentos em tempo de execução, diferente dos filtros de campo comuns.
629
+ | Option | Tipo | Padrão | Descrição |
630
+ | ----------- | --------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- |
631
+ | `args` | `any[]` | `undefined` | Parâmetros posicionais injetados nos placeholders `$1`, `$2`, ... Nunca interpole valores diretamente na string SQL. |
632
+ | `db` | `any` | Client padrão do repository | Client ou transação do banco em que essa query deve rodar. |
633
+ | `modifying` | `boolean` | `false` | Quando `true`, trata a instrução como `INSERT`/`UPDATE`/`DELETE`. |
1079
634
 
1080
- `Distinct` está disponível nos prefixos que leem múltiplos ou um único registro: `findMany`, `findManyBy`, `findFirst`, `findFirstBy`, `findFirstOrThrow`, `findFirstOrThrowBy`, `findBy`, `findOneBy`, `findWhere`, `findOneWhere`, `findListWhere`, `existsBy` e `existsWhere`.
635
+ Assim como os métodos base, dinâmicos e query, `query()` aceita `db` em `options` para participar de um bloco `transaction()`.
1081
636
 
1082
637
  ---
1083
638
 
1084
- ### Configuração de método
1085
-
1086
- Cada entrada em `methods` aceita as seguintes opções:
639
+ ## Transações
1087
640
 
1088
- | Opção | Tipo | Padrão | Descrição |
1089
- | --------------------- | ---------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
1090
- | `map` | `boolean` | — | **Obrigatório.** Define se o método será exposto no repositório. |
1091
- | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combina com `requiredWhere`. `overwrite` ignora `requiredWhere`. |
1092
- | `selectModel` | `keyof SelectModels \| false` | — | Sobrescreve o `defaultSelectModel` para este método. |
1093
- | `fbMode` | `'one'` \| `'list'` | `'list'` | (**Deprecated. Use `findOneBy`**) Apenas para `findBy`. `'one'` retorna `T \| null`; `'list'` retorna `T[]`. |
1094
- | `proxyTo` | `Padrão de método válido` | — | Delega a lógica para outro padrão de método válido. |
1095
- | `pushWhere` | `WhereModel<M>` | — | `where` extra adicionado à query além do `requiredWhere`. |
1096
- | `injectOrdering` | `OrderingModel<M>` | — | Ordenação fixa injetada automaticamente na query. |
1097
- | `injectPagination` | `PaginationModel<M>` | — | Paginação fixa injetada automaticamente na query. |
1098
- | `query` | `{ value: string; modifying?: boolean }` | — | Transforma o método em um **Query Method** (SQL bruto). Ignora todas as outras opções acima — veja [Query Methods](#query-methods). |
641
+ Todos os métodos (base, dinâmicos e de query) aceitam `options.db` para participar de uma transação compartilhada:
1099
642
 
1100
- ---
643
+ ```typescript
644
+ await userRepository.transaction(async tx => {
645
+ const usuario = await userRepository.save(
646
+ { name: "Maria", email: "maria@email.com" },
647
+ { db: tx },
648
+ );
1101
649
 
1102
- ### Aggregate e GroupBy
1103
-
1104
- ```ts
1105
- const userRepository = setupVSRepo<User, "user">()(({
1106
- tableName: "user",
1107
- pkName: "id",
1108
- methods: {
1109
- aggregate: { map: true },
1110
- groupBy: { map: true },
1111
- },
1112
- }).build(prisma);
650
+ await userLogsRepository.save(
651
+ { action: "Usuário criado", data: { userId: usuario.id } },
652
+ { db: tx },
653
+ );
654
+ });
1113
655
  ```
1114
656
 
1115
- > [!NOTE]
1116
- > Esses métodos devem ter exatamente esses nomes (`aggregate` e `groupBy`).
1117
- > Diferente dos demais métodos dinâmicos, eles recebem argumentos nativos do Prisma e **ignoram** as configurações `selectModels`, `pushWhere` e `requiredWhere`.
1118
-
1119
- ---
1120
-
1121
- ### Query Methods
1122
-
1123
- Query Methods permitem que um método execute **SQL bruto** diretamente, contornando totalmente o parser de nomes dos métodos dinâmicos. São úteis para consultas complexas (joins pesados, CTEs, funções específicas do banco) que não são práticas de expressar com os prefixos/sufixos padrão.
657
+ Repositories diferentes podem compartilhar a mesma transação, desde que seus adapters apontem para a mesma conexão do ORM por trás deles.
1124
658
 
1125
- Internamente, o VSRepository executa a SQL através do Prisma usando `$queryRawUnsafe` (para leitura) ou `$executeRawUnsafe` (para escrita), e os valores do array `args` são passados como **parâmetros posicionais** (`$1`, `$2`, ...) — a mesma técnica de *prepared statements* usada pelo próprio Prisma. Isso significa que os valores nunca são concatenados na string SQL, o que é o que efetivamente previne SQL Injection.
659
+ `transaction()` aceita um `VSRepoTransactionOptions` opcional como segundo argumento:
1126
660
 
1127
- > [!WARNING]
1128
- > `$1`, `$2`, ... na sua SQL devem representar sempre **valores** (parâmetros de dados), nunca nomes de colunas, tabelas ou trechos de SQL dinâmicos. Nomes de identificadores (colunas/tabelas) não podem ser passados como parâmetro posicional — se seu método precisar variar isso, monte o SQL a partir de um conjunto fixo e conhecido de opções no seu próprio código, nunca a partir de entrada não confiável.
1129
-
1130
- ```ts
1131
- const userRepository = setupVSRepo<User, "user">()({
1132
- tableName: "user",
1133
- pkName: "id",
1134
- methods: {
1135
- // Query method de leitura (não-modifying)
1136
- findActiveUsersRaw: {
1137
- map: true,
1138
- query: {
1139
- value: 'SELECT * FROM "user" WHERE active = $1',
1140
- },
1141
- },
661
+ ```typescript
662
+ import { TransactionIsolationLevel } from "vsrepo";
1142
663
 
1143
- // Query method de escrita (modifying: true)
1144
- deactivateUsersOlderThanRaw: {
1145
- map: true,
1146
- query: {
1147
- value: 'UPDATE "user" SET active = false WHERE "createdAt" < $1',
1148
- modifying: true,
1149
- },
664
+ await userRepository.transaction(
665
+ async tx => {
666
+ await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
1150
667
  },
1151
- },
1152
- }).build(prisma);
668
+ { isolationLevel: TransactionIsolationLevel.SERIALIZABLE, timeoutMs: 5000 },
669
+ );
1153
670
  ```
1154
671
 
1155
- **Chamando um Query Method:**
672
+ | Option | Type | Descrição |
673
+ | ----------------- | -------------------------- | -------------------------------------------------------------------------------- |
674
+ | `isolationLevel` | `TransactionIsolationLevel` | Nível de isolamento usado na transação. O padrão é o default do ORM por trás dela. |
675
+ | `timeoutMs` | `number` | Tempo máximo (em ms) que a transação pode rodar antes de ser abortada. |
1156
676
 
1157
- Todo Query Method recebe um único argumento no formato `{ args: [...], db? }`:
1158
-
1159
- ```ts
1160
- // Não-modifying: retorna 'any' por padrão, mas aceita um generic para
1161
- // inferir/afirmar o tipo de retorno na própria chamada
1162
- const usuariosAtivos = await userRepository.findActiveUsersRaw<User[]>({
1163
- args: [true],
1164
- });
1165
-
1166
- // Modifying: sempre retorna 'number' (quantidade de linhas afetadas)
1167
- const afetados = await userRepository.deactivateUsersOlderThanRaw({
1168
- args: [new Date("2024-01-01")],
1169
- });
1170
-
1171
- // Participando de uma transação, via 'db'
1172
- await userRepository.prisma.$transaction(async (tx) => {
1173
- await userRepository.deactivateUsersOlderThanRaw({
1174
- args: [new Date("2024-01-01")],
1175
- db: tx,
1176
- });
1177
- });
1178
- ```
1179
-
1180
- | Opção | Tipo | Padrão | Descrição |
1181
- | ------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1182
- | `value` | `string` | — | **Obrigatório.** SQL bruto a ser executado. Use `$1`, `$2`, ... para os placeholders dos valores em `args`. |
1183
- | `modifying` | `boolean` | `false` | Quando `true`, executa via `$executeRawUnsafe` e o método sempre resolve para `number`. Quando `false`, executa via `$queryRawUnsafe` e o método resolve para `TReturn` (`any` por padrão, inferível via generic na chamada). |
1184
-
1185
- > [!NOTE]
1186
- > Diferente dos demais métodos dinâmicos, Query Methods **ignoram completamente** `selectModels`, `requiredWhere`, `pushWhere`, `whereType`, `injectOrdering`, `injectPagination` e `proxyTo` — nada disso se aplica, já que não há parsing de nome nem montagem de `where`/`select` pelo VSRepository. Nomes de métodos livres (fora dos padrões de `findBy`, `updateBy`, etc.) também **não** exigem `proxyTo`.
1187
-
1188
- A mesma funcionalidade está disponível na abordagem baseada em classes através do decorator `@QueryMethod` — veja o [README-DynamicRepo.pt-BR.md](./README-DynamicRepo.pt-BR.md#o-decorator-querymethod).
677
+ `TransactionIsolationLevel` espelha os níveis de isolamento SQL padrão: `READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`. O suporte a um determinado nível depende do adapter/ORM e do banco de dados por trás dele.
1189
678
 
1190
679
  ---
1191
680
 
1192
- ## Relações no save
1193
-
1194
- Configure as relações para que `save` e `patch` as gerenciem automaticamente (`saveList` e `patchList` também gerenciam relações automaticamente).
1195
-
1196
- ```ts
1197
- import type { Prisma } from "../../generated/prisma/client";
681
+ ## Tipos utilitários
1198
682
 
1199
- type User = Prisma.userGetPayload<{
1200
- include: { profile: true; posts: true };
1201
- }>;
683
+ Além dos tipos que descrevem o formato da entidade já vistos acima (`VSRepoSelect`, `VSRepoRelations`, `VSRepoWhere`), o VSRepository exporta um conjunto de tipos utilitários. Eles aparecem ao longo de várias seções anteriores, mas aqui está uma referência consolidada. Todos fazem parte da API pública e podem ser importados diretamente:
1202
684
 
1203
- const userRepository = setupVSRepo<User, "user">()(({
1204
- tableName: "user",
1205
- pkName: "id",
685
+ ```typescript
686
+ import type {
687
+ MethodOptions,
688
+ RestrictMethodOptions,
689
+ Pagination,
690
+ Ordering,
691
+ OrderByField,
692
+ SortDirection,
693
+ SeeMode,
694
+ DeepPartial,
695
+ CountResult,
696
+ QueryMethodArg,
697
+ KeysOfType,
698
+ NumericKeys,
699
+ NumericLike,
700
+ DecimalLike,
701
+ Primitive,
702
+ VSRepoWhere,
703
+ VSRepoOrmTypes,
704
+ VSRepoTransactionOptions,
705
+ TransactionIsolationLevel,
706
+ } from "vsrepo";
707
+ ```
708
+
709
+ | Tipo | Descrição | Usado por |
710
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
711
+ | `MethodOptions<T, K>` | Options aceitas como último argumento pela maioria dos métodos base e dinâmicos: `select`, `relations`, `see`, `db`. | [Métodos base](#métodos-base), [Métodos Dinâmicos](#métodos-dinâmicos). |
712
+ | `RestrictMethodOptions<T, K>` | `MethodOptions<T, K>` restrito, expondo só `see`/`db` — usado pelos métodos que não retornam/moldam uma `Entity` (`total`, `has`, `sum`, `average`, `min`, `max`, `removeList`, `softRemoveList`, `restoreList`). | [Métodos base](#métodos-base), [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação). |
713
+ | `Pagination` | `{ limit?, offset? }` aceito por `getAll` e pelos métodos dinâmicos com `Paginated`. | [Métodos base](#métodos-base), [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct). |
714
+ | `Ordering<T>` / `OrderByField<T>` / `SortDirection` | Formato de ordenação aceito por `getAll`, `defaultOrdering` e `injectOrdering`, e pelos métodos dinâmicos com `Ordered`. Pode ser um único objeto ou um array encadeado; objetos aninhados ordenam relações to-one. | [Options do construtor](#options-do-construtor), [Options do decorador](#options-do-decorador), [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct). |
715
+ | `SeeMode` | `"active" \| "removed" \| "all"` — controla a visibilidade de registros com soft-delete. | [Soft-delete](#soft-delete). |
716
+ | `DeepPartial<T>` | Torna todas as propriedades de `T` opcionais recursivamente, incluindo objetos aninhados e elementos de array. | `save`, `saveList`, `patch`, `merge`, e todo método de escrita do `VSRepoAdapter`. |
717
+ | `CountResult` | `{ count: number }` — o formato retornado por operações em lote. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
718
+ | `QueryMethodArg<T>` | `{ args?: T, db? }` — parâmetros posicionais do SQL (`$1`, `$2`, ...) e cliente de transação para o `@QueryMethod`. | [Query methods (SQL raw)](#query-methods-sql-raw). |
719
+ | `KeysOfType<T, K>` | Extrai as chaves de `T` cujo tipo de valor é atribuível a `K`. | Restringe `pkName`, em [Options do construtor](#options-do-construtor), aos campos da entidade compatíveis com o tipo de chave primária configurado. |
720
+ | `NumericKeys<T>` | Extrai as chaves de `T` cujo tipo de valor (ignorando `null`/`undefined`) é atribuível a `NumericLike`. Campos numéricos nullable (`number \| null`) são incluídos. | Restringe `field` em [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação) (`increment`, `sum`, etc). |
721
+ | `NumericLike` | `number \| bigint \| DecimalLike`. | [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação). |
722
+ | `DecimalLike` | Formato estrutural de um valor decimal de precisão arbitrária (`{ toNumber(): number; decimalPlaces(): number }`), compatível com o `Prisma.Decimal` do Prisma sem precisar importá-lo diretamente. | [Quais campos são elegíveis](#quais-campos-são-elegíveis). |
723
+ | `Primitive` | União de tipos escalares (`string \| number \| boolean \| bigint \| symbol \| undefined \| null \| Date`) tratados como valores-folha — e não relações — ao percorrer o formato de uma entidade. | Usado por `Ordering<T>` para distinguir campos escalares de campos de relação. |
724
+ | `VSRepoWhere<T>` | Tipo de filtro agnóstico de ORM aceito pelos métodos dinâmicos `*Where` (ex.: `findWhere`, `findOneWhere`, `updateWhere`). Suporta filtros de campo, operadores lógicos (`AND`/`OR`/`NOT`) e filtros de relação. | [Prefixos `findWhere`, `findOneWhere` e demais `*Where`](#prefixos-disponíveis). |
725
+ | `VSRepoOrmTypes` | `{ dbClient; dbTransaction }` — descreve os tipos de client/transaction do seu ORM. Passado como terceiro generic de `VSRepository<Entity, PKType, OrmTypes>` para tipar `getDbClient()`, `transaction()` e a option `db` em vez de `any`. | [Criando um repository](#criando-um-repository). |
726
+ | `VSRepoTransactionOptions` | `{ isolationLevel?, timeoutMs? }` — options aceitas como segundo argumento de `transaction()`. | [Transações](#transações). |
727
+ | `TransactionIsolationLevel` | Enum dos níveis de isolamento SQL padrão (`READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`) aceitos por `VSRepoTransactionOptions.isolationLevel`. | [Transações](#transações). |
728
+
729
+ ### `DeepPartial<T>`
730
+
731
+ Torna todas as propriedades opcionais recursivamente, percorrendo objetos aninhados e elementos de array — diferente do `Partial<T>` nativo do TypeScript, que só torna o nível superior opcional:
732
+
733
+ ```typescript
734
+ type User = { id: string; name: string; address: { city: string; zip: string } };
735
+
736
+ const patch: DeepPartial<User> = {
737
+ address: { city: "São Paulo" }, // zip pode ser omitido; city mantém seu tipo
738
+ };
1206
739
 
1207
- relations: {
1208
- profile: {
1209
- pk: "id",
1210
- mode: "oto",
1211
- restriction: "set",
1212
- },
1213
- posts: {
1214
- pk: "id",
1215
- mode: "otm",
1216
- restriction: "add",
1217
- },
1218
- },
1219
- }).build(prisma);
740
+ await userRepository.patch(id, patch);
1220
741
  ```
1221
742
 
1222
- **Modos de relação:**
743
+ ### `KeysOfType<T, K>`
1223
744
 
1224
- | Modo | Relação |
1225
- | ----- | ------------------ |
1226
- | `oto` | um-para-um |
1227
- | `otm` | um-para-muitos |
1228
- | `mto` | muitos-para-um |
1229
- | `mtm` | muitos-para-muitos |
745
+ Filtra um tipo de objeto para as chaves cujo valor corresponde a um tipo dado — é isso que permite que `pkName` aceite apenas campos da entidade que sejam de fato atribuíveis ao tipo de chave primária do repository:
1230
746
 
1231
- **Restrições:**
747
+ ```typescript
748
+ type User = { id: string; age: number; name: string };
749
+ type StringKeys = KeysOfType<User, string>; // "id" | "name"
750
+ ```
1232
751
 
1233
- | Restrição | Comportamento na atualização |
1234
- | ----------- | ----------------------------------------------------------- |
1235
- | `set` | Substitui totalmente (remove os que não foram enviados) |
1236
- | `add` | Adiciona/atualiza sem remover os existentes |
752
+ ### `Ordering<T>`
1237
753
 
1238
- > [!WARNING]
1239
- > **`set` significa coisas diferentes dependendo do `mode` da relação — e isso pode causar perda de dados se você não tomar cuidado.**
1240
- >
1241
- > Em relações onde o registro relacionado **pertence** ao registro pai (`oto` e `otm`), "remover os que não foram enviados" significa **apagar o registro do banco de dados** (`delete`/`deleteMany`). Em relações onde o registro relacionado é **independente** (`mto` e `mtm`), "remover" significa apenas **desvincular** (`disconnect`/`set: []`) — o registro relacionado continua existindo no banco, apenas deixa de apontar para o pai (ou de estar na tabela de junção).
1242
- >
1243
- > | Modo | `restriction: "set"` quando um item é omitido | O item continua existindo no banco de dados? |
1244
- > | ----- | ------------------------------------------------ | ----------------------------------------------------- |
1245
- > | `oto` | Passar `null` no campo → **apaga** o registro relacionado (`delete: true`) | Não |
1246
- > | `otm` | Itens fora da lista enviada → **apagados** (`deleteMany` com `notIn`) | Não |
1247
- > | `mto` | Passar `null` no campo (com `nullable: true`) → **desvincula** (`disconnect: true`) | Sim |
1248
- > | `mtm` | Itens fora da lista enviada → **desvinculados** da tabela de junção (`set: []`) | Sim |
1249
- >
1250
- > Exemplo prático: se `posts` for `otm` com `restriction: "set"`, um `save`/`patch` que envia o usuário com apenas 2 dos 5 posts existentes vai **apagar os outros 3 posts do banco de dados**, não apenas desvinculá-los do usuário. Se o comportamento esperado for apenas desvincular sem apagar, use `restriction: "add"` (que nunca remove nada) e trate a remoção manualmente.
1251
-
1252
- **Relação `mto` com nullable:**
754
+ Aceita um único objeto de ordenação ou um array deles, aplicados na ordem declarada:
1253
755
 
1254
- Use `nullable` (minúsculo) para permitir desvincular uma relação many-to-one:
756
+ ```typescript
757
+ const order: Ordering<User> = { createdAt: "desc" };
758
+ const chained: Ordering<User> = [{ name: "asc" }, { createdAt: "desc" }];
1255
759
 
1256
- ```ts
1257
- relations: {
1258
- category: {
1259
- pk: "id",
1260
- mode: "mto",
1261
- restriction: "set",
1262
- nullable: true, // permite passar null para desvincular
1263
- },
1264
- }
760
+ await userRepository.getAll({ order: chained });
1265
761
  ```
1266
762
 
1267
763
  ---
1268
764
 
1269
- ## Transações
765
+ ## Escrevendo seu próprio adapter
766
+
767
+ Como o núcleo é agnóstico de ORM e é distribuído sem um adapter embutido, adicionar suporte a um ORM/banco — seja como solução provisória para o seu próprio projeto, seja como candidato a um futuro pacote `@vsrepo/*-adapter` — significa implementar a classe abstrata `VSRepoAdapter<T>`:
768
+
769
+ ```typescript
770
+ export abstract class VSRepoAdapter<T> {
771
+ abstract runInTransaction<R>(
772
+ fn: (tx: any) => Promise<R>,
773
+ options?: VSRepoTransactionOptions,
774
+ ): Promise<R>;
775
+ abstract getDbClient(): any;
776
+ abstract query<T = any>(query: string, options?: AdapterQueryOptions): Promise<T>;
777
+ abstract findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T | null>;
778
+ abstract findOneOrThrow(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
779
+ abstract findMany(
780
+ where: VSRepoWhere<T>,
781
+ options?: AdapterMethodOptions<T> & { distinct?: (keyof T)[] },
782
+ ): Promise<T[]>;
783
+ abstract save(obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
784
+ abstract saveMany(objs: DeepPartial<T>[], options?: AdapterMethodOptions<T>): Promise<T[]>;
785
+ abstract create(objs: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
786
+ abstract createMany(
787
+ objs: DeepPartial<T>[],
788
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
789
+ ): Promise<CountResult>;
790
+ abstract createManyReturning(
791
+ objs: DeepPartial<T>[],
792
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
793
+ ): Promise<T[]>;
794
+ abstract delete(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
795
+ abstract deleteMany(
796
+ where: VSRepoWhere<T>,
797
+ options?: AdapterMethodOptions<T>,
798
+ ): Promise<CountResult>;
799
+ abstract deleteManyReturning(
800
+ where: VSRepoWhere<T>,
801
+ options?: AdapterMethodOptions<T>,
802
+ ): Promise<T[]>;
803
+ abstract update(
804
+ where: VSRepoWhere<T>,
805
+ obj: DeepPartial<T>,
806
+ options?: AdapterMethodOptions<T>,
807
+ ): Promise<T>;
808
+ abstract updateMany(
809
+ where: VSRepoWhere<T>,
810
+ obj: DeepPartial<T>,
811
+ options?: AdapterMethodOptions<T>,
812
+ ): Promise<CountResult>;
813
+ abstract updateManyReturning(
814
+ where: VSRepoWhere<T>,
815
+ obj: DeepPartial<T>,
816
+ options?: AdapterMethodOptions<T>,
817
+ ): Promise<T[]>;
818
+ abstract count(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number>;
819
+ abstract exists(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<boolean>;
820
+ abstract merge<K>(
821
+ where: VSRepoWhere<T>,
822
+ obj: DeepPartial<T>,
823
+ options?: AdapterMethodOptions<T>,
824
+ ): Promise<K & T>;
825
+ abstract upsert(
826
+ where: VSRepoWhere<T>,
827
+ create: DeepPartial<T>,
828
+ update: DeepPartial<T>,
829
+ options?: AdapterMethodOptions<T>,
830
+ ): Promise<T>;
831
+
832
+ abstract incrementOne<K extends NumericKeys<T>>(
833
+ field: K,
834
+ value: NonNullable<T[K]>,
835
+ where: VSRepoWhere<T>,
836
+ options?: AdapterMethodOptions<T>,
837
+ ): Promise<T>;
838
+ abstract decrementOne<K extends NumericKeys<T>>(
839
+ field: K,
840
+ value: NonNullable<T[K]>,
841
+ where: VSRepoWhere<T>,
842
+ options?: AdapterMethodOptions<T>,
843
+ ): Promise<T>;
844
+ abstract multiplyOne<K extends NumericKeys<T>>(
845
+ field: K,
846
+ value: NonNullable<T[K]>,
847
+ where: VSRepoWhere<T>,
848
+ options?: AdapterMethodOptions<T>,
849
+ ): Promise<T>;
850
+ abstract divideOne<K extends NumericKeys<T>>(
851
+ field: K,
852
+ value: NonNullable<T[K]>,
853
+ where: VSRepoWhere<T>,
854
+ options?: AdapterMethodOptions<T>,
855
+ ): Promise<T>;
856
+ abstract sum(
857
+ field: NumericKeys<T>,
858
+ where?: VSRepoWhere<T>,
859
+ options?: AdapterMethodOptions<T>,
860
+ ): Promise<number | null>;
861
+ abstract average(
862
+ field: NumericKeys<T>,
863
+ where?: VSRepoWhere<T>,
864
+ options?: AdapterMethodOptions<T>,
865
+ ): Promise<number | null>;
866
+ abstract min(
867
+ field: NumericKeys<T>,
868
+ where?: VSRepoWhere<T>,
869
+ options?: AdapterMethodOptions<T>,
870
+ ): Promise<number | null>;
871
+ abstract max(
872
+ field: NumericKeys<T>,
873
+ where?: VSRepoWhere<T>,
874
+ options?: AdapterMethodOptions<T>,
875
+ ): Promise<number | null>;
876
+ }
877
+ ```
1270
878
 
1271
- Todos os métodos aceitam `options.db` para participar de uma transação:
879
+ O `VSRepository` nunca fala diretamente com o ORM — ele só chama esses métodos com um `VSRepoWhere<T>` e um `AdapterMethodOptions<T>` já resolvidos. Uma vez que um adapter implemente esse contrato, todo método base, método dinâmico e query method passa a funcionar com ele automaticamente. Pra uma implementação completa e funcional, veja o repositório externo [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter).
1272
880
 
1273
- ```ts
1274
- await userRepository.prisma.$transaction(async (tx) => {
1275
- const user = await userRepository.save(
1276
- { name: "Mary", email: "mary@email.com", password: "password" },
1277
- { db: tx }
1278
- );
881
+ ### Logging a partir do seu adapter
1279
882
 
1280
- await userLogsRepository.save(
1281
- { action: "User registration", data: { registeredUser: user.id } },
1282
- { db: tx }
1283
- );
1284
- });
1285
- ```
883
+ O `vsrepo` exporta a mesma classe `VSLogger` usada internamente pelo core, então seu adapter pode logar no mesmo formato/estilo (timestamps, labels de nível coloridos, avisos de operação lenta) em vez de implementar o seu próprio:
1286
884
 
1287
- Para `saveList` e `patchList`, o campo `db` deve ser um `DbTransaction`:
885
+ ```typescript
886
+ import { VSLogger, VSLogLevel } from "vsrepo";
1288
887
 
1289
- ```ts
1290
- await prisma.$transaction(async (tx) => {
1291
- // CORRETO: tx é um DbTransaction
1292
- const registeredUsers = await userRepository.saveList([{ name: "Mary" }, { name: "Lucas" }], { db: tx });
888
+ export class MyOrmAdapter<T> extends VSRepoAdapter<T> {
889
+ private readonly logger = new VSLogger(VSLogLevel.WARN, "MyOrmAdapterLogger");
1293
890
 
1294
- await userLogsRepository.save(
1295
- { action: "User registration", data: { registeredUsers: registeredUsers.map(u => u.id) } },
1296
- { db: tx }
1297
- );
1298
- });
891
+ async findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>) {
892
+ const start = this.logger.startPerformLog("adapter findOne");
893
+ try {
894
+ // ... fala com o ORM ...
895
+ this.logger.endPerformLog(start);
896
+ return result;
897
+ } catch (err) {
898
+ this.logger.endPerformLog(start);
899
+ this.logger.logError("adapter findOne falhou", err);
900
+ throw err;
901
+ }
902
+ }
903
+ }
1299
904
  ```
1300
905
 
1301
- ---
1302
-
1303
- ## Estendendo um repositório
1304
-
1305
- ```ts
1306
- const userRepository = setupVSRepo<User, "user">()(({
1307
- tableName: "user",
1308
- pkName: "id",
1309
- methods: {
1310
- findOneByEmailEndsWith: { map: true },
1311
- },
1312
- })
1313
- .build(prisma)
1314
- .extend((repo) => ({
1315
- findActiveByDomain: async (domain: string) => {
1316
- return repo.findOneByEmailEndsWith(`@${domain}`);
1317
- },
906
+ | Método | Descrição |
907
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
908
+ | `new VSLogger(logLevel, name, slowThresholdMs?)` | Cria um logger; `name` prefixa cada linha, `slowThresholdMs` tem default 300. |
909
+ | `logDebug/logInfo/logWarn(text, obj?)` | Loga no nível dado se `logLevel` permitir; `obj` é anexado como JSON formatado. |
910
+ | `logError(text, err?)` | Loga em `ERROR`; se `err` for uma `Error`, só `name`/`message`/`stack`/`cause` são logados. |
911
+ | `startPerformLog(operation)` / `endPerformLog(data)` | Envolve um trecho de código para logar sua duração, escalando pra `WARN` se ultrapassar `slowThresholdMs`. |
912
+ | `getLogLevel()` | Retorna o `VSLogLevel` configurado do logger. |
1318
913
 
1319
- activateMultiple: async (ids: string[]) => {
1320
- return repo.patchList(ids.map(id => [id, { active: true }]));
1321
- },
1322
- }));
1323
- ```
914
+ Isso é puramente uma conveniência para autores de adapters — nada no core exige que seu adapter o utilize.
1324
915
 
1325
916
  ---
1326
917
 
1327
918
  ## Tratamento de erros
1328
919
 
1329
- O VSRepository lança `VSRepoError` e suas subclasses em situações específicas (erros do Prisma não são sobrescritos):
920
+ A v2 simplifica a hierarquia de erros da v1: em vez de várias subclasses, existe uma classe base `VSRepoError` carregando um campo `type: VSRepoErrorType`, além de uma subclasse dedicada `VSRepoAdapterError` (veja abaixo) para falhas vindas do ORM/banco subjacente.
1330
921
 
1331
- ```ts
1332
- import { VSRepoError, VSRepoRuntimeError } from "../../generated/vsrepo";
922
+ ```typescript
923
+ import { VSRepoError } from "vsrepo";
1333
924
 
1334
925
  try {
1335
- const user = await userRepository.getOrThrow("id-that-does-not-exist");
926
+ await userRepository.get(id);
1336
927
  } catch (error) {
1337
- if (error instanceof VSRepoRuntimeError && error.code === "20727") {
1338
- console.error("Record not found");
1339
- } else if (error instanceof VSRepoError) {
1340
- console.error("Repository error:", error.message);
1341
- } else {
1342
- console.error("Error:", error.message)
1343
- }
928
+ if (error instanceof VSRepoError) {
929
+ console.error(`[${error.type}] ${error.message}`);
930
+ }
1344
931
  }
1345
932
  ```
1346
933
 
1347
- **Subclasses disponíveis:**
1348
-
1349
- | Classe | Quando é lançada |
1350
- | ------------------------- | -------------------------------------------------------------------- |
1351
- | `VSRepoConfigError` | Configuração inválida em `setupVSRepo` |
1352
- | `VSRepoBuildError` | Nome de método, tipo de campo ou configuração inválida em `build` |
1353
- | `VSRepoExtendError` | Argumento inválido em `extend` |
1354
- | `VSRepoDecoratorError` | Argumento inválido passado para `@DynamicMethod` ou `@QueryMethod` |
1355
- | `VSRepoRuntimeError` | Erro em tempo de execução durante uma operação |
1356
-
1357
- `VSRepoRuntimeError` tem uma propriedade `code: VSRepoRuntimeErrorCode` para identificação programática, sem precisar fazer parsing da mensagem (legível por humanos, e possivelmente localizada):
1358
-
1359
- | Código | Significado |
1360
- | ------ | ----------- |
1361
- | `"65706"` | Um argumento obrigatório está ausente ou tem formato inválido — ex.: `pk` ausente, `pks`/`objs`/`tuples` que não são um array, ou `options`/`obj` que não são um objeto válido. |
1362
- | `"20727"` | Nenhum registro foi encontrado para a primary key informada (`getOrThrow` ao buscar o registro base). |
1363
- | `"67542"` | A validação (zod) de `options` de um método, ou de um argumento de `@QueryMethod`, falhou. |
1364
- | `"91868"` | Uma relação passada em `save`/`patch`/`merge` tem formato inválido para o `mode`/`restriction` configurados (ex.: `null` numa relação `mtm`/`otm`, ou um array numa relação `oto`/`mto`). |
1365
- | `"48670"` | Um método dinâmico (`config.methods`) foi chamado com menos argumentos posicionais do que os campos do `where` exigem. |
1366
-
1367
- ---
1368
-
1369
- ## Tipos utilitários
1370
-
1371
- ### Tipos de client
1372
-
1373
- ```ts
1374
- import type { DbClient, DbTransaction, ClientOrTransaction } from "../../generated/vsrepo";
1375
-
1376
- type DbClient = PrismaClient;
1377
- type DbTransaction = Prisma.TransactionClient;
1378
- type ClientOrTransaction = DbClient | DbTransaction;
1379
- ```
1380
-
1381
- ### Tipo de visibilidade do soft-delete
1382
-
1383
- ```ts
1384
- import type { SeeMode } from "../../generated/vsrepo";
1385
-
1386
- type SeeMode = "active" | "removed" | "all";
1387
- ```
1388
-
1389
- ### Tipos derivados do modelo Prisma
1390
-
1391
- ```ts
1392
- import type {
1393
- SelectModel,
1394
- SelectModels,
1395
- IncludeModel,
1396
- IncludeModels,
1397
- WhereModel,
1398
- OrderingModel,
1399
- PaginationModel,
1400
- ModelUpsertInput,
1401
- PrismaModelInputs,
1402
- } from "../../generated/vsrepo";
1403
- ```
1404
-
1405
- ### Tipos de opções de método
1406
-
1407
- ```ts
1408
- import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
1409
-
1410
- // MethodOptions<S, IM> — opções passadas para os métodos do repositório
1411
- type Opts = MethodOptions<"public" | "minimal", "withPosts">;
1412
-
1413
- // MethodOptionsModel<TRepo> — derivado de uma instância configurada do VSRepository
1414
- const userVSRepo = setupVSRepo<User, "user">()(config);
1415
- type OptsModel = MethodOptionsModel<typeof userVSRepo>;
1416
- ```
1417
-
1418
- > O segundo parâmetro de `MethodOptions` (`IM`) representa as chaves válidas de `includeModels`. Quando informado, `selectModel` e `includeModel` se tornam mutuamente exclusivos no tipo — não é possível passar os dois na mesma chamada.
1419
- >
1420
- > `MethodOptions` também aceita mais dois parâmetros genéricos, `RI` e `RS`, para tipar `include` e `select` brutos respectivamente (ambos com padrão `never`, ou seja, não são aceitos a menos que tipados explicitamente): `MethodOptions<"public", "withPosts", "usuario", IncludeModel<"usuario">, SelectModel<"usuario">>`. O `MethodOptionsModel`, derivado diretamente de um repositório configurado, não expõe `RI`/`RS` — use `MethodOptions` diretamente se precisar tipar as opções de `include`/`select` brutos.
1421
-
1422
- ### Tipos de configuração
934
+ | `VSRepoErrorType` | Quando é lançado |
935
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
936
+ | `DECORATOR` | Argumentos inválidos foram passados para `@DynamicMethod` ou `@QueryMethod`. |
937
+ | `RESOLVER` | A biblioteca falhou ao resolver a configuração de um método dinâmico/de query em um método chamável (ex.: um nome de método desconhecido). |
938
+ | `DYNAMIC` | Um método dinâmico já resolvido falhou em tempo de execução (ex.: argumentos faltando). |
939
+ | `VALIDATOR` | Options ou argumentos de método inválidos foram detectados durante a validação. |
940
+ | `BASE` | Uso inválido de um método base (`get`, `save`, `remove`, etc). |
941
+ | `ADAPTER` | Um `VSRepoAdapter` falhou ao falar com o ORM/banco subjacente — sempre é lançado como `VSRepoAdapterError`. |
1423
942
 
1424
- ```ts
1425
- import type {
1426
- MethodConfig,
1427
- RepoConfig,
1428
- BuildConfig,
1429
- RepositoryRelations,
1430
- ExtractRelationConfig,
1431
- } from "../../generated/vsrepo";
1432
- ```
943
+ ### `VSRepoAdapterError` e `AdapterErrorCode`
1433
944
 
1434
- ### Tipo do repositório construído
945
+ Quando um adapter fala com o ORM/banco subjacente e essa operação falha, o adapter encapsula a falha em um `VSRepoAdapterError` — uma subclasse de `VSRepoError` com `type: VSRepoErrorType.ADAPTER`. Ele carrega um `code: AdapterErrorCode` **estável e agnóstico de adapter** além do erro bruto lançado pelo ORM/driver, para que quem chama possa reagir às falhas sem depender do formato de erro de nenhum ORM específico:
1435
946
 
1436
- ```ts
1437
- import type { RepositoryOf } from "../../generated/vsrepo";
947
+ ```typescript
948
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
1438
949
 
1439
- const userVSRepo = setupVSRepo<User, "user">()({ ... });
1440
- type UserRepository = RepositoryOf<typeof userVSRepo>;
1441
- ```
1442
-
1443
- `RepositoryOf` aceita três parâmetros:
950
+ try {
951
+ await userRepository.save({ name: "Maria" });
952
+ } catch (error) {
953
+ if (error instanceof VSRepoAdapterError) {
954
+ console.error(`[${error.code}] ${error.message}`, error.originalError);
1444
955
 
1445
- ```ts
1446
- type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
956
+ if (error.code === AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
957
+ // tratar chave duplicada, ex.: retornar uma mensagem amigável
958
+ }
959
+ }
960
+ }
1447
961
  ```
1448
962
 
1449
- ### Tipos de payload para `save` e `patch`
963
+ | Propriedade | Tipo | Descrição |
964
+ | --------------- | ------------------ | --------------------------------------------------------------------------------- |
965
+ | `code` | `AdapterErrorCode` | Código estável e agnóstico que classifica a falha. |
966
+ | `originalError` | `unknown` | O erro bruto (ou `null`/`undefined`) lançado pelo driver do ORM/banco subjacente. |
967
+ | `message` | `string` | Descrição legível da falha do adapter. |
968
+ | `type` | `VSRepoErrorType` | Sempre `VSRepoErrorType.ADAPTER`. |
969
+ | `cause` | `unknown` | Causa raiz opcional da qual o erro foi encadeado. |
970
+
971
+ As implementações de adapter o constroem diretamente ao mapear uma falha do ORM:
972
+
973
+ ```typescript
974
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
975
+
976
+ throw new VSRepoAdapterError(
977
+ "falha ao criar o usuário",
978
+ AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
979
+ originalError, // erro bruto do banco/driver
980
+ );
981
+ ```
982
+
983
+ #### `AdapterErrorCode`
984
+
985
+ `AdapterErrorCode` é um enum de códigos granulares e agnósticos que um adapter pode lançar através de `VSRepoAdapterError`. Eles espelham as falhas mais comuns lançadas por ORMs e drivers de banco, para que erros de qualquer ORM possam ser mapeados para o mesmo código estável:
986
+
987
+ ```typescript
988
+ import { AdapterErrorCode } from "vsrepo";
989
+
990
+ console.log(AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION); // "UNIQUE_CONSTRAINT_VIOLATION"
991
+ ```
992
+
993
+ | Código | Significado |
994
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
995
+ | `UNKNOWN` | Erro não classificado/desconhecido; o fallback quando nenhum código mais específico corresponde. |
996
+ | `MISSING_DB_CLIENT` | Cliente de banco (ou pool de conexões) não fornecido ou que não pôde ser resolvido. |
997
+ | `CONNECTION_FAILED` | Não foi possível alcançar/conectar ao banco, ou uma conexão estabelecida foi perdida/terminada. |
998
+ | `CONNECTION_POOL_EXHAUSTED` | Pool de conexões esgotado/depletado — nenhuma conexão disponível, todas ocupadas ou o limite foi atingido. |
999
+ | `TIMEOUT` | O banco não respondeu a tempo; uma query excedeu o timeout permitido. |
1000
+ | `UNIQUE_CONSTRAINT_VIOLATION` | Violação de constraint unique (chave duplicada). Ex.: Postgres/SQLite `23505`, MySQL `1062`. |
1001
+ | `FOREIGN_KEY_VIOLATION` | Violação de constraint de foreign key (linha referenciada não existe). |
1002
+ | `NOT_NULL_VIOLATION` | Violação de constraint NOT NULL. |
1003
+ | `CHECK_VIOLATION` | Violação de constraint CHECK. |
1004
+ | `CONSTRAINT_VIOLATION` | Violação geral de integridade/constraint não coberta por um código mais específico. |
1005
+ | `NOT_FOUND` | Registro solicitado não encontrado (ex.: uma operação tipo `findOneOrThrow`). |
1006
+ | `INVALID_DATA` | Valor de campo inválido para o tipo/tamanho, ou um valor obrigatório ausente. |
1007
+ | `VALUE_TOO_LONG` | Valor fornecido excede o limite de tamanho da coluna/campo. |
1008
+ | `CONVERSION_ERROR` | Um valor não pôde ser convertido/convertido para o tipo alvo. Ex.: Postgres `22P02`, MySQL `1366`. |
1009
+ | `INVALID_QUERY` | A query/stored procedure SQL está malformada ou é inválida. |
1010
+ | `TABLE_OR_COLUMN_NOT_FOUND` | A tabela/coluna/relação referenciada não existe. |
1011
+ | `DEADLOCK` | Operação abortada por timeout de lock ou deadlock entre transações concorrentes. |
1012
+ | `LOCK_TIMEOUT` | Não foi possível adquirir um lock de banco obrigatório a tempo. |
1013
+ | `LOCKED` | O registro está travado e não pode ser modificado. |
1014
+ | `ACCESS_DENIED` | O usuário/role atual não tem permissão para a operação. |
1015
+ | `INVALID_CREDENTIALS` | Credenciais de conexão inválidas (host/usuário/senha). |
1016
+ | `ROW_NOT_ALLOWED` | O usuário autenticado não é dono do registro / a segurança em nível de linha rejeitou. |
1017
+ | `MODEL_NOT_FOUND` | Entidade/modelo ou tabela não definida/mapeada no ORM, ou o adapter não tem os metadados do modelo. |
1018
+ | `FIELD_NOT_FOUND` | Nome de campo/coluna nos dados ou no `where` não existe na entidade/modelo. |
1019
+ | `TRANSACTION_CLOSED` | Transação usada depois de commit/rollback. |
1020
+ | `TRANSACTION_ALREADY_STARTED` | Uma transação aninhada não pôde ser aberta (ex.: chamadas `transaction()` aninhadas). |
1021
+ | `TRANSACTION_CONFLICT` | Uma transação falhou ao commitar e foi desfeita. |
1022
+ | `TRANSACTION_NOT_STARTED` | Nenhuma transação ativa quando uma era obrigatória. |
1023
+ | `CONNECTION_CLOSED` | Conexão fechada/terminada enquanto uma transação ou query estava em andamento. |
1024
+ | `INVALID_PARTIAL` | `merge`/`upsert`/`update` recebeu um objeto parcial inválido ou faltando chaves obrigatórias. |
1025
+ | `NOT_SUPPORTED` | Feature/operação não suportada solicitada ao adapter (ex.: `query()` bruto não suportado). |
1026
+ | `INVALID_ADAPTER_CONFIG` | Configuração do adapter inválida ou incompleta (options obrigatórias ausentes, ou com tipo/valor inválido). |
1027
+ | `INTERNAL` | Bug interno do adapter ou estado irrecuperável; deve raramente ser usado — prefira um código mais específico. |
1028
+
1029
+ #### `VSRepoError` vs. erros brutos do ORM
1030
+
1031
+ Erros de uso/configuração fora do adapter lançam o `VSRepoError` base. Falhas lançadas _pelo ORM subjacente_ enquanto um método do adapter roda são **encapsuladas** em `VSRepoAdapterError` (classificadas por um `AdapterErrorCode`, com o erro original preservado em `originalError`) em vez de se propagarem cruas — é isso que torna quem chama independente do formato de erro de qualquer ORM específico.
1450
1032
 
1451
- ```ts
1452
- import type { SaveObject, PatchObject } from "../../generated/vsrepo";
1033
+ ---
1453
1034
 
1454
- const userVSRepo = setupVSRepo<User, "user">()(({
1455
- tableName: "user",
1456
- pkName: "id",
1457
- relations: {
1458
- profile: { pk: "id", mode: "oto", restriction: "set" },
1459
- },
1460
- });
1035
+ ## Logging
1461
1036
 
1462
- type UserSavePayload = SaveObject<Prisma.UserCreateInput, typeof userVSRepo>;
1463
- type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
1464
- ```
1037
+ Todo repository tem um logger interno, configurado via `logLevel` e `logSlowThresholdMs` nas options do construtor:
1465
1038
 
1466
- ---
1039
+ ```typescript
1040
+ import { VSLogLevel } from "vsrepo";
1467
1041
 
1468
- ## Referência da API
1469
-
1470
- ### `setupVSRepo<T, M>()(config)`
1471
-
1472
- ```ts
1473
- setupVSRepo<TPayload, TTableName>()({
1474
- tableName: Uncapitalize<M>; // Nome da tabela no Prisma
1475
- pkName: keyof T; // Nome da chave primária
1476
- softRemovekName?: keyof T & string; // Campo DateTime para soft-delete
1477
- selectModels?: SelectModels<M>; // Projeções de dados nomeadas (select)
1478
- defaultSelectModel?: keyof SM; // Select aplicado por padrão
1479
- includeModels?: IncludeModels<M>; // Projeções de dados nomeadas (include) — sem padrão, apenas na chamada
1480
- requiredWhere?: WhereModel<M>; // Filtros sempre aplicados
1481
- defaultOrdering?: OrderingModel<M>; // Ordenação padrão para queries sem Ordered/injectOrdering
1482
- relations?: RepositoryRelations<T>; // Configuração de relações
1483
- methods?: Record<string, MethodConfig<M, SM>>; // Métodos dinâmicos
1042
+ super({
1043
+ pkName: "id",
1044
+ adapter,
1045
+ logLevel: VSLogLevel.DEBUG,
1046
+ logSlowThresholdMs: 200,
1484
1047
  });
1485
1048
  ```
1486
1049
 
1487
- ### `.build(prisma, config?)`
1488
-
1489
- ```ts
1490
- vsRepo.build(prisma, {
1491
- showWorking?: boolean; // Exibe os logs internos no console (padrão = false)
1492
-
1493
- baseMethods?: {
1494
- // Métodos que podem usar um defaultSelect
1495
- get?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1496
- getOrThrow?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1497
- getList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1498
- remove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1499
- save?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1500
- saveList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1501
- patch?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1502
- patchList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1503
- merge?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1504
- getAll?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1505
- softRemove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1506
- restore?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1507
-
1508
- // Métodos que NÃO aceitam defaultSelect
1509
- removeList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1510
- softRemoveList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1511
- restoreList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1512
- total?: { active?: boolean; ignoreRequiredWhere?: boolean };
1513
- has?: { active?: boolean; ignoreRequiredWhere?: boolean };
1514
- };
1515
- });
1516
- ```
1050
+ | Nível | Significado |
1051
+ | --------------- | ------------------------------------------------------------------------------------------------------- |
1052
+ | `DEBUG` | Detalhes internos verbosos, incluindo toda query resolvida — muito útil para debugar métodos dinâmicos. |
1053
+ | `INFO` | Eventos de alto nível do ciclo de vida, como a inicialização do repository. |
1054
+ | `WARN` (padrão) | Problemas recuperáveis e operações lentas (veja `logSlowThresholdMs`, padrão de 300ms). |
1055
+ | `ERROR` | Falhas lançadas durante a execução de uma operação. |
1517
1056
 
1518
- ### `.extend(fn)`
1057
+ ---
1519
1058
 
1520
- ```ts
1521
- repo.extend((repo) => ({
1522
- myMethod: () => { ... }
1523
- }));
1524
- ```
1059
+ ## Desenvolvimento
1525
1060
 
1526
- ---
1061
+ O core da v2 é compilado e empacotado a partir desta branch como um pacote npm padrão:
1527
1062
 
1528
- ## Exemplos práticos
1529
-
1530
- Além deste README, o repositório tem uma pasta **[`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples)** com exemplos práticos, comentados e prontos para executar — é o melhor lugar para ver o VSRepository sendo usado em cenários reais.
1531
-
1532
- ```text
1533
- examples/
1534
- ├── prisma.ts # Instância do PrismaClient usada pelos exemplos
1535
- ├── repositories.ts # Configuração dos repositórios (User, Address, Product) com setupVSRepo
1536
- └── tests/
1537
- ├── base-methods.test.ts # Métodos base: get, save, patch, remove, getAll, total, has...
1538
- ├── relations.test.ts # Como configurar e usar relações em save/patch e em filtros
1539
- ├── required-where.test.ts # Como requiredWhere é aplicado automaticamente às queries
1540
- ├── dynamic-methods.test.ts # Prefixos, filtros de campo, operadores lógicos e paginação/ordenação
1541
- ├── transactions.test.ts # Transações com options.db e acesso à instância via repository.prisma
1542
- ├── soft-delete.test.ts # Soft-delete: softRemove, softRemoveList, restore, restoreList e SeeMode
1543
- └── batch-methods.test.ts # Operações em lote: getList, saveList, patchList e merge
1544
- ```
1063
+ ```bash
1064
+ # 1. Instalar as dependências
1065
+ pnpm install
1545
1066
 
1546
- Cada arquivo em `tests/` é um script independente e executável (via `tsx`) que demonstra um conjunto específico de funcionalidades, com `console.log` em cada etapa para você acompanhar o resultado no terminal. A própria pasta tem um [README](https://github.com/jaobrabo123/VSRepository/blob/main/examples/README.md) explicando a ordem de leitura sugerida, como configurar o ambiente e como rodar os testes.
1067
+ # 2. Compilar os fontes TypeScript em dist/ (remove um dist/ anterior primeiro)
1068
+ pnpm build
1547
1069
 
1548
- ---
1070
+ # 3. (Opcional) Inspecionar o que seria publicado sem gerar um tarball
1071
+ npm pack --dry-run
1549
1072
 
1550
- ## Contribuindo
1073
+ # 4. Gerar o tarball instalável (roda `prepack` -> `pnpm build` automaticamente)
1074
+ npm pack
1551
1075
 
1552
- Contribuições são bem-vindas! Se você encontrou um bug, tem uma ideia de melhoria ou quer ajudar com a documentação, sinta-se à vontade para participar (**[Repositório no GitHub](https://github.com/jaobrabo123/VSRepository)**):
1076
+ # 5. Consumir localmente em outro projeto
1077
+ npm install ../caminho/vsrepo-1.4.0.tgz
1078
+ ```
1553
1079
 
1554
- 1. Faça um **fork** do projeto.
1555
- 2. Crie uma nova branch com sua alteração: `git checkout -b fixing-bug`.
1556
- 3. Envie para sua branch: `git push origin fixing-bug`.
1557
- 4. Abra um **Pull Request**.
1080
+ Observações:
1558
1081
 
1559
- Para reportar problemas ou sugerir novas funcionalidades, abra uma **Issue**.
1082
+ - `pnpm build` executa `tsc -p tsconfig.build.json`, que gera o JS compilado e as declarações de tipo em `dist/` com `rootDir: src`.
1083
+ - O pacote publicado contém **apenas** a pasta `dist/` além dos READMEs e da `LICENSE` (veja `files` no `package.json`). Os adapters viverão em seus próprios pacotes `@vsrepo/*-adapter`.
1084
+ - O core é ORM-agnóstico e não tem dependência peer de `@prisma/client`.
1560
1085
 
1561
1086
  ---
1562
1087
 
1563
1088
  ## Requisitos
1564
1089
 
1565
- - Node.js 18+ (ESM)
1566
- - Prisma
1567
- - TypeScript (opcional, mas fortemente recomendado)
1568
- - `"moduleResolution": "bundler"` ou `"nodenext"` no tsconfig
1569
-
1570
- `tsconfig.json` recomendado:
1090
+ - Node.js 18+
1091
+ - TypeScript, com **decorators legacy/experimentais** habilitados (necessário para `@DynamicMethod`/`@QueryMethod`):
1571
1092
 
1572
1093
  ```json
1573
1094
  {
1574
- "compilerOptions": {
1575
- "target": "ES2020",
1576
- "module": "NodeNext",
1577
- "moduleResolution": "NodeNext",
1578
- "strict": true,
1579
- "skipLibCheck": true,
1580
- "lib": ["ES2020"]
1581
- }
1095
+ "compilerOptions": {
1096
+ "experimentalDecorators": true
1097
+ }
1582
1098
  }
1583
1099
  ```
1584
1100
 
1585
- ---
1586
-
1587
- ## Solução de problemas
1588
-
1589
- **Tipos genéricos não são inferidos** — Verifique se `strict: true` e `moduleResolution: "bundler"` ou `"nodenext"` estão configurados no `tsconfig.json`.
1590
-
1591
- **Método dinâmico não existe em tempo de execução** — O campo referenciado no nome do método deve existir no modelo do Prisma. Ex.: `findByEmail` requer que o modelo tenha um campo `email`.
1101
+ - `reflect-metadata` (já incluso como dependência, importado internamente — você não precisa importá-lo você mesmo)
1102
+ - Pelo menos um `VSRepoAdapter` funcional para o seu banco — no Prisma 7, instale o [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) já publicado (veja [Status dos adapters](#status-dos-adapters)); adapters oficiais para outros ORMs estão planejados, mas ainda não publicados, então por enquanto isso significa escrever o seu próprio (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)) — e, se publicá-lo, contribuir de volta com o projeto é bem-vindo
1592
1103
 
1593
- **`proxyTo` obrigatório** — Nomes fora dos padrões conhecidos (ex.: `searchByEmail`) não são interpretados diretamente. Use `proxyTo: "findByEmail"` nesses casos.
1594
-
1595
- **Select model retorna campos inesperados** — Verifique se o select model define exatamente os campos que o seu tipo TypeScript espera.
1596
-
1597
- **`selectModel`, `includeModel` e `include` juntos na mesma chamada** — Não é permitido. Apenas um dos três pode ser informado por chamada: se `includeModel` ou `include` for informado, o `select` (incluindo `defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
1598
-
1599
- **`includeModel` não aparece como opção padrão do repositório** — Isso é esperado. Diferente do `defaultSelectModel`, não existe `defaultIncludeModel`/`defaultInclude`. Um `includeModel` só pode ser definido na chamada do método, via `options.includeModel`. Um include bruto e ad hoc pode ser definido via `options.include`, sem precisar ser registrado em `includeModels`.
1104
+ ---
1600
1105
 
1601
- **`softRemovekName` lança um 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.
1106
+ ## Contribuindo
1602
1107
 
1603
- **`defaultOrdering` não está sendo aplicado** — Verifique se o método usa o sufixo `Ordered`, `OrderedAndPaginated` ou `PaginatedAndOrdered`, e se tem `injectOrdering` configurado. Ambos têm prioridade sobre a ordenação padrão.
1108
+ Contribuições são bem-vindas, especialmente para finalizar os adapters do Prisma e do TypeORM! (**[Repositório do GitHub](https://github.com/jaobrabo123/VSRepository)**):
1604
1109
 
1605
- **Sufixo `Distinct` não é reconhecido** — `Distinct` só é resolvido em prefixos de leitura (`findMany`, `findFirst`, `findBy`, `existsBy`, etc). Em métodos como `count`, `createMany`, `updateMany` ou `deleteMany` o sufixo é ignorado.
1110
+ 1. Faça um **Fork** do projeto.
1111
+ 2. Crie uma branch a partir de `v2` para sua alteração: `git checkout -b v2-minha-alteracao`.
1112
+ 3. Faça o push da sua branch: `git push origin v2-minha-alteracao`.
1113
+ 4. Abra um **Pull Request** contra a `v2`.
1606
1114
 
1607
- **`saveList`/`patchList` com `db` inválido** — O campo `db` nesses métodos só aceita uma `DbTransaction` (o retorno de `prisma.$transaction`), não o client principal. Passar o `PrismaClient` diretamente causará comportamento inesperado.
1115
+ Para reportar problemas ou sugerir funcionalidades, abra uma **Issue**.