vsrepo 2.0.0 → 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.
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  > ✅ **Released.** VSRepository v2.0.0 (the ORM-agnostic core) and the [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) are both published and ready to use. Prisma 7 is the first fully supported adapter; other ORMs (TypeORM, Drizzle, etc.) are still in progress — see [Adapter status](#adapter-status). If you need the previous Prisma-only release, use the [`v1`](https://github.com/jaobrabo123/VSRepository/tree/v1) code/docs instead.
17
17
 
18
- **ORM-agnostic** repository pattern library, with full **TypeScript** support and automatic **type inference**. VSRepository v2 is a rewrite of the [v1](./v1) library: instead of talking to Prisma directly, the core now delegates every operation to a pluggable **adapter**, so the same repository API can work against Prisma, TypeORM, or any other ORM/database that implements the adapter contract.
18
+ **ORM-agnostic** repository pattern library, with full **TypeScript** support and automatic **type inference**. VSRepository v2 is a rewrite of the [v1](https://github.com/jaobrabo123/VSRepository/tree/v1) library: instead of talking to Prisma directly, the core now delegates every operation to a pluggable **adapter**, so the same repository API can work against Prisma, TypeORM, or any other ORM/database that implements the adapter contract.
19
19
 
20
20
  VSRepository lets you create strongly-typed repositories with:
21
21
 
@@ -39,6 +39,9 @@ VSRepository lets you create strongly-typed repositories with:
39
39
  - [Constructor options](#constructor-options)
40
40
  - [Base methods](#base-methods)
41
41
  - [Soft-delete](#soft-delete)
42
+ - [Atomic and aggregate methods](#atomic-and-aggregate-methods)
43
+ - [Which fields are eligible](#which-fields-are-eligible)
44
+ - [Writing an adapter](#writing-an-adapter)
42
45
  - [`select` and `relations`](#select-and-relations)
43
46
  - [Dynamic methods](#dynamic-methods)
44
47
  - [Available prefixes](#available-prefixes)
@@ -63,7 +66,7 @@ VSRepository lets you create strongly-typed repositories with:
63
66
 
64
67
  ## What changed from v1
65
68
 
66
- If you're coming from the [v1](./v1) code/docs, here's the short version. See each linked section for details.
69
+ If you're coming from the [v1](https://github.com/jaobrabo123/VSRepository/tree/v1) code/docs, here's the short version. See each linked section for details.
67
70
 
68
71
  | Area | v1 | v2 |
69
72
  | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -97,7 +100,7 @@ The Prisma 7 adapter has now been published to npm as `@vsrepo/prisma7-adapter`
97
100
 
98
101
  | Adapter | Status |
99
102
  | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
- | Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Released** — published to npm, implements the entire `VSRepoAdapter` contract (CRUD, relations, transactions, `merge`, logging) with tests; see [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) for source and docs. |
103
+ | Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Released** — published to npm, implements the `VSRepoAdapter` contract (CRUD, relations, transactions, `merge`, logging) with tests; see [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) for source and docs. **Note:** the atomic/aggregate methods (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — see [Atomic and aggregate methods](#atomic-and-aggregate-methods)) were added to the `VSRepoAdapter` contract after this adapter's last release; confirm its changelog/version implements them before relying on `increment`/`sum`/etc. against Prisma 7. |
101
104
  | TypeORM (`@vsrepo/typeorm-adapter`) | 🟡 **Planned, not published yet.** Only a reference `where`-clause parser (`parseVSRepoWhere`) was written to validate the design; it's the planned starting point for the future `@vsrepo/typeorm-adapter` package. Community contributions toward this are welcome. |
102
105
  | Other ORMs (Prisma 8, Drizzle, etc.) | 🟡 **Planned, not published yet.** No official package exists yet — write your own adapter for now (see [Writing your own adapter](#writing-your-own-adapter)), and consider publishing/contributing it back. |
103
106
  | Custom adapters | 🟢 Fully supported today — implement the [`VSRepoAdapter`](#writing-your-own-adapter) abstract class yourself for any ORM/database you need, in your own project or package, following the same shape as `@vsrepo/*-adapter` is expected to have. |
@@ -231,11 +234,19 @@ Available automatically on every `VSRepository` subclass:
231
234
  | `removeList(pks, options?)` | Deletes multiple records by primary key, returning `{ count }`. |
232
235
  | `total(options?)` | Returns the total number of records. |
233
236
  | `has(pk, options?)` | Checks whether a record exists, returning `boolean`. |
237
+ | `increment(pk, field, value, options?)` | Atomically adds `value` to a numeric field. See [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
238
+ | `decrement(pk, field, value, options?)` | Atomically subtracts `value` from a numeric field. |
239
+ | `multiply(pk, field, value, options?)` | Atomically multiplies a numeric field by `value`. |
240
+ | `divide(pk, field, value, options?)` | Atomically divides a numeric field by `value`. |
241
+ | `sum(field, where?, options?)` | Sums a numeric field across every matching record; `null` if none match. |
242
+ | `average(field, where?, options?)` | Arithmetic mean of a numeric field across every matching record; `null` if none match. |
243
+ | `min(field, where?, options?)` | Minimum value of a numeric field across every matching record; `null` if none match. |
244
+ | `max(field, where?, options?)` | Maximum value of a numeric field across every matching record; `null` if none match. |
234
245
  | `transaction(fn, options?)` | Runs `fn` inside a native transaction of the underlying ORM. |
235
246
  | `getDbClient()` | Returns the underlying ORM client instance used outside of transactions. |
236
247
  | `query<T>(query, options?)` | Executes a raw SQL statement directly against the database. See [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query). |
237
248
 
238
- All of the above (except `transaction`, `query`, and `getDbClient`, which accept their own options or none at all) accept a `MethodOptions<Entity, OrmTypes>` object as their last argument (`select`, `relations`, `see`, `db`).
249
+ Most of the above accept a `MethodOptions<Entity, OrmTypes>` object as their last argument (`select`, `relations`, `see`, `db`). A few — `total`, `has`, `removeList`, `sum`, `average`, `min`, `max`, and the soft-delete batch methods (`softRemoveList`/`restoreList`) — don't return/shape an `Entity`, so they accept the narrower `RestrictMethodOptions<Entity, OrmTypes>` instead (`see`, `db` only; no `select`/`relations`). `transaction`, `query`, and `getDbClient` accept their own options or none at all.
239
250
 
240
251
  ---
241
252
 
@@ -270,6 +281,62 @@ await userRepository.getAll({ see: "all" }); // everything, ignoring soft-delete
270
281
 
271
282
  ---
272
283
 
284
+ ## Atomic and aggregate methods
285
+
286
+ Every `VSRepository` subclass gets 8 extra methods for working with numeric fields, split into two groups:
287
+
288
+ **Atomic updates** — evaluated server-side against the row's *current* value (`UPDATE ... SET field = field + value`), not a client-side read-modify-write:
289
+
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
295
+ ```
296
+
297
+ All four return the updated `Entity` and accept the full `MethodOptions<Entity, OrmTypes>` (`select`, `relations`, `see`, `db`) as their last argument, same as `get`/`save`/`patch`.
298
+
299
+ **Aggregates** — computed across every record matching an (optional) `where`:
300
+
301
+ ```typescript
302
+ await userRepository.sum("balance"); // total balance across every active record
303
+ await userRepository.sum("balance", { active: true }); // ...restricted by a where
304
+ await userRepository.average("balance");
305
+ await userRepository.min("balance");
306
+ await userRepository.max("balance");
307
+ ```
308
+
309
+ All four return `number | null` — `null` when no record matches, mirroring SQL's `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL` (not `0`) over an empty set. Unlike the atomic methods, they accept the narrower `RestrictMethodOptions<Entity, OrmTypes>` (`see`, `db` only — no `select`/`relations`, since the result is a plain number, not a shaped `Entity`).
310
+
311
+ Both groups respect `softRemoveKey`/`see` the same way every other base method does — `sum("balance")` only totals non-deleted records by default, pass `{ see: "all" }` or `{ see: "removed" }` to change that.
312
+
313
+ ### Which fields are eligible
314
+
315
+ `field` is constrained to `NumericKeys<Entity>` — keys whose (non-nullable) value type is a `number`, a `bigint`, or a `DecimalLike` object (anything exposing `toNumber()` and `decimalPlaces()`, matching e.g. Prisma's `Prisma.Decimal`):
316
+
317
+ ```typescript
318
+ type Product = { id: string; name: string; price: Decimal; stock: number | null };
319
+
320
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok — Decimal-like
321
+ await productRepository.increment(id, "stock", 5); // ok — nullable numeric fields are included
322
+ await productRepository.increment(id, "name", 1); // compile error — "name" isn't numeric
323
+ ```
324
+
325
+ `value` is typed as `NonNullable<Entity[Field]>` — it must match the field's own type exactly. A `Decimal` field expects a `Decimal` instance, not a plain `number`/`string`:
326
+
327
+ ```typescript
328
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok
329
+ await productRepository.increment(id, "price", 10.5); // compile error — wrap it: new Decimal(10.5)
330
+ ```
331
+
332
+ Note that several ORMs (Drizzle, MikroORM, TypeORM) represent `decimal`/`numeric` columns as plain `string` by default, to avoid floating-point precision loss — a `string` field does **not** satisfy `NumericKeys<Entity>` out of the box. Configure the column in a numeric mode (or a transformer) on those ORMs if you want the field to be usable with these 8 methods.
333
+
334
+ ### Writing an adapter
335
+
336
+ `VSRepoAdapter` mirrors the same 8 operations (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — see [Writing your own adapter](#writing-your-own-adapter)). Each adapter translates them into whatever its ORM/database considers "native": Prisma has a built-in `{ field: { increment: value } }` update shape and an `aggregate()` call; other ORMs typically need a `QueryBuilder`/raw-`sql` expression (e.g. `SET field = field * :value`, `SELECT SUM(field) ...`) instead. The atomic methods must return the record reflecting the state *after* the write — if the ORM's atomic-update API only returns an affected-row count, issue a follow-up read rather than returning a stale in-memory copy.
337
+
338
+ ---
339
+
273
340
  ## `select` and `relations`
274
341
 
275
342
  v1's named, reusable `selectModels`/`defaultSelectModel` are gone. In v2 you pass `select` and `relations` directly on each call — there's nothing to pre-register:
@@ -615,6 +682,7 @@ Beyond the entity-shaping types covered above (`VSRepoSelect`, `VSRepoRelations`
615
682
  ```typescript
616
683
  import type {
617
684
  MethodOptions,
685
+ RestrictMethodOptions,
618
686
  Pagination,
619
687
  Ordering,
620
688
  OrderByField,
@@ -624,6 +692,9 @@ import type {
624
692
  CountResult,
625
693
  QueryMethodArg,
626
694
  KeysOfType,
695
+ NumericKeys,
696
+ NumericLike,
697
+ DecimalLike,
627
698
  Primitive,
628
699
  VSRepoWhere,
629
700
  VSRepoOrmTypes,
@@ -634,7 +705,8 @@ import type {
634
705
 
635
706
  | Type | Description | Used by |
636
707
  | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
637
- | `MethodOptions<T, K>` | Options accepted as the last argument of every base and dynamic method: `select`, `relations`, `see`, `db`. | [Base methods](#base-methods), [Dynamic methods](#dynamic-methods). |
708
+ | `MethodOptions<T, K>` | Options accepted as the last argument of most base and dynamic methods: `select`, `relations`, `see`, `db`. | [Base methods](#base-methods), [Dynamic methods](#dynamic-methods). |
709
+ | `RestrictMethodOptions<T, K>` | Narrowed `MethodOptions<T, K>` exposing only `see`/`db` — used by methods that don't shape/return an `Entity` (`total`, `has`, `sum`, `average`, `min`, `max`, `removeList`, `softRemoveList`, `restoreList`). | [Base methods](#base-methods), [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
638
710
  | `Pagination` | `{ limit?, offset? }` accepted by `getAll` and by `Paginated` dynamic methods. | [Base methods](#base-methods), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
639
711
  | `Ordering<T>` / `OrderByField<T>` / `SortDirection` | Ordering shape accepted by `getAll`, `defaultOrdering` and `injectOrdering`, and by `Ordered` dynamic methods. A single object or a chained array; nested objects order to-one relations. | [Constructor options](#constructor-options), [Decorator options](#decorator-options), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
640
712
  | `SeeMode` | `"active" \| "removed" \| "all"` — controls visibility of soft-deleted records. | [Soft-delete](#soft-delete). |
@@ -642,6 +714,9 @@ import type {
642
714
  | `CountResult` | `{ count: number }` — the shape returned by batch operations. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
643
715
  | `QueryMethodArg<T>` | `{ args?: T, db? }` — positional SQL parameters (`$1`, `$2`, ...) and transaction client for `@QueryMethod`. | [Query methods (raw SQL)](#query-methods-raw-sql). |
644
716
  | `KeysOfType<T, K>` | Extracts the keys of `T` whose value type is assignable to `K`. | Constrains `pkName` in [Constructor options](#constructor-options) to fields of the entity matching the configured primary-key type. |
717
+ | `NumericKeys<T>` | Extracts the keys of `T` whose (non-nullable) value type is assignable to `NumericLike`. Nullable numeric fields (`number \| null`) are included. | Constrains `field` in [Atomic and aggregate methods](#atomic-and-aggregate-methods) (`increment`, `sum`, etc). |
718
+ | `NumericLike` | `number \| bigint \| DecimalLike`. | [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
719
+ | `DecimalLike` | Structural shape of an arbitrary-precision decimal value (`{ toNumber(): number; decimalPlaces(): number }`), matching e.g. Prisma's `Prisma.Decimal` without importing it directly. | [Which fields are eligible](#which-fields-are-eligible). |
645
720
  | `Primitive` | Union of scalar types (`string \| number \| boolean \| bigint \| symbol \| undefined \| null \| Date`) treated as leaves — not relations — when walking an entity's shape. | Used by `Ordering<T>` to tell scalar fields apart from relation fields. |
646
721
  | `VSRepoWhere<T>` | ORM-agnostic filter type accepted by `*Where` dynamic methods (e.g. `findWhere`, `findOneWhere`, `updateWhere`). Supports field filters, logical operators (`AND`/`OR`/`NOT`), and relation filters. | [`findWhere`, `findOneWhere` and other `*Where` prefixes](#available-prefixes). |
647
722
  | `VSRepoOrmTypes` | `{ dbClient; dbTransaction }` — describes your ORM's client/transaction types. Passed as the third generic to `VSRepository<Entity, PKType, OrmTypes>` to type `getDbClient()`, `transaction()` and the `db` option instead of `any`. | [Creating a repository](#creating-a-repository). |
@@ -750,6 +825,51 @@ export abstract class VSRepoAdapter<T> {
750
825
  update: DeepPartial<T>,
751
826
  options?: AdapterMethodOptions<T>,
752
827
  ): Promise<T>;
828
+
829
+ abstract incrementOne<K extends NumericKeys<T>>(
830
+ field: K,
831
+ value: NonNullable<T[K]>,
832
+ where: VSRepoWhere<T>,
833
+ options?: AdapterMethodOptions<T>,
834
+ ): Promise<T>;
835
+ abstract decrementOne<K extends NumericKeys<T>>(
836
+ field: K,
837
+ value: NonNullable<T[K]>,
838
+ where: VSRepoWhere<T>,
839
+ options?: AdapterMethodOptions<T>,
840
+ ): Promise<T>;
841
+ abstract multiplyOne<K extends NumericKeys<T>>(
842
+ field: K,
843
+ value: NonNullable<T[K]>,
844
+ where: VSRepoWhere<T>,
845
+ options?: AdapterMethodOptions<T>,
846
+ ): Promise<T>;
847
+ abstract divideOne<K extends NumericKeys<T>>(
848
+ field: K,
849
+ value: NonNullable<T[K]>,
850
+ where: VSRepoWhere<T>,
851
+ options?: AdapterMethodOptions<T>,
852
+ ): Promise<T>;
853
+ abstract sum(
854
+ field: NumericKeys<T>,
855
+ where?: VSRepoWhere<T>,
856
+ options?: AdapterMethodOptions<T>,
857
+ ): Promise<number | null>;
858
+ abstract average(
859
+ field: NumericKeys<T>,
860
+ where?: VSRepoWhere<T>,
861
+ options?: AdapterMethodOptions<T>,
862
+ ): Promise<number | null>;
863
+ abstract min(
864
+ field: NumericKeys<T>,
865
+ where?: VSRepoWhere<T>,
866
+ options?: AdapterMethodOptions<T>,
867
+ ): Promise<number | null>;
868
+ abstract max(
869
+ field: NumericKeys<T>,
870
+ where?: VSRepoWhere<T>,
871
+ options?: AdapterMethodOptions<T>,
872
+ ): Promise<number | null>;
753
873
  }
754
874
  ```
755
875
 
@@ -957,7 +1077,7 @@ npm install ../path/to/vsrepo-1.4.0.tgz
957
1077
  Notes:
958
1078
 
959
1079
  - `pnpm build` runs `tsc -p tsconfig.build.json`, which outputs the compiled JS and generated type declarations into `dist/` with `rootDir: src`.
960
- - The published package contains **only** the `dist/` folder plus the READMEs and `LICENSE` (see `files` in `package.json`). Source, tests, the `v1/` folder and `generated/` are **not** shipped — the adapters will live in their own `@vsrepo/*-adapter` packages.
1080
+ - The published package contains **only** the `dist/` folder plus the READMEs and `LICENSE` (see `files` in `package.json`). The adapters will live in their own `@vsrepo/*-adapter` packages.
961
1081
  - The core is ORM-agnostic and has no `@prisma/client` peer dependency.
962
1082
 
963
1083
  ---
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)
@@ -63,7 +66,7 @@ O VSRepository permite criar repositories fortemente tipados com:
63
66
 
64
67
  ## O que mudou da v1
65
68
 
66
- Se você vem do código/docs da [v1](./v1), aqui está o resumo. Veja cada seção linkada para detalhes.
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.
67
70
 
68
71
  | Área | v1 | v2 |
69
72
  | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -97,7 +100,7 @@ O adapter do Prisma 7 já foi publicado no npm como `@vsrepo/prisma7-adapter`
97
100
 
98
101
  | Adapter | Status |
99
102
  | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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. |
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. |
101
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. |
102
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. |
103
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`. |
@@ -231,11 +234,19 @@ Disponíveis automaticamente em toda subclasse de `VSRepository`:
231
234
  | `removeList(pks, options?)` | Remove vários registros pela primary key, retornando `{ count }`. |
232
235
  | `total(options?)` | Retorna o total de registros. |
233
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. |
234
245
  | `transaction(fn, options?)` | Executa `fn` dentro de uma transação nativa do ORM. |
235
246
  | `getDbClient()` | Retorna a instância do client do ORM usada fora de transações. |
236
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). |
237
248
 
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`).
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.
239
250
 
240
251
  ---
241
252
 
@@ -270,6 +281,62 @@ await userRepository.getAll({ see: "all" }); // todos, ignorando o soft-delete
270
281
 
271
282
  ---
272
283
 
284
+ ## Métodos atômicos e de agregação
285
+
286
+ Toda subclasse de `VSRepository` ganha 8 métodos extras para trabalhar com campos numéricos, divididos em dois grupos:
287
+
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:
289
+
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
295
+ ```
296
+
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`.
298
+
299
+ **Agregações** — calculadas sobre todos os registros que baterem num `where` (opcional):
300
+
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");
307
+ ```
308
+
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).
310
+
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.
312
+
313
+ ### Quais campos são elegíveis
314
+
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):
316
+
317
+ ```typescript
318
+ type Product = { id: string; name: string; price: Decimal; stock: number | null };
319
+
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
323
+ ```
324
+
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:
326
+
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)
330
+ ```
331
+
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.
333
+
334
+ ### Escrevendo um adapter
335
+
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.
337
+
338
+ ---
339
+
273
340
  ## `select` e `relations`
274
341
 
275
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:
@@ -618,6 +685,7 @@ Além dos tipos que descrevem o formato da entidade já vistos acima (`VSRepoSel
618
685
  ```typescript
619
686
  import type {
620
687
  MethodOptions,
688
+ RestrictMethodOptions,
621
689
  Pagination,
622
690
  Ordering,
623
691
  OrderByField,
@@ -627,6 +695,9 @@ import type {
627
695
  CountResult,
628
696
  QueryMethodArg,
629
697
  KeysOfType,
698
+ NumericKeys,
699
+ NumericLike,
700
+ DecimalLike,
630
701
  Primitive,
631
702
  VSRepoWhere,
632
703
  VSRepoOrmTypes,
@@ -637,7 +708,8 @@ import type {
637
708
 
638
709
  | Tipo | Descrição | Usado por |
639
710
  | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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). |
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). |
641
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). |
642
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). |
643
715
  | `SeeMode` | `"active" \| "removed" \| "all"` — controla a visibilidade de registros com soft-delete. | [Soft-delete](#soft-delete). |
@@ -645,6 +717,9 @@ import type {
645
717
  | `CountResult` | `{ count: number }` — o formato retornado por operações em lote. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
646
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). |
647
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). |
648
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. |
649
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). |
650
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). |
@@ -753,6 +828,51 @@ export abstract class VSRepoAdapter<T> {
753
828
  update: DeepPartial<T>,
754
829
  options?: AdapterMethodOptions<T>,
755
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>;
756
876
  }
757
877
  ```
758
878
 
@@ -960,7 +1080,7 @@ npm install ../caminho/vsrepo-1.4.0.tgz
960
1080
  Observações:
961
1081
 
962
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`.
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`.
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`.
964
1084
  - O core é ORM-agnóstico e não tem dependência peer de `@prisma/client`.
965
1085
 
966
1086
  ---
@@ -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`
@@ -111,7 +114,7 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
111
114
  /** Deletes a record identified by its primary key (PK). */
112
115
  remove(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
113
116
  /** Deletes multiple records by their primary keys, returning the count of affected rows. */
114
- removeList(pks: PKType[], options?: MethodOptions<Entity, OrmTypes>): Promise<CountResult>;
117
+ removeList(pks: PKType[], options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<CountResult>;
115
118
  /** Partially updates an existing record by its primary key (PK). */
116
119
  patch(pk: PKType, obj: DeepPartial<Entity>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
117
120
  /**
@@ -121,15 +124,43 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
121
124
  */
122
125
  merge<U extends DeepPartial<Entity>>(pk: PKType, obj: U, options?: MethodOptions<Entity, OrmTypes>): Promise<(U & Entity) | null>;
123
126
  /** Returns the total number of records. */
124
- total(options?: MethodOptions<Entity, OrmTypes>): Promise<number>;
127
+ total(options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number>;
125
128
  /** Checks whether a record exists by its primary key (PK). */
126
- has(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<boolean>;
129
+ has(pk: PKType, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<boolean>;
127
130
  /** Marks a record as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
128
131
  softRemove(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
129
132
  /** 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>;
133
+ softRemoveList(pks: PKType[], options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<CountResult>;
131
134
  /** Restores a record previously marked as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
132
135
  restore(pk: PKType, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
133
136
  /** 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>;
137
+ restoreList(pks: PKType[], options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<CountResult>;
138
+ /**
139
+ * Atomically adds `value` to a numeric field of the record identified
140
+ * by `pk`, evaluated server-side against the row's current value (e.g.
141
+ * `saldo = saldo + value`) — not a fetch-then-save round trip.
142
+ */
143
+ increment<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
144
+ /** Same as {@link VSRepository.increment}, subtracting `value` instead of adding it. */
145
+ decrement<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
146
+ /** Same as {@link VSRepository.increment}, multiplying the field's current value by `value`. */
147
+ multiply<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
148
+ /**
149
+ * Same as {@link VSRepository.increment}, dividing the field's current
150
+ * value by `value`. Division-by-zero behavior depends on the adapter/
151
+ * underlying database (see {@link VSRepoAdapter.divideOne}).
152
+ */
153
+ divide<Field extends NumericKeys<Entity>>(pk: PKType, field: Field, value: NonNullable<Entity[Field]>, options?: MethodOptions<Entity, OrmTypes>): Promise<Entity>;
154
+ /**
155
+ * Returns the sum of a numeric field across every record matching
156
+ * `where` (all records if omitted), or `null` if none match — mirrors
157
+ * SQL's `SUM()`, which returns `NULL` (not `0`) over an empty set.
158
+ */
159
+ sum(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
160
+ /** Same as {@link VSRepository.sum}, but the arithmetic mean instead of the total. */
161
+ average(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
162
+ /** Same as {@link VSRepository.sum}, but the minimum value instead of the total. */
163
+ min(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
164
+ /** Same as {@link VSRepository.sum}, but the maximum value instead of the total. */
165
+ max(field: NumericKeys<Entity>, where?: VSRepoWhere<Entity>, options?: RestrictMethodOptions<Entity, OrmTypes>): Promise<number | null>;
135
166
  }
@@ -102,10 +102,12 @@ class VSRepository {
102
102
  wherePkIn(pks) {
103
103
  return { [this.pkName]: { in: pks } };
104
104
  }
105
- async execBaseMethod(fn, methodName, optionsUnchecked) {
106
- const optionsChecked = methodName === "getAll"
107
- ? this.validator.validateGetAllMethodOptions(optionsUnchecked)
108
- : this.validator.validateMethodOptions(optionsUnchecked);
105
+ async execBaseMethod(fn, methodName, optionsUnchecked, opsType = "common") {
106
+ const optionsChecked = opsType === "common"
107
+ ? this.validator.validateMethodOptions(optionsUnchecked)
108
+ : opsType === "restrict"
109
+ ? this.validator.validateRestrictMethodOptions(optionsUnchecked)
110
+ : this.validator.validateGetAllMethodOptions(optionsUnchecked);
109
111
  optionsChecked.db ??= this.getDbClient();
110
112
  const start = this.logger.startPerformLog("run " + methodName);
111
113
  try {
@@ -195,7 +197,7 @@ class VSRepository {
195
197
  return this.execBaseMethod((opt) => this.adapter.findMany(this.mergeWheresResolver.resolve(opt.see, {}), {
196
198
  ...opt,
197
199
  order: opt.order ?? this.defaultOrdering,
198
- }), "getAll", options);
200
+ }), "getAll", options, "getAll");
199
201
  }
200
202
  /** Creates or updates (upsert) a record. */
201
203
  async save(obj, options) {
@@ -217,7 +219,7 @@ class VSRepository {
217
219
  if (!Array.isArray(pks)) {
218
220
  this.fail("'pks' must be a valid array", vsrepo_error_type_enum_1.VSRepoErrorType.BASE);
219
221
  }
220
- return this.execBaseMethod(opt => this.adapter.deleteMany(this.mergeWheresResolver.resolve(opt.see, this.wherePkIn(pks)), opt), "removeList", options);
222
+ return this.execBaseMethod(opt => this.adapter.deleteMany(this.mergeWheresResolver.resolve(opt.see, this.wherePkIn(pks)), opt), "removeList", options, "restrict");
221
223
  }
222
224
  /** Partially updates an existing record by its primary key (PK). */
223
225
  async patch(pk, obj, options) {
@@ -233,11 +235,11 @@ class VSRepository {
233
235
  }
234
236
  /** Returns the total number of records. */
235
237
  async total(options) {
236
- return this.execBaseMethod(opt => this.adapter.count(this.mergeWheresResolver.resolve(opt.see, {}), opt), "total", options);
238
+ return this.execBaseMethod(opt => this.adapter.count(this.mergeWheresResolver.resolve(opt.see, {}), opt), "total", options, "restrict");
237
239
  }
238
240
  /** Checks whether a record exists by its primary key (PK). */
239
241
  async has(pk, options) {
240
- return this.execBaseMethod(opt => this.adapter.exists(this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "has", options);
242
+ return this.execBaseMethod(opt => this.adapter.exists(this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "has", options, "restrict");
241
243
  }
242
244
  /** Marks a record as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
243
245
  async softRemove(pk, options) {
@@ -256,7 +258,7 @@ class VSRepository {
256
258
  this.fail("'pks' must be a valid array", vsrepo_error_type_enum_1.VSRepoErrorType.BASE);
257
259
  }
258
260
  const key = this.softRemoveKey;
259
- return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: new Date() }, opt), "softRemoveList", options);
261
+ return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: new Date() }, opt), "softRemoveList", options, "restrict");
260
262
  }
261
263
  /** Restores a record previously marked as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
262
264
  async restore(pk, options) {
@@ -275,7 +277,59 @@ class VSRepository {
275
277
  this.fail("'pks' must be a valid array", vsrepo_error_type_enum_1.VSRepoErrorType.BASE);
276
278
  }
277
279
  const key = this.softRemoveKey;
278
- return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: null }, opt), "restoreList", options);
280
+ return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: null }, opt), "restoreList", options, "restrict");
281
+ }
282
+ /**
283
+ * Atomically adds `value` to a numeric field of the record identified
284
+ * by `pk`, evaluated server-side against the row's current value (e.g.
285
+ * `saldo = saldo + value`) — not a fetch-then-save round trip.
286
+ */
287
+ async increment(pk, field, value, options) {
288
+ this.validator.assertIsNumericLike(value);
289
+ return this.execBaseMethod(opt => this.adapter.incrementOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "increment", options);
290
+ }
291
+ /** Same as {@link VSRepository.increment}, subtracting `value` instead of adding it. */
292
+ async decrement(pk, field, value, options) {
293
+ this.validator.assertIsNumericLike(value);
294
+ return this.execBaseMethod(opt => this.adapter.decrementOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "decrement", options);
295
+ }
296
+ /** Same as {@link VSRepository.increment}, multiplying the field's current value by `value`. */
297
+ async multiply(pk, field, value, options) {
298
+ this.validator.assertIsNumericLike(value);
299
+ return this.execBaseMethod(opt => this.adapter.multiplyOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "multiply", options);
300
+ }
301
+ /**
302
+ * Same as {@link VSRepository.increment}, dividing the field's current
303
+ * value by `value`. Division-by-zero behavior depends on the adapter/
304
+ * underlying database (see {@link VSRepoAdapter.divideOne}).
305
+ */
306
+ async divide(pk, field, value, options) {
307
+ this.validator.assertIsNumericLike(value);
308
+ return this.execBaseMethod(opt => this.adapter.divideOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "divide", options);
309
+ }
310
+ /**
311
+ * Returns the sum of a numeric field across every record matching
312
+ * `where` (all records if omitted), or `null` if none match — mirrors
313
+ * SQL's `SUM()`, which returns `NULL` (not `0`) over an empty set.
314
+ */
315
+ async sum(field, where, options) {
316
+ const validatedWhere = this.validator.validateWhere(where ?? {});
317
+ return this.execBaseMethod(opt => this.adapter.sum(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "sum", options, "restrict");
318
+ }
319
+ /** Same as {@link VSRepository.sum}, but the arithmetic mean instead of the total. */
320
+ async average(field, where, options) {
321
+ const validatedWhere = this.validator.validateWhere(where ?? {});
322
+ return this.execBaseMethod(opt => this.adapter.average(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "average", options, "restrict");
323
+ }
324
+ /** Same as {@link VSRepository.sum}, but the minimum value instead of the total. */
325
+ async min(field, where, options) {
326
+ const validatedWhere = this.validator.validateWhere(where ?? {});
327
+ return this.execBaseMethod(opt => this.adapter.min(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "min", options, "restrict");
328
+ }
329
+ /** Same as {@link VSRepository.sum}, but the maximum value instead of the total. */
330
+ async max(field, where, options) {
331
+ const validatedWhere = this.validator.validateWhere(where ?? {});
332
+ return this.execBaseMethod(opt => this.adapter.max(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "max", options, "restrict");
279
333
  }
280
334
  }
281
335
  exports.VSRepository = VSRepository;
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export type { VSRepoOptions } from "./types/vsrepo/vsrepo-options.type.js";
13
13
  export type { VSRepoOrmTypes } from "./types/vsrepo/vsrepo-orm-types.type.js";
14
14
  export type { VSRepoArgs } from "./types/vsrepo/vsrepo-args.type.js";
15
15
  export type { VSRepoMethod } from "./types/vsrepo/vsrepo-method.type.js";
16
- export type { VSRepoWhere, VSRepoWherePlain, VSRepoFieldWhere, VSRepoFieldOperators } from "./types/vsrepo/vsrepo-where.type.js";
16
+ export type { VSRepoWhere, VSRepoWherePlain, VSRepoFieldWhere, VSRepoFieldOperators, } from "./types/vsrepo/vsrepo-where.type.js";
17
17
  export type { VSRepoRelations, RelationKeys } from "./types/vsrepo/vsrepo-relations.type.js";
18
18
  export type { VSRepoSelect } from "./types/vsrepo/vsrepo-select.type.js";
19
19
  export type { VSRepoTransactionOptions } from "./types/vsrepo/vsrepo-transaction-options.type.js";
@@ -31,4 +31,8 @@ export type { Pagination } from "./types/utils/pagination.type.js";
31
31
  export type { Primitive } from "./types/utils/primitive.type.js";
32
32
  export type { SeeMode } from "./types/utils/see-mode.type.js";
33
33
  export type { VSRepoQueryOptions } from "./types/vsrepo/vsrepo-query-options.type";
34
+ export type { DecimalLike } from "./types/utils/decimal-like.type.js";
35
+ export type { NumericKeys } from "./types/utils/numeric-keys.type.js";
36
+ export type { NumericLike } from "./types/utils/numeric-like.type.js";
37
+ export type { RestrictMethodOptions } from "./types/utils/restrict-method-options.type.js";
34
38
  export { VSLogger } from "./internal/utils/vs-logger.util.js";
@@ -8,6 +8,8 @@ import { Ordering } from "../../types/utils/ordering.type";
8
8
  import { VSLogger } from "../utils/vs-logger.util";
9
9
  import { VSRepoWhere } from "../../types/vsrepo/vsrepo-where.type";
10
10
  import { VSRepoQueryOptions } from "../../types/vsrepo/vsrepo-query-options.type";
11
+ import { NumericLike } from "../../types/utils/numeric-like.type";
12
+ import { RestrictMethodOptions } from "../../types/utils/restrict-method-options.type";
11
13
  export declare class VSRepoValidator<T, K, O extends VSRepoOrmTypes = VSRepoOrmTypes> {
12
14
  private logger?;
13
15
  setLogger(logger: VSLogger): void;
@@ -16,6 +18,8 @@ export declare class VSRepoValidator<T, K, O extends VSRepoOrmTypes = VSRepoOrmT
16
18
  validateConstructorOptions(options: unknown): VSRepoOptions<T, K>;
17
19
  private readonly methodOptionsSchema;
18
20
  validateMethodOptions(options?: unknown): MethodOptions<T, O>;
21
+ private readonly restrictMethodOptionsSchema;
22
+ validateRestrictMethodOptions(options?: unknown): RestrictMethodOptions<T, O>;
19
23
  private readonly getAllMethodOptionsSchema;
20
24
  validateGetAllMethodOptions(options?: unknown): MethodOptions<T, O> & {
21
25
  pagination?: Pagination;
@@ -30,4 +34,7 @@ export declare class VSRepoValidator<T, K, O extends VSRepoOrmTypes = VSRepoOrmT
30
34
  validateOrdering(value: unknown): Ordering<T>;
31
35
  validatePagination(value: unknown): Pagination;
32
36
  validateWhere(value: unknown): VSRepoWhere<T>;
37
+ private decimalLikeSchema;
38
+ private numericLikeSchema;
39
+ assertIsNumericLike(value: unknown): asserts value is NumericLike;
33
40
  }
@@ -89,6 +89,17 @@ class VSRepoValidator {
89
89
  }
90
90
  return parsed.output;
91
91
  }
92
+ restrictMethodOptionsSchema = v.object({
93
+ db: v.optional(v.any()),
94
+ see: v.optional(v.picklist(["active", "removed", "all"])),
95
+ });
96
+ validateRestrictMethodOptions(options) {
97
+ const parsed = v.safeParse(this.restrictMethodOptionsSchema, options ?? {});
98
+ if (!parsed.success) {
99
+ this.failValidation(parsed.issues[0], vsrepo_error_type_enum_1.VSRepoErrorType.VALIDATOR);
100
+ }
101
+ return parsed.output;
102
+ }
92
103
  getAllMethodOptionsSchema = v.object({
93
104
  ...this.methodOptionsSchema.entries,
94
105
  order: v.optional(ordering_schema_1.default),
@@ -156,5 +167,16 @@ class VSRepoValidator {
156
167
  }
157
168
  return parsed.output;
158
169
  }
170
+ decimalLikeSchema = v.looseObject({
171
+ toNumber: v.function(),
172
+ decimalPlaces: v.function(),
173
+ });
174
+ numericLikeSchema = v.union([v.number(), v.bigint(), this.decimalLikeSchema]);
175
+ assertIsNumericLike(value) {
176
+ const parsed = v.safeParse(this.numericLikeSchema, value);
177
+ if (!parsed.success) {
178
+ this.failValidation(parsed.issues[0], vsrepo_error_type_enum_1.VSRepoErrorType.VALIDATOR);
179
+ }
180
+ }
159
181
  }
160
182
  exports.VSRepoValidator = VSRepoValidator;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Structural shape of an arbitrary-precision "Decimal" value, as commonly
3
+ * returned by ORMs for `decimal`/`numeric` columns (e.g. Prisma's
4
+ * `Prisma.Decimal`, built on top of `decimal.js`).
5
+ *
6
+ * Matched structurally (duck-typed) instead of importing a concrete class,
7
+ * so the core stays ORM-agnostic — any object exposing both `toNumber()`
8
+ * and `decimalPlaces()` is treated as Decimal-like by {@link NumericLike}
9
+ * and, transitively, by {@link NumericKeys}.
10
+ *
11
+ * Note that several ORMs (e.g. Drizzle, MikroORM, TypeORM) represent
12
+ * `decimal`/`numeric` columns as plain `string` by default, to avoid
13
+ * floating-point precision loss — a `string` value does **not** satisfy
14
+ * `DecimalLike`. Configure the column in a numeric mode (or provide a
15
+ * transformer) on those ORMs if you want the field to be eligible for
16
+ * `increment`/`decrement`/`multiply`/`divide`/`sum`/`average`/`min`/`max`.
17
+ *
18
+ * @publicApi
19
+ */
20
+ export type DecimalLike = {
21
+ toNumber(): number;
22
+ decimalPlaces(): number;
23
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,24 @@
1
+ import { NumericLike } from "./numeric-like.type";
2
+ /**
3
+ * Extracts the keys of `T` whose (non-nullable) value type is assignable to
4
+ * {@link NumericLike} — i.e. the fields eligible as the `field` argument of
5
+ * `increment`/`decrement`/`multiply`/`divide`/`sum`/`average`/`min`/`max`.
6
+ *
7
+ * Nullable/optional numeric fields (e.g. `number | null`) ARE included —
8
+ * the `null`/`undefined` part is stripped before the check, it isn't a
9
+ * reason to exclude the field. This means a field that is currently `NULL`
10
+ * in the database can be targeted; be aware that in standard SQL, arithmetic
11
+ * against a `NULL` value (`NULL + 5`) itself stays `NULL` — this type only
12
+ * governs what compiles, not the row's runtime value.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * type Product = { id: string; price: Decimal; stock: number | null; name: string };
17
+ * type Numeric = NumericKeys<Product>; // "price" | "stock"
18
+ * ```
19
+ *
20
+ * @publicApi
21
+ */
22
+ export type NumericKeys<T> = {
23
+ [P in keyof T]: NonNullable<T[P]> extends NumericLike ? P : never;
24
+ }[keyof T];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
1
+ import { DecimalLike } from "./decimal-like.type";
2
+ /**
3
+ * Union of value types accepted as "numeric" by the atomic
4
+ * (`increment`/`decrement`/`multiply`/`divide`) and aggregate
5
+ * (`sum`/`average`/`min`/`max`) operations: a native `number`, a native
6
+ * `bigint`, or a {@link DecimalLike} object.
7
+ *
8
+ * @publicApi
9
+ */
10
+ export type NumericLike = number | bigint | DecimalLike;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,6 +1,7 @@
1
+ import { DecimalLike } from "./decimal-like.type";
1
2
  /**
2
3
  * Types treated as scalar (non-relation) values when walking an entity's shape.
3
4
  *
4
5
  * @publicApi
5
6
  */
6
- export type Primitive = string | number | boolean | bigint | symbol | undefined | null | Date;
7
+ export type Primitive = string | number | boolean | bigint | symbol | undefined | null | Date | DecimalLike;
@@ -0,0 +1,14 @@
1
+ import { VSRepoOrmTypes } from "../vsrepo/vsrepo-orm-types.type";
2
+ import { MethodOptions } from "./methods-options.type";
3
+ /**
4
+ * Narrowed variant of {@link MethodOptions} exposing only `db` and `see`.
5
+ *
6
+ * Used by base methods that don't shape/return an `Entity` — count-like
7
+ * operations (`total`, `has`, `sum`, `average`, `min`, `max`) and
8
+ * batch-delete-like operations (`removeList`, `softRemoveList`,
9
+ * `restoreList`) — where `select`/`relations` (which only make sense when
10
+ * an `Entity` is being returned) don't apply.
11
+ *
12
+ * @publicApi
13
+ */
14
+ export type RestrictMethodOptions<T, O extends VSRepoOrmTypes = VSRepoOrmTypes> = Pick<MethodOptions<T, O>, "db" | "see">;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vsrepo",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "ORM-agnostic repository pattern library with full TypeScript support and automatic type inference.",
5
5
  "homepage": "https://github.com/jaobrabo123/VSRepository#readme",
6
6
  "repository": {