vsrepo 2.5.0 β 2.6.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/CHANGELOG.md +587 -531
- package/LICENSE +20 -20
- package/README.md +220 -1243
- package/README.pt-BR.md +220 -1246
- package/dist/VSRepoAdapter.js.map +1 -1
- package/dist/VSRepository.d.ts +43 -1
- package/dist/VSRepository.js +61 -9
- package/dist/VSRepository.js.map +1 -1
- package/dist/decorators/dynamic-method.decorator.d.ts +1 -1
- package/dist/decorators/dynamic-method.decorator.js +2 -4
- package/dist/decorators/dynamic-method.decorator.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/enums/vsrepo-error-type.enum.d.ts +3 -1
- package/dist/internal/enums/vsrepo-error-type.enum.js +2 -0
- package/dist/internal/enums/vsrepo-error-type.enum.js.map +1 -1
- package/dist/internal/resolvers/dynamic-methods.resolver.d.ts +5 -0
- package/dist/internal/resolvers/dynamic-methods.resolver.js +163 -98
- package/dist/internal/resolvers/dynamic-methods.resolver.js.map +1 -1
- package/dist/internal/utils/vs-logger.util.js.map +1 -1
- package/dist/internal/utils/vs-query-builder.util.d.ts +235 -0
- package/dist/internal/utils/vs-query-builder.util.js +413 -0
- package/dist/internal/utils/vs-query-builder.util.js.map +1 -0
- package/dist/internal/utils/with-db.util.js.map +1 -1
- package/dist/internal/validators/decorators.validator.js +2 -6
- package/dist/internal/validators/decorators.validator.js.map +1 -1
- package/dist/internal/validators/schemas/pagination.schema.d.ts +2 -2
- package/dist/internal/validators/schemas/pagination.schema.js +2 -2
- package/dist/internal/validators/schemas/pagination.schema.js.map +1 -1
- package/dist/internal/validators/schemas/relations.schema.d.ts +4 -0
- package/dist/internal/validators/schemas/relations.schema.js +39 -0
- package/dist/internal/validators/schemas/relations.schema.js.map +1 -0
- package/dist/internal/validators/schemas/see-mode.schema.d.ts +3 -0
- package/dist/internal/validators/schemas/see-mode.schema.js +38 -0
- package/dist/internal/validators/schemas/see-mode.schema.js.map +1 -0
- package/dist/internal/validators/schemas/select.schema.d.ts +4 -0
- package/dist/internal/validators/schemas/select.schema.js +39 -0
- package/dist/internal/validators/schemas/select.schema.js.map +1 -0
- package/dist/internal/validators/vsrepo.validator.js +6 -5
- package/dist/internal/validators/vsrepo.validator.js.map +1 -1
- package/dist/types/dynamic-methods/dynamic-method-info.type.d.ts +1 -0
- package/dist/types/utils/query-args.type.d.ts +1 -4
- package/dist/types/vsrepo/vsrepo-options.type.d.ts +9 -0
- package/package.json +87 -80
package/README.md
CHANGED
|
@@ -1,1243 +1,220 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<img src="https://res.cloudinary.com/ddbfifdxd/image/upload/w_200,q_auto,f_auto/v1786386427/VS_logo_TextoAbaixo_yev4tq.png" alt="VSRepository Logo" width="200"/>
|
|
3
|
-
|
|
4
|
-
<p style="margin-top: 12px;">
|
|
5
|
-
<img src="https://img.shields.io/npm/v/vsrepo?style=flat-square" alt="npm version"/>
|
|
6
|
-
<img src="https://img.shields.io/npm/l/vsrepo?style=flat-square" alt="npm license"/>
|
|
7
|
-
<img src="https://img.shields.io/npm/dt/vsrepo?style=flat-square" alt="npm downloads"/>
|
|
8
|
-
<img src="https://img.shields.io/badge/inspired%20by-JpaRepository-E73121?style=flat-square" alt="inspired by JpaRepository"/>
|
|
9
|
-
</p>
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
# VSRepository
|
|
13
|
-
|
|
14
|
-
πΊπΈ You're reading the English version. [π§π· Ler em portuguΓͺs](./README.pt-BR.md)
|
|
15
|
-
|
|
16
|
-
**ORM-agnostic** repository pattern library, with full **TypeScript** support and automatic **type inference**.
|
|
17
|
-
|
|
18
|
-
VSRepository lets you create strongly-typed repositories with:
|
|
19
|
-
|
|
20
|
-
- Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `merge`, `getAll`, `total`, `has`
|
|
21
|
-
- **Native soft-delete**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
|
|
22
|
-
- **Dynamic methods** inferred from a `declare` field name via the `@DynamicMethod` decorator: `
|
|
23
|
-
- **Raw SQL query methods** via the `@QueryMethod` decorator, bypassing the name-parsing engine entirely
|
|
24
|
-
- Ad-hoc **`select`/`relations`** per call β no more pre-declared named projections
|
|
25
|
-
- **Type safety** across 100% of operations
|
|
26
|
-
- Native ORM **transactions**, shared across repositories
|
|
27
|
-
- An **ORM-agnostic core** β the same repository class works with any `VSRepoAdapter` implementation
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
##
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
## Base methods
|
|
223
|
-
|
|
224
|
-
Available automatically on every `VSRepository` subclass:
|
|
225
|
-
|
|
226
|
-
| Method | Description |
|
|
227
|
-
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
228
|
-
| `get(pk, options?)` | Fetches a record by primary key. |
|
|
229
|
-
| `getOrThrow(pk, options?)` | Fetches a record by primary key, throwing if not found. |
|
|
230
|
-
| `getList(pks, options?)` | Fetches multiple records by a list of primary keys. |
|
|
231
|
-
| `getAll(options?)` | Fetches all records; accepts `pagination` and `order` in `options`. |
|
|
232
|
-
| `save(obj, options?)` | Creates or updates (upsert) a single record. |
|
|
233
|
-
| `saveList(objs, options?)` | Creates or updates (upsert) multiple records in one call. |
|
|
234
|
-
| `patch(pk, obj, options?)` | Partially updates a record by primary key. |
|
|
235
|
-
| `merge(pk, obj, options?)` | Fetches a record and returns it deep-merged, in memory, with the given object β does **not** persist anything. |
|
|
236
|
-
| `remove(pk, options?)` | Deletes a record by primary key. |
|
|
237
|
-
| `removeList(pks, options?)` | Deletes multiple records by primary key, returning `{ count }`. |
|
|
238
|
-
| `total(options?)` | Returns the total number of records. |
|
|
239
|
-
| `has(pk, options?)` | Checks whether a record exists, returning `boolean`. |
|
|
240
|
-
| `increment(pk, field, value, options?)` | Atomically adds `value` to a numeric field. See [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
|
|
241
|
-
| `decrement(pk, field, value, options?)` | Atomically subtracts `value` from a numeric field. |
|
|
242
|
-
| `multiply(pk, field, value, options?)` | Atomically multiplies a numeric field by `value`. |
|
|
243
|
-
| `divide(pk, field, value, options?)` | Atomically divides a numeric field by `value`. |
|
|
244
|
-
| `sum(field, where?, options?)` | Sums a numeric field across every matching record; `null` if none match. |
|
|
245
|
-
| `average(field, where?, options?)` | Arithmetic mean of a numeric field across every matching record; `null` if none match. |
|
|
246
|
-
| `min(field, where?, options?)` | Minimum value of a numeric field across every matching record; `null` if none match. |
|
|
247
|
-
| `max(field, where?, options?)` | Maximum value of a numeric field across every matching record; `null` if none match. |
|
|
248
|
-
| `transaction(fn, options?)` | Runs `fn` inside a native transaction of the underlying ORM. |
|
|
249
|
-
| `getDbClient()` | Returns the ORM client instance. |
|
|
250
|
-
| `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). |
|
|
251
|
-
|
|
252
|
-
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.
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## Soft-delete
|
|
257
|
-
|
|
258
|
-
Soft-delete is a **first-class, built-in concept**. Configure `softRemoveKey` once on the repository:
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
super({
|
|
262
|
-
pkName: "id",
|
|
263
|
-
adapter,
|
|
264
|
-
softRemoveKey: "deletedAt",
|
|
265
|
-
});
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
This unlocks four extra methods:
|
|
269
|
-
|
|
270
|
-
| Method | Effect |
|
|
271
|
-
| ------------------------------- | ------------------------------------- |
|
|
272
|
-
| `softRemove(pk, options?)` | Sets `deletedAt` to the current date. |
|
|
273
|
-
| `softRemoveList(pks, options?)` | Same, in batch β returns `{ count }`. |
|
|
274
|
-
| `restore(pk, options?)` | Sets `deletedAt` back to `null`. |
|
|
275
|
-
| `restoreList(pks, options?)` | Same, in batch β returns `{ count }`. |
|
|
276
|
-
|
|
277
|
-
Every other method accepts a `see` option controlling visibility of soft-deleted rows:
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
await userRepository.getAll({ see: "active" }); // default β only non-deleted records
|
|
281
|
-
await userRepository.getAll({ see: "removed" }); // only soft-deleted records
|
|
282
|
-
await userRepository.getAll({ see: "all" }); // everything, ignoring soft-delete
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## Atomic and aggregate methods
|
|
288
|
-
|
|
289
|
-
Every `VSRepository` subclass gets 8 methods for working with numeric fields, split into two groups:
|
|
290
|
-
|
|
291
|
-
**Atomic updates** β evaluated server-side against the row's _current_ value (`UPDATE ... SET field = field + value`), not a client-side read-modify-write:
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
294
|
-
await userRepository.increment("user-1", "balance", 50); // balance = balance + 50
|
|
295
|
-
await userRepository.decrement("user-1", "balance", 50); // balance = balance - 50
|
|
296
|
-
await userRepository.multiply("user-1", "balance", 2); // balance = balance * 2
|
|
297
|
-
await userRepository.divide("user-1", "balance", 4); // balance = balance / 4
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
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`.
|
|
301
|
-
|
|
302
|
-
**Aggregates** β computed across every record matching an (optional) `where`:
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
await userRepository.sum("balance"); // total balance across every active record
|
|
306
|
-
await userRepository.sum("balance", { active: true }); // ...restricted by a where
|
|
307
|
-
await userRepository.average("balance");
|
|
308
|
-
await userRepository.min("balance");
|
|
309
|
-
await userRepository.max("balance");
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
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`).
|
|
313
|
-
|
|
314
|
-
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.
|
|
315
|
-
|
|
316
|
-
### Which fields are eligible
|
|
317
|
-
|
|
318
|
-
`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`):
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
type Product = { id: string; name: string; price: Decimal; stock: number | null };
|
|
322
|
-
|
|
323
|
-
await productRepository.increment(id, "price", new Decimal(10.5)); // ok β Decimal-like
|
|
324
|
-
await productRepository.increment(id, "stock", 5); // ok β nullable numeric fields are included
|
|
325
|
-
await productRepository.increment(id, "name", 1); // compile error β "name" isn't numeric
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
`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`:
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
await productRepository.increment(id, "price", new Decimal(10.5)); // ok
|
|
332
|
-
await productRepository.increment(id, "price", 10.5); // compile error β wrap it: new Decimal(10.5)
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
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.
|
|
336
|
-
|
|
337
|
-
### Writing an adapter
|
|
338
|
-
|
|
339
|
-
`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.
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
## `select` and `relations`
|
|
344
|
-
|
|
345
|
-
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:
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
const user = await userRepository.get(id, {
|
|
349
|
-
select: { id: true, name: true, address: { city: true } },
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
const userWithAddress = await userRepository.get(id, {
|
|
353
|
-
relations: { address: true },
|
|
354
|
-
});
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
- `select` mirrors the entity's shape: scalar fields take a `boolean`; relation fields take a `boolean` or a nested `select`.
|
|
358
|
-
- `relations` eagerly loads related records; each relation field takes a `boolean` or a nested `relations` object.
|
|
359
|
-
- Whether `select` and `relations` can be combined depends on the adapter (see below).
|
|
360
|
-
|
|
361
|
-
> β οΈ **Adapter-dependent behavior for `relations`:**
|
|
362
|
-
>
|
|
363
|
-
> The core only forwards `MethodOptions.select` and `MethodOptions.relations` to the adapter β each adapter decides how to translate them to the underlying ORM:
|
|
364
|
-
>
|
|
365
|
-
> - **Prisma 7 (`@vsrepo/prisma7-adapter` / `VSRepoPrisma7Adapter`)** β `relations` is converted to Prisma `include` (`parsePrismaInclude`). **If `select` is present, `relations` is ignored** because Prisma does not allow `select` + `include` in the same query:
|
|
366
|
-
> ```typescript
|
|
367
|
-
> // Prisma7: relations is ignored when select exists
|
|
368
|
-
> await userRepository.get(id, {
|
|
369
|
-
> select: { id: true, name: true },
|
|
370
|
-
> relations: { address: true }, // β ignored, include = undefined
|
|
371
|
-
> });
|
|
372
|
-
> ```
|
|
373
|
-
>
|
|
374
|
-
> Custom adapters may map `relations` differently β consult the adapter's documentation for the exact semantics.
|
|
375
|
-
|
|
376
|
-
### Strict return typing with `InferMethodReturn`
|
|
377
|
-
|
|
378
|
-
By default, methods are typed as returning the **whole entity**, ignoring the `select` and `relations` you pass (the same approach TypeORM takes). If you prefer a stricter type, `InferMethodReturn<T, Options>` narrows it to what was actually requested. It is opt-in and purely a type-level utility β nothing changes at runtime.
|
|
379
|
-
|
|
380
|
-
```typescript
|
|
381
|
-
import type { InferMethodReturn, MethodOptions } from "vsrepo";
|
|
382
|
-
|
|
383
|
-
type Address = { id: string; city: string };
|
|
384
|
-
type Product = { id: string; name: string };
|
|
385
|
-
type User = {
|
|
386
|
-
id: string;
|
|
387
|
-
name: string;
|
|
388
|
-
email: string;
|
|
389
|
-
address: Address | null;
|
|
390
|
-
products: Product[];
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const options = {
|
|
394
|
-
select: { id: true, name: true, products: { id: true } },
|
|
395
|
-
} satisfies MethodOptions<User>;
|
|
396
|
-
|
|
397
|
-
const users: InferMethodReturn<User[], typeof options> = await userRepository.getAll(options);
|
|
398
|
-
// { id: string; name: string; products: { id: string }[] }[]
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
- The **first** type argument is what the method returns: `User`, `User | null` or `User[]`. `null` and array-ness are preserved β also on relation fields (`address: Address | null`).
|
|
402
|
-
- The **second** is the options object passed to the method (`typeof options`). It can be omitted, which is the same as passing no options.
|
|
403
|
-
|
|
404
|
-
| Options passed | Inferred result |
|
|
405
|
-
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
406
|
-
| None (or `{}`) | Only the scalar fields of the entity β no relations. |
|
|
407
|
-
| `relations` | The scalar fields plus the requested relations (nested ones included); each relation brings all of its scalar fields. |
|
|
408
|
-
| `select` | Only the selected fields. A relation set to `true` brings all of its scalar fields; a nested `select` restricts it further. Relations selected this way are loaded even without `relations`. |
|
|
409
|
-
| `select` + `relations` | `select` wins and `relations` is ignored. |
|
|
410
|
-
|
|
411
|
-
- `see` and `db` don't affect the result.
|
|
412
|
-
- Keep the options' literal types, using `satisfies MethodOptions<T>` (as above) or passing them inline. If they are typed as a plain `MethodOptions<T>` (e.g. `const options: MethodOptions<User> = ...`), nothing is known at compile time and `T` is returned unchanged.
|
|
413
|
-
- Optional (`?`) fields and relations of the entity stay optional.
|
|
414
|
-
- To get this inference directly on dynamic methods, see [Strict return typing with `InferMethodType`](#strict-return-typing-with-infermethodtype).
|
|
415
|
-
|
|
416
|
-
---
|
|
417
|
-
|
|
418
|
-
## Dynamic methods
|
|
419
|
-
|
|
420
|
-
Dynamic methods are declared as a `declare` field annotated with `@DynamicMethod()`. Their behavior β which adapter method to call, which filters to apply, and how arguments map to them β is inferred entirely from the field's **name**.
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
class UserRepository extends VSRepository<User, string> {
|
|
424
|
-
@DynamicMethod()
|
|
425
|
-
declare findByEmail: (email: string, options?: MethodOptions<User>) => Promise<User[]>;
|
|
426
|
-
|
|
427
|
-
@DynamicMethod()
|
|
428
|
-
declare findOneByEmail: (email: string) => Promise<User | null>;
|
|
429
|
-
|
|
430
|
-
@DynamicMethod()
|
|
431
|
-
declare updateById: (id: string, data: DeepPartial<User>) => Promise<User>;
|
|
432
|
-
|
|
433
|
-
// Where-based: VSRepoWhere<T> as the first param, pagination penultimate, MethodOptions last
|
|
434
|
-
@DynamicMethod()
|
|
435
|
-
declare findWherePaginated: (
|
|
436
|
-
where: VSRepoWhere<User>,
|
|
437
|
-
pagination: Pagination,
|
|
438
|
-
options?: MethodOptions<User>,
|
|
439
|
-
) => Promise<User[]>;
|
|
440
|
-
|
|
441
|
-
// field filters, then pagination, then MethodOptions
|
|
442
|
-
@DynamicMethod()
|
|
443
|
-
declare findByNameIgnoreCaseOrAgeBetweenOrderByCreatedAtAscPaginated: (
|
|
444
|
-
name: string,
|
|
445
|
-
age: [number, number],
|
|
446
|
-
pagination: Pagination,
|
|
447
|
-
options?: MethodOptions<User>,
|
|
448
|
-
) => Promise<User[]>;
|
|
449
|
-
}
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
> Want the return type to follow the `select`/`relations` you pass, instead of always being the whole entity? Declare the method with [`InferMethodType`](#strict-return-typing-with-infermethodtype).
|
|
453
|
-
|
|
454
|
-
### Available prefixes
|
|
455
|
-
|
|
456
|
-
| Prefix | Adapter method | Notes |
|
|
457
|
-
| -------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
458
|
-
| `findBy` | `findMany` | Field filters follow the prefix. |
|
|
459
|
-
| `findOneBy` | `findOne` | Field filters follow the prefix; single result. |
|
|
460
|
-
| `findOneOrThrowBy` | `findOneOrThrow` | Throws if no record is found. |
|
|
461
|
-
| `findOneOrThrow` | `findOneOrThrow` | No field filters; applies only soft-delete/`see`. |
|
|
462
|
-
| `findOneOrThrowWhere` | `findOneOrThrow` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
463
|
-
| `findWhere` | `findMany` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
464
|
-
| `findOneWhere` | `findOne` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
465
|
-
| `findOne` | `findOne` | No field filters; applies only soft-delete/`see`. |
|
|
466
|
-
| `countBy` | `count` | Field filters follow the prefix. |
|
|
467
|
-
| `countWhere` | `count` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
468
|
-
| `count` | `count` | No field filters. |
|
|
469
|
-
| `existsBy` | `exists` | Returns `boolean`. |
|
|
470
|
-
| `existsWhere` | `exists` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
471
|
-
| `create` | `create` | Receives `DeepPartial<Entity>` as argument. |
|
|
472
|
-
| `createMany` | `createMany` | Receives `DeepPartial<Entity>[]` as argument; supports `IgnoreConflicts`. |
|
|
473
|
-
| `createManyReturning` | `createManyReturning` | Receives `DeepPartial<Entity>[]` as argument; supports `IgnoreConflicts`; returns the created records (`T[]`) instead of `CountResult`. |
|
|
474
|
-
| `updateBy` | `update` | Field filters + `DeepPartial<Entity>` as argument. |
|
|
475
|
-
| `updateWhere` | `update` | Receives a `VSRepoWhere<T>` as the first argument, then `DeepPartial<Entity>`. |
|
|
476
|
-
| `updateManyBy` | `updateMany` | Field filters + `DeepPartial<Entity>`. |
|
|
477
|
-
| `updateManyWhere` | `updateMany` | Receives a `VSRepoWhere<T>` as the first argument, then `DeepPartial<Entity>`. |
|
|
478
|
-
| `updateManyReturningBy` | `updateManyReturning` | Field filters + `DeepPartial<Entity>`; returns updated records. |
|
|
479
|
-
| `updateManyReturningWhere` | `updateManyReturning` | Receives a `VSRepoWhere<T>` as the first argument, then `DeepPartial<Entity>`; returns updated records. |
|
|
480
|
-
| `upsertBy` | `upsert` | Field filters + `create`/`update` payloads. |
|
|
481
|
-
| `upsertWhere` | `upsert` | Receives a `VSRepoWhere<T>` as the first argument, then `create`/`update` payloads. |
|
|
482
|
-
| `deleteBy` | `delete` | Field filters follow the prefix. |
|
|
483
|
-
| `deleteWhere` | `delete` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
484
|
-
| `deleteManyBy` | `deleteMany` | Field filters follow the prefix. |
|
|
485
|
-
| `deleteManyWhere` | `deleteMany` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
486
|
-
| `deleteManyReturningBy` | `deleteManyReturning` | Field filters follow the prefix; returns deleted records. |
|
|
487
|
-
| `deleteManyReturningWhere` | `deleteManyReturning` | Receives a `VSRepoWhere<T>` as the first argument; returns deleted records. |
|
|
488
|
-
|
|
489
|
-
> `groupBy` is **not planned** for v2 β it doesn't map cleanly onto the ORM-agnostic contract. `aggregate` as a separate prefix is also unlikely to be implemented: the most common aggregate operations (`sum`, `average`, `min`, `max`, `increment`, `decrement`, `multiply`, `divide`) are already available as dedicated base methods β see [Atomic and aggregate methods](#atomic-and-aggregate-methods). For anything more complex, use a `@QueryMethod` with raw SQL.
|
|
490
|
-
|
|
491
|
-
### Field filters
|
|
492
|
-
|
|
493
|
-
Applied as suffixes to the field name inside the method (same idea as v1, one renamed suffix):
|
|
494
|
-
|
|
495
|
-
| Suffix | Meaning | Argument |
|
|
496
|
-
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
497
|
-
| _(none)_ | equality (`=`) | yes |
|
|
498
|
-
| `Not` | negation | yes |
|
|
499
|
-
| `In` | is one of | yes (array) |
|
|
500
|
-
| `NotIn` | is none of | yes (array) |
|
|
501
|
-
| `Contains` | substring match | yes |
|
|
502
|
-
| `NotContains` | negated substring match | yes |
|
|
503
|
-
| `StartsWith` | prefix match | yes |
|
|
504
|
-
| `NotStartsWith` | negated prefix match | yes |
|
|
505
|
-
| `EndsWith` | suffix match | yes |
|
|
506
|
-
| `NotEndsWith` | negated suffix match | yes |
|
|
507
|
-
| `GreaterThan` | `>` | yes |
|
|
508
|
-
| `GreaterThanEqual` | `>=` | yes |
|
|
509
|
-
| `LessThan` | `<` | yes |
|
|
510
|
-
| `LessThanEqual` | `<=` | yes |
|
|
511
|
-
| `Between` | inclusive range | yes (`[min, max]` tuple) |
|
|
512
|
-
| `NotBetween` | outside an inclusive range | yes (`[min, max]` tuple) |
|
|
513
|
-
| `IsNull` | field is `null` | no |
|
|
514
|
-
| `IsNotNull` | field is not `null` | no |
|
|
515
|
-
| `IsTrue` | field is `true` | no |
|
|
516
|
-
| `IsFalse` | field is `false` | no |
|
|
517
|
-
| `IgnoreCase` | case-insensitive combinator for text filters | no _(renamed from v1's `Insensitive`)_ |
|
|
518
|
-
| `Optional` | **explicitly** marks the field's argument as optional β it's already optional by default, so this suffix is itself optional and only used to make it explicit | β |
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
@DynamicMethod()
|
|
522
|
-
declare findByNameContainsIgnoreCase: (name: string) => Promise<User[]>;
|
|
523
|
-
|
|
524
|
-
@DynamicMethod()
|
|
525
|
-
declare findByAgeBetween: (age: [number, number]) => Promise<User[]>;
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### Logical operators
|
|
529
|
-
|
|
530
|
-
| Operator | Usage in the name | Example |
|
|
531
|
-
| -------- | ------------------------------- | --------------------------------------------------- |
|
|
532
|
-
| `And` | between two fields | `findOneByIdAndEmail` |
|
|
533
|
-
| `Or` | between two fields | `findByNameOrEmail` |
|
|
534
|
-
| `AND` | splits a final block into `AND` | `findByEmailOrNameANDActiveStatusAndAgeGreaterThan` |
|
|
535
|
-
|
|
536
|
-
`AND` (all caps) rules, same as v1: only one `AND` per method name is allowed; every field connected with `And` after it is nested inside `AND: []`; `Or` cannot appear after an `AND`.
|
|
537
|
-
|
|
538
|
-
### Relation filters
|
|
539
|
-
|
|
540
|
-
Filter by fields of related entities. Internally these map to the `_some`/`_every`/`_none`/`_with`/`_without` operators of `VSRepoWhere` (see [`select` and `relations`](#select-and-relations) for the eager-loading counterpart).
|
|
541
|
-
|
|
542
|
-
| Suffix | Meaning | Restriction |
|
|
543
|
-
| -------------- | ----------------------------------------------- | ---------------------------------------------------------- |
|
|
544
|
-
| `Some` | at least one related record matches | to-many relations only |
|
|
545
|
-
| `SomeField` | filters within the related records | to-many relations only |
|
|
546
|
-
| `Every` | every related record matches | to-many relations only (needs `Field` to be a real filter) |
|
|
547
|
-
| `EveryField` | filters within the related records | to-many relations only |
|
|
548
|
-
| `None` | no related record matches | to-many relations only |
|
|
549
|
-
| `NoneField` | filters within the related records | to-many relations only |
|
|
550
|
-
| `With` | related record exists | to-one relations only |
|
|
551
|
-
| `WithField` | filters a field within the related record | to-one relations only |
|
|
552
|
-
| `Without` | related record does not exist | to-one relations only |
|
|
553
|
-
| `WithoutField` | negated filter on a field of the related record | to-one relations only |
|
|
554
|
-
|
|
555
|
-
```typescript
|
|
556
|
-
@DynamicMethod()
|
|
557
|
-
declare findByAddressWithCityStartsWithIgnoreCase: (city: string) => Promise<User[]>;
|
|
558
|
-
|
|
559
|
-
@DynamicMethod()
|
|
560
|
-
declare findByProductsSome: () => Promise<User[]>;
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Ordering, pagination and distinct
|
|
564
|
-
|
|
565
|
-
| Suffix | Effect |
|
|
566
|
-
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
567
|
-
| `Paginated` | Injects a `pagination` argument (`{ limit?, offset? }`) as the **penultimate** parameter (before the optional `MethodOptions`). |
|
|
568
|
-
| `Ordered` | Injects an `order: Ordering<T>` argument as the **penultimate** parameter (before the optional `MethodOptions`). |
|
|
569
|
-
| `OrderedAndPaginated` | Injects `order` as the antepenultimate, then `pagination` as the penultimate β both before `MethodOptions`. |
|
|
570
|
-
| `PaginatedAndOrdered` | Injects `pagination` as the antepenultimate, then `order` as the penultimate β both before `MethodOptions`. |
|
|
571
|
-
| `OrderBy<Field>Asc` / `OrderBy<Field>Desc` | Bakes a fixed ordering directly into the method name β chain fields with `And` (e.g. `OrderByCreatedAtAscAndNameDesc`). No `order` argument needed. *Note: If you do not specify `Asc` or `Desc`, it defaults to `Asc`.* |
|
|
572
|
-
| `Distinct<Field>And<Field>...` | Bakes fixed `distinct` fields directly into the method name (only valid on `findBy`/`findWhere`-family methods). |
|
|
573
|
-
| `IgnoreConflicts` | On `createMany`/`createManyReturning`, skips records that would violate a unique constraint instead of throwing. _(Renamed from v1's `SkipDuplicates`.)_ |
|
|
574
|
-
|
|
575
|
-
> β οΈ **Parameter order:** `pagination` and `order` are always placed **before** the optional `MethodOptions<T>` last argument. When both `order` and `pagination` are present, their relative order follows the suffix name (`OrderedAndPaginated` β order, pagination; `PaginatedAndOrdered` β pagination, order).
|
|
576
|
-
|
|
577
|
-
```typescript
|
|
578
|
-
// Paginated: pagination is the penultimate param (before MethodOptions)
|
|
579
|
-
@DynamicMethod()
|
|
580
|
-
declare findByActiveOrderByCreatedAtDescPaginated:
|
|
581
|
-
(active: boolean, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
|
|
582
|
-
|
|
583
|
-
// OrderedAndPaginated: order, then pagination, then MethodOptions
|
|
584
|
-
@DynamicMethod()
|
|
585
|
-
declare findByNameContainsIgnoreCaseOrderedAndPaginated:
|
|
586
|
-
(name: string, order: Ordering<User>, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
|
|
587
|
-
|
|
588
|
-
@DynamicMethod()
|
|
589
|
-
declare createManyIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<{ count: number }>;
|
|
590
|
-
|
|
591
|
-
// createManyReturning: same as createMany, but returns the created records
|
|
592
|
-
@DynamicMethod()
|
|
593
|
-
declare createManyReturningIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<User[]>;
|
|
594
|
-
|
|
595
|
-
// findOne with no filter (equivalent to findOneOrThrow with no filter, but returns null instead of throwing)
|
|
596
|
-
@DynamicMethod()
|
|
597
|
-
declare findOne: (options?: MethodOptions<User>) => Promise<User | null>;
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
> β οΈ **Precedence between `Distinct` and `OrderBy`:** when both are used in the same method name, **`Distinct` must come before `OrderBy`**:
|
|
601
|
-
>
|
|
602
|
-
> ```typescript
|
|
603
|
-
> @DynamicMethod()
|
|
604
|
-
> declare findByActiveDistinctNameOrderByCreatedAtDesc:
|
|
605
|
-
> (active: boolean) => Promise<User[]>;
|
|
606
|
-
> ```
|
|
607
|
-
>
|
|
608
|
-
> Putting `OrderBy` before `Distinct` (e.g. `findByActiveOrderByCreatedAtDescDistinctName`) is not a valid pattern and won't be parsed as expected.
|
|
609
|
-
|
|
610
|
-
### Decorator options
|
|
611
|
-
|
|
612
|
-
`@DynamicMethod<T>(options?)` accepts:
|
|
613
|
-
|
|
614
|
-
| Option | Type | Description |
|
|
615
|
-
| ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
616
|
-
| `proxyTo` | `string` | Redirects the method's logic to another valid dynamic-method pattern β useful for names that don't follow the naming convention. |
|
|
617
|
-
| `injectOrdering` | `Ordering<T>` | Fixed ordering automatically injected, overriding the repository's `defaultOrdering`. |
|
|
618
|
-
|
|
619
|
-
```typescript
|
|
620
|
-
// proxyTo: gives the method a custom name while reusing an existing pattern
|
|
621
|
-
@DynamicMethod<User>({ proxyTo: "findByEmail" })
|
|
622
|
-
declare buscarPorEmail: (email: string, options?: MethodOptions<User>) => Promise<User[]>;
|
|
623
|
-
|
|
624
|
-
// injectOrdering: always sorts by createdAt desc, overriding defaultOrdering
|
|
625
|
-
@DynamicMethod<User>({ injectOrdering: { createdAt: "desc" } })
|
|
626
|
-
declare findByStatus: (status: string) => Promise<User[]>;
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
### Strict return typing with `InferMethodType`
|
|
630
|
-
|
|
631
|
-
Normally you write a dynamic method's signature by hand, and its return is whatever you declare (usually the whole entity). `InferMethodType<Args, Return, OrmTypes?>` declares the method for you and infers the return **on each call** from the `select`/`relations` you pass β with the same rules as [`InferMethodReturn`](#strict-return-typing-with-infermethodreturn):
|
|
632
|
-
|
|
633
|
-
```typescript
|
|
634
|
-
class UserRepository extends VSRepository<User, string, MyOrmTypes> {
|
|
635
|
-
@DynamicMethod()
|
|
636
|
-
declare findByName: InferMethodType<[name: string], User[], MyOrmTypes>;
|
|
637
|
-
|
|
638
|
-
// the third generic (OrmTypes) is optional
|
|
639
|
-
@DynamicMethod()
|
|
640
|
-
declare findOneByEmail: InferMethodType<[email: string], User | null>;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
await userRepository.findByName("John");
|
|
644
|
-
// { id: string; name: string; email: string }[] (only the scalar fields)
|
|
645
|
-
|
|
646
|
-
await userRepository.findByName("John", { select: { id: true, products: { id: true } } });
|
|
647
|
-
// { id: string; products: { id: string }[] }[]
|
|
648
|
-
|
|
649
|
-
await userRepository.findOneByEmail("john@example.com", { relations: { address: true } });
|
|
650
|
-
// { id: string; name: string; email: string; address: Address | null } | null
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
| Generic | Description |
|
|
654
|
-
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
655
|
-
| `Args` | Tuple with the method's positional arguments, **without** `options` β e.g. `[name: string]` or `[where: VSRepoWhere<User>, pagination: Pagination]`. |
|
|
656
|
-
| `Return` | What the method resolves to: `Entity`, `Entity \| null` or `Entity[]`. The entity type used for `select`/`relations` is taken from here. |
|
|
657
|
-
| `OrmTypes` | _Optional._ `VSRepoOrmTypes` for your ORM, used to type the `db` option (see [Creating a repository](#creating-a-repository)). Defaults to `VSRepoOrmTypes`. |
|
|
658
|
-
|
|
659
|
-
- `options` (`MethodOptions<Entity, OrmTypes>`) is always the **last**, optional parameter, after every argument in `Args`. If one of those arguments is optional, pass `undefined` explicitly to reach `options`.
|
|
660
|
-
- Without `options` the result has only the scalar fields; with them it follows the [same rules](#strict-return-typing-with-infermethodreturn) as `InferMethodReturn` (including `select` winning over `relations`).
|
|
661
|
-
- Unknown keys in `select`/`relations` (at any depth) are rejected at compile time, and the editor autocompletes them β just like with a plain `MethodOptions<Entity>` parameter.
|
|
662
|
-
- It works together with the [decorator options](#decorator-options) (`proxyTo`, `injectOrdering`).
|
|
663
|
-
- It is meant for dynamic methods that return entities (`findByβ¦`, `findOneByβ¦`, `findWhereβ¦`, β¦). Methods that don't β `countByβ¦`, `existsByβ¦` β keep their regular signature.
|
|
664
|
-
|
|
665
|
-
---
|
|
666
|
-
|
|
667
|
-
## Query methods (raw SQL)
|
|
668
|
-
|
|
669
|
-
`@QueryMethod` bypasses the name-parsing engine entirely and executes a raw SQL statement through the adapter's `query()` method. Use placeholders for the values passed via `args` β never interpolate values directly into the SQL string. **The placeholder syntax depends on the database/driver behind your adapter:** the `$1`, `$2`, ... style used in the examples below is the PostgreSQL convention β MySQL, for instance, uses `?`. Check your adapter's documentation for the exact syntax.
|
|
670
|
-
|
|
671
|
-
```typescript
|
|
672
|
-
class UserRepository extends VSRepository<User, string> {
|
|
673
|
-
@QueryMethod('SELECT * FROM "user" WHERE email = $1')
|
|
674
|
-
declare findByEmailRaw: (arg: QueryMethodArg<[email: string]>) => Promise<User[]>;
|
|
675
|
-
|
|
676
|
-
@QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
|
|
677
|
-
declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
|
|
678
|
-
|
|
679
|
-
// Only one row is ever expected here, so `singleResult` collapses the
|
|
680
|
-
// array into a single object (or `null` when no row matches).
|
|
681
|
-
@QueryMethod('SELECT * FROM "user" WHERE id = $1 LIMIT 1', { singleResult: true })
|
|
682
|
-
declare findByIdRaw: (arg: QueryMethodArg<[id: string]>) => Promise<User | null>;
|
|
683
|
-
}
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
| Option | Type | Default | Description |
|
|
687
|
-
| -------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
688
|
-
| `modifying` | `boolean` | `false` | When `true`, the method resolves to the number of affected rows. When `false`, runs as a read query and resolves to the declared return type. |
|
|
689
|
-
| `singleResult` | `boolean` | `false` | When `true`, collapses an array result into its first element (`null` if empty), so you can declare the return type as a single object instead of an array. Has no effect on non-array results (e.g. a `modifying` query's affected-row count). |
|
|
690
|
-
|
|
691
|
-
Query methods accept `{ args, db? }` at the call site β `db` lets them participate in a `transaction()` block just like base and dynamic methods.
|
|
692
|
-
|
|
693
|
-
### Spread arguments with `spreadArgs`
|
|
694
|
-
|
|
695
|
-
By default, a `@QueryMethod` receives its placeholder values through a single `QueryMethodArg` object (`method({ args: [...] })`). Set `spreadArgs: true` to receive them as separate positional arguments instead, JpaRepository style:
|
|
696
|
-
|
|
697
|
-
```typescript
|
|
698
|
-
class UserRepository extends VSRepository<User, string> {
|
|
699
|
-
@QueryMethod('SELECT * FROM "user" WHERE email = $1 AND "userType" = $2', {
|
|
700
|
-
spreadArgs: true,
|
|
701
|
-
})
|
|
702
|
-
declare findByEmailAndType: (
|
|
703
|
-
...args: QueryArgs<[email: string, userType: string]>
|
|
704
|
-
) => Promise<User[]>;
|
|
705
|
-
|
|
706
|
-
// Instead of using `QueryArgs`, you can also simply set `DbArg` as the last parameter
|
|
707
|
-
@QueryMethod('SELECT * FROM "user" WHERE id = $1', { spreadArgs: true })
|
|
708
|
-
declare findById: (id: string, db?: DbArg) => Promise<User[]>;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const admins = await userRepository.findByEmailAndType("joao@email.com", "admin");
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
To run the query against a specific client or transaction instead of the repository's default one, pass `withDb(tx)` as the trailing argument β it wraps `tx` in a `DbArg`, which the resolver recognizes with `instanceof`, so it's never confused with a regular positional argument even if that argument happens to be an object:
|
|
715
|
-
|
|
716
|
-
```typescript
|
|
717
|
-
await userRepository.transaction(async tx => {
|
|
718
|
-
await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
|
|
719
|
-
});
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
`spreadArgs` only affects `@QueryMethod`-declared fields β it's `false` by default, and calling a method declared without it using more than one argument throws, since the single-`QueryMethodArg` call style is expected instead. It has no effect on `query()`, which always accepts `{ args, db? }`.
|
|
723
|
-
|
|
724
|
-
### Ad-hoc raw queries with `query()`
|
|
725
|
-
|
|
726
|
-
For one-off raw SQL that doesn't warrant declaring a `@QueryMethod` on the repository class, call `query()` directly β it's available on every `VSRepository` instance and uses the adapter's `query()` under the hood:
|
|
727
|
-
|
|
728
|
-
```typescript
|
|
729
|
-
query<T = any>(query: string, options?: VSRepoQueryOptions<OrmTypes>): Promise<T>;
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
```typescript
|
|
733
|
-
const users = await userRepository.query<User[]>('SELECT * FROM "user" WHERE email = $1', {
|
|
734
|
-
args: ["maria@email.com"],
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
const affectedRows = await userRepository.query<number>(
|
|
738
|
-
'UPDATE "user" SET active = true WHERE id = $1',
|
|
739
|
-
{ args: ["123"], modifying: true },
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
// Only one row is ever expected here, so `singleResult` collapses the
|
|
743
|
-
// array into a single object (or `null` when no row matches).
|
|
744
|
-
const user = await userRepository.query<User | null>('SELECT * FROM "user" WHERE id = $1 LIMIT 1', {
|
|
745
|
-
args: ["123"],
|
|
746
|
-
singleResult: true,
|
|
747
|
-
});
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
| Option | Type | Default | Description |
|
|
751
|
-
| -------------- | --------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
752
|
-
| `args` | `any[]` | `undefined` | Positional parameters injected into the SQL placeholders β the placeholder syntax depends on the database/driver behind your adapter. Never interpolate values directly into the SQL string. |
|
|
753
|
-
| `db` | `any` | Repository's default client | Database client or transaction to run this query in. |
|
|
754
|
-
| `modifying` | `boolean` | `false` | When `true`, returns the number of affected rows. |
|
|
755
|
-
| `singleResult` | `boolean` | `false` | When `true`, collapses an array result into its first element (`null` if empty). Has no effect on non-array results (e.g. a `modifying` query's affected-row count). |
|
|
756
|
-
|
|
757
|
-
Just like base, dynamic and query methods, `query()` accepts `db` in `options` to participate in a `transaction()` block.
|
|
758
|
-
|
|
759
|
-
---
|
|
760
|
-
|
|
761
|
-
## Transactions
|
|
762
|
-
|
|
763
|
-
All methods (base, dynamic, and query) accept `options.db` to participate in a shared transaction:
|
|
764
|
-
|
|
765
|
-
```typescript
|
|
766
|
-
await userRepository.transaction(async tx => {
|
|
767
|
-
const user = await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
|
|
768
|
-
|
|
769
|
-
await userLogsRepository.save(
|
|
770
|
-
{ action: "User created", data: { userId: user.id } },
|
|
771
|
-
{ db: tx },
|
|
772
|
-
);
|
|
773
|
-
});
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
Different repositories can share the same transaction as long as their adapters point to the same underlying ORM connection.
|
|
777
|
-
|
|
778
|
-
`transaction()` accepts an optional `VSRepoTransactionOptions` as its second argument:
|
|
779
|
-
|
|
780
|
-
```typescript
|
|
781
|
-
import { TransactionIsolationLevel } from "vsrepo";
|
|
782
|
-
|
|
783
|
-
await userRepository.transaction(
|
|
784
|
-
async tx => {
|
|
785
|
-
await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
|
|
786
|
-
},
|
|
787
|
-
{ isolationLevel: TransactionIsolationLevel.SERIALIZABLE, timeoutMs: 5000 },
|
|
788
|
-
);
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
| Option | Type | Description |
|
|
792
|
-
| ---------------- | --------------------------- | ------------------------------------------------------------------------------------- |
|
|
793
|
-
| `isolationLevel` | `TransactionIsolationLevel` | Isolation level to use for the transaction. Defaults to the underlying ORM's default. |
|
|
794
|
-
| `timeoutMs` | `number` | Maximum time (in ms) the transaction is allowed to run before being aborted. |
|
|
795
|
-
|
|
796
|
-
`TransactionIsolationLevel` mirrors the standard SQL isolation levels: `READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`. Support for a given level depends on the adapter/underlying ORM and database.
|
|
797
|
-
|
|
798
|
-
---
|
|
799
|
-
|
|
800
|
-
## Utility types
|
|
801
|
-
|
|
802
|
-
Beyond the entity-shaping types covered above (`VSRepoSelect`, `VSRepoRelations`, `VSRepoWhere`), VSRepository exports a set of utility types. They show up throughout the sections above, but here's a consolidated reference. All of them are part of the public API and can be imported directly:
|
|
803
|
-
|
|
804
|
-
```typescript
|
|
805
|
-
import type {
|
|
806
|
-
MethodOptions,
|
|
807
|
-
RestrictMethodOptions,
|
|
808
|
-
InferMethodReturn,
|
|
809
|
-
InferMethodType,
|
|
810
|
-
Pagination,
|
|
811
|
-
Ordering,
|
|
812
|
-
OrderByField,
|
|
813
|
-
SortDirection,
|
|
814
|
-
SeeMode,
|
|
815
|
-
DeepPartial,
|
|
816
|
-
CountResult,
|
|
817
|
-
QueryMethodArg,
|
|
818
|
-
QueryArgs,
|
|
819
|
-
KeysOfType,
|
|
820
|
-
NumericKeys,
|
|
821
|
-
NumericLike,
|
|
822
|
-
DecimalLike,
|
|
823
|
-
Primitive,
|
|
824
|
-
VSRepoWhere,
|
|
825
|
-
VSRepoOrmTypes,
|
|
826
|
-
VSRepoTransactionOptions,
|
|
827
|
-
TransactionIsolationLevel,
|
|
828
|
-
} from "vsrepo";
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
| Type | Description | Used by |
|
|
832
|
-
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
833
|
-
| `MethodOptions<T, K>` | Options accepted as the last argument by all dynamic methods and most base methods: `select`, `relations`, `see`, `db`. | [Base methods](#base-methods), [Dynamic methods](#dynamic-methods). |
|
|
834
|
-
| `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). |
|
|
835
|
-
| `InferMethodReturn<T, Options>` | Opt-in strict return typing: narrows `T` (`Entity`, `Entity \| null` or `Entity[]`) to the fields and relations actually requested through `select`/`relations`. `select` wins over `relations`. | [Strict return typing with `InferMethodReturn`](#strict-return-typing-with-infermethodreturn). |
|
|
836
|
-
| `InferMethodType<Args, Return, OrmTypes?>` | Declares a dynamic method whose return is inferred on each call from the `select`/`relations` passed as `options`. `OrmTypes` is optional and types the `db` option. | [Strict return typing with `InferMethodType`](#strict-return-typing-with-infermethodtype). |
|
|
837
|
-
| `Pagination` | `{ limit?, offset? }` accepted by `getAll` and by `Paginated` dynamic methods. | [Base methods](#base-methods), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
|
|
838
|
-
| `Ordering<T>` / `OrderByField<T>` / `SortDirection` | Ordering shape accepted by `getAll`, `defaultOrdering`, `injectOrdering` and by `Ordered` dynamic methods. A single object or a chained array. | [Constructor options](#constructor-options), [Decorator options](#decorator-options), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
|
|
839
|
-
| `SeeMode` | `"active" \| "removed" \| "all"` β controls visibility of soft-deleted records. | [Soft-delete](#soft-delete). |
|
|
840
|
-
| `DeepPartial<T>` | Recursively makes every property of `T` optional, including nested objects and array elements. | `save`, `saveList`, `patch`, `merge`, and all the dynamic writing methods. |
|
|
841
|
-
| `CountResult` | `{ count: number }` β the shape returned by batch operations. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
|
|
842
|
-
| `QueryMethodArg<T>` | `{ args?: T, db? }` β positional SQL parameters (the placeholder syntax depends on the database/driver behind your adapter: `$1`, `$2`, ... for PostgreSQL, `?` for MySQL) and transaction client for `@QueryMethod`. | [Query methods (raw SQL)](#query-methods-raw-sql). |
|
|
843
|
-
| `QueryArgs<T, O>` | Types the spread parameter list of a `@QueryMethod` declared with `{ spreadArgs: true }`: `T`'s values in order, followed by an optional trailing `DbArg<O>` built via `withDb()`. | [Spread arguments with `spreadArgs`](#spread-arguments-with-spreadargs). |
|
|
844
|
-
| `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. |
|
|
845
|
-
| `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). |
|
|
846
|
-
| `NumericLike` | `number \| bigint \| DecimalLike`. | [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
|
|
847
|
-
| `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). |
|
|
848
|
-
| `Primitive` | Union of scalar types (`string \| number \| boolean \| bigint \| symbol \| undefined \| null \| Date \| DecimalLike`) treated as leaves β not relations β when walking an entity's shape. | Used by `Ordering<T>` to tell scalar fields apart from relation fields. |
|
|
849
|
-
| `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). |
|
|
850
|
-
| `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). |
|
|
851
|
-
| `VSRepoTransactionOptions` | `{ isolationLevel?, timeoutMs? }` β options accepted as the second argument of `transaction()`. | [Transactions](#transactions). |
|
|
852
|
-
| `TransactionIsolationLevel` | Enum of standard SQL isolation levels (`READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`) accepted by `VSRepoTransactionOptions.isolationLevel`. | [Transactions](#transactions). |
|
|
853
|
-
|
|
854
|
-
### `DeepPartial<T>`
|
|
855
|
-
|
|
856
|
-
Recursively makes all properties optional, walking into nested objects and array elements β unlike TypeScript's built-in `Partial<T>`, which only makes the top level optional:
|
|
857
|
-
|
|
858
|
-
```typescript
|
|
859
|
-
type User = { id: string; name: string; address: { city: string; zip: string } };
|
|
860
|
-
|
|
861
|
-
const patch: DeepPartial<User> = {
|
|
862
|
-
address: { city: "SΓ£o Paulo" }, // zip can be omitted; city keeps its type
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
await userRepository.patch(id, patch);
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
### `KeysOfType<T, K>`
|
|
869
|
-
|
|
870
|
-
Filters an object type down to the keys whose value matches a given type β this is what lets `pkName` accept only fields of the entity that are actually assignable to the repository's primary-key type:
|
|
871
|
-
|
|
872
|
-
```typescript
|
|
873
|
-
type User = { id: string; age: number; name: string };
|
|
874
|
-
type StringKeys = KeysOfType<User, string>; // "id" | "name"
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
### `Ordering<T>`
|
|
878
|
-
|
|
879
|
-
Accepts either a single ordering object or an array of them, applied in the order they're declared:
|
|
880
|
-
|
|
881
|
-
```typescript
|
|
882
|
-
const order: Ordering<User> = { createdAt: "desc" };
|
|
883
|
-
const chained: Ordering<User> = [{ name: "asc" }, { createdAt: "desc" }];
|
|
884
|
-
|
|
885
|
-
await userRepository.getAll({ order: chained });
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
---
|
|
889
|
-
|
|
890
|
-
## Writing your own adapter
|
|
891
|
-
|
|
892
|
-
Because the core is ORM-agnostic and ships without a bundled adapter, adding support for an ORM/database β whether that's a stopgap for your own project or a candidate for a future `@vsrepo/*-adapter` package β means implementing the `VSRepoAdapter<T>` abstract class:
|
|
893
|
-
|
|
894
|
-
```typescript
|
|
895
|
-
export abstract class VSRepoAdapter<T> {
|
|
896
|
-
abstract runInTransaction<R>(
|
|
897
|
-
fn: (tx: any) => Promise<R>,
|
|
898
|
-
options?: VSRepoTransactionOptions,
|
|
899
|
-
): Promise<R>;
|
|
900
|
-
abstract getDbClient(): any;
|
|
901
|
-
abstract query<T = any>(query: string, options?: AdapterQueryOptions): Promise<T>;
|
|
902
|
-
abstract findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T | null>;
|
|
903
|
-
abstract findOneOrThrow(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
904
|
-
abstract findMany(
|
|
905
|
-
where: VSRepoWhere<T>,
|
|
906
|
-
options?: AdapterMethodOptions<T> & { distinct?: (keyof T)[] },
|
|
907
|
-
): Promise<T[]>;
|
|
908
|
-
abstract save(obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
909
|
-
abstract saveMany(objs: DeepPartial<T>[], options?: AdapterMethodOptions<T>): Promise<T[]>;
|
|
910
|
-
abstract create(objs: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
911
|
-
abstract createMany(
|
|
912
|
-
objs: DeepPartial<T>[],
|
|
913
|
-
options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
|
|
914
|
-
): Promise<CountResult>;
|
|
915
|
-
abstract createManyReturning(
|
|
916
|
-
objs: DeepPartial<T>[],
|
|
917
|
-
options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
|
|
918
|
-
): Promise<T[]>;
|
|
919
|
-
abstract delete(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
920
|
-
abstract deleteMany(
|
|
921
|
-
where: VSRepoWhere<T>,
|
|
922
|
-
options?: AdapterMethodOptions<T>,
|
|
923
|
-
): Promise<CountResult>;
|
|
924
|
-
abstract deleteManyReturning(
|
|
925
|
-
where: VSRepoWhere<T>,
|
|
926
|
-
options?: AdapterMethodOptions<T>,
|
|
927
|
-
): Promise<T[]>;
|
|
928
|
-
abstract update(
|
|
929
|
-
where: VSRepoWhere<T>,
|
|
930
|
-
obj: DeepPartial<T>,
|
|
931
|
-
options?: AdapterMethodOptions<T>,
|
|
932
|
-
): Promise<T>;
|
|
933
|
-
abstract updateMany(
|
|
934
|
-
where: VSRepoWhere<T>,
|
|
935
|
-
obj: DeepPartial<T>,
|
|
936
|
-
options?: AdapterMethodOptions<T>,
|
|
937
|
-
): Promise<CountResult>;
|
|
938
|
-
abstract updateManyReturning(
|
|
939
|
-
where: VSRepoWhere<T>,
|
|
940
|
-
obj: DeepPartial<T>,
|
|
941
|
-
options?: AdapterMethodOptions<T>,
|
|
942
|
-
): Promise<T[]>;
|
|
943
|
-
abstract count(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number>;
|
|
944
|
-
abstract exists(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<boolean>;
|
|
945
|
-
abstract merge<K>(
|
|
946
|
-
where: VSRepoWhere<T>,
|
|
947
|
-
obj: DeepPartial<T>,
|
|
948
|
-
options?: AdapterMethodOptions<T>,
|
|
949
|
-
): Promise<K & T>;
|
|
950
|
-
abstract upsert(
|
|
951
|
-
where: VSRepoWhere<T>,
|
|
952
|
-
create: DeepPartial<T>,
|
|
953
|
-
update: DeepPartial<T>,
|
|
954
|
-
options?: AdapterMethodOptions<T>,
|
|
955
|
-
): Promise<T>;
|
|
956
|
-
abstract incrementOne<K extends NumericKeys<T>>(
|
|
957
|
-
field: K,
|
|
958
|
-
value: NonNullable<T[K]>,
|
|
959
|
-
where: VSRepoWhere<T>,
|
|
960
|
-
options?: AdapterMethodOptions<T>,
|
|
961
|
-
): Promise<T>;
|
|
962
|
-
abstract decrementOne<K extends NumericKeys<T>>(
|
|
963
|
-
field: K,
|
|
964
|
-
value: NonNullable<T[K]>,
|
|
965
|
-
where: VSRepoWhere<T>,
|
|
966
|
-
options?: AdapterMethodOptions<T>,
|
|
967
|
-
): Promise<T>;
|
|
968
|
-
abstract multiplyOne<K extends NumericKeys<T>>(
|
|
969
|
-
field: K,
|
|
970
|
-
value: NonNullable<T[K]>,
|
|
971
|
-
where: VSRepoWhere<T>,
|
|
972
|
-
options?: AdapterMethodOptions<T>,
|
|
973
|
-
): Promise<T>;
|
|
974
|
-
abstract divideOne<K extends NumericKeys<T>>(
|
|
975
|
-
field: K,
|
|
976
|
-
value: NonNullable<T[K]>,
|
|
977
|
-
where: VSRepoWhere<T>,
|
|
978
|
-
options?: AdapterMethodOptions<T>,
|
|
979
|
-
): Promise<T>;
|
|
980
|
-
abstract sum(
|
|
981
|
-
field: NumericKeys<T>,
|
|
982
|
-
where?: VSRepoWhere<T>,
|
|
983
|
-
options?: AdapterMethodOptions<T>,
|
|
984
|
-
): Promise<number | null>;
|
|
985
|
-
abstract average(
|
|
986
|
-
field: NumericKeys<T>,
|
|
987
|
-
where?: VSRepoWhere<T>,
|
|
988
|
-
options?: AdapterMethodOptions<T>,
|
|
989
|
-
): Promise<number | null>;
|
|
990
|
-
abstract min(
|
|
991
|
-
field: NumericKeys<T>,
|
|
992
|
-
where?: VSRepoWhere<T>,
|
|
993
|
-
options?: AdapterMethodOptions<T>,
|
|
994
|
-
): Promise<number | null>;
|
|
995
|
-
abstract max(
|
|
996
|
-
field: NumericKeys<T>,
|
|
997
|
-
where?: VSRepoWhere<T>,
|
|
998
|
-
options?: AdapterMethodOptions<T>,
|
|
999
|
-
): Promise<number | null>;
|
|
1000
|
-
getPkName?(): string;
|
|
1001
|
-
}
|
|
1002
|
-
```
|
|
1003
|
-
|
|
1004
|
-
The optional `getPkName()` lets the adapter declare the entity's primary-key field to the repository. When instantiating a `VSRepository`, you can omit `pkName` from the constructor options and it will be read from `adapter.getPkName()`. If you omit it and the adapter doesn't implement `getPkName()`, the constructor throws a `VSRepoError`.
|
|
1005
|
-
|
|
1006
|
-
`VSRepository` never talks to the ORM directly β it only calls these methods with an already-resolved `VSRepoWhere<T>` and `AdapterMethodOptions<T>`. Once an adapter implements this contract, every base method, dynamic method, and query method works against it automatically. For a full, working implementation, see the external [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) repo.
|
|
1007
|
-
|
|
1008
|
-
### Logging from your adapter
|
|
1009
|
-
|
|
1010
|
-
`vsrepo` exports the same `VSLogger` class the core uses internally, so your adapter can log in the same format/style (timestamps, colored level labels, slow-operation warnings) instead of rolling its own:
|
|
1011
|
-
|
|
1012
|
-
```typescript
|
|
1013
|
-
import { VSLogger, VSLogLevel } from "vsrepo";
|
|
1014
|
-
|
|
1015
|
-
export class MyOrmAdapter<T> extends VSRepoAdapter<T> {
|
|
1016
|
-
private readonly logger = new VSLogger(VSLogLevel.WARN, "MyOrmAdapterLogger");
|
|
1017
|
-
|
|
1018
|
-
async findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>) {
|
|
1019
|
-
const start = this.logger.startPerformLog("adapter findOne");
|
|
1020
|
-
try {
|
|
1021
|
-
// ... talk to the ORM ...
|
|
1022
|
-
this.logger.endPerformLog(start);
|
|
1023
|
-
return result;
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
this.logger.endPerformLog(start);
|
|
1026
|
-
this.logger.logError("adapter findOne failed", err);
|
|
1027
|
-
throw err;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
| Method | Description |
|
|
1034
|
-
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1035
|
-
| `new VSLogger(logLevel, name, slowThresholdMs?)` | Creates a logger; `name` prefixes every line. `slowThresholdMs` controls the slow-operation threshold: a `number` sets it in ms (default 300), `false` disables slow-operation warnings entirely, `true` or omitted uses the 300ms default. |
|
|
1036
|
-
| `logDebug/logInfo/logWarn(text, obj?)` | Logs at the given level if `logLevel` allows it; `obj` is appended as pretty-printed JSON. |
|
|
1037
|
-
| `logError(text, err?)` | Logs at `ERROR`; if `err` is an `Error`, only `name`/`message`/`stack`/`cause` are logged. |
|
|
1038
|
-
| `startPerformLog(operation)` / `endPerformLog(data)` | Bracket a block to log its duration, escalating to `WARN` if it exceeds `slowThresholdMs`. |
|
|
1039
|
-
| `getLogLevel()` | Returns the logger's configured `VSLogLevel`. |
|
|
1040
|
-
|
|
1041
|
-
This is purely a convenience for adapter authors β nothing in the core requires your adapter to use it.
|
|
1042
|
-
|
|
1043
|
-
---
|
|
1044
|
-
|
|
1045
|
-
## Error handling
|
|
1046
|
-
|
|
1047
|
-
v2 simplifies the error hierarchy from v1: instead of several subclasses, there's a base `VSRepoError` class carrying a `type: VSRepoErrorType`, plus a dedicated `VSRepoAdapterError` subclass (see below) for failures coming from the underlying ORM/database.
|
|
1048
|
-
|
|
1049
|
-
```typescript
|
|
1050
|
-
import { VSRepoError } from "vsrepo";
|
|
1051
|
-
|
|
1052
|
-
try {
|
|
1053
|
-
await userRepository.get(id);
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
if (error instanceof VSRepoError) {
|
|
1056
|
-
console.error(`[${error.type}] ${error.message}`);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
```
|
|
1060
|
-
|
|
1061
|
-
| `VSRepoErrorType` | Raised when |
|
|
1062
|
-
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1063
|
-
| `DECORATOR` | Invalid arguments were passed to `@DynamicMethod` or `@QueryMethod`. |
|
|
1064
|
-
| `RESOLVER` | The library failed to resolve a dynamic/query method's configuration into a callable method (e.g. an unknown method name). |
|
|
1065
|
-
| `DYNAMIC` | A resolved dynamic/query method failed at runtime (e.g. missing arguments). |
|
|
1066
|
-
| `VALIDATOR` | Invalid method options or arguments were detected during validation (e.g. a missing `pkName` when the adapter has no `getPkName()`). |
|
|
1067
|
-
| `BASE` | Invalid usage of a base method (`get`, `save`, `remove`, etc). |
|
|
1068
|
-
| `ADAPTER` | A `VSRepoAdapter` failed while talking to the underlying ORM/database β always thrown as `VSRepoAdapterError`. |
|
|
1069
|
-
|
|
1070
|
-
### `VSRepoAdapterError` and `AdapterErrorCode`
|
|
1071
|
-
|
|
1072
|
-
When an adapter talks to the underlying ORM/database and that operation fails, the adapter wraps the failure in a `VSRepoAdapterError` β a subclass of `VSRepoError` with `type: VSRepoErrorType.ADAPTER`. It carries a **stable, adapter-agnostic** `code: AdapterErrorCode` plus the raw error thrown by the ORM/driver, so callers can react to failures without depending on any single ORM's error shape:
|
|
1073
|
-
|
|
1074
|
-
```typescript
|
|
1075
|
-
import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
|
|
1076
|
-
|
|
1077
|
-
try {
|
|
1078
|
-
await userRepository.save({ name: "Maria" });
|
|
1079
|
-
} catch (error) {
|
|
1080
|
-
if (error instanceof VSRepoAdapterError) {
|
|
1081
|
-
console.error(`[${error.code}] ${error.message}`, error.originalError);
|
|
1082
|
-
|
|
1083
|
-
if (error.code === AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
|
|
1084
|
-
// handle a duplicate key, e.g. return a friendly message
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
| Property | Type | Description |
|
|
1091
|
-
| --------------- | ------------------ | ----------------------------------------------------------------------------------- |
|
|
1092
|
-
| `code` | `AdapterErrorCode` | Stable, adapter-agnostic code classifying the failure. |
|
|
1093
|
-
| `originalError` | `unknown` | The raw error (or `null`/`undefined`) thrown by the underlying ORM/database driver. |
|
|
1094
|
-
| `message` | `string` | Human-readable description of the adapter failure. |
|
|
1095
|
-
| `type` | `VSRepoErrorType` | Always `VSRepoErrorType.ADAPTER`. |
|
|
1096
|
-
| `cause` | `unknown` | Optional root cause the error was chained from. |
|
|
1097
|
-
|
|
1098
|
-
Adapter implementations construct it directly when mapping an ORM failure:
|
|
1099
|
-
|
|
1100
|
-
```typescript
|
|
1101
|
-
import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
|
|
1102
|
-
|
|
1103
|
-
throw new VSRepoAdapterError(
|
|
1104
|
-
"user creation failed",
|
|
1105
|
-
AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
|
|
1106
|
-
originalError, // raw DB/driver error
|
|
1107
|
-
);
|
|
1108
|
-
```
|
|
1109
|
-
|
|
1110
|
-
#### `AdapterErrorCode`
|
|
1111
|
-
|
|
1112
|
-
`AdapterErrorCode` is an enum of granular, adapter-agnostic codes an adapter can raise through `VSRepoAdapterError`. They mirror the most common failures thrown by ORMs and database drivers so any ORM's errors can be mapped to the same stable code:
|
|
1113
|
-
|
|
1114
|
-
```typescript
|
|
1115
|
-
import { AdapterErrorCode } from "vsrepo";
|
|
1116
|
-
|
|
1117
|
-
console.log(AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION); // "UNIQUE_CONSTRAINT_VIOLATION"
|
|
1118
|
-
```
|
|
1119
|
-
|
|
1120
|
-
| Code | Meaning |
|
|
1121
|
-
| ----------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
1122
|
-
| `UNKNOWN` | Unclassified/unknown error; the fallback when no more specific code matches. |
|
|
1123
|
-
| `TRANSACTION_ROLLED_BACK` | Some adapters might use this code for forced transaction rollbacks (like Drizzle's `tx.rollback()`) |
|
|
1124
|
-
| `MISSING_DB_CLIENT` | Database client (or connection pool) not provided or could not be resolved. |
|
|
1125
|
-
| `CONNECTION_FAILED` | Could not reach/connect to the database, or an established connection was lost/terminated. |
|
|
1126
|
-
| `CONNECTION_POOL_EXHAUSTED` | Connection pool exhausted/depleted β no connection available, all busy or the limit was reached. |
|
|
1127
|
-
| `TIMEOUT` | Database did not respond in time; a query exceeded its allowed timeout. |
|
|
1128
|
-
| `UNIQUE_CONSTRAINT_VIOLATION` | Unique constraint (duplicate key) violated. E.g. Postgres/SQLite `23505`, MySQL `1062`. |
|
|
1129
|
-
| `FOREIGN_KEY_VIOLATION` | Foreign key constraint violated (referenced row missing). |
|
|
1130
|
-
| `NOT_NULL_VIOLATION` | NOT NULL constraint violated. |
|
|
1131
|
-
| `CHECK_VIOLATION` | CHECK constraint violated. |
|
|
1132
|
-
| `CONSTRAINT_VIOLATION` | General integrity/constraint violation not covered by a more specific code. |
|
|
1133
|
-
| `NOT_FOUND` | Requested record not found (e.g. a `findOneOrThrow`-style operation). |
|
|
1134
|
-
| `INVALID_DATA` | Field value invalid for its type/length, or a required value is missing. |
|
|
1135
|
-
| `VALUE_TOO_LONG` | Provided value exceeds the column/field length limit. |
|
|
1136
|
-
| `CONVERSION_ERROR` | Value could not be converted/cast to the target type. E.g. Postgres `22P02`, MySQL `1366`. |
|
|
1137
|
-
| `INVALID_QUERY` | SQL query/stored procedure is malformed or invalid. |
|
|
1138
|
-
| `TABLE_OR_COLUMN_NOT_FOUND` | Referenced table/column/relation does not exist. |
|
|
1139
|
-
| `DEADLOCK` | Operation aborted by a lock timeout or deadlock between concurrent transactions. |
|
|
1140
|
-
| `LOCK_TIMEOUT` | Could not acquire a required database lock in time. |
|
|
1141
|
-
| `LOCKED` | Record is locked and cannot be modified. |
|
|
1142
|
-
| `ACCESS_DENIED` | Current user/role does not have permission for the operation. |
|
|
1143
|
-
| `INVALID_CREDENTIALS` | Invalid connection credentials (host/user/password). |
|
|
1144
|
-
| `ROW_NOT_ALLOWED` | Authenticated user does not own the record / row-level security rejected it. |
|
|
1145
|
-
| `MODEL_NOT_FOUND` | Entity/model or table not defined/mapped in the ORM, or the adapter lacks model metadata to build the query. |
|
|
1146
|
-
| `FIELD_NOT_FOUND` | Field/column name in the data or `where` does not exist on the entity/model. |
|
|
1147
|
-
| `TRANSACTION_CLOSED` | Transaction used after it was committed/rolled back. |
|
|
1148
|
-
| `TRANSACTION_ALREADY_STARTED` | A nested transaction could not be opened (e.g. nested `transaction()` calls). |
|
|
1149
|
-
| `TRANSACTION_CONFLICT` | Transaction failed to commit and was rolled back. |
|
|
1150
|
-
| `TRANSACTION_NOT_STARTED` | No active transaction when one was required. |
|
|
1151
|
-
| `CONNECTION_CLOSED` | Connection closed/terminated while a transaction or query was in progress. |
|
|
1152
|
-
| `INVALID_PARTIAL` | `merge`/`upsert`/`update` received a partial object that is invalid or missing required keys. |
|
|
1153
|
-
| `NOT_SUPPORTED` | Unsupported feature/operation requested from the adapter (e.g. raw `query()` not supported). |
|
|
1154
|
-
| `INVALID_ADAPTER_CONFIG` | Adapter configuration invalid or incomplete (missing required options, or options with an invalid type/value). |
|
|
1155
|
-
| `INTERNAL` | Internal adapter bug or unrecoverable state; should rarely be used β prefer a more specific code. |
|
|
1156
|
-
|
|
1157
|
-
#### `VSRepoError` vs. raw ORM errors
|
|
1158
|
-
|
|
1159
|
-
Non-adapter usage/config mistakes throw the base `VSRepoError`. Failures raised _by the underlying ORM_ while an adapter method runs are **wrapped** in `VSRepoAdapterError` (classified by an `AdapterErrorCode`, with the original error preserved in `originalError`) instead of propagating raw β this is what makes callers independent of any specific ORM's error shape.
|
|
1160
|
-
|
|
1161
|
-
---
|
|
1162
|
-
|
|
1163
|
-
## Logging
|
|
1164
|
-
|
|
1165
|
-
Every repository has an internal logger, configured via `logLevel` and `logSlowThresholdMs` on the constructor options:
|
|
1166
|
-
|
|
1167
|
-
```typescript
|
|
1168
|
-
import { VSLogLevel } from "vsrepo";
|
|
1169
|
-
|
|
1170
|
-
super({
|
|
1171
|
-
pkName: "id",
|
|
1172
|
-
adapter,
|
|
1173
|
-
logLevel: VSLogLevel.DEBUG,
|
|
1174
|
-
logSlowThresholdMs: 200, // warn if any operation takes > 200ms
|
|
1175
|
-
// logSlowThresholdMs: false, // disable slow-operation warnings entirely
|
|
1176
|
-
});
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
| Level | Meaning |
|
|
1180
|
-
| ---------------- | ----------------------------------------------------------------------------------------------------- |
|
|
1181
|
-
| `DEBUG` | Verbose internal details, including every resolved query β very useful for debugging dynamic methods. |
|
|
1182
|
-
| `INFO` | High-level lifecycle events, such as repository initialization. |
|
|
1183
|
-
| `WARN` (default) | Recoverable issues and slow operations (see `logSlowThresholdMs`, defaults to 300ms). |
|
|
1184
|
-
| `ERROR` | Failures raised while executing an operation. |
|
|
1185
|
-
|
|
1186
|
-
---
|
|
1187
|
-
|
|
1188
|
-
## Development
|
|
1189
|
-
|
|
1190
|
-
The v2 core is built and packed from this branch as a standard npm package:
|
|
1191
|
-
|
|
1192
|
-
```bash
|
|
1193
|
-
# 1. Install dependencies
|
|
1194
|
-
pnpm install
|
|
1195
|
-
|
|
1196
|
-
# 2. Compile the TypeScript sources into dist/ (removes a previous dist/ first)
|
|
1197
|
-
pnpm build
|
|
1198
|
-
|
|
1199
|
-
# 3. (Optional) Inspect what would be published without writing a tarball
|
|
1200
|
-
npm pack --dry-run
|
|
1201
|
-
|
|
1202
|
-
# 4. Produce the installable tarball (runs `prepack` -> `pnpm build` automatically)
|
|
1203
|
-
npm pack
|
|
1204
|
-
|
|
1205
|
-
# 5. Consume it locally in another project
|
|
1206
|
-
npm install ../path/to/vsrepo-*.tgz
|
|
1207
|
-
```
|
|
1208
|
-
|
|
1209
|
-
Notes:
|
|
1210
|
-
|
|
1211
|
-
- `pnpm build` runs `tsc -p tsconfig.build.json`, which outputs the compiled JS and generated type declarations into `dist/` with `rootDir: src`.
|
|
1212
|
-
- 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.
|
|
1213
|
-
|
|
1214
|
-
---
|
|
1215
|
-
|
|
1216
|
-
## Requirements
|
|
1217
|
-
|
|
1218
|
-
- Node.js 18+
|
|
1219
|
-
- TypeScript, with **legacy/experimental decorators** enabled (required by `@DynamicMethod`/`@QueryMethod`):
|
|
1220
|
-
|
|
1221
|
-
```json
|
|
1222
|
-
{
|
|
1223
|
-
"compilerOptions": {
|
|
1224
|
-
"experimentalDecorators": true
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
```
|
|
1228
|
-
|
|
1229
|
-
- `reflect-metadata` (bundled as a dependency, imported internally β you don't need to import it yourself)
|
|
1230
|
-
- At least one working `VSRepoAdapter` for your database β on Prisma 7, install the published [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) (see [Adapter status](#adapter-status)); official adapters for other ORMs are planned but not published yet, so for now this means writing your own (see [Writing your own adapter](#writing-your-own-adapter)) β and if you publish it, contributing it back to the project is welcome
|
|
1231
|
-
|
|
1232
|
-
---
|
|
1233
|
-
|
|
1234
|
-
## Contributing
|
|
1235
|
-
|
|
1236
|
-
Contributions are welcome, especially for improving the Prisma adapter and finishing the Drizzle one! (**[GitHub repository](https://github.com/jaobrabo123/VSRepository)**):
|
|
1237
|
-
|
|
1238
|
-
1. **Fork** the project.
|
|
1239
|
-
2. Create a branch for your change: `git checkout -b my-change`.
|
|
1240
|
-
3. Push your branch: `git push origin my-change`.
|
|
1241
|
-
4. Open a **Pull Request**.
|
|
1242
|
-
|
|
1243
|
-
To report issues or suggest features, open an **Issue**.
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://res.cloudinary.com/ddbfifdxd/image/upload/w_200,q_auto,f_auto/v1786386427/VS_logo_TextoAbaixo_yev4tq.png" alt="VSRepository Logo" width="200"/>
|
|
3
|
+
|
|
4
|
+
<p style="margin-top: 12px;">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/vsrepo?style=flat-square" alt="npm version"/>
|
|
6
|
+
<img src="https://img.shields.io/npm/l/vsrepo?style=flat-square" alt="npm license"/>
|
|
7
|
+
<img src="https://img.shields.io/npm/dt/vsrepo?style=flat-square" alt="npm downloads"/>
|
|
8
|
+
<img src="https://img.shields.io/badge/inspired%20by-JpaRepository-E73121?style=flat-square" alt="inspired by JpaRepository"/>
|
|
9
|
+
</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
# VSRepository
|
|
13
|
+
|
|
14
|
+
πΊπΈ You're reading the English version. [π§π· Ler em portuguΓͺs](./README.pt-BR.md)
|
|
15
|
+
|
|
16
|
+
**ORM-agnostic** repository pattern library, with full **TypeScript** support and automatic **type inference**. The core delegates every operation to a pluggable **adapter**, so the same repository API can work against Prisma, Drizzle, or any other ORM/database that implements the adapter contract. Coming from the old [v1](https://github.com/jaobrabo123/VSRepository/tree/v1)? See [Migrating from v1](./docs/migrating-from-v1.md).
|
|
17
|
+
|
|
18
|
+
VSRepository lets you create strongly-typed repositories with:
|
|
19
|
+
|
|
20
|
+
- Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `merge`, `getAll`, `total`, `has`
|
|
21
|
+
- **Native soft-delete**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
|
|
22
|
+
- **Dynamic methods** inferred from a `declare` field name via the `@DynamicMethod` decorator: `findOneByEmail`, `findByStatusPaginated`, `updateById`
|
|
23
|
+
- **Raw SQL query methods** via the `@QueryMethod` decorator, bypassing the name-parsing engine entirely
|
|
24
|
+
- Ad-hoc **`select`/`relations`** per call β no more pre-declared named projections
|
|
25
|
+
- **Type safety** across 100% of operations
|
|
26
|
+
- Native ORM **transactions**, shared across repositories
|
|
27
|
+
- An **ORM-agnostic core** β the same repository class works with any `VSRepoAdapter` implementation
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
The sections below (adapter status, installation, basic usage) are the essentials to get you started. Everything about a specific feature β with more detail and more examples β lives in its own guide under [`docs/`](./docs/README.md), each available in English and in [PortuguΓͺs](./docs/README.pt-BR.md):
|
|
34
|
+
|
|
35
|
+
| Guide | Covers |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| [Base methods, configuration & soft-delete](./docs/base-methods.md) | Constructor options, the 12 automatic CRUD methods, native soft-delete, and the 8 atomic/aggregate methods (`increment`, `sum`, ...). |
|
|
38
|
+
| [`select` and `relations`](./docs/select-and-relations.md) | Ad-hoc field selection and eager relation loading on any call, and `InferMethodReturn` to narrow the return type accordingly. |
|
|
39
|
+
| [Dynamic methods](./docs/dynamic-methods.md) | `findByEmail`-style methods parsed from a `declare`d method name: prefixes, field filters, logical operators, relation filters, ordering/pagination/distinct. |
|
|
40
|
+
| [Query methods (raw SQL)](./docs/query-methods.md) | Raw SQL methods via `@QueryMethod`, bypassing the dynamic-method name parser entirely. |
|
|
41
|
+
| [Query builder](./docs/query-builder.md) | The fluent `createQueryBuilder()` API for queries assembled at runtime, including pagination, soft-delete visibility and transactions. |
|
|
42
|
+
| [Transactions](./docs/transactions.md) | Running several repositories against the same native ORM transaction. |
|
|
43
|
+
| [Utility types](./docs/utility-types.md) | The exported helper types (`InferMethodType`, `InferMethodReturn`, `KeysOfType`, ...) and where each one is used. |
|
|
44
|
+
| [Writing your own adapter](./docs/writing-an-adapter.md) | How to implement `VSRepoAdapter` for a new ORM or database, method by method. |
|
|
45
|
+
| [Error handling](./docs/error-handling.md) | `VSRepoError`, `VSRepoErrorType`, and `VSRepoAdapterError`/`AdapterErrorCode`. |
|
|
46
|
+
| [Logging](./docs/logging.md) | `logLevel`, `logSlowThresholdMs`, and the log format used by the repository and the query builder. |
|
|
47
|
+
| [Migrating from v1](./docs/migrating-from-v1.md) | Everything that changed between v1 and v2 β API, config, renamed suffixes, removed features β in a single reference for migrating existing repositories. |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Adapter status
|
|
52
|
+
|
|
53
|
+
VSRepository is **ORM-agnostic by design**. The core package (`vsrepo`) only ships the repository class, the decorators, the name-parsing engine, error handling and logging β it does **not** ship a production adapter. Actual ORM/database support is meant to live in **separate, independently versioned packages**, one per ORM (and, where it makes sense, one per major ORM version), for example:
|
|
54
|
+
|
|
55
|
+
- `@vsrepo/prisma7-adapter`
|
|
56
|
+
- `@vsrepo/prisma8-adapter`
|
|
57
|
+
- `@vsrepo/typeorm-adapter`
|
|
58
|
+
- `@vsrepo/drizzle-adapter`
|
|
59
|
+
|
|
60
|
+
The Prisma 7 adapter is published to npm as `@vsrepo/prisma7-adapter`. The Drizzle adapter is available as an **alpha** release β install it with `@vsrepo/drizzle-adapter@alpha`. Adapters for other ORMs are **planned** but not published yet. Until an official `@vsrepo/*-adapter` package exists for your ORM, you're welcome to write your own for your project, and if you'd like, publish it and open a PR to help grow the ecosystem β contributions here are very welcome.
|
|
61
|
+
|
|
62
|
+
| Adapter | Status |
|
|
63
|
+
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| Prisma 7 (`@vsrepo/prisma7-adapter`) | π’ **Released** β published to npm, implements the `VSRepoAdapter` contract (CRUD, relations, transactions, `merge`, logging, etc.) with tests; see [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) for source and docs. |
|
|
65
|
+
| Drizzle (`@vsrepo/drizzle-adapter`) | π΅ **Alpha** β an early release is available on npm; install it with `npm i @vsrepo/drizzle-adapter@alpha`. The API may still change before the stable release. Check the [`DrizzleAdapter`](https://github.com/jaobrabo123/VSRepoDrizzleAdapter) repository for the current status and known limitations, and feel free to contribute. |
|
|
66
|
+
| Other ORMs (Prisma 8, TypeORM, etc.) | π‘ **Planned, not published yet.** No official package exists yet β write your own adapter for now (see [Writing your own adapter](./docs/writing-an-adapter.md#writing-your-own-adapter)), and consider publishing/contributing it back. |
|
|
67
|
+
| Custom adapters | π’ Fully supported today β implement the [`VSRepoAdapter`](./docs/writing-an-adapter.md#writing-your-own-adapter) abstract class yourself for any ORM/database you need, in your own project or package. |
|
|
68
|
+
|
|
69
|
+
In short: the repository class, the `@DynamicMethod`/`@QueryMethod` decorators, the name-parsing engine, error handling and logging are all working end-to-end, and Prisma 7 support is a released, published adapter. The Drizzle adapter is available in alpha. Official adapters for the remaining ORMs are on the roadmap and will ship as separate `@vsrepo/*-adapter` packages rather than as part of the core `vsrepo` package β but you don't have to wait for that: writing (and optionally publishing) your own adapter in the meantime is a fully supported way to use VSRepository today and to contribute back to the project.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
VSRepository is installed as the core package plus one adapter package for your ORM, for example:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm i vsrepo @vsrepo/prisma7-adapter
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Basic usage
|
|
84
|
+
|
|
85
|
+
### Implementing/choosing an adapter
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// src/configs/db.ts
|
|
89
|
+
import { PrismaClient } from "../../generated/prisma/client";
|
|
90
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
91
|
+
import "dotenv/config";
|
|
92
|
+
|
|
93
|
+
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
|
94
|
+
const prisma = new PrismaClient({ adapter });
|
|
95
|
+
|
|
96
|
+
export default prisma;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Creating a repository
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// src/repositories/user.repository.ts
|
|
103
|
+
import { VSRepository, DynamicMethod } from "vsrepo";
|
|
104
|
+
import { VSRepoPrisma7Adapter } from "@vsrepo/prisma7-adapter";
|
|
105
|
+
import prisma from "../configs/db";
|
|
106
|
+
import type { UserGetPayload } from "../../generated/prisma/models";
|
|
107
|
+
|
|
108
|
+
type User = UserGetPayload<{ include: { address: true } }>;
|
|
109
|
+
|
|
110
|
+
class UserRepository extends VSRepository<User, string> {
|
|
111
|
+
constructor() {
|
|
112
|
+
super({
|
|
113
|
+
pkName: "id",
|
|
114
|
+
adapter: new VSRepoPrisma7Adapter<User>(prisma, { tableName: "user", pkName: "id" }),
|
|
115
|
+
softRemoveKey: "deletedAt",
|
|
116
|
+
defaultOrdering: { createdAt: "desc" },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@DynamicMethod()
|
|
121
|
+
declare findByEmail: (email: string) => Promise<User[]>;
|
|
122
|
+
|
|
123
|
+
@DynamicMethod()
|
|
124
|
+
declare findOneByEmail: (email: string) => Promise<User | null>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default new UserRepository();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> The core API (`VSRepository`, `VSRepoAdapter`, `DynamicMethod`, `QueryMethod`, `VSRepoError`, enums and types) is imported from the single `vsrepo` entry point. The concrete adapter comes from a **separate** package (`@vsrepo/*-adapter`). On Prisma 7, install the [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter).
|
|
131
|
+
|
|
132
|
+
> **The third generic parameter (`OrmTypes`):** `VSRepository<Entity, PKType, OrmTypes>` accepts an optional third type parameter describing your ORM's client/transaction types, via `VSRepoOrmTypes` (`{ dbClient; dbTransaction }`). Supplying it gives you a correctly-typed `getDbClient()`, `transaction()` callback, and `db` option on every method, instead of `any`:
|
|
133
|
+
>
|
|
134
|
+
> ```typescript
|
|
135
|
+
> import { Prisma7OrmTypes } from "@vsrepo/prisma7-adapter";
|
|
136
|
+
>
|
|
137
|
+
> type MyOrmTypes = Prisma7OrmTypes<PrismaClient>;
|
|
138
|
+
>
|
|
139
|
+
> class UserRepository extends VSRepository<User, string, MyOrmTypes> {
|
|
140
|
+
> // getDbClient() now returns PrismaClient, and transaction(fn) types `tx` as Prisma.TransactionClient
|
|
141
|
+
> }
|
|
142
|
+
> ```
|
|
143
|
+
>
|
|
144
|
+
> If omitted, it defaults to `VSRepoOrmTypes` (`dbClient`/`dbTransaction` both `any`).
|
|
145
|
+
|
|
146
|
+
### Using the repository
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import userRepository from "./repositories/user.repository";
|
|
150
|
+
|
|
151
|
+
const user = await userRepository.save({
|
|
152
|
+
name: "Joao",
|
|
153
|
+
email: "joao@email.com",
|
|
154
|
+
password: "password",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const found = await userRepository.get(user.id);
|
|
158
|
+
const all = await userRepository.getAll();
|
|
159
|
+
const byEmail = await userRepository.findByEmail("joao@email.com");
|
|
160
|
+
|
|
161
|
+
await userRepository.patch(user.id, { name: "Joao Pedro" });
|
|
162
|
+
await userRepository.remove(user.id);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# 1. Install dependencies
|
|
171
|
+
bun install
|
|
172
|
+
|
|
173
|
+
# 2. Compile the TypeScript sources into dist/ (removes a previous dist/ first)
|
|
174
|
+
bun run build
|
|
175
|
+
|
|
176
|
+
# 3. (Optional) Inspect what would be published without writing a tarball
|
|
177
|
+
npm pack --dry-run
|
|
178
|
+
|
|
179
|
+
# 4. Produce the installable tarball (runs `prepack` -> `bun run build` automatically)
|
|
180
|
+
npm pack
|
|
181
|
+
|
|
182
|
+
# 5. Consume it locally in another project
|
|
183
|
+
npm install ../path/to/vsrepo-*.tgz
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Notes:
|
|
187
|
+
|
|
188
|
+
- `bun run build` runs `tsc -p tsconfig.build.json`, which outputs the compiled JS and generated type declarations into `dist/` with `rootDir: src`.
|
|
189
|
+
- The published package contains **only** the `dist/` folder, the READMEs, `CHANGELOG.md` and `LICENSE` (see `files` in `package.json`). The adapters will live in their own `@vsrepo/*-adapter` packages.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Requirements
|
|
194
|
+
|
|
195
|
+
- Node.js 18+
|
|
196
|
+
- TypeScript, with **legacy/experimental decorators** enabled (required by `@DynamicMethod`/`@QueryMethod`):
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"compilerOptions": {
|
|
201
|
+
"experimentalDecorators": true
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- `reflect-metadata` (bundled as a dependency, imported internally β you don't need to import it yourself)
|
|
207
|
+
- At least one working `VSRepoAdapter` for your database β on Prisma 7, install the published [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) (see [Adapter status](#adapter-status)); official adapters for other ORMs are planned but not published yet, so for now this means writing your own (see [Writing your own adapter](./docs/writing-an-adapter.md#writing-your-own-adapter)) β and if you publish it, contributing it back to the project is welcome
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Contributing
|
|
212
|
+
|
|
213
|
+
Contributions are welcome, especially for improving the Prisma adapter and finishing the Drizzle one! (**[GitHub repository](https://github.com/jaobrabo123/VSRepository)**):
|
|
214
|
+
|
|
215
|
+
1. **Fork** the project.
|
|
216
|
+
2. Create a branch for your change: `git checkout -b my-change`.
|
|
217
|
+
3. Push your branch: `git push origin my-change`.
|
|
218
|
+
4. Open a **Pull Request**.
|
|
219
|
+
|
|
220
|
+
To report issues or suggest features, open an **Issue**.
|