vsrepo 2.0.0 → 2.2.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 (34) hide show
  1. package/README.md +180 -16
  2. package/README.pt-BR.md +180 -16
  3. package/dist/VSRepoAdapter.d.ts +38 -0
  4. package/dist/VSRepository.d.ts +45 -5
  5. package/dist/VSRepository.js +77 -11
  6. package/dist/decorators/query-method.decorator.d.ts +26 -3
  7. package/dist/decorators/query-method.decorator.js +25 -2
  8. package/dist/index.d.ts +8 -1
  9. package/dist/index.js +6 -1
  10. package/dist/internal/enums/vsrepo-error-type.enum.d.ts +1 -1
  11. package/dist/internal/enums/vsrepo-error-type.enum.js +1 -1
  12. package/dist/internal/resolvers/dynamic-methods.resolver.js +32 -6
  13. package/dist/internal/utils/db-arg.util.d.ts +15 -0
  14. package/dist/internal/utils/db-arg.util.js +22 -0
  15. package/dist/internal/utils/with-db.util.d.ts +19 -0
  16. package/dist/internal/utils/with-db.util.js +23 -0
  17. package/dist/internal/validators/decorators.validator.js +2 -0
  18. package/dist/internal/validators/vsrepo.validator.d.ts +8 -1
  19. package/dist/internal/validators/vsrepo.validator.js +23 -0
  20. package/dist/types/decorators/query-method-options.type.d.ts +35 -1
  21. package/dist/types/utils/decimal-like.type.d.ts +23 -0
  22. package/dist/types/utils/decimal-like.type.js +2 -0
  23. package/dist/types/utils/numeric-keys.type.d.ts +24 -0
  24. package/dist/types/utils/numeric-keys.type.js +2 -0
  25. package/dist/types/utils/numeric-like.type.d.ts +10 -0
  26. package/dist/types/utils/numeric-like.type.js +2 -0
  27. package/dist/types/utils/primitive.type.d.ts +2 -1
  28. package/dist/types/utils/query-args.type.d.ts +30 -0
  29. package/dist/types/utils/query-args.type.js +2 -0
  30. package/dist/types/utils/query-method-arg.type.d.ts +3 -2
  31. package/dist/types/utils/restrict-method-options.type.d.ts +14 -0
  32. package/dist/types/utils/restrict-method-options.type.js +2 -0
  33. package/dist/types/vsrepo/vsrepo-query-options.type.d.ts +14 -0
  34. package/package.json +1 -1
package/README.pt-BR.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
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
- 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](./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.
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
20
  O VSRepository permite criar repositories fortemente tipados com:
21
21
 
@@ -39,6 +39,9 @@ O VSRepository permite criar repositories fortemente tipados com:
39
39
  - [Options do construtor](#options-do-construtor)
40
40
  - [Métodos base](#métodos-base)
41
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)
42
45
  - [`select` e `relations`](#select-e-relations)
43
46
  - [Métodos dinâmicos](#métodos-dinâmicos)
44
47
  - [Prefixos disponíveis](#prefixos-disponíveis)
@@ -48,6 +51,7 @@ O VSRepository permite criar repositories fortemente tipados com:
48
51
  - [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct)
49
52
  - [Options do decorador](#options-do-decorador)
50
53
  - [Query methods (SQL raw)](#query-methods-sql-raw)
54
+ - [Argumentos via spread com `spreadArgs`](#argumentos-via-spread-com-spreadargs)
51
55
  - [Queries raw pontuais com `query()`](#queries-raw-pontuais-com-query)
52
56
  - [Transações](#transações)
53
57
  - [Tipos utilitários](#tipos-utilitários)
@@ -63,7 +67,7 @@ O VSRepository permite criar repositories fortemente tipados com:
63
67
 
64
68
  ## O que mudou da v1
65
69
 
66
- Se você vem do código/docs da [v1](./v1), aqui está o resumo. Veja cada seção linkada para detalhes.
70
+ 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.
67
71
 
68
72
  | Área | v1 | v2 |
69
73
  | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -97,7 +101,7 @@ O adapter do Prisma 7 já foi publicado no npm como `@vsrepo/prisma7-adapter`
97
101
 
98
102
  | Adapter | Status |
99
103
  | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
- | Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Lançado** — publicado no npm, implementa todo 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. |
104
+ | 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. |
101
105
  | 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. |
102
106
  | 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. |
103
107
  | 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`. |
@@ -231,11 +235,19 @@ Disponíveis automaticamente em toda subclasse de `VSRepository`:
231
235
  | `removeList(pks, options?)` | Remove vários registros pela primary key, retornando `{ count }`. |
232
236
  | `total(options?)` | Retorna o total de registros. |
233
237
  | `has(pk, options?)` | Verifica se um registro existe, retornando `boolean`. |
238
+ | `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). |
239
+ | `decrement(pk, field, value, options?)` | Subtrai `value` de um campo numérico de forma atômica. |
240
+ | `multiply(pk, field, value, options?)` | Multiplica um campo numérico por `value` de forma atômica. |
241
+ | `divide(pk, field, value, options?)` | Divide um campo numérico por `value` de forma atômica. |
242
+ | `sum(field, where?, options?)` | Soma um campo numérico em todos os registros que baterem no filtro; `null` se nenhum bater. |
243
+ | `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. |
244
+ | `min(field, where?, options?)` | Valor mínimo de um campo numérico em todos os registros que baterem no filtro; `null` se nenhum bater. |
245
+ | `max(field, where?, options?)` | Valor máximo de um campo numérico em todos os registros que baterem no filtro; `null` se nenhum bater. |
234
246
  | `transaction(fn, options?)` | Executa `fn` dentro de uma transação nativa do ORM. |
235
247
  | `getDbClient()` | Retorna a instância do client do ORM usada fora de transações. |
236
248
  | `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). |
237
249
 
238
- Todos os métodos acima (exceto `transaction`, `query` e `getDbClient`, que recebem options proprias ou nenhuma) aceitam um objeto `MethodOptions<Entity, OrmTypes>` como último argumento (`select`, `relations`, `see`, `db`).
250
+ 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.
239
251
 
240
252
  ---
241
253
 
@@ -270,6 +282,62 @@ await userRepository.getAll({ see: "all" }); // todos, ignorando o soft-delete
270
282
 
271
283
  ---
272
284
 
285
+ ## Métodos atômicos e de agregação
286
+
287
+ Toda subclasse de `VSRepository` ganha 8 métodos extras para trabalhar com campos numéricos, divididos em dois grupos:
288
+
289
+ **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:
290
+
291
+ ```typescript
292
+ await userRepository.increment("user-1", "balance", 50); // balance = balance + 50
293
+ await userRepository.decrement("user-1", "balance", 50); // balance = balance - 50
294
+ await userRepository.multiply("user-1", "balance", 2); // balance = balance * 2
295
+ await userRepository.divide("user-1", "balance", 4); // balance = balance / 4
296
+ ```
297
+
298
+ Os quatro retornam a `Entity` atualizada e aceitam o `MethodOptions<Entity, OrmTypes>` completo (`select`, `relations`, `see`, `db`) como último argumento, igual `get`/`save`/`patch`.
299
+
300
+ **Agregações** — calculadas sobre todos os registros que baterem num `where` (opcional):
301
+
302
+ ```typescript
303
+ await userRepository.sum("balance"); // soma do saldo de todos os registros ativos
304
+ await userRepository.sum("balance", { active: true }); // ...restrito por um where
305
+ await userRepository.average("balance");
306
+ await userRepository.min("balance");
307
+ await userRepository.max("balance");
308
+ ```
309
+
310
+ 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).
311
+
312
+ 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.
313
+
314
+ ### Quais campos são elegíveis
315
+
316
+ `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):
317
+
318
+ ```typescript
319
+ type Product = { id: string; name: string; price: Decimal; stock: number | null };
320
+
321
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok — Decimal-like
322
+ await productRepository.increment(id, "stock", 5); // ok — campos numéricos nullable são incluídos
323
+ await productRepository.increment(id, "name", 1); // erro de compilação — "name" não é numérico
324
+ ```
325
+
326
+ `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:
327
+
328
+ ```typescript
329
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok
330
+ await productRepository.increment(id, "price", 10.5); // erro de compilação — envolva: new Decimal(10.5)
331
+ ```
332
+
333
+ 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.
334
+
335
+ ### Escrevendo um adapter
336
+
337
+ 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.
338
+
339
+ ---
340
+
273
341
  ## `select` e `relations`
274
342
 
275
343
  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:
@@ -531,21 +599,54 @@ class UserRepository extends VSRepository<User, string> {
531
599
 
532
600
  @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
533
601
  declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
602
+
603
+ // Aqui só se espera uma linha, então `singleResult` transforma o array
604
+ // em um único objeto (ou `null` quando nenhuma linha corresponde).
605
+ @QueryMethod('SELECT * FROM "user" WHERE id = $1 LIMIT 1', { singleResult: true })
606
+ declare findByIdRaw: (arg: QueryMethodArg<[id: string]>) => Promise<User | null>;
534
607
  }
535
608
  ```
536
609
 
537
- | Option | Tipo | Padrão | Descrição |
538
- | ----------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
539
- | `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. |
610
+ | Option | Tipo | Padrão | Descrição |
611
+ | -------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
612
+ | `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. |
613
+ | `singleResult` | `boolean` | `false` | Quando `true`, transforma um resultado em array no seu primeiro elemento (`null` se vazio), permitindo declarar o tipo de retorno como um objeto único em vez de array. Não tem efeito em resultados que não são array (ex.: o número de linhas afetadas de uma query `modifying`). |
540
614
 
541
615
  Query methods aceitam `{ args, db? }` na chamada — `db` permite que participem de um bloco `transaction()`, assim como os métodos base e dinâmicos.
542
616
 
617
+ ### Argumentos via spread com `spreadArgs`
618
+
619
+ Por padrão, um `@QueryMethod` recebe seus valores de placeholder através de um único objeto `QueryMethodArg` (`method({ args: [...] })`). Defina `spreadArgs: true` para recebê-los como argumentos posicionais separados, no estilo do JpaRepository:
620
+
621
+ ```typescript
622
+ class UserRepository extends VSRepository<User, string> {
623
+ @QueryMethod('SELECT * FROM "user" WHERE email = $1 AND "userType" = $2', {
624
+ spreadArgs: true,
625
+ })
626
+ declare findByEmailAndType: (
627
+ ...args: QueryArgs<[email: string, userType: string]>
628
+ ) => Promise<User[]>;
629
+ }
630
+
631
+ const admins = await userRepository.findByEmailAndType("joao@email.com", "admin");
632
+ ```
633
+
634
+ Para rodar a query com um client ou transação específico em vez do client padrão do repository, passe `withDb(tx)` como argumento final — ele embrulha `tx` em um `DbArg`, que o resolver reconhece via `instanceof`, então nunca é confundido com um argumento posicional comum, mesmo que esse argumento seja um objeto:
635
+
636
+ ```typescript
637
+ await userRepository.transaction(async (tx) => {
638
+ await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
639
+ });
640
+ ```
641
+
642
+ `spreadArgs` afeta apenas campos declarados com `@QueryMethod` — o padrão é `false`, e chamar um método declarado sem essa opção usando mais de um argumento lança erro, já que se espera o estilo de chamada com um único `QueryMethodArg`. Não tem efeito sobre `query()`, que sempre aceita `{ args, db? }`.
643
+
543
644
  ### Queries raw pontuais com `query()`
544
645
 
545
646
  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:
546
647
 
547
648
  ```typescript
548
- query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
649
+ query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean; singleResult?: boolean }): Promise<T>;
549
650
  ```
550
651
 
551
652
  ```typescript
@@ -557,13 +658,21 @@ const linhasAfetadas = await userRepository.query<number>(
557
658
  'UPDATE "user" SET active = true WHERE id = $1',
558
659
  { args: ["123"], modifying: true },
559
660
  );
661
+
662
+ // Aqui só se espera uma linha, então `singleResult` transforma o array
663
+ // em um único objeto (ou `null` quando nenhuma linha corresponde).
664
+ const user = await userRepository.query<User | null>(
665
+ 'SELECT * FROM "user" WHERE id = $1 LIMIT 1',
666
+ { args: ["123"], singleResult: true },
667
+ );
560
668
  ```
561
669
 
562
- | Option | Tipo | Padrão | Descrição |
563
- | ----------- | --------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- |
564
- | `args` | `any[]` | `undefined` | Parâmetros posicionais injetados nos placeholders `$1`, `$2`, ... Nunca interpole valores diretamente na string SQL. |
565
- | `db` | `any` | Client padrão do repository | Client ou transação do banco em que essa query deve rodar. |
566
- | `modifying` | `boolean` | `false` | Quando `true`, trata a instrução como `INSERT`/`UPDATE`/`DELETE`. |
670
+ | Option | Tipo | Padrão | Descrição |
671
+ | -------------- | --------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
672
+ | `args` | `any[]` | `undefined` | Parâmetros posicionais injetados nos placeholders `$1`, `$2`, ... Nunca interpole valores diretamente na string SQL. |
673
+ | `db` | `any` | Client padrão do repository | Client ou transação do banco em que essa query deve rodar. |
674
+ | `modifying` | `boolean` | `false` | Quando `true`, trata a instrução como `INSERT`/`UPDATE`/`DELETE`. |
675
+ | `singleResult` | `boolean` | `false` | Quando `true`, transforma um resultado em array no seu primeiro elemento (`null` se vazio). Não tem efeito em resultados que não são array (ex.: o número de linhas afetadas de uma query `modifying`). |
567
676
 
568
677
  Assim como os métodos base, dinâmicos e query, `query()` aceita `db` em `options` para participar de um bloco `transaction()`.
569
678
 
@@ -618,6 +727,7 @@ Além dos tipos que descrevem o formato da entidade já vistos acima (`VSRepoSel
618
727
  ```typescript
619
728
  import type {
620
729
  MethodOptions,
730
+ RestrictMethodOptions,
621
731
  Pagination,
622
732
  Ordering,
623
733
  OrderByField,
@@ -626,7 +736,11 @@ import type {
626
736
  DeepPartial,
627
737
  CountResult,
628
738
  QueryMethodArg,
739
+ QueryArgs,
629
740
  KeysOfType,
741
+ NumericKeys,
742
+ NumericLike,
743
+ DecimalLike,
630
744
  Primitive,
631
745
  VSRepoWhere,
632
746
  VSRepoOrmTypes,
@@ -637,14 +751,19 @@ import type {
637
751
 
638
752
  | Tipo | Descrição | Usado por |
639
753
  | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
640
- | `MethodOptions<T, K>` | Options aceitas como último argumento por todo método base e dinâmico: `select`, `relations`, `see`, `db`. | [Métodos base](#métodos-base), [Métodos Dinâmicos](#métodos-dinâmicos). |
754
+ | `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). |
755
+ | `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). |
641
756
  | `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). |
642
757
  | `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). |
643
758
  | `SeeMode` | `"active" \| "removed" \| "all"` — controla a visibilidade de registros com soft-delete. | [Soft-delete](#soft-delete). |
644
759
  | `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`. |
645
760
  | `CountResult` | `{ count: number }` — o formato retornado por operações em lote. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
646
761
  | `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). |
762
+ | `QueryArgs<T, O>` | Tipa a lista de parâmetros via spread de um `@QueryMethod` declarado com `{ spreadArgs: true }`: os valores de `T`, em ordem, seguidos de um `DbArg<O>` opcional construído via `withDb()`. | [Argumentos via spread com `spreadArgs`](#argumentos-via-spread-com-spreadargs). |
647
763
  | `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. |
764
+ | `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). |
765
+ | `NumericLike` | `number \| bigint \| DecimalLike`. | [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação). |
766
+ | `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). |
648
767
  | `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. |
649
768
  | `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). |
650
769
  | `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). |
@@ -753,6 +872,51 @@ export abstract class VSRepoAdapter<T> {
753
872
  update: DeepPartial<T>,
754
873
  options?: AdapterMethodOptions<T>,
755
874
  ): Promise<T>;
875
+
876
+ abstract incrementOne<K extends NumericKeys<T>>(
877
+ field: K,
878
+ value: NonNullable<T[K]>,
879
+ where: VSRepoWhere<T>,
880
+ options?: AdapterMethodOptions<T>,
881
+ ): Promise<T>;
882
+ abstract decrementOne<K extends NumericKeys<T>>(
883
+ field: K,
884
+ value: NonNullable<T[K]>,
885
+ where: VSRepoWhere<T>,
886
+ options?: AdapterMethodOptions<T>,
887
+ ): Promise<T>;
888
+ abstract multiplyOne<K extends NumericKeys<T>>(
889
+ field: K,
890
+ value: NonNullable<T[K]>,
891
+ where: VSRepoWhere<T>,
892
+ options?: AdapterMethodOptions<T>,
893
+ ): Promise<T>;
894
+ abstract divideOne<K extends NumericKeys<T>>(
895
+ field: K,
896
+ value: NonNullable<T[K]>,
897
+ where: VSRepoWhere<T>,
898
+ options?: AdapterMethodOptions<T>,
899
+ ): Promise<T>;
900
+ abstract sum(
901
+ field: NumericKeys<T>,
902
+ where?: VSRepoWhere<T>,
903
+ options?: AdapterMethodOptions<T>,
904
+ ): Promise<number | null>;
905
+ abstract average(
906
+ field: NumericKeys<T>,
907
+ where?: VSRepoWhere<T>,
908
+ options?: AdapterMethodOptions<T>,
909
+ ): Promise<number | null>;
910
+ abstract min(
911
+ field: NumericKeys<T>,
912
+ where?: VSRepoWhere<T>,
913
+ options?: AdapterMethodOptions<T>,
914
+ ): Promise<number | null>;
915
+ abstract max(
916
+ field: NumericKeys<T>,
917
+ where?: VSRepoWhere<T>,
918
+ options?: AdapterMethodOptions<T>,
919
+ ): Promise<number | null>;
756
920
  }
757
921
  ```
758
922
 
@@ -815,7 +979,7 @@ try {
815
979
  | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
816
980
  | `DECORATOR` | Argumentos inválidos foram passados para `@DynamicMethod` ou `@QueryMethod`. |
817
981
  | `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). |
818
- | `DYNAMIC` | Um método dinâmico já resolvido falhou em tempo de execução (ex.: argumentos faltando). |
982
+ | `DYNAMIC` | Um dynamic/query method já resolvido falhou em tempo de execução (ex.: argumentos faltando). |
819
983
  | `VALIDATOR` | Options ou argumentos de método inválidos foram detectados durante a validação. |
820
984
  | `BASE` | Uso inválido de um método base (`get`, `save`, `remove`, etc). |
821
985
  | `ADAPTER` | Um `VSRepoAdapter` falhou ao falar com o ORM/banco subjacente — sempre é lançado como `VSRepoAdapterError`. |
@@ -960,7 +1124,7 @@ npm install ../caminho/vsrepo-1.4.0.tgz
960
1124
  Observações:
961
1125
 
962
1126
  - `pnpm build` executa `tsc -p tsconfig.build.json`, que gera o JS compilado e as declarações de tipo em `dist/` com `rootDir: src`.
963
- - O pacote publicado contém **apenas** a pasta `dist/` além dos READMEs e da `LICENSE` (veja `files` no `package.json`). Fontes, testes, a pasta `v1/` e `generated/` **não** são enviados — os adapters viverão em seus próprios pacotes `@vsrepo/*-adapter`.
1127
+ - 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`.
964
1128
  - O core é ORM-agnóstico e não tem dependência peer de `@prisma/client`.
965
1129
 
966
1130
  ---
@@ -2,6 +2,7 @@ import { AdapterMethodOptions } from "./types/adapter/adapter-method-options.typ
2
2
  import { AdapterQueryOptions } from "./types/adapter/adapter-query-options.type";
3
3
  import { CountResult } from "./types/utils/count-result.type";
4
4
  import { DeepPartial } from "./types/utils/deep-partial.type";
5
+ import { NumericKeys } from "./types/utils/numeric-keys.type";
5
6
  import { VSRepoTransactionOptions } from "./types/vsrepo/vsrepo-transaction-options.type";
6
7
  import { VSRepoWhere } from "./types/vsrepo/vsrepo-where.type";
7
8
  /**
@@ -68,4 +69,41 @@ export declare abstract class VSRepoAdapter<T> {
68
69
  abstract merge<K>(where: VSRepoWhere<T>, obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<K & T>;
69
70
  /** Creates a record if none matches `where`, otherwise updates it. */
70
71
  abstract upsert(where: VSRepoWhere<T>, create: DeepPartial<T>, update: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
72
+ /**
73
+ * Atomically adds `value` to `field` on the single record matching
74
+ * `where` — equivalent to `UPDATE ... SET field = field + value WHERE
75
+ * ...`, evaluated server-side against the row's current value (not a
76
+ * client-side read-modify-write).
77
+ *
78
+ * Implementations must return the record reflecting the state *after*
79
+ * the write. If the underlying ORM's atomic-update API doesn't already
80
+ * return the updated row (e.g. it only returns an affected-row count),
81
+ * issue a follow-up read instead of returning a stale in-memory copy.
82
+ */
83
+ abstract incrementOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
84
+ /** Same as {@link VSRepoAdapter.incrementOne}, subtracting `value` instead of adding it. */
85
+ abstract decrementOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
86
+ /** Same as {@link VSRepoAdapter.incrementOne}, multiplying the field's current value by `value`. */
87
+ abstract multiplyOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
88
+ /**
89
+ * Same as {@link VSRepoAdapter.incrementOne}, dividing the field's
90
+ * current value by `value`. Division-by-zero behavior is not
91
+ * standardized by this contract — it is whatever the underlying
92
+ * database/driver does natively (e.g. Postgres raises a
93
+ * `division_by_zero` error); document your adapter's actual behavior.
94
+ */
95
+ abstract divideOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
96
+ /**
97
+ * Returns the sum of `field` across every record matching `where` (all
98
+ * records if `where` is omitted/empty), or `null` if no record
99
+ * matches — mirroring SQL's `SUM()`, which returns `NULL` (not `0`)
100
+ * over an empty set.
101
+ */
102
+ abstract sum(field: NumericKeys<T>, where?: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
103
+ /** Same as {@link VSRepoAdapter.sum}, but the arithmetic mean (`AVG()`) instead of the total. */
104
+ abstract average(field: NumericKeys<T>, where?: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
105
+ /** Same as {@link VSRepoAdapter.sum}, but the minimum value (`MIN()`) instead of the total. */
106
+ abstract min(field: NumericKeys<T>, where?: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
107
+ /** Same as {@link VSRepoAdapter.sum}, but the maximum value (`MAX()`) instead of the total. */
108
+ abstract max(field: NumericKeys<T>, where?: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
71
109
  }
@@ -3,6 +3,7 @@ import { VSRepoOptions } from "./types/vsrepo/vsrepo-options.type";
3
3
  import { CountResult } from "./types/utils/count-result.type";
4
4
  import { DeepPartial } from "./types/utils/deep-partial.type";
5
5
  import { KeysOfType } from "./types/utils/keys-of-type.type";
6
+ import { VSRepoWhere } from "./types/vsrepo/vsrepo-where.type";
6
7
  import { VSRepoOrmTypes } from "./types/vsrepo/vsrepo-orm-types.type";
7
8
  import { VSRepoTransactionOptions } from "./types/vsrepo/vsrepo-transaction-options.type";
8
9
  import { MethodOptions } from "./types/utils/methods-options.type";
@@ -10,6 +11,8 @@ import { Ordering } from "./types/utils/ordering.type";
10
11
  import { Pagination } from "./types/utils/pagination.type";
11
12
  import { VSRepoArgs } from "./types/vsrepo/vsrepo-args.type";
12
13
  import { VSRepoQueryOptions } from "./types/vsrepo/vsrepo-query-options.type";
14
+ import { NumericKeys } from "./types/utils/numeric-keys.type";
15
+ import { RestrictMethodOptions } from "./types/utils/restrict-method-options.type";
13
16
  /**
14
17
  * ORM-agnostic base repository, exposing a complete set of ready-to-use CRUD
15
18
  * and soft-delete methods around an entity, plus any `@DynamicMethod`/`@QueryMethod`
@@ -76,6 +79,8 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
76
79
  * Use `$1`, `$2`, ... placeholders for values passed via `options.args` —
77
80
  * never interpolate values directly into `query`, to avoid SQL injection.
78
81
  * Set `options.modifying: true` for `INSERT`/`UPDATE`/`DELETE` statements.
82
+ * Set `options.singleResult: true` to collapse an array result into its
83
+ * first element (`null` if empty) — see {@link VSRepoQueryOptions.singleResult}.
79
84
  *
80
85
  * @example
81
86
  * ```typescript
@@ -88,6 +93,13 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
88
93
  * 'UPDATE "user" SET active = true WHERE id = $1',
89
94
  * { args: ["123"], modifying: true },
90
95
  * );
96
+ *
97
+ * // Only one row is ever expected here, so `singleResult` collapses the
98
+ * // array into a single object (or `null` when no row matches).
99
+ * const user = await userRepository.query<User | null>(
100
+ * 'SELECT * FROM "user" WHERE id = $1 LIMIT 1',
101
+ * { args: ["123"], singleResult: true },
102
+ * );
91
103
  * ```
92
104
  */
93
105
  query<T = any>(query: string, options?: VSRepoQueryOptions<OrmTypes>): Promise<T>;
@@ -111,7 +123,7 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
111
123
  /** Deletes a record identified by its primary key (PK). */
112
124
  remove(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
113
125
  /** Deletes multiple records by their primary keys, returning the count of affected rows. */
114
- removeList(pks: PKType[], options?: MethodOptions<Entity, OrmTypes>): Promise<CountResult>;
126
+ removeList(pks: PKType[], options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<CountResult>;
115
127
  /** Partially updates an existing record by its primary key (PK). */
116
128
  patch(pk: PKType, obj: DeepPartial<Entity>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
117
129
  /**
@@ -121,15 +133,43 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
121
133
  */
122
134
  merge<U extends DeepPartial<Entity>>(pk: PKType, obj: U, options?: MethodOptions<Entity, OrmTypes>): Promise<(U & Entity) | null>;
123
135
  /** Returns the total number of records. */
124
- total(options?: MethodOptions<Entity, OrmTypes>): Promise<number>;
136
+ total(options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number>;
125
137
  /** Checks whether a record exists by its primary key (PK). */
126
- has(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<boolean>;
138
+ has(pk: PKType, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<boolean>;
127
139
  /** Marks a record as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
128
140
  softRemove(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
129
141
  /** Marks multiple records as deleted (soft-delete) in batch. Requires `softRemoveKey` to be configured on the repository. */
130
- softRemoveList(pks: PKType[], options?: MethodOptions<Entity, OrmTypes>): Promise<CountResult>;
142
+ softRemoveList(pks: PKType[], options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<CountResult>;
131
143
  /** Restores a record previously marked as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
132
144
  restore(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
133
145
  /** Restores multiple records previously marked as deleted (soft-delete) in batch. Requires `softRemoveKey` to be configured on the repository. */
134
- restoreList(pks: PKType[], options?: MethodOptions<Entity, OrmTypes>): Promise<CountResult>;
146
+ restoreList(pks: PKType[], options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<CountResult>;
147
+ /**
148
+ * Atomically adds `value` to a numeric field of the record identified
149
+ * by `pk`, evaluated server-side against the row's current value (e.g.
150
+ * `saldo = saldo + value`) — not a fetch-then-save round trip.
151
+ */
152
+ increment<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
153
+ /** Same as {@link VSRepository.increment}, subtracting `value` instead of adding it. */
154
+ decrement<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
155
+ /** Same as {@link VSRepository.increment}, multiplying the field's current value by `value`. */
156
+ multiply<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
157
+ /**
158
+ * Same as {@link VSRepository.increment}, dividing the field's current
159
+ * value by `value`. Division-by-zero behavior depends on the adapter/
160
+ * underlying database (see {@link VSRepoAdapter.divideOne}).
161
+ */
162
+ divide<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
163
+ /**
164
+ * Returns the sum of a numeric field across every record matching
165
+ * `where` (all records if omitted), or `null` if none match — mirrors
166
+ * SQL's `SUM()`, which returns `NULL` (not `0`) over an empty set.
167
+ */
168
+ sum(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
169
+ /** Same as {@link VSRepository.sum}, but the arithmetic mean instead of the total. */
170
+ average(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
171
+ /** Same as {@link VSRepository.sum}, but the minimum value instead of the total. */
172
+ min(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
173
+ /** Same as {@link VSRepository.sum}, but the maximum value instead of the total. */
174
+ max(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
135
175
  }