@vsrepo/prisma7-adapter 1.0.3 → 1.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
@@ -33,6 +33,7 @@
33
33
  - [How each write method resolves relations](#how-each-write-method-resolves-relations)
34
34
  - [`relations` in method options (read)](#relations-in-method-options-read)
35
35
  - [`merge`](#merge)
36
+ - [Atomic and aggregation methods](#atomic-and-aggregation-methods)
36
37
  - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-dont-support-nested-writes)
37
38
  - [Provider-specific limitations](#provider-specific-limitations)
38
39
  - [Transactions](#transactions)
@@ -197,6 +198,31 @@ const prismaInclude = prismaSelect
197
198
 
198
199
  For to-many relations (`otm`/`mtm`), items in the stored record and items in `obj` are matched by the configured `pk`: a match merges the two items, a new `pk` (or no `pk`) is appended. Nothing is ever removed by `merge`.
199
200
 
201
+ ## Atomic and aggregation methods
202
+
203
+ > Requires `vsrepo` **2.1.0+** — these methods were added to the `VSRepoAdapter` contract in that release (see the [VSRepository CHANGELOG](https://github.com/jaobrabo123/VSRepository/blob/main/CHANGELOG.md#210---2026-09-04)).
204
+
205
+ The adapter implements the 8 abstract methods `VSRepository`'s `increment`/`decrement`/`multiply`/`divide`/`sum`/`average`/`min`/`max` delegate to: `incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max`.
206
+
207
+ - `incrementOne`/`decrementOne`/`multiplyOne`/`divideOne` translate directly into Prisma's native single-field atomic write — `data: { [field]: { increment: value } }` (and `decrement`/`multiply`/`divide`) — so the operation is evaluated **server-side** against the row's *current* value (`UPDATE ... SET field = field + value`), not as a fetch-then-save round trip on the client. Prisma's `update()` already returns the row reflecting the state *after* the write, so the adapter never issues a follow-up read.
208
+ - `sum`/`average`/`min`/`max` translate into Prisma's `aggregate()` with `_sum`/`_avg`/`_min`/`_max: { [field]: true }`. The raw result (`number`, `bigint`, a `Decimal` instance, or `null`) is normalized to `number | null` — `null` is returned as-is (mirroring SQL's `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL`, not `0`, over an empty set), and a `Decimal`/`DecimalLike` value is converted via its `.toNumber()`.
209
+
210
+ ```typescript
211
+ // Atomic — evaluated server-side, no read-modify-write:
212
+ await productRepository.increment(productId, "stock", 10);
213
+ await accountRepository.decrement(accountId, "balance", 50);
214
+ await productRepository.multiply(productId, "price", 1.1); // e.g. a 10% price bump
215
+ await productRepository.divide(productId, "price", 2);
216
+
217
+ // Aggregation — across every record matching an optional `where` (all if omitted):
218
+ const total = await productRepository.sum("price"); // number | null
219
+ const avgPrice = await productRepository.average("price", { active: true });
220
+ const cheapest = await productRepository.min("price");
221
+ const mostExpensive = await productRepository.max("price");
222
+ ```
223
+
224
+ **Decimal fields** (`Prisma.Decimal`) are fully supported as the `field` for all 8 methods — `value` for the atomic methods is passed straight through to Prisma (which accepts a `number`, `string`, or `Decimal`/`DecimalJsLike`), and the aggregate methods always normalize the result to a `number`, never a `Decimal` instance.
225
+
200
226
  ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes
201
227
 
202
228
  `createMany`, `createManyReturning`, `updateMany` and `updateManyReturning` only accept scalar fields in their `data`. If your payload includes a field configured in `relations` (regardless of its value), the adapter throws a `VSRepoPrisma7AdapterError` naming the offending field. For a full nested write, use `create`/`update`/`save` one record at a time, or wrap several `save` calls in a `saveMany`/`transaction`.
@@ -272,5 +298,5 @@ The adapter uses `VSLogger` (from `vsrepo`) internally: every method logs a `DEB
272
298
 
273
299
  ## Requirements
274
300
 
275
- - `vsrepo` v2
301
+ - `vsrepo` ^2.1.0
276
302
  - `@prisma/client` ^7.10.0
package/README.pt-BR.md CHANGED
@@ -33,6 +33,7 @@ Implementação de `VSRepoAdapter` para o [VSRepository v2](https://github.com/j
33
33
  - [Como cada método de escrita resolve relations](#como-cada-método-de-escrita-resolve-relations)
34
34
  - [`relations` nas options (leitura)](#relations-nas-options-leitura)
35
35
  - [`merge`](#merge)
36
+ - [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação)
36
37
  - [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-não-suportam-nested-writes)
37
38
  - [Limitações por provider](#limitações-por-provider)
38
39
  - [Transactions](#transactions)
@@ -197,6 +198,31 @@ const prismaInclude = prismaSelect
197
198
 
198
199
  Pra relations to-many (`otm`/`mtm`), os itens do registro salvo e os itens de `obj` são casados pela `pk` configurada: um match faz merge dos dois itens, uma `pk` nova (ou sem `pk`) é apenas adicionada. `merge` nunca remove nada.
199
200
 
201
+ ## Métodos atômicos e de agregação
202
+
203
+ > Requer `vsrepo` **2.1.0+** — esses métodos foram adicionados ao contrato do `VSRepoAdapter` nessa versão (veja o [CHANGELOG do VSRepository](https://github.com/jaobrabo123/VSRepository/blob/main/CHANGELOG.md#210---2026-09-04-português)).
204
+
205
+ O adapter implementa os 8 métodos abstratos pros quais `increment`/`decrement`/`multiply`/`divide`/`sum`/`average`/`min`/`max` do `VSRepository` delegam: `incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max`.
206
+
207
+ - `incrementOne`/`decrementOne`/`multiplyOne`/`divideOne` traduzem direto pra escrita atômica nativa de campo único do Prisma — `data: { [field]: { increment: value } }` (e `decrement`/`multiply`/`divide`) — então a operação é avaliada **server-side** contra o valor *atual* do registro (`UPDATE ... SET field = field + value`), e não como um fetch-then-save no cliente. O `update()` do Prisma já retorna a linha refletindo o estado *após* a escrita, então o adapter nunca precisa fazer uma leitura extra.
208
+ - `sum`/`average`/`min`/`max` traduzem pro `aggregate()` do Prisma, com `_sum`/`_avg`/`_min`/`_max: { [field]: true }`. O resultado bruto (`number`, `bigint`, uma instância de `Decimal`, ou `null`) é normalizado pra `number | null` — `null` é repassado como está (espelhando o `SUM()`/`AVG()`/`MIN()`/`MAX()` do SQL, que retornam `NULL`, não `0`, sobre um conjunto vazio), e um valor `Decimal`/`DecimalLike` é convertido via seu `.toNumber()`.
209
+
210
+ ```typescript
211
+ // Atômico — avaliado server-side, sem read-modify-write:
212
+ await productRepository.increment(productId, "stock", 10);
213
+ await accountRepository.decrement(accountId, "balance", 50);
214
+ await productRepository.multiply(productId, "price", 1.1); // ex: um reajuste de 10% no preço
215
+ await productRepository.divide(productId, "price", 2);
216
+
217
+ // Agregação — entre todos os registros que casam com um `where` opcional (todos se omitido):
218
+ const total = await productRepository.sum("price"); // number | null
219
+ const avgPrice = await productRepository.average("price", { active: true });
220
+ const cheapest = await productRepository.min("price");
221
+ const mostExpensive = await productRepository.max("price");
222
+ ```
223
+
224
+ **Campos Decimal** (`Prisma.Decimal`) são totalmente suportados como o `field` dos 8 métodos — o `value` dos métodos atômicos é repassado direto pro Prisma (que aceita `number`, `string`, ou `Decimal`/`DecimalJsLike`), e os métodos de agregação sempre normalizam o resultado pra `number`, nunca uma instância de `Decimal`.
225
+
200
226
  ## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` não suportam nested writes
201
227
 
202
228
  `createMany`, `createManyReturning`, `updateMany` e `updateManyReturning` só aceitam campos escalares no `data`. Se seu payload incluir um campo configurado em `relations` (independente do valor), o adapter lança um `VSRepoPrisma7AdapterError` apontando o campo problemático. Pra um nested write completo, use `create`/`update`/`save` registro por registro, ou envolva várias chamadas de `save` num `saveMany`/`transaction`.
@@ -272,5 +298,5 @@ O adapter usa o `VSLogger` (de `vsrepo`) internamente: todo método loga uma lin
272
298
 
273
299
  ## Requisitos
274
300
 
275
- - `vsrepo` v2
301
+ - `vsrepo` ^2.1.0
276
302
  - `@prisma/client` ^7.10.0
@@ -1,4 +1,4 @@
1
- import { AdapterMethodOptions, AdapterQueryOptions, CountResult, DeepPartial, VSRepoAdapter, VSRepoWhere } from "vsrepo";
1
+ import { AdapterMethodOptions, AdapterQueryOptions, CountResult, DeepPartial, NumericKeys, VSRepoAdapter, VSRepoWhere } from "vsrepo";
2
2
  import { VSRepoPrisma7AdapterConfig } from "./types/adapter-config.type";
3
3
  /**
4
4
  * `VSRepoAdapter` implementation for Prisma 7. Translates every method of the
@@ -50,6 +50,35 @@ export declare class VSRepoPrisma7Adapter<T> extends VSRepoAdapter<T> {
50
50
  * started on it (or on `this.prisma` when `db` isn't provided at all).
51
51
  */
52
52
  private runTransactional;
53
+ /**
54
+ * Runs a single-field atomic update (`increment`/`decrement`/`multiply`/
55
+ * `divide`) via Prisma's native `{ field: { <op>: value } }` write —
56
+ * evaluated server-side against the row's current value, never as a
57
+ * client-side read-modify-write. `value` is passed through as-is:
58
+ * Prisma accepts a `number`, `bigint`, or any `Decimal`/`DecimalJsLike`
59
+ * value (which covers a {@link DecimalLike} like `Prisma.Decimal`)
60
+ * depending on the target column's type.
61
+ *
62
+ * Prisma's `update()` already returns the row reflecting the state
63
+ * *after* the write, so no follow-up read is needed here.
64
+ */
65
+ private atomicUpdate;
66
+ /**
67
+ * Converts a raw value returned by Prisma's `aggregate()` (`number`,
68
+ * `bigint`, a `Decimal` instance, or `null` when no record matches)
69
+ * into the `number | null` shape required by the `VSRepoAdapter`
70
+ * aggregate contract.
71
+ */
72
+ private toNullableNumber;
73
+ /**
74
+ * Runs a single-field aggregation (`sum`/`avg`/`min`/`max`) via Prisma's
75
+ * `aggregate()`, across every record matching `where` (all records if
76
+ * empty). Returns `null` when no record matches — mirroring SQL's
77
+ * `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL` (not `0`) over an
78
+ * empty set — instead of Prisma's own `_sum`/`_avg`/`_min`/`_max: null`
79
+ * shape.
80
+ */
81
+ private aggregate;
53
82
  /** Returns the underlying ORM client instance used outside of transactions. */
54
83
  getDbClient(): any;
55
84
  /** Runs `fn` inside a native transaction of the underlying ORM/database. */
@@ -130,4 +159,32 @@ export declare class VSRepoPrisma7Adapter<T> extends VSRepoAdapter<T> {
130
159
  merge<K>(where: VSRepoWhere<T>, obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<K & T>;
131
160
  /** Creates a record if none matches `where`, otherwise updates it. */
132
161
  upsert(where: VSRepoWhere<T>, create: DeepPartial<T>, update: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
162
+ /**
163
+ * Atomically adds `value` to `field` on the record matching `where`,
164
+ * via Prisma's native `{ field: { increment: value } }` write.
165
+ */
166
+ incrementOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
167
+ /** Same as {@link VSRepoPrisma7Adapter.incrementOne}, subtracting `value` instead of adding it. */
168
+ decrementOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
169
+ /** Same as {@link VSRepoPrisma7Adapter.incrementOne}, multiplying the field's current value by `value`. */
170
+ multiplyOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
171
+ /**
172
+ * Same as {@link VSRepoPrisma7Adapter.incrementOne}, dividing the
173
+ * field's current value by `value`. Division-by-zero is not handled by
174
+ * the adapter — it's whatever Postgres does natively (raises a
175
+ * `division_by_zero` error for integer columns; `Infinity`/`NaN` are not
176
+ * representable in a numeric/decimal column, so those also error).
177
+ */
178
+ divideOne<K extends NumericKeys<T>>(field: K, value: NonNullable<T[K]>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
179
+ /**
180
+ * Returns the sum of `field` across every record matching `where` (all
181
+ * records if `where` is empty), or `null` if no record matches.
182
+ */
183
+ sum(field: NumericKeys<T>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
184
+ /** Same as {@link VSRepoPrisma7Adapter.sum}, but the arithmetic mean instead of the total. */
185
+ average(field: NumericKeys<T>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
186
+ /** Same as {@link VSRepoPrisma7Adapter.sum}, but the minimum value instead of the total. */
187
+ min(field: NumericKeys<T>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
188
+ /** Same as {@link VSRepoPrisma7Adapter.sum}, but the maximum value instead of the total. */
189
+ max(field: NumericKeys<T>, where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number | null>;
133
190
  }
@@ -106,6 +106,79 @@ class VSRepoPrisma7Adapter extends vsrepo_1.VSRepoAdapter {
106
106
  }
107
107
  return (db ?? this.prisma).$transaction(fn);
108
108
  }
109
+ /**
110
+ * Runs a single-field atomic update (`increment`/`decrement`/`multiply`/
111
+ * `divide`) via Prisma's native `{ field: { <op>: value } }` write —
112
+ * evaluated server-side against the row's current value, never as a
113
+ * client-side read-modify-write. `value` is passed through as-is:
114
+ * Prisma accepts a `number`, `bigint`, or any `Decimal`/`DecimalJsLike`
115
+ * value (which covers a {@link DecimalLike} like `Prisma.Decimal`)
116
+ * depending on the target column's type.
117
+ *
118
+ * Prisma's `update()` already returns the row reflecting the state
119
+ * *after* the write, so no follow-up read is needed here.
120
+ */
121
+ async atomicUpdate(operation, field, value, where, options) {
122
+ const start = this.logger.startPerformLog(`run ${operation}One`);
123
+ try {
124
+ const arg = {
125
+ ...this.resolveReadArg(options),
126
+ where: (0, where_parser_1.parsePrismaWhere)(where),
127
+ data: { [field]: { [operation]: value } },
128
+ };
129
+ this.logger.logDebug(`Resolved Prisma arg for '${operation}One'`, arg);
130
+ return await this.getPrismaRepository(options?.db).update(arg);
131
+ }
132
+ catch (error) {
133
+ throw (0, map_prisma_error_resolver_1.mapPrismaError)(error, `${operation}One`);
134
+ }
135
+ finally {
136
+ this.logger.endPerformLog(start);
137
+ }
138
+ }
139
+ /**
140
+ * Converts a raw value returned by Prisma's `aggregate()` (`number`,
141
+ * `bigint`, a `Decimal` instance, or `null` when no record matches)
142
+ * into the `number | null` shape required by the `VSRepoAdapter`
143
+ * aggregate contract.
144
+ */
145
+ toNullableNumber(value) {
146
+ if (value === null || value === undefined)
147
+ return null;
148
+ if (typeof value === "bigint")
149
+ return Number(value);
150
+ if (typeof value?.toNumber === "function") {
151
+ return value.toNumber();
152
+ }
153
+ return Number(value);
154
+ }
155
+ /**
156
+ * Runs a single-field aggregation (`sum`/`avg`/`min`/`max`) via Prisma's
157
+ * `aggregate()`, across every record matching `where` (all records if
158
+ * empty). Returns `null` when no record matches — mirroring SQL's
159
+ * `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL` (not `0`) over an
160
+ * empty set — instead of Prisma's own `_sum`/`_avg`/`_min`/`_max: null`
161
+ * shape.
162
+ */
163
+ async aggregate(operation, field, where, options) {
164
+ const start = this.logger.startPerformLog(`run ${operation}`);
165
+ const aggKey = `_${operation}`;
166
+ try {
167
+ const arg = {
168
+ where: (0, where_parser_1.parsePrismaWhere)(where),
169
+ [aggKey]: { [field]: true },
170
+ };
171
+ this.logger.logDebug(`Resolved Prisma arg for '${operation}'`, arg);
172
+ const result = await this.getPrismaRepository(options?.db).aggregate(arg);
173
+ return this.toNullableNumber(result?.[aggKey]?.[field]);
174
+ }
175
+ catch (error) {
176
+ throw (0, map_prisma_error_resolver_1.mapPrismaError)(error, operation);
177
+ }
178
+ finally {
179
+ this.logger.endPerformLog(start);
180
+ }
181
+ }
109
182
  /** Returns the underlying ORM client instance used outside of transactions. */
110
183
  getDbClient() {
111
184
  return this.prisma;
@@ -509,5 +582,49 @@ class VSRepoPrisma7Adapter extends vsrepo_1.VSRepoAdapter {
509
582
  this.logger.endPerformLog(start);
510
583
  }
511
584
  }
585
+ /**
586
+ * Atomically adds `value` to `field` on the record matching `where`,
587
+ * via Prisma's native `{ field: { increment: value } }` write.
588
+ */
589
+ async incrementOne(field, value, where, options) {
590
+ return this.atomicUpdate("increment", field, value, where, options);
591
+ }
592
+ /** Same as {@link VSRepoPrisma7Adapter.incrementOne}, subtracting `value` instead of adding it. */
593
+ async decrementOne(field, value, where, options) {
594
+ return this.atomicUpdate("decrement", field, value, where, options);
595
+ }
596
+ /** Same as {@link VSRepoPrisma7Adapter.incrementOne}, multiplying the field's current value by `value`. */
597
+ async multiplyOne(field, value, where, options) {
598
+ return this.atomicUpdate("multiply", field, value, where, options);
599
+ }
600
+ /**
601
+ * Same as {@link VSRepoPrisma7Adapter.incrementOne}, dividing the
602
+ * field's current value by `value`. Division-by-zero is not handled by
603
+ * the adapter — it's whatever Postgres does natively (raises a
604
+ * `division_by_zero` error for integer columns; `Infinity`/`NaN` are not
605
+ * representable in a numeric/decimal column, so those also error).
606
+ */
607
+ async divideOne(field, value, where, options) {
608
+ return this.atomicUpdate("divide", field, value, where, options);
609
+ }
610
+ /**
611
+ * Returns the sum of `field` across every record matching `where` (all
612
+ * records if `where` is empty), or `null` if no record matches.
613
+ */
614
+ async sum(field, where, options) {
615
+ return this.aggregate("sum", field, where, options);
616
+ }
617
+ /** Same as {@link VSRepoPrisma7Adapter.sum}, but the arithmetic mean instead of the total. */
618
+ async average(field, where, options) {
619
+ return this.aggregate("avg", field, where, options);
620
+ }
621
+ /** Same as {@link VSRepoPrisma7Adapter.sum}, but the minimum value instead of the total. */
622
+ async min(field, where, options) {
623
+ return this.aggregate("min", field, where, options);
624
+ }
625
+ /** Same as {@link VSRepoPrisma7Adapter.sum}, but the maximum value instead of the total. */
626
+ async max(field, where, options) {
627
+ return this.aggregate("max", field, where, options);
628
+ }
512
629
  }
513
630
  exports.VSRepoPrisma7Adapter = VSRepoPrisma7Adapter;
@@ -2,6 +2,12 @@ import { PrismaArgLike } from "./prisma-arg-like.type";
2
2
  type AnyFn = (arg: PrismaArgLike) => Promise<any>;
3
3
  /** `count` do Prisma resolve pra um `number` (na forma simples, sem `select` de agregação). */
4
4
  type CountFn = (arg: PrismaArgLike) => Promise<number>;
5
+ /**
6
+ * `aggregate` do Prisma resolve pra um objeto com uma chave por operação
7
+ * pedida (`_sum`/`_avg`/`_min`/`_max`/`_count`), cada uma mapeando campo ->
8
+ * valor agregado (`number | bigint | Decimal | null`).
9
+ */
10
+ type AggregateFn = (arg: PrismaArgLike & Record<string, Record<string, boolean>>) => Promise<Record<string, Record<string, unknown> | undefined>>;
5
11
  export type PrismaRepositoryLike = {
6
12
  findFirst: AnyFn;
7
13
  findFirstOrThrow: AnyFn;
@@ -16,5 +22,6 @@ export type PrismaRepositoryLike = {
16
22
  delete: AnyFn;
17
23
  deleteMany: AnyFn;
18
24
  count: CountFn;
25
+ aggregate: AggregateFn;
19
26
  };
20
27
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsrepo/prisma7-adapter",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "VSRepository adapter implementation backed by Prisma 7, translating every VSRepository operation into Prisma Client calls.",
5
5
  "homepage": "https://github.com/jaobrabo123/VSRepoPrisma7Adapter#readme",
6
6
  "repository": {
@@ -49,7 +49,7 @@
49
49
  "license": "MIT",
50
50
  "peerDependencies": {
51
51
  "@prisma/client": "^7.10.0",
52
- "vsrepo": "^2.0.0"
52
+ "vsrepo": "^2.1.0"
53
53
  },
54
54
  "dependencies": {
55
55
  "deepmerge": "^4.3.1",