@vsrepo/prisma7-adapter 1.0.1 → 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`.
@@ -236,11 +262,41 @@ await userRepository.transaction(async tx => {
236
262
  });
237
263
  ```
238
264
 
265
+ ### Concurrency in `deleteManyReturning`
266
+
267
+ `deleteManyReturning` runs a `findMany` on the given `where` (to capture the records it will return) and then **re-applies the same `where`** to a `deleteMany`. Prisma has no native `deleteManyAndReturn`, which is why the delete is driven by the `where` instead of by the records fetched in the `findMany`.
268
+
269
+ Because of this two-step shape, a concurrent change between the `findMany` and the `deleteMany` can make them diverge:
270
+
271
+ - a row **inserted after** the `findMany` that matches the `where` will still be deleted, even though it wasn't returned;
272
+ - a row that **stops matching** the `where` before the `deleteMany` runs won't be deleted, even though it was returned.
273
+
274
+ In other words, the returned records and the rows actually deleted are not guaranteed to be identical under concurrency.
275
+
276
+ If you need to guarantee there are no concurrency issues, run this method inside a `repository.transaction()` at a higher isolation level — e.g. `SERIALIZABLE`:
277
+
278
+ ```typescript
279
+ import { VSRepository, TransactionIsolationLevel } from "vsrepo";
280
+
281
+ await userRepository.transaction(async tx => {
282
+ // Atomic: no concurrent insert/update can slip between the findMany
283
+ // and the deleteMany, since SERIALIZABLE isolates this transaction.
284
+ // `deleteManyReturning` is reached through the dynamic method you mapped
285
+ // to the adapter (e.g. a `@DynamicMethod()` exposing `deleteManyReturning`),
286
+ // always passing `{ db: tx }` so it runs inside this transaction.
287
+ const deleted = await userRepository.deleteManyReturningByIdIn([...], { db: tx });
288
+ }, {
289
+ isolationLevel: TransactionIsolationLevel.SERIALIZABLE,
290
+ });
291
+ ```
292
+
293
+ > The isolation level applies only when the repository call actually runs **inside** that transaction (i.e. with `{ db: tx }`). When `deleteManyReturning` starts its own internal transaction (no `options.db` passed), it falls back to the default isolation level.
294
+
239
295
  ## Logging
240
296
 
241
297
  The adapter uses `VSLogger` (from `vsrepo`) internally: every method logs a `DEBUG` line with the resolved Prisma arg, and start/end performance logs (surfacing a `WARN` for slow operations). Set `logLevel` in the constructor config to control verbosity; it defaults to `VSLogLevel.WARN`.
242
298
 
243
299
  ## Requirements
244
300
 
245
- - `vsrepo` v2
301
+ - `vsrepo` ^2.1.0
246
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`.
@@ -236,11 +262,41 @@ await userRepository.transaction(async tx => {
236
262
  });
237
263
  ```
238
264
 
265
+ ### Concorrência em `deleteManyReturning`
266
+
267
+ O `deleteManyReturning` roda um `findMany` no `where` informado (pra capturar os registros que vai devolver) e depois **re-aplica o mesmo `where`** num `deleteMany`. Como o Prisma não tem um `deleteManyAndReturn` nativo, o delete é guiado pelo `where` — e não pelos registros retornados no `findMany`.
268
+
269
+ Por causa desse formato em duas etapas, uma alteração concorrente entre o `findMany` e o `deleteMany` pode fazer os dois divergirem:
270
+
271
+ - uma linha **inserida depois** do `findMany` que bate com o `where` ainda será deletada, mesmo não tendo sido retornada;
272
+ - uma linha que **deixa de bater** com o `where` antes do `deleteMany` rodar não será deletada, mesmo tendo sido retornada.
273
+
274
+ Ou seja: os registros retornados e as linhas realmente deletadas não têm garantia de serem idênticos sob concorrência.
275
+
276
+ Se você quiser garantir que não haverá problemas de concorrência, rode esse método **dentro** de um `repository.transaction()` num nível de isolamento mais alto — ex.: `SERIALIZABLE`:
277
+
278
+ ```typescript
279
+ import { VSRepository, TransactionIsolationLevel } from "vsrepo";
280
+
281
+ await userRepository.transaction(async tx => {
282
+ // Atômico: nenhum insert/update concorrente consegue entrar entre o
283
+ // findMany e o deleteMany, pois o SERIALIZABLE isola esta transaction.
284
+ // O `deleteManyReturning` é acessado através do método dinâmico que você
285
+ // mapeou pro adapter (ex.: um `@DynamicMethod()` expondo `deleteManyReturning`),
286
+ // sempre passando `{ db: tx }` pra rodar dentro desta transaction.
287
+ const deleted = await userRepository.deleteManyReturningByIdIn([...], { db: tx });
288
+ }, {
289
+ isolationLevel: TransactionIsolationLevel.SERIALIZABLE,
290
+ });
291
+ ```
292
+
293
+ > O nível de isolamento só se aplica quando a chamada roda **dentro** daquela transaction (ou seja, com `{ db: tx }`). Quando o `deleteManyReturning` inicia a própria transaction interna (sem `options.db`), ele usa o nível de isolamento padrão.
294
+
239
295
  ## Logging
240
296
 
241
297
  O adapter usa o `VSLogger` (de `vsrepo`) internamente: todo método loga uma linha `DEBUG` com o arg resolvido do Prisma, além de logs de performance de início/fim (com `WARN` pra operações lentas). Configure `logLevel` na config do construtor pra controlar a verbosidade; o default é `VSLogLevel.WARN`.
242
298
 
243
299
  ## Requisitos
244
300
 
245
- - `vsrepo` v2
301
+ - `vsrepo` ^2.1.0
246
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. */
@@ -79,10 +108,18 @@ export declare class VSRepoPrisma7Adapter<T> extends VSRepoAdapter<T> {
79
108
  deleteMany(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<CountResult>;
80
109
  /**
81
110
  * Deletes every record matching `where`, returning the deleted records.
82
- * Prisma has no `deleteManyAndReturn`, so this fetches then deletes inside
83
- * a transaction — reusing `options.db` when it's already a transaction
84
- * client (see `runTransactional`), so it stays atomic even when it's not
85
- * the one starting the transaction.
111
+ *
112
+ * Prisma has no `deleteManyAndReturn`, so this does a `findMany` on
113
+ * `where` first and then re-applies the same `where` to a `deleteMany`,
114
+ * both inside a transaction (`runTransactional`).
115
+ *
116
+ * Because the delete is driven by the same `where` (not by the fetched
117
+ * records), a concurrent change between the two operations can make them
118
+ * diverge — e.g. a row inserted after the `findMany` matching the `where`
119
+ * would still be deleted, while a row changed to stop matching wouldn't.
120
+ * If you need to guarantee no concurrency issues, run this method inside a
121
+ * `repository.transaction()` with a higher isolation level, e.g.
122
+ * `TransactionIsolationLevel.SERIALIZABLE`.
86
123
  */
87
124
  deleteManyReturning(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T[]>;
88
125
  /** Updates a single record matching `where`. */
@@ -122,4 +159,32 @@ export declare class VSRepoPrisma7Adapter<T> extends VSRepoAdapter<T> {
122
159
  merge<K>(where: VSRepoWhere<T>, obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<K & T>;
123
160
  /** Creates a record if none matches `where`, otherwise updates it. */
124
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>;
125
190
  }
@@ -40,7 +40,7 @@ class VSRepoPrisma7Adapter extends vsrepo_1.VSRepoAdapter {
40
40
  this.tableName = validated.tableName;
41
41
  this.pkName = validated.pkName;
42
42
  this.relations = validated.relations;
43
- this.logger = new vsrepo_1.VSLogger(validated.logLevel ?? vsrepo_1.VSLogLevel.WARN, this.constructor.name + "Logger", validated.logSlowThresholdMs);
43
+ this.logger = new vsrepo_1.VSLogger(validated.logLevel ?? vsrepo_1.VSLogLevel.WARN, this.constructor.name + "Logger", validated.logSlowThresholdMs ?? 300);
44
44
  this.logger.logInfo(`${this.constructor.name} initialized for table '${this.tableName}' (pkName: '${this.pkName}'${this.relations ? `, relations: [${Object.keys(this.relations).join(", ")}]` : ""})`);
45
45
  }
46
46
  getPrismaRepository(db) {
@@ -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;
@@ -241,10 +314,18 @@ class VSRepoPrisma7Adapter extends vsrepo_1.VSRepoAdapter {
241
314
  }
242
315
  /**
243
316
  * Deletes every record matching `where`, returning the deleted records.
244
- * Prisma has no `deleteManyAndReturn`, so this fetches then deletes inside
245
- * a transaction — reusing `options.db` when it's already a transaction
246
- * client (see `runTransactional`), so it stays atomic even when it's not
247
- * the one starting the transaction.
317
+ *
318
+ * Prisma has no `deleteManyAndReturn`, so this does a `findMany` on
319
+ * `where` first and then re-applies the same `where` to a `deleteMany`,
320
+ * both inside a transaction (`runTransactional`).
321
+ *
322
+ * Because the delete is driven by the same `where` (not by the fetched
323
+ * records), a concurrent change between the two operations can make them
324
+ * diverge — e.g. a row inserted after the `findMany` matching the `where`
325
+ * would still be deleted, while a row changed to stop matching wouldn't.
326
+ * If you need to guarantee no concurrency issues, run this method inside a
327
+ * `repository.transaction()` with a higher isolation level, e.g.
328
+ * `TransactionIsolationLevel.SERIALIZABLE`.
248
329
  */
249
330
  async deleteManyReturning(where, options) {
250
331
  const start = this.logger.startPerformLog("run deleteManyReturning");
@@ -501,5 +582,49 @@ class VSRepoPrisma7Adapter extends vsrepo_1.VSRepoAdapter {
501
582
  this.logger.endPerformLog(start);
502
583
  }
503
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
+ }
504
629
  }
505
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 {};
@@ -10,4 +10,5 @@ export declare const adapterConfigSchema: v.ObjectSchema<{
10
10
  readonly nullable: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
11
11
  }, undefined>, undefined>, undefined>;
12
12
  readonly logLevel: v.OptionalSchema<v.EnumSchema<typeof VSLogLevel, undefined>, undefined>;
13
+ readonly logSlowThresholdMs: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
13
14
  }, undefined>;
@@ -42,4 +42,5 @@ exports.adapterConfigSchema = v.object({
42
42
  pkName: v.pipe(v.string(), v.minLength(1)),
43
43
  relations: v.optional(v.record(v.string(), relation_schema_1.relationSchema)),
44
44
  logLevel: v.optional(v.enum(vsrepo_1.VSLogLevel)),
45
+ logSlowThresholdMs: v.optional(v.pipe(v.number(), v.gtValue(0))),
45
46
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsrepo/prisma7-adapter",
3
- "version": "1.0.1",
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",