@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 +57 -1
- package/README.pt-BR.md +57 -1
- package/dist/prisma7.adapter.d.ts +70 -5
- package/dist/prisma7.adapter.js +130 -5
- package/dist/types/prisma-repository-like.type.d.ts +7 -0
- package/dist/validators/adapter-config.schema.d.ts +1 -0
- package/dist/validators/adapter-config.schema.js +1 -0
- package/package.json +2 -2
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`
|
|
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`
|
|
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
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
}
|
package/dist/prisma7.adapter.js
CHANGED
|
@@ -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
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
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
|
|
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.
|
|
52
|
+
"vsrepo": "^2.1.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"deepmerge": "^4.3.1",
|