vsrepo 1.4.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +827 -1322
- package/README.pt-BR.md +833 -1325
- package/dist/VSRepoAdapter.d.ts +109 -0
- package/dist/VSRepoAdapter.js +18 -0
- package/dist/VSRepository.d.ts +166 -1201
- package/dist/VSRepository.js +327 -237
- package/dist/decorators/dynamic-method.decorator.d.ts +25 -0
- package/dist/decorators/dynamic-method.decorator.js +38 -0
- package/dist/decorators/query-method.decorator.d.ts +27 -0
- package/dist/decorators/query-method.decorator.js +45 -0
- package/dist/errors/VSRepoAdapterError.d.ts +22 -0
- package/dist/errors/VSRepoAdapterError.js +31 -0
- package/dist/errors/VSRepoError.d.ts +15 -0
- package/dist/errors/VSRepoError.js +21 -0
- package/dist/index.d.ts +38 -1
- package/dist/index.js +28 -15
- package/dist/internal/constants/debug-arg-symbol.constant.d.ts +1 -0
- package/dist/internal/constants/debug-arg-symbol.constant.js +4 -0
- package/dist/internal/constants/dynamic-methods-key.constant.d.ts +1 -0
- package/dist/internal/constants/query-methods-key.constant.d.ts +1 -0
- package/dist/internal/constants/query-methods-key.constant.js +4 -0
- package/dist/internal/enums/adapter-error-code.enum.d.ts +125 -0
- package/dist/internal/enums/adapter-error-code.enum.js +129 -0
- package/dist/internal/enums/transaction-isolation-level.enum.d.ts +16 -0
- package/dist/internal/enums/transaction-isolation-level.enum.js +20 -0
- package/dist/internal/enums/vs-log-level.enum.d.ts +18 -0
- package/dist/internal/enums/vs-log-level.enum.js +22 -0
- package/dist/internal/enums/vsrepo-error-type.enum.d.ts +19 -0
- package/dist/internal/enums/vsrepo-error-type.enum.js +23 -0
- package/dist/internal/resolvers/dynamic-methods.resolver.d.ts +23 -0
- package/dist/internal/resolvers/dynamic-methods.resolver.js +910 -0
- package/dist/internal/resolvers/merge-wheres.resolver.d.ts +7 -0
- package/dist/internal/resolvers/merge-wheres.resolver.js +26 -0
- package/dist/internal/utils/uncapitalize.util.d.ts +1 -0
- package/dist/internal/utils/vs-logger.util.d.ts +25 -0
- package/dist/internal/utils/vs-logger.util.js +139 -0
- package/dist/internal/validators/decorators.validator.d.ts +8 -0
- package/dist/internal/validators/decorators.validator.js +75 -0
- package/dist/internal/validators/schemas/ordering.schema.d.ts +3 -0
- package/dist/internal/validators/schemas/ordering.schema.js +38 -0
- package/dist/internal/validators/schemas/pagination.schema.d.ts +6 -0
- package/dist/internal/validators/schemas/pagination.schema.js +40 -0
- package/dist/internal/validators/schemas/where.schema.d.ts +7 -0
- package/dist/internal/validators/schemas/where.schema.js +42 -0
- package/dist/internal/validators/vsrepo.validator.d.ts +40 -0
- package/dist/internal/validators/vsrepo.validator.js +182 -0
- package/dist/types/adapter/adapter-method-options.type.d.ts +24 -0
- package/dist/types/adapter/adapter-query-options.type.d.ts +5 -0
- package/dist/types/decorators/dynamic-method-options.type.d.ts +14 -0
- package/dist/types/decorators/query-method-options.type.d.ts +15 -0
- package/dist/types/dynamic-methods/dynamic-method-customization.type.d.ts +7 -0
- package/dist/types/dynamic-methods/dynamic-method-info.type.d.ts +18 -0
- package/dist/types/dynamic-methods/dynamic-method-where-ops.type.d.ts +6 -0
- package/dist/types/utils/count-result.type.d.ts +9 -0
- package/dist/types/utils/decimal-like.type.d.ts +23 -0
- package/dist/types/utils/deep-partial.type.d.ts +14 -0
- package/dist/types/utils/keys-of-type.type.d.ts +20 -0
- package/dist/types/utils/methods-options.type.d.ts +23 -0
- package/dist/types/utils/numeric-keys.type.d.ts +24 -0
- package/dist/types/utils/numeric-like.type.d.ts +10 -0
- package/dist/types/utils/ordering.type.d.ts +39 -0
- package/dist/types/utils/pagination.type.d.ts +11 -0
- package/dist/types/utils/perform-data.type.d.ts +4 -0
- package/dist/types/utils/primitive.type.d.ts +7 -0
- package/dist/types/utils/query-method-arg.type.d.ts +27 -0
- package/dist/types/utils/restrict-method-options.type.d.ts +14 -0
- package/dist/types/utils/see-mode.type.d.ts +12 -0
- package/dist/types/vsrepo/vsrepo-args.type.d.ts +9 -0
- package/dist/types/vsrepo/vsrepo-method.type.d.ts +4 -0
- package/dist/types/vsrepo/vsrepo-method.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-options.type.d.ts +34 -0
- package/dist/types/vsrepo/vsrepo-options.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-orm-types.type.d.ts +17 -0
- package/dist/types/vsrepo/vsrepo-orm-types.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-pretty-where.type.d.ts +7 -0
- package/dist/types/vsrepo/vsrepo-pretty-where.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-query-options.type.d.ts +17 -0
- package/dist/types/vsrepo/vsrepo-query-options.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-query.type.d.ts +5 -0
- package/dist/types/vsrepo/vsrepo-query.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-relations.type.d.ts +26 -0
- package/dist/types/vsrepo/vsrepo-relations.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-resolve-args-data.type.d.ts +19 -0
- package/dist/types/vsrepo/vsrepo-resolve-args-data.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-select.type.d.ts +15 -0
- package/dist/types/vsrepo/vsrepo-select.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-transaction-options.type.d.ts +12 -0
- package/dist/types/vsrepo/vsrepo-transaction-options.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-ugly-where.type.d.ts +9 -0
- package/dist/types/vsrepo/vsrepo-ugly-where.type.js +2 -0
- package/dist/types/vsrepo/vsrepo-where.type.d.ts +99 -0
- package/dist/types/vsrepo/vsrepo-where.type.js +2 -0
- package/package.json +16 -37
- package/README-DynamicRepo.md +0 -625
- package/README-DynamicRepo.pt-BR.md +0 -625
- package/dist/DynamicRepository.d.ts +0 -497
- package/dist/DynamicRepository.js +0 -26
- package/dist/VSRepoError.d.ts +0 -83
- package/dist/VSRepoError.js +0 -17
- package/dist/internal/decorators/dynamic-method.decorator.js +0 -14
- package/dist/internal/decorators/query-method.decorator.js +0 -20
- package/dist/internal/entities/dynamic-method-metadata.entity.js +0 -26
- package/dist/internal/errors/vs-repo.error.js +0 -31
- package/dist/internal/resolvers/base-methods.resolve.js +0 -541
- package/dist/internal/resolvers/create-update-payloads-with-relations.resolve.js +0 -143
- package/dist/internal/resolvers/data-payload-with-relations.resolve.js +0 -60
- package/dist/internal/resolvers/dbAndPrismaArgs.resolve.js +0 -63
- package/dist/internal/resolvers/dynamic-method-customization.resolve.js +0 -57
- package/dist/internal/resolvers/dynamic-method-info.resolve.js +0 -279
- package/dist/internal/resolvers/dynamic-methods-metadata.resolve.js +0 -15
- package/dist/internal/resolvers/merge-wheres.resolve.js +0 -22
- package/dist/internal/resolvers/pretty-wheres.resolve.js +0 -87
- package/dist/internal/resolvers/select.resolve.js +0 -7
- package/dist/internal/resolvers/specific-where.resolve.js +0 -84
- package/dist/internal/resolvers/ugly-where.resolve.js +0 -178
- package/dist/internal/utils/logger.util.js +0 -21
- package/dist/internal/utils/schemas.util.js +0 -31
- package/dist/internal/validation/build-config.validate.js +0 -84
- package/dist/internal/validation/constructor-config.validate.js +0 -64
- package/dist/internal/validation/dynamic-method-config.validate.js +0 -19
- package/dist/internal/validation/extension.validate.js +0 -15
- package/dist/internal/validation/is-object.validate.js +0 -6
- package/dist/internal/validation/method-options.validate.js +0 -42
- package/dist/internal/validation/obj-with-relations.validate.js +0 -37
- package/dist/internal/validation/prisma-client.validate.js +0 -10
- package/dist/internal/validation/query-method-arg.validate.js +0 -22
- package/dist/internal/validation/query-method-options.validate.js +0 -24
- package/scripts/configure-prisma-import.mjs +0 -283
- package/scripts/copy-types.mjs +0 -24
- /package/dist/{internal/decorators/types/dynamic-method-config.type.js → types/adapter/adapter-method-options.type.js} +0 -0
- /package/dist/{internal/errors/types/vs-repo-error-type.type.js → types/adapter/adapter-query-options.type.js} +0 -0
- /package/dist/{internal/errors/types/vs-repo-runtime-error-code.type.js → types/decorators/dynamic-method-options.type.js} +0 -0
- /package/dist/{internal/validation/types → types/decorators}/query-method-options.type.js +0 -0
- /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-customization.type.js +0 -0
- /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-info.type.js +0 -0
- /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-where-ops.type.js +0 -0
- /package/dist/{internal/resolvers/types/base-method-function.type.js → types/utils/count-result.type.js} +0 -0
- /package/dist/{internal/resolvers/types/pretty-where.type.js → types/utils/decimal-like.type.js} +0 -0
- /package/dist/{internal/resolvers/types/prisma-args.type.js → types/utils/deep-partial.type.js} +0 -0
- /package/dist/{internal/resolvers/types/repository-build-instance.type.js → types/utils/keys-of-type.type.js} +0 -0
- /package/dist/{internal/resolvers/types/resolve-db-and-prisma-args-data.type.js → types/utils/methods-options.type.js} +0 -0
- /package/dist/{internal/resolvers/types/ugly-where.type.js → types/utils/numeric-keys.type.js} +0 -0
- /package/dist/{internal/validation/types/base-methods.type.js → types/utils/numeric-like.type.js} +0 -0
- /package/dist/{internal/validation/types/build-config.type.js → types/utils/ordering.type.js} +0 -0
- /package/dist/{internal/validation/types → types/utils}/pagination.type.js +0 -0
- /package/dist/{internal/validation/types/constructor-config.type.js → types/utils/perform-data.type.js} +0 -0
- /package/dist/{internal/validation/types/method-options.type.js → types/utils/primitive.type.js} +0 -0
- /package/dist/{internal/validation/types → types/utils}/query-method-arg.type.js +0 -0
- /package/dist/{internal/validation/types/method.type.js → types/utils/restrict-method-options.type.js} +0 -0
- /package/dist/{internal/validation/types → types/utils}/see-mode.type.js +0 -0
- /package/dist/{internal/validation/types/relation.type.js → types/vsrepo/vsrepo-args.type.js} +0 -0
package/README.md
CHANGED
|
@@ -9,139 +9,127 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
# VSRepository
|
|
12
|
+
# VSRepository v2
|
|
13
13
|
|
|
14
14
|
🇺🇸 You're reading the English version. [🇧🇷 Ler em português](./README.pt-BR.md)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
> ✅ **Released.** VSRepository v2.0.0 (the ORM-agnostic core) and the [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) are both published and ready to use. Prisma 7 is the first fully supported adapter; other ORMs (TypeORM, Drizzle, etc.) are still in progress — see [Adapter status](#adapter-status). If you need the previous Prisma-only release, use the [`v1`](https://github.com/jaobrabo123/VSRepository/tree/v1) code/docs instead.
|
|
17
|
+
|
|
18
|
+
**ORM-agnostic** repository pattern library, with full **TypeScript** support and automatic **type inference**. VSRepository v2 is a rewrite of the [v1](https://github.com/jaobrabo123/VSRepository/tree/v1) library: instead of talking to Prisma directly, the core now delegates every operation to a pluggable **adapter**, so the same repository API can work against Prisma, TypeORM, or any other ORM/database that implements the adapter contract.
|
|
17
19
|
|
|
18
20
|
VSRepository lets you create strongly-typed repositories with:
|
|
19
21
|
|
|
20
|
-
- Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `
|
|
22
|
+
- Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `merge`, `getAll`, `total`, `has`
|
|
21
23
|
- **Native soft-delete**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
|
|
22
|
-
- **Dynamic methods** inferred from
|
|
23
|
-
-
|
|
24
|
+
- **Dynamic methods** inferred from a `declare` field name via the `@DynamicMethod` decorator: `findByEmail`, `findManyByStatusPaginated`, `updateById`
|
|
25
|
+
- **Raw SQL query methods** via the new `@QueryMethod` decorator, bypassing the name-parsing engine entirely
|
|
26
|
+
- Ad-hoc **`select`/`relations`** per call — no more pre-declared named projections
|
|
24
27
|
- **Type safety** across 100% of operations
|
|
25
|
-
- Native
|
|
26
|
-
- **
|
|
27
|
-
|
|
28
|
-
> 💡 Want to see all of this in practice? The repository's [`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples) folder has commented, runnable examples for every feature — see the [Practical examples](#practical-examples) section below.
|
|
28
|
+
- Native ORM **transactions**, shared across repositories
|
|
29
|
+
- An **ORM-agnostic core** — the same repository class works with any `VSRepoAdapter` implementation
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
32
33
|
## Table of contents
|
|
33
34
|
|
|
35
|
+
- [What changed from v1](#what-changed-from-v1)
|
|
36
|
+
- [Adapter status](#adapter-status)
|
|
34
37
|
- [Installation](#installation)
|
|
35
|
-
- [Generating the types](#generating-the-types)
|
|
36
38
|
- [Basic usage](#basic-usage)
|
|
37
|
-
- [
|
|
38
|
-
- [NestJS integration](#nestjs-integration)
|
|
39
|
+
- [Constructor options](#constructor-options)
|
|
39
40
|
- [Base methods](#base-methods)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- [
|
|
45
|
-
- [Raw select (options.select)](#raw-select-optionsselect)
|
|
46
|
-
- [Include Models](#include-models)
|
|
47
|
-
- [Raw include (options.include)](#raw-include-optionsinclude)
|
|
48
|
-
- [Required Where](#required-where)
|
|
49
|
-
- [Default Ordering](#default-ordering)
|
|
50
|
-
- [`see` option](#see-option)
|
|
41
|
+
- [Soft-delete](#soft-delete)
|
|
42
|
+
- [Atomic and aggregate methods](#atomic-and-aggregate-methods)
|
|
43
|
+
- [Which fields are eligible](#which-fields-are-eligible)
|
|
44
|
+
- [Writing an adapter](#writing-an-adapter)
|
|
45
|
+
- [`select` and `relations`](#select-and-relations)
|
|
51
46
|
- [Dynamic methods](#dynamic-methods)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- [Query Methods](#query-methods)
|
|
61
|
-
- [Relations in save](#relations-in-save)
|
|
47
|
+
- [Available prefixes](#available-prefixes)
|
|
48
|
+
- [Field filters](#field-filters)
|
|
49
|
+
- [Logical operators](#logical-operators)
|
|
50
|
+
- [Relation filters](#relation-filters)
|
|
51
|
+
- [Ordering, pagination and distinct](#ordering-pagination-and-distinct)
|
|
52
|
+
- [Decorator options](#decorator-options)
|
|
53
|
+
- [Query methods (raw SQL)](#query-methods-raw-sql)
|
|
54
|
+
- [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query)
|
|
62
55
|
- [Transactions](#transactions)
|
|
63
|
-
- [Extending a repository](#extending-a-repository)
|
|
64
|
-
- [Error handling](#error-handling)
|
|
65
56
|
- [Utility types](#utility-types)
|
|
66
|
-
- [
|
|
67
|
-
- [
|
|
68
|
-
- [
|
|
57
|
+
- [Writing your own adapter](#writing-your-own-adapter)
|
|
58
|
+
- [Error handling](#error-handling)
|
|
59
|
+
- [`VSRepoAdapterError` and `AdapterErrorCode`](#vsrepoadaptererror-and-adaptererrorcode)
|
|
60
|
+
- [Logging](#logging)
|
|
61
|
+
- [Development](#development)
|
|
69
62
|
- [Requirements](#requirements)
|
|
70
|
-
- [
|
|
63
|
+
- [Contributing](#contributing)
|
|
71
64
|
|
|
72
65
|
---
|
|
73
66
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
67
|
+
## What changed from v1
|
|
68
|
+
|
|
69
|
+
If you're coming from the [v1](https://github.com/jaobrabo123/VSRepository/tree/v1) code/docs, here's the short version. See each linked section for details.
|
|
70
|
+
|
|
71
|
+
| Area | v1 | v2 |
|
|
72
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
73
|
+
| Database access | Talks to **Prisma** directly, bundled in the core package | Talks to a **`VSRepoAdapter`**; ORM support ships as separate packages (`@vsrepo/prisma7-adapter`, `@vsrepo/typeorm-adapter`, ...) instead of being bundled in the core `vsrepo` package |
|
|
74
|
+
| Defining a repository | Functional `setupVSRepo<T, M>()({...}).build(prisma)`, **or** a `DynamicRepository` class | A single **class-based** API: `extends VSRepository<Entity, PKType, OrmTypes>` |
|
|
75
|
+
| Dynamic methods | `methods: { findByEmail: { map: true } }` config object | `@DynamicMethod()` decorator on a `declare` field |
|
|
76
|
+
| Data projections | Named, reusable `selectModels` + `defaultSelectModel` | Ad-hoc `select`/`relations` passed per call (no named models) |
|
|
77
|
+
| Eager loading | `include`/`includeModels` (Prisma-specific) | ORM-agnostic `relations` option |
|
|
78
|
+
| Global filters | `requiredWhere` (any arbitrary filter, always applied) | **Removed**; Now it only accepts `softRemoveKey` + `see: "active" \| "removed" \| "all"` |
|
|
79
|
+
| Case-insensitive filter suffix | `Insensitive` | `IgnoreCase` |
|
|
80
|
+
| Inline ordering in method name | Not supported (`order` had to be passed as an argument via `Ordered`/`Paginated`) | `OrderBy<Field>Asc`/`OrderBy<Field>Desc` chains baked directly into the method name |
|
|
81
|
+
| Duplicate handling on `createMany` | `SkipDuplicates` suffix | `IgnoreConflicts` suffix |
|
|
82
|
+
| `aggregate` / `groupBy` | Supported (Prisma-native passthrough) | **Not implemented yet** |
|
|
83
|
+
| Error types | `VSRepoError` + subclasses (`VSRepoConfigError`, `VSRepoBuildError`, `VSRepoExtendError`, `VSRepoRuntimeError`) | A base `VSRepoError` class with a `type: VSRepoErrorType` field (`DECORATOR`, `RESOLVER`, `DYNAMIC`, `VALIDATOR`, `BASE`, `ADAPTER`), plus a `VSRepoAdapterError` subclass carrying an `AdapterErrorCode` and the original ORM error |
|
|
84
|
+
| Debug logging | `showWorking: true` boolean | `logLevel: VSLogLevel` (`DEBUG`/`INFO`/`WARN`/`ERROR`) + `logSlowThresholdMs` for slow-query warnings |
|
|
85
|
+
| `vsrepo generate` CLI (type generation step) | Required before use | Not part of the v2 core — types come directly from your entity/ORM types |
|
|
86
|
+
| CRUD extras | `patchList`, raw `options.select`/`options.include` | `select`/`relations` are the default (always "raw"); `patch`/`merge` keep the same semantics. **`patchList` was removed** — for a batch partial update, use a `updateManyBy`/`updateManyWhere` dynamic method instead |
|
|
85
87
|
|
|
86
88
|
---
|
|
87
89
|
|
|
88
|
-
##
|
|
89
|
-
|
|
90
|
-
VSRepository needs to know the real path of your Prisma Client to generate the typings correctly.
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
npx vsrepo generate
|
|
94
|
-
```
|
|
90
|
+
## Adapter status
|
|
95
91
|
|
|
96
|
-
|
|
92
|
+
VSRepository v2 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:
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```
|
|
94
|
+
- `@vsrepo/prisma7-adapter`
|
|
95
|
+
- `@vsrepo/prisma8-adapter`
|
|
96
|
+
- `@vsrepo/typeorm-adapter`
|
|
97
|
+
- `@vsrepo/drizzle-adapter`
|
|
103
98
|
|
|
104
|
-
**
|
|
99
|
+
The Prisma 7 adapter has now been published to npm as `@vsrepo/prisma7-adapter` — it's currently the **only** published adapter. Adapters for the other ORMs listed above (Prisma 8, TypeORM, Drizzle) are **planned**; they just haven't been 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.
|
|
105
100
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
101
|
+
| Adapter | Status |
|
|
102
|
+
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Released** — published to npm, implements the `VSRepoAdapter` contract (CRUD, relations, transactions, `merge`, logging) with tests; see [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) for source and docs. **Note:** the atomic/aggregate methods (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — see [Atomic and aggregate methods](#atomic-and-aggregate-methods)) were added to the `VSRepoAdapter` contract after this adapter's last release; confirm its changelog/version implements them before relying on `increment`/`sum`/etc. against Prisma 7. |
|
|
104
|
+
| TypeORM (`@vsrepo/typeorm-adapter`) | 🟡 **Planned, not published yet.** Only a reference `where`-clause parser (`parseVSRepoWhere`) was written to validate the design; it's the planned starting point for the future `@vsrepo/typeorm-adapter` package. Community contributions toward this are welcome. |
|
|
105
|
+
| Other ORMs (Prisma 8, Drizzle, etc.) | 🟡 **Planned, not published yet.** No official package exists yet — write your own adapter for now (see [Writing your own adapter](#writing-your-own-adapter)), and consider publishing/contributing it back. |
|
|
106
|
+
| Custom adapters | 🟢 Fully supported today — implement the [`VSRepoAdapter`](#writing-your-own-adapter) abstract class yourself for any ORM/database you need, in your own project or package, following the same shape as `@vsrepo/*-adapter` is expected to have. |
|
|
110
107
|
|
|
111
|
-
|
|
108
|
+
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 now a released, published adapter. 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 v2 today and to contribute back to the project.
|
|
112
109
|
|
|
113
|
-
|
|
114
|
-
generated/vsrepo/
|
|
115
|
-
├── DynamicRepository.ts
|
|
116
|
-
├── DynamicRepository.types.d.ts
|
|
117
|
-
├── VSRepoError.ts
|
|
118
|
-
├── VSRepoError.types.d.ts
|
|
119
|
-
├── VSRepository.ts
|
|
120
|
-
├── VSRepository.types.d.ts
|
|
121
|
-
└── index.ts
|
|
122
|
-
```
|
|
110
|
+
---
|
|
123
111
|
|
|
124
|
-
|
|
112
|
+
## Installation
|
|
125
113
|
|
|
126
|
-
|
|
127
|
-
// CORRECT ✅
|
|
128
|
-
import { setupVSRepo } from "../../generated/vsrepo";
|
|
114
|
+
v2 is installed as the core package plus one adapter package for your ORM, for example:
|
|
129
115
|
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
```bash
|
|
117
|
+
npm i vsrepo @vsrepo/prisma7-adapter
|
|
132
118
|
```
|
|
133
119
|
|
|
120
|
+
> `vsrepo` v2.0.0 and `@vsrepo/prisma7-adapter` are both published to npm and ready to use. For any ORM other than Prisma 7, no adapter package exists yet — install the core and write your own adapter (see [Writing your own adapter](#writing-your-own-adapter)).
|
|
121
|
+
|
|
134
122
|
---
|
|
135
123
|
|
|
136
124
|
## Basic usage
|
|
137
125
|
|
|
138
|
-
###
|
|
126
|
+
### Implementing/choosing an adapter
|
|
139
127
|
|
|
140
|
-
```
|
|
128
|
+
```typescript
|
|
141
129
|
// src/configs/db.ts
|
|
142
|
-
import { PrismaClient } from
|
|
143
|
-
import { PrismaPg } from
|
|
144
|
-
import
|
|
130
|
+
import { PrismaClient } from "../../generated/prisma/client";
|
|
131
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
132
|
+
import "dotenv/config";
|
|
145
133
|
|
|
146
134
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
|
147
135
|
const prisma = new PrismaClient({ adapter });
|
|
@@ -151,1457 +139,974 @@ export default prisma;
|
|
|
151
139
|
|
|
152
140
|
### Creating a repository
|
|
153
141
|
|
|
154
|
-
```
|
|
155
|
-
// src/repositories/
|
|
142
|
+
```typescript
|
|
143
|
+
// src/repositories/user.repository.ts
|
|
144
|
+
import { VSRepository, DynamicMethod } from "vsrepo";
|
|
145
|
+
import { VSRepoPrisma7Adapter } from "@vsrepo/prisma7-adapter";
|
|
156
146
|
import prisma from "../configs/db";
|
|
157
|
-
import {
|
|
158
|
-
import type { User } from "../../generated/prisma/client";
|
|
159
|
-
|
|
160
|
-
const userRepository = setupVSRepo<User, "User">()(({
|
|
161
|
-
tableName: "user",
|
|
162
|
-
pkName: "id",
|
|
163
|
-
selectModels: {
|
|
164
|
-
public: { id: true, name: true, email: true },
|
|
165
|
-
},
|
|
166
|
-
defaultSelectModel: "public",
|
|
167
|
-
}).build(prisma);
|
|
168
|
-
|
|
169
|
-
export default userRepository;
|
|
170
|
-
```
|
|
147
|
+
import type { UserGetPayload } from "../../generated/prisma/models";
|
|
171
148
|
|
|
172
|
-
|
|
149
|
+
type User = UserGetPayload<{ include: { address: true } }>;
|
|
173
150
|
|
|
174
|
-
|
|
175
|
-
|
|
151
|
+
class UserRepository extends VSRepository<User, string> {
|
|
152
|
+
constructor() {
|
|
153
|
+
super({
|
|
154
|
+
pkName: "id",
|
|
155
|
+
adapter: new VSRepoPrisma7Adapter<User>(prisma, { tableName: "user", pkName: "id" }),
|
|
156
|
+
softRemoveKey: "deletedAt",
|
|
157
|
+
defaultOrdering: { createdAt: "desc" },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
176
160
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
email: "john@email.com",
|
|
180
|
-
password: "password",
|
|
181
|
-
});
|
|
161
|
+
@DynamicMethod()
|
|
162
|
+
declare findByEmail: (email: string) => Promise<User[]>;
|
|
182
163
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
user.name = "John Smith";
|
|
164
|
+
@DynamicMethod()
|
|
165
|
+
declare findOneByEmail: (email: string) => Promise<User | null>;
|
|
166
|
+
}
|
|
187
167
|
|
|
188
|
-
|
|
189
|
-
await userRepository.remove(user.id);
|
|
168
|
+
export default new UserRepository();
|
|
190
169
|
```
|
|
191
170
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
## Class-based approach (DynamicRepository)
|
|
195
|
-
|
|
196
|
-
If you prefer an OOP style with decorators instead of the functional `setupVSRepo` approach, VSRepository also provides `DynamicRepository` — a class you can extend with `@DynamicMethod()` decorators to define your dynamic methods.
|
|
197
|
-
|
|
198
|
-
See **[README-DynamicRepo.md](./README-DynamicRepo.md)** (or the [pt-BR version](./README-DynamicRepo.pt-BR.md)) for full documentation on the class-based approach, including NestJS integration examples, decorator config, and a comparison with `setupVSRepo`.
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
## NestJS integration
|
|
171
|
+
> 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 published [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) (its constructor takes a config object — `tableName`, `pkName`, optional `relations`/`logLevel` — as shown above). Official adapters for other ORMs are planned but not published yet; until they are, you can implement the `VSRepoAdapter` contract yourself (see [Writing your own adapter](#writing-your-own-adapter)) — and publishing it to help the project is very welcome.
|
|
203
172
|
|
|
204
|
-
VSRepository
|
|
173
|
+
> **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`:
|
|
174
|
+
> ```typescript
|
|
175
|
+
> type PrismaOrmTypes = { dbClient: PrismaClient; dbTransaction: Prisma.TransactionClient };
|
|
176
|
+
>
|
|
177
|
+
> class UserRepository extends VSRepository<User, string, PrismaOrmTypes> {
|
|
178
|
+
> // getDbClient() now returns PrismaClient, and transaction(fn) types `tx` as Prisma.TransactionClient
|
|
179
|
+
> }
|
|
180
|
+
> ```
|
|
181
|
+
> If omitted, it defaults to `VSRepoOrmTypes` (`dbClient`/`dbTransaction` both `any`).
|
|
205
182
|
|
|
206
|
-
###
|
|
183
|
+
### Using the repository
|
|
207
184
|
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
import { Provider } from "@nestjs/common";
|
|
211
|
-
import { PrismaService } from "../../database/prisma.service";
|
|
212
|
-
import { UserGetPayload } from "../../../generated/prisma/models";
|
|
213
|
-
import { setupVSRepo } from "../../../generated/vsrepo";
|
|
185
|
+
```typescript
|
|
186
|
+
import userRepository from "./repositories/user.repository";
|
|
214
187
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
tableName: "user",
|
|
220
|
-
pkName: "id",
|
|
221
|
-
selectModels: {
|
|
222
|
-
public: {
|
|
223
|
-
id: true,
|
|
224
|
-
email: true,
|
|
225
|
-
createdAt: true,
|
|
226
|
-
updatedAt: true,
|
|
227
|
-
},
|
|
228
|
-
auth: {
|
|
229
|
-
id: true,
|
|
230
|
-
email: true,
|
|
231
|
-
password: true,
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
defaultSelectModel: "public",
|
|
235
|
-
requiredWhere: {
|
|
236
|
-
deletedAt: null,
|
|
237
|
-
},
|
|
238
|
-
relations: {
|
|
239
|
-
profile: {
|
|
240
|
-
mode: "oto",
|
|
241
|
-
pk: "id",
|
|
242
|
-
restriction: "add",
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
methods: {
|
|
246
|
-
findAuthByEmail: {
|
|
247
|
-
map: true,
|
|
248
|
-
proxyTo: "findUniqueByEmail",
|
|
249
|
-
selectModel: "auth",
|
|
250
|
-
},
|
|
251
|
-
findByEmailEndsWith: {
|
|
252
|
-
map: true,
|
|
253
|
-
}
|
|
254
|
-
},
|
|
188
|
+
const user = await userRepository.save({
|
|
189
|
+
name: "Joao",
|
|
190
|
+
email: "joao@email.com",
|
|
191
|
+
password: "password",
|
|
255
192
|
});
|
|
256
193
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
export type UserRepository = ReturnType<typeof setupUserRepository>;
|
|
262
|
-
/*
|
|
263
|
-
The type can also be inferred using VSRepository's `RepositoryOf`, passing the `userVSRepo` type:
|
|
264
|
-
|
|
265
|
-
export type UserRepository = RepositoryOf<typeof userVSRepo>;
|
|
266
|
-
|
|
267
|
-
NOTE: If you use `.extend` to extend the repository or configure the base methods,
|
|
268
|
-
using `ReturnType` is recommended since it's simpler to infer the type
|
|
269
|
-
*/
|
|
270
|
-
|
|
271
|
-
export const USER_REPOSITORY = Symbol("USER_REPOSITORY");
|
|
272
|
-
|
|
273
|
-
export const UserRepositoryProvider: Provider = {
|
|
274
|
-
provide: USER_REPOSITORY,
|
|
275
|
-
inject: [PrismaService],
|
|
276
|
-
useFactory: setupUserRepository,
|
|
277
|
-
};
|
|
278
|
-
```
|
|
194
|
+
const found = await userRepository.get(user.id);
|
|
195
|
+
const all = await userRepository.getAll();
|
|
196
|
+
const byEmail = await userRepository.findByEmail("joao@email.com");
|
|
279
197
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
// src/modules/user/user.module.ts
|
|
284
|
-
import { Module } from "@nestjs/common";
|
|
285
|
-
import { UserRepositoryProvider } from "./user.repository";
|
|
286
|
-
import { UserService } from "./user.service";
|
|
287
|
-
import { UserController } from "./user.controller";
|
|
288
|
-
|
|
289
|
-
@Module({
|
|
290
|
-
imports: [DatabaseModule],
|
|
291
|
-
providers: [UserRepositoryProvider, UserService],
|
|
292
|
-
controllers: [UserController],
|
|
293
|
-
exports: [UserService],
|
|
294
|
-
})
|
|
295
|
-
export class UserModule {}
|
|
198
|
+
await userRepository.patch(user.id, { name: "Joao Pedro" });
|
|
199
|
+
await userRepository.remove(user.id);
|
|
296
200
|
```
|
|
297
201
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
// src/modules/user/user.service.ts
|
|
302
|
-
import { Injectable, Inject } from "@nestjs/common";
|
|
303
|
-
import { USER_REPOSITORY, type UserRepository } from "./user.repository";
|
|
304
|
-
|
|
305
|
-
@Injectable()
|
|
306
|
-
export class UserService {
|
|
307
|
-
constructor(
|
|
308
|
-
@Inject(USER_REPOSITORY)
|
|
309
|
-
private readonly userRepository: UserRepository,
|
|
310
|
-
) {}
|
|
311
|
-
|
|
312
|
-
async getUserById(id: string) {
|
|
313
|
-
return this.userRepository.get(id);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async getUserAuthByEmail(email: string) {
|
|
317
|
-
return this.userRepository.findAuthByEmail(email);
|
|
318
|
-
}
|
|
202
|
+
---
|
|
319
203
|
|
|
320
|
-
|
|
321
|
-
return this.userRepository.save({
|
|
322
|
-
email: data.email,
|
|
323
|
-
password: data.password,
|
|
324
|
-
name: data.name,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
```
|
|
204
|
+
## Constructor options
|
|
329
205
|
|
|
330
|
-
|
|
206
|
+
`VSRepoOptions<T, K>`, passed to `super(...)` inside your repository's constructor:
|
|
331
207
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
208
|
+
| Option | Type | Description |
|
|
209
|
+
| -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
|
210
|
+
| `adapter` | `VSRepoAdapter<T>` | **Required.** The adapter instance that translates repository calls into calls against the underlying ORM/database. |
|
|
211
|
+
| `pkName` | `keyof T` | **Required.** Name of the field that represents the entity's primary key. |
|
|
212
|
+
| `softRemoveKey` | `keyof T` | Optional. When set, enables `softRemove`, `softRemoveList`, `restore` and `restoreList`. |
|
|
213
|
+
| `defaultOrdering` | `Ordering<T>` | Optional. Default ordering applied automatically to queries that accept `order`, unless overridden per call. |
|
|
214
|
+
| `logLevel` | `VSLogLevel` | Optional. Minimum severity printed by the internal logger. Defaults to `VSLogLevel.WARN`. |
|
|
215
|
+
| `logSlowThresholdMs` | `number` | Optional. Duration (ms) above which a finished operation is logged as `WARN` instead of `DEBUG`. Defaults to 300ms. |
|
|
337
216
|
|
|
338
217
|
---
|
|
339
218
|
|
|
340
219
|
## Base methods
|
|
341
220
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
| Method
|
|
345
|
-
|
|
|
346
|
-
| `get(pk)`
|
|
347
|
-
| `getOrThrow(pk)`
|
|
348
|
-
| `getList(pks)`
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `merge(pk, obj)`
|
|
354
|
-
| `remove(pk)`
|
|
355
|
-
| `removeList(pks)`
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
|
367
|
-
|
|
|
368
|
-
| `
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
| `restoreList(pks)` | Restores multiple soft-deleted records in batch — returns `{ count }` |
|
|
372
|
-
|
|
373
|
-
```ts
|
|
374
|
-
const userRepository = setupVSRepo<User, "user">()(({
|
|
375
|
-
tableName: "user",
|
|
376
|
-
pkName: "id",
|
|
377
|
-
softRemovekName: "deletedAt", // must be a DateTime field in the Prisma schema
|
|
378
|
-
}).build(prisma);
|
|
379
|
-
|
|
380
|
-
await userRepository.softRemove(1);
|
|
381
|
-
await userRepository.restore(1);
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
> The field provided in `softRemovekName` **must** be of type `DateTime` in the Prisma schema. VSRepository validates this at `build` time and throws `VSRepoBuildError` if the type is incorrect.
|
|
385
|
-
|
|
386
|
-
### Batch operations
|
|
387
|
-
|
|
388
|
-
`saveList` and `patchList` automatically run all operations inside a single Prisma transaction. If any operation fails, all previous ones are rolled back.
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
// saveList — creates or updates multiple objects in an automatic transaction
|
|
392
|
-
const users = await userRepository.saveList([
|
|
393
|
-
{ name: "Mary", email: "mary@email.com" },
|
|
394
|
-
{ id: 2, name: "John Updated", email: "john@email.com" },
|
|
395
|
-
]);
|
|
396
|
-
|
|
397
|
-
// patchList — partially updates multiple records via [pk, obj] tuples
|
|
398
|
-
const updated = await userRepository.patchList([
|
|
399
|
-
[1, { active: false }],
|
|
400
|
-
[2, { name: "New Name" }],
|
|
401
|
-
]);
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
When you're already inside an existing transaction, pass it in `options.db`. In this case, `db` must be a `DbTransaction` (not the main client):
|
|
405
|
-
|
|
406
|
-
```ts
|
|
407
|
-
await prisma.$transaction(async (tx) => {
|
|
408
|
-
await userRepository.saveList([{ name: "Mary" }, { name: "Gus" }], { db: tx });
|
|
409
|
-
await userRepository.patchList([[1, { active: false }], [2, { active: true }]], { db: tx });
|
|
410
|
-
});
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### Merge
|
|
414
|
-
|
|
415
|
-
The `merge` method fetches a record by its PK and deeply merges (`deepmerge`) the provided object with the existing data **in memory**. It **does not persist** the changes — it returns the merged result so you can decide what to do with it.
|
|
221
|
+
Available automatically on every `VSRepository` subclass:
|
|
222
|
+
|
|
223
|
+
| Method | Description |
|
|
224
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
225
|
+
| `get(pk, options?)` | Fetches a record by primary key. |
|
|
226
|
+
| `getOrThrow(pk, options?)` | Fetches a record by primary key, throwing if not found. |
|
|
227
|
+
| `getList(pks, options?)` | Fetches multiple records by a list of primary keys. |
|
|
228
|
+
| `getAll(options?)` | Fetches all records; accepts `pagination` and `order` in `options`. |
|
|
229
|
+
| `save(obj, options?)` | Creates or updates (upsert) a single record. |
|
|
230
|
+
| `saveList(objs, options?)` | Creates or updates (upsert) multiple records in one call. |
|
|
231
|
+
| `patch(pk, obj, options?)` | Partially updates a record by primary key. |
|
|
232
|
+
| `merge(pk, obj, options?)` | Fetches a record and returns it deep-merged, in memory, with the given object — does **not** persist anything. |
|
|
233
|
+
| `remove(pk, options?)` | Deletes a record by primary key. |
|
|
234
|
+
| `removeList(pks, options?)` | Deletes multiple records by primary key, returning `{ count }`. |
|
|
235
|
+
| `total(options?)` | Returns the total number of records. |
|
|
236
|
+
| `has(pk, options?)` | Checks whether a record exists, returning `boolean`. |
|
|
237
|
+
| `increment(pk, field, value, options?)` | Atomically adds `value` to a numeric field. See [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
|
|
238
|
+
| `decrement(pk, field, value, options?)` | Atomically subtracts `value` from a numeric field. |
|
|
239
|
+
| `multiply(pk, field, value, options?)` | Atomically multiplies a numeric field by `value`. |
|
|
240
|
+
| `divide(pk, field, value, options?)` | Atomically divides a numeric field by `value`. |
|
|
241
|
+
| `sum(field, where?, options?)` | Sums a numeric field across every matching record; `null` if none match. |
|
|
242
|
+
| `average(field, where?, options?)` | Arithmetic mean of a numeric field across every matching record; `null` if none match. |
|
|
243
|
+
| `min(field, where?, options?)` | Minimum value of a numeric field across every matching record; `null` if none match. |
|
|
244
|
+
| `max(field, where?, options?)` | Maximum value of a numeric field across every matching record; `null` if none match. |
|
|
245
|
+
| `transaction(fn, options?)` | Runs `fn` inside a native transaction of the underlying ORM. |
|
|
246
|
+
| `getDbClient()` | Returns the underlying ORM client instance used outside of transactions. |
|
|
247
|
+
| `query<T>(query, options?)` | Executes a raw SQL statement directly against the database. See [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query). |
|
|
248
|
+
|
|
249
|
+
Most of the above accept a `MethodOptions<Entity, OrmTypes>` object as their last argument (`select`, `relations`, `see`, `db`). A few — `total`, `has`, `removeList`, `sum`, `average`, `min`, `max`, and the soft-delete batch methods (`softRemoveList`/`restoreList`) — don't return/shape an `Entity`, so they accept the narrower `RestrictMethodOptions<Entity, OrmTypes>` instead (`see`, `db` only; no `select`/`relations`). `transaction`, `query`, and `getDbClient` accept their own options or none at all.
|
|
416
250
|
|
|
417
|
-
|
|
418
|
-
const existing = await userRepository.get(1);
|
|
419
|
-
// existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
|
|
251
|
+
---
|
|
420
252
|
|
|
421
|
-
|
|
422
|
-
profile: { bio: "Updated bio" },
|
|
423
|
-
});
|
|
424
|
-
// merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
|
|
253
|
+
## Soft-delete
|
|
425
254
|
|
|
426
|
-
|
|
427
|
-
await userRepository.save(merged);
|
|
428
|
-
```
|
|
255
|
+
Soft-delete is now a **first-class, built-in concept**. Configure `softRemoveKey` once on the repository:
|
|
429
256
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const existing = await userRepository.get(1);
|
|
436
|
-
// existing: {
|
|
437
|
-
// id: 1,
|
|
438
|
-
// posts: [
|
|
439
|
-
// { id: 10, title: "Post A", published: false },
|
|
440
|
-
// { id: 11, title: "Post B", published: true },
|
|
441
|
-
// ],
|
|
442
|
-
// }
|
|
443
|
-
|
|
444
|
-
const merged = await userRepository.merge(1, {
|
|
445
|
-
posts: [
|
|
446
|
-
{ id: 10, published: true }, // same PK (id: 10) → merges with the existing item
|
|
447
|
-
{ title: "Post C" }, // no PK → added as a new item
|
|
448
|
-
],
|
|
257
|
+
```typescript
|
|
258
|
+
super({
|
|
259
|
+
pkName: "id",
|
|
260
|
+
adapter,
|
|
261
|
+
softRemoveKey: "deletedAt",
|
|
449
262
|
});
|
|
450
|
-
// merged: {
|
|
451
|
-
// id: 1,
|
|
452
|
-
// posts: [
|
|
453
|
-
// { id: 10, title: "Post A", published: true }, // merged
|
|
454
|
-
// { id: 11, title: "Post B", published: true }, // kept, wasn't in the sent array
|
|
455
|
-
// { title: "Post C" }, // added
|
|
456
|
-
// ],
|
|
457
|
-
// }
|
|
458
263
|
```
|
|
459
264
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
### Configuring the base methods
|
|
463
|
-
|
|
464
|
-
The second argument of `.build(prisma, config)` lets you adjust the repository's global behavior and customize each base method individually through `baseMethods`.
|
|
265
|
+
This unlocks four extra methods:
|
|
465
266
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
267
|
+
| Method | Effect |
|
|
268
|
+
| ------------------------------- | ------------------------------------- |
|
|
269
|
+
| `softRemove(pk, options?)` | Sets `deletedAt` to the current date. |
|
|
270
|
+
| `softRemoveList(pks, options?)` | Same, in batch — returns `{ count }`. |
|
|
271
|
+
| `restore(pk, options?)` | Sets `deletedAt` back to `null`. |
|
|
272
|
+
| `restoreList(pks, options?)` | Same, in batch — returns `{ count }`. |
|
|
471
273
|
|
|
472
|
-
|
|
473
|
-
get: {
|
|
474
|
-
// Enables/disables the method on the final repository. If `false`, the method
|
|
475
|
-
// doesn't even appear in the repository's type (it's not just a runtime error). Default = true.
|
|
476
|
-
active: true,
|
|
274
|
+
Every other method accepts a `see` option controlling visibility of soft-deleted rows:
|
|
477
275
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
remove: {
|
|
483
|
-
active: true,
|
|
484
|
-
defaultSelect: "minimal",
|
|
485
|
-
|
|
486
|
-
// When `true`, ignores the `requiredWhere` configured in setupVSRepo for
|
|
487
|
-
// this specific method — useful when a method needs to "punch through" a
|
|
488
|
-
// global filter (e.g. multi-tenancy) in a specific case. Default = false.
|
|
489
|
-
ignoreRequiredWhere: false,
|
|
490
|
-
},
|
|
491
|
-
save: {
|
|
492
|
-
// Here only `ignoreRequiredWhere` is set — `active` and `defaultSelect`
|
|
493
|
-
// keep their defaults (true and the global `defaultSelectModel`).
|
|
494
|
-
ignoreRequiredWhere: true,
|
|
495
|
-
},
|
|
496
|
-
patch: {
|
|
497
|
-
// Only the select is overridden; the method stays active normally.
|
|
498
|
-
defaultSelect: "minimal",
|
|
499
|
-
},
|
|
500
|
-
has: {
|
|
501
|
-
active: false, // Disables 'has' (default = true) — the method disappears from the repository
|
|
502
|
-
},
|
|
503
|
-
softRemove: {
|
|
504
|
-
// Soft-delete methods follow the same options (`active`, `defaultSelect`,
|
|
505
|
-
// `ignoreRequiredWhere`). They're only available if `softRemovekName` is configured.
|
|
506
|
-
active: true,
|
|
507
|
-
defaultSelect: "minimal",
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
});
|
|
276
|
+
```typescript
|
|
277
|
+
await userRepository.getAll({ see: "active" }); // default — only non-deleted records
|
|
278
|
+
await userRepository.getAll({ see: "removed" }); // only soft-deleted records
|
|
279
|
+
await userRepository.getAll({ see: "all" }); // everything, ignoring soft-delete
|
|
511
280
|
```
|
|
512
281
|
|
|
513
|
-
> Batch/aggregate methods like `removeList`, `softRemoveList`, `restoreList`, `total`, and `has` **do not** accept `defaultSelect` (they don't return a selectable record — they return `{ count }` or `boolean`). In these cases `BaseMethodConfig` is restricted to `active` and `ignoreRequiredWhere`.
|
|
514
|
-
|
|
515
282
|
---
|
|
516
283
|
|
|
517
|
-
##
|
|
518
|
-
|
|
519
|
-
`selectModels` defines named, reusable data projections.
|
|
520
|
-
|
|
521
|
-
```ts
|
|
522
|
-
selectModels: {
|
|
523
|
-
public: { id: true, name: true, email: true },
|
|
524
|
-
internal: { id: true, name: true, email: true, password: true },
|
|
525
|
-
minimal: { id: true },
|
|
526
|
-
},
|
|
527
|
-
defaultSelectModel: "public",
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
`defaultSelectModel` defines which select is used automatically when none is specified in the call. It's recommended to always define it together with `selectModels`.
|
|
284
|
+
## Atomic and aggregate methods
|
|
531
285
|
|
|
532
|
-
|
|
286
|
+
Every `VSRepository` subclass gets 8 extra methods for working with numeric fields, split into two groups:
|
|
533
287
|
|
|
534
|
-
|
|
535
|
-
const user = await userRepository.get(id, { selectModel: "minimal" });
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
**Returning Prisma's default payload (without select):**
|
|
288
|
+
**Atomic updates** — evaluated server-side against the row's *current* value (`UPDATE ... SET field = field + value`), not a client-side read-modify-write:
|
|
539
289
|
|
|
540
|
-
```
|
|
541
|
-
|
|
290
|
+
```typescript
|
|
291
|
+
await userRepository.increment("user-1", "balance", 50); // balance = balance + 50
|
|
292
|
+
await userRepository.decrement("user-1", "balance", 50); // balance = balance - 50
|
|
293
|
+
await userRepository.multiply("user-1", "balance", 2); // balance = balance * 2
|
|
294
|
+
await userRepository.divide("user-1", "balance", 4); // balance = balance / 4
|
|
542
295
|
```
|
|
543
296
|
|
|
544
|
-
|
|
297
|
+
All four return the updated `Entity` and accept the full `MethodOptions<Entity, OrmTypes>` (`select`, `relations`, `see`, `db`) as their last argument, same as `get`/`save`/`patch`.
|
|
545
298
|
|
|
546
|
-
|
|
299
|
+
**Aggregates** — computed across every record matching an (optional) `where`:
|
|
547
300
|
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
301
|
+
```typescript
|
|
302
|
+
await userRepository.sum("balance"); // total balance across every active record
|
|
303
|
+
await userRepository.sum("balance", { active: true }); // ...restricted by a where
|
|
304
|
+
await userRepository.average("balance");
|
|
305
|
+
await userRepository.min("balance");
|
|
306
|
+
await userRepository.max("balance");
|
|
552
307
|
```
|
|
553
308
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
**Rules and behavior:**
|
|
309
|
+
All four return `number | null` — `null` when no record matches, mirroring SQL's `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL` (not `0`) over an empty set. Unlike the atomic methods, they accept the narrower `RestrictMethodOptions<Entity, OrmTypes>` (`see`, `db` only — no `select`/`relations`, since the result is a plain number, not a shaped `Entity`).
|
|
557
310
|
|
|
558
|
-
|
|
559
|
-
- **Ad hoc, not reusable.** Unlike `selectModel`, it doesn't need to be declared in `selectModels`. Use it for one-off projections that don't justify a named select model.
|
|
560
|
-
- **No `defaultSelectModel` is applied.** When `select` is provided, the default select (`defaultSelectModel`) is ignored and only the raw `select` is sent to Prisma.
|
|
311
|
+
Both groups respect `softRemoveKey`/`see` the same way every other base method does — `sum("balance")` only totals non-deleted records by default, pass `{ see: "all" }` or `{ see: "removed" }` to change that.
|
|
561
312
|
|
|
562
|
-
|
|
563
|
-
// CORRECT ✅ — raw select only
|
|
564
|
-
await userRepository.get(id, { select: { id: true, name: true } });
|
|
313
|
+
### Which fields are eligible
|
|
565
314
|
|
|
566
|
-
|
|
567
|
-
await userRepository.get(id, { selectModel: "public", select: { id: true } });
|
|
568
|
-
await userRepository.get(id, { include: { posts: true }, select: { id: true } });
|
|
569
|
-
```
|
|
315
|
+
`field` is constrained to `NumericKeys<Entity>` — keys whose (non-nullable) value type is a `number`, a `bigint`, or a `DecimalLike` object (anything exposing `toNumber()` and `decimalPlaces()`, matching e.g. Prisma's `Prisma.Decimal`):
|
|
570
316
|
|
|
571
|
-
|
|
317
|
+
```typescript
|
|
318
|
+
type Product = { id: string; name: string; price: Decimal; stock: number | null };
|
|
572
319
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
`includeModels` works similarly to `selectModels`, but instead of receiving a `select`, it receives a valid Prisma `include`.
|
|
578
|
-
|
|
579
|
-
```ts
|
|
580
|
-
const userRepository = setupVSRepo<User, "user">()(({
|
|
581
|
-
tableName: "user",
|
|
582
|
-
pkName: "id",
|
|
583
|
-
selectModels: {
|
|
584
|
-
public: { id: true, name: true, email: true },
|
|
585
|
-
},
|
|
586
|
-
defaultSelectModel: "public",
|
|
587
|
-
includeModels: {
|
|
588
|
-
withPosts: { posts: true },
|
|
589
|
-
withPostsAndProfile: { posts: true, profile: true },
|
|
590
|
-
},
|
|
591
|
-
}).build(prisma);
|
|
320
|
+
await productRepository.increment(id, "price", new Decimal(10.5)); // ok — Decimal-like
|
|
321
|
+
await productRepository.increment(id, "stock", 5); // ok — nullable numeric fields are included
|
|
322
|
+
await productRepository.increment(id, "name", 1); // compile error — "name" isn't numeric
|
|
592
323
|
```
|
|
593
324
|
|
|
594
|
-
|
|
325
|
+
`value` is typed as `NonNullable<Entity[Field]>` — it must match the field's own type exactly. A `Decimal` field expects a `Decimal` instance, not a plain `number`/`string`:
|
|
595
326
|
|
|
596
|
-
```
|
|
597
|
-
|
|
327
|
+
```typescript
|
|
328
|
+
await productRepository.increment(id, "price", new Decimal(10.5)); // ok
|
|
329
|
+
await productRepository.increment(id, "price", 10.5); // compile error — wrap it: new Decimal(10.5)
|
|
598
330
|
```
|
|
599
331
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
### Differences from `selectModels`
|
|
332
|
+
Note that several ORMs (Drizzle, MikroORM, TypeORM) represent `decimal`/`numeric` columns as plain `string` by default, to avoid floating-point precision loss — a `string` field does **not** satisfy `NumericKeys<Entity>` out of the box. Configure the column in a numeric mode (or a transformer) on those ORMs if you want the field to be usable with these 8 methods.
|
|
603
333
|
|
|
604
|
-
|
|
605
|
-
- **`includeModel` and `selectModel` cannot be passed together** in the same call. If an `includeModel` is provided, any `selectModel` (including the default one) is ignored.
|
|
334
|
+
### Writing an adapter
|
|
606
335
|
|
|
607
|
-
|
|
608
|
-
// CORRECT ✅ — includeModel only
|
|
609
|
-
await userRepository.get(id, { includeModel: "withPosts" });
|
|
336
|
+
`VSRepoAdapter` mirrors the same 8 operations (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — see [Writing your own adapter](#writing-your-own-adapter)). Each adapter translates them into whatever its ORM/database considers "native": Prisma has a built-in `{ field: { increment: value } }` update shape and an `aggregate()` call; other ORMs typically need a `QueryBuilder`/raw-`sql` expression (e.g. `SET field = field * :value`, `SELECT SUM(field) ...`) instead. The atomic methods must return the record reflecting the state *after* the write — if the ORM's atomic-update API only returns an affected-row count, issue a follow-up read rather than returning a stale in-memory copy.
|
|
610
337
|
|
|
611
|
-
|
|
612
|
-
await userRepository.get(id, { selectModel: "public" });
|
|
613
|
-
|
|
614
|
-
// WRONG ❌ — combining both is not allowed
|
|
615
|
-
await userRepository.get(id, { selectModel: "public", includeModel: "withPosts" });
|
|
616
|
-
```
|
|
338
|
+
---
|
|
617
339
|
|
|
618
|
-
|
|
340
|
+
## `select` and `relations`
|
|
619
341
|
|
|
620
|
-
|
|
342
|
+
v1's named, reusable `selectModels`/`defaultSelectModel` are gone. In v2 you pass `select` and `relations` directly on each call — there's nothing to pre-register:
|
|
621
343
|
|
|
622
|
-
```
|
|
344
|
+
```typescript
|
|
623
345
|
const user = await userRepository.get(id, {
|
|
624
|
-
|
|
346
|
+
select: { id: true, name: true, address: { city: true } },
|
|
625
347
|
});
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
`options.include` accepts any valid `include` for the repository's Prisma model — it's fully typed and offers the same autocomplete/validation as calling `prisma.user.findMany({ include: ... })` directly.
|
|
629
|
-
|
|
630
|
-
**Rules and behavior:**
|
|
631
|
-
|
|
632
|
-
- **Mutually exclusive with `selectModel` and `includeModel`.** Only one of the three can be provided per call; the types enforce this — passing more than one is a compile-time error.
|
|
633
|
-
- **Ad hoc, not reusable.** Unlike `includeModel`, it doesn't need to be declared in `includeModels`. Use it for one-off includes that don't justify a named model.
|
|
634
|
-
- **No `selectModel` default is applied.** As with `includeModel`, when `include` is provided the select (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
|
|
635
|
-
|
|
636
|
-
```ts
|
|
637
|
-
// CORRECT ✅ — raw include only
|
|
638
|
-
await userRepository.get(id, { include: { posts: true } });
|
|
639
|
-
|
|
640
|
-
// WRONG ❌ — combining include with selectModel/includeModel is not allowed
|
|
641
|
-
await userRepository.get(id, { selectModel: "public", include: { posts: true } });
|
|
642
|
-
await userRepository.get(id, { includeModel: "withPosts", include: { posts: true } });
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
> **When to use `includeModel` vs. `include`:** prefer `includeModel` for includes reused across multiple calls (defined once in `includeModels`); use `include` for specific, occasional includes that don't need a name.
|
|
646
|
-
|
|
647
|
-
---
|
|
648
|
-
|
|
649
|
-
## Required Where
|
|
650
|
-
|
|
651
|
-
`requiredWhere` defines filters that are automatically applied to every query on the repository.
|
|
652
|
-
|
|
653
|
-
```ts
|
|
654
|
-
requiredWhere: { active: true },
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
Now every query will automatically include `active: true`:
|
|
658
|
-
|
|
659
|
-
```ts
|
|
660
|
-
// Internally: WHERE active = true
|
|
661
|
-
const users = await userRepository.findMany();
|
|
662
|
-
|
|
663
|
-
// Internally: WHERE email = 'john@email.com' AND active = true
|
|
664
|
-
const user = await userRepository.findByEmail("john@email.com");
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
Useful for manual soft-deletes, multi-tenancy, and global filters of any kind.
|
|
668
|
-
|
|
669
|
-
---
|
|
670
|
-
|
|
671
|
-
## Default Ordering
|
|
672
|
-
|
|
673
|
-
`defaultOrdering` defines a default ordering that's automatically applied to every query that accepts `orderBy`, without needing to repeat the `order` argument on every call.
|
|
674
|
-
|
|
675
|
-
```ts
|
|
676
|
-
const userRepository = setupVSRepo<User, "user">()(({
|
|
677
|
-
tableName: "user",
|
|
678
|
-
pkName: "id",
|
|
679
|
-
defaultOrdering: { createdAt: "desc" },
|
|
680
|
-
}).build(prisma);
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
With this, every listing query will already come ordered by `createdAt` descending:
|
|
684
|
-
|
|
685
|
-
```ts
|
|
686
|
-
// Internally: ORDER BY createdAt DESC
|
|
687
|
-
const users = await userRepository.getAll();
|
|
688
|
-
|
|
689
|
-
// Also applies to getAll with pagination
|
|
690
|
-
const paginated = await userRepository.getAll({ pagination: { take: 10 } });
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
**`defaultOrdering` is ignored when:**
|
|
694
348
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
```ts
|
|
699
|
-
methods: {
|
|
700
|
-
findManyPaginatedAndOrdered: { map: true }, // order comes from the argument → defaultOrdering ignored
|
|
701
|
-
findManyByActive: { map: true }, // no Ordered → defaultOrdering applied
|
|
702
|
-
findManyByStatus: {
|
|
703
|
-
map: true,
|
|
704
|
-
injectOrdering: { name: "asc" }, // injectOrdering → defaultOrdering ignored
|
|
705
|
-
},
|
|
706
|
-
}
|
|
349
|
+
const userWithAddress = await userRepository.get(id, {
|
|
350
|
+
relations: { address: true },
|
|
351
|
+
});
|
|
707
352
|
```
|
|
708
353
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
## `see` option
|
|
354
|
+
- `select` mirrors the entity's shape: scalar fields take a `boolean`; relation fields take a `boolean` or a nested `select`.
|
|
355
|
+
- `relations` eagerly loads related records; each relation field takes a `boolean` or a nested `relations` object.
|
|
356
|
+
- Whether `select` and `relations` can be combined depends on the adapter (see below).
|
|
714
357
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
>
|
|
358
|
+
> ⚠️ **Adapter-dependent behavior for `relations`:**
|
|
359
|
+
>
|
|
360
|
+
> The core only forwards `MethodOptions.select` and `MethodOptions.relations` to the adapter — each adapter decides how to translate them to the underlying ORM:
|
|
361
|
+
>
|
|
362
|
+
> - **TypeORM (`@vsrepo/typeorm-adapter`)** — `relations` is **required** to load any relation, even when you only want a nested projection via `select`. TypeORM will not JOIN/emit the relation unless it is listed in `relations`:
|
|
363
|
+
> ```typescript
|
|
364
|
+
> // TypeORM: select alone is NOT enough
|
|
365
|
+
> await userRepository.get(id, {
|
|
366
|
+
> select: { id: true, address: { city: true } },
|
|
367
|
+
> relations: { address: true }, // ← required in TypeORM
|
|
368
|
+
> });
|
|
369
|
+
> ```
|
|
370
|
+
> - **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:
|
|
371
|
+
> ```typescript
|
|
372
|
+
> // Prisma7: relations is ignored when select exists
|
|
373
|
+
> await userRepository.get(id, {
|
|
374
|
+
> select: { id: true, name: true },
|
|
375
|
+
> relations: { address: true }, // ← ignored, include = undefined
|
|
376
|
+
> });
|
|
377
|
+
> ```
|
|
378
|
+
>
|
|
379
|
+
> Custom adapters may map `relations` differently — consult the adapter's documentation for the exact semantics.
|
|
735
380
|
|
|
736
381
|
---
|
|
737
382
|
|
|
738
383
|
## Dynamic methods
|
|
739
384
|
|
|
740
|
-
Dynamic methods are
|
|
741
|
-
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
385
|
+
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**, following the same convention-over-configuration philosophy as v1.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
class UserRepository extends VSRepository<User, string> {
|
|
389
|
+
@DynamicMethod()
|
|
390
|
+
declare findByEmail: (email: string) => Promise<User[]>;
|
|
391
|
+
|
|
392
|
+
@DynamicMethod()
|
|
393
|
+
declare findOneByEmail: (email: string) => Promise<User | null>;
|
|
394
|
+
|
|
395
|
+
@DynamicMethod()
|
|
396
|
+
declare updateById: (id: string, data: DeepPartial<User>) => Promise<User>;
|
|
397
|
+
|
|
398
|
+
// Where-based: VSRepoWhere<T> as the first param, pagination penultimate, MethodOptions last
|
|
399
|
+
@DynamicMethod()
|
|
400
|
+
declare findWherePaginated: (
|
|
401
|
+
where: VSRepoWhere<User>,
|
|
402
|
+
pagination: Pagination,
|
|
403
|
+
options?: MethodOptions<User>,
|
|
404
|
+
) => Promise<User[]>;
|
|
405
|
+
|
|
406
|
+
// OrderedAndPaginated: field filters, then order, then pagination, then MethodOptions
|
|
407
|
+
@DynamicMethod()
|
|
408
|
+
declare findByNameIgnoreCaseOrAgeBetweenOrderByCreatedAtAscPaginated: (
|
|
409
|
+
name: string,
|
|
410
|
+
age: [number, number],
|
|
411
|
+
order: Ordering<User>,
|
|
412
|
+
pagination: Pagination,
|
|
413
|
+
options?: MethodOptions<User>,
|
|
414
|
+
) => Promise<User[]>;
|
|
748
415
|
}
|
|
749
416
|
```
|
|
750
417
|
|
|
751
|
-
---
|
|
752
|
-
|
|
753
418
|
### Available prefixes
|
|
754
419
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
|
758
|
-
|
|
|
759
|
-
| `
|
|
760
|
-
| `
|
|
761
|
-
| `
|
|
762
|
-
| `
|
|
763
|
-
| `
|
|
764
|
-
| `
|
|
765
|
-
| `
|
|
766
|
-
| `
|
|
767
|
-
| `
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
770
|
-
| `
|
|
771
|
-
| `
|
|
772
|
-
| `
|
|
773
|
-
| `
|
|
774
|
-
| `
|
|
775
|
-
| `
|
|
776
|
-
| `
|
|
777
|
-
| `
|
|
778
|
-
| `
|
|
779
|
-
| `
|
|
780
|
-
| `
|
|
781
|
-
| `
|
|
782
|
-
| `
|
|
783
|
-
| `
|
|
784
|
-
| `
|
|
785
|
-
| `
|
|
786
|
-
| `
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
| `groupBy` | `groupBy` | `Dynamic[]` | Name must be exact; receives native Prisma args; ignores `selectModels`, `pushWhere`, and `requiredWhere` |
|
|
790
|
-
|
|
791
|
-
---
|
|
420
|
+
| Prefix | Adapter method | Notes |
|
|
421
|
+
| -------------------------- | --------------------- | ---------------------------------------------------------------------------------------- |
|
|
422
|
+
| `findBy` | `findMany` | Field filters follow the prefix. |
|
|
423
|
+
| `findOneBy` | `findOne` | Field filters follow the prefix; single result. |
|
|
424
|
+
| `findOneOrThrowBy` | `findOneOrThrow` | Throws if no record is found. |
|
|
425
|
+
| `findOneOrThrow` | `findOneOrThrow` | No field filters; applies only soft-delete/`see`. |
|
|
426
|
+
| `findOneOrThrowWhere` | `findOneOrThrow` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
427
|
+
| `findWhere` | `findMany` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
428
|
+
| `findOneWhere` | `findOne` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
429
|
+
| `findOne` | `findOne` | No field filters; applies only soft-delete/`see`. |
|
|
430
|
+
| `countBy` | `count` | Field filters follow the prefix. |
|
|
431
|
+
| `countWhere` | `count` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
432
|
+
| `count` | `count` | No field filters. |
|
|
433
|
+
| `existsBy` | `exists` | Returns `boolean`. |
|
|
434
|
+
| `existsWhere` | `exists` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
435
|
+
| `create` | `create` | Receives `data` as argument. |
|
|
436
|
+
| `createMany` | `createMany` | Receives `data[]` as argument; supports `IgnoreConflicts`. |
|
|
437
|
+
| `createManyReturning` | `createManyReturning` | Receives `data[]` as argument; supports `IgnoreConflicts`; returns the created records (`T[]`) instead of `CountResult`. |
|
|
438
|
+
| `updateBy` | `update` | Field filters + `data` as argument. |
|
|
439
|
+
| `updateWhere` | `update` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
|
|
440
|
+
| `updateManyBy` | `updateMany` | Field filters + `data`. |
|
|
441
|
+
| `updateManyWhere` | `updateMany` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
|
|
442
|
+
| `updateManyReturningBy` | `updateManyReturning` | Field filters + `data`; returns updated records. |
|
|
443
|
+
| `updateManyReturningWhere` | `updateManyReturning` | Receives a `VSRepoWhere<T>` as the first argument, then `data`; returns updated records. |
|
|
444
|
+
| `upsertBy` | `upsert` | Field filters + `create`/`update` payloads. |
|
|
445
|
+
| `upsertWhere` | `upsert` | Receives a `VSRepoWhere<T>` as the first argument, then `create`/`update` payloads. |
|
|
446
|
+
| `deleteBy` | `delete` | Field filters follow the prefix. |
|
|
447
|
+
| `deleteWhere` | `delete` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
448
|
+
| `deleteManyBy` | `deleteMany` | Field filters follow the prefix. |
|
|
449
|
+
| `deleteManyWhere` | `deleteMany` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
450
|
+
| `deleteManyReturningBy` | `deleteManyReturning` | Field filters follow the prefix; returns deleted records. |
|
|
451
|
+
| `deleteManyReturningWhere` | `deleteManyReturning` | Receives a `VSRepoWhere<T>` as the first argument; returns deleted records. |
|
|
452
|
+
|
|
453
|
+
> `aggregate` and `groupBy` are **not implemented yet** in v2 (they existed in v1). This is planned but not currently available.
|
|
792
454
|
|
|
793
455
|
### Field filters
|
|
794
456
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
| Suffix
|
|
798
|
-
|
|
|
799
|
-
|
|
|
800
|
-
| `Not`
|
|
801
|
-
| `In`
|
|
802
|
-
| `NotIn`
|
|
803
|
-
| `Contains`
|
|
804
|
-
| `NotContains`
|
|
805
|
-
| `StartsWith`
|
|
806
|
-
| `NotStartsWith`
|
|
807
|
-
| `EndsWith`
|
|
808
|
-
| `NotEndsWith`
|
|
809
|
-
| `GreaterThan`
|
|
810
|
-
| `GreaterThanEqual`
|
|
811
|
-
| `LessThan`
|
|
812
|
-
| `LessThanEqual`
|
|
813
|
-
| `Between`
|
|
814
|
-
| `NotBetween`
|
|
815
|
-
| `IsNull`
|
|
816
|
-
| `IsNotNull`
|
|
817
|
-
| `IsTrue`
|
|
818
|
-
| `IsFalse`
|
|
819
|
-
| `
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
`Between` and `NotBetween` receive a **tuple `[minValue, maxValue]`**:
|
|
830
|
-
|
|
831
|
-
```ts
|
|
832
|
-
methods: {
|
|
833
|
-
findManyByAgeBetween: { map: true },
|
|
834
|
-
findManyBySalaryNotBetween: { map: true },
|
|
835
|
-
findManyByCreatedAtBetween: { map: true },
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
await userRepository.findManyByAgeBetween([18, 65]);
|
|
839
|
-
await userRepository.findManyBySalaryNotBetween([1000, 5000]);
|
|
840
|
-
await userRepository.findManyByCreatedAtBetween([new Date("2024-01-01"), new Date("2024-12-31")]);
|
|
457
|
+
Applied as suffixes to the field name inside the method (same idea as v1, one renamed suffix):
|
|
458
|
+
|
|
459
|
+
| Suffix | Meaning | Argument |
|
|
460
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
461
|
+
| _(none)_ | equality (`=`) | yes |
|
|
462
|
+
| `Not` | negation | yes |
|
|
463
|
+
| `In` | is one of | yes (array) |
|
|
464
|
+
| `NotIn` | is none of | yes (array) |
|
|
465
|
+
| `Contains` | substring match | yes |
|
|
466
|
+
| `NotContains` | negated substring match | yes |
|
|
467
|
+
| `StartsWith` | prefix match | yes |
|
|
468
|
+
| `NotStartsWith` | negated prefix match | yes |
|
|
469
|
+
| `EndsWith` | suffix match | yes |
|
|
470
|
+
| `NotEndsWith` | negated suffix match | yes |
|
|
471
|
+
| `GreaterThan` | `>` | yes |
|
|
472
|
+
| `GreaterThanEqual` | `>=` | yes |
|
|
473
|
+
| `LessThan` | `<` | yes |
|
|
474
|
+
| `LessThanEqual` | `<=` | yes |
|
|
475
|
+
| `Between` | inclusive range | yes (`[min, max]` tuple) |
|
|
476
|
+
| `NotBetween` | outside an inclusive range | yes (`[min, max]` tuple) |
|
|
477
|
+
| `IsNull` | field is `null` | no |
|
|
478
|
+
| `IsNotNull` | field is not `null` | no |
|
|
479
|
+
| `IsTrue` | field is `true` | no |
|
|
480
|
+
| `IsFalse` | field is `false` | no |
|
|
481
|
+
| `IgnoreCase` | case-insensitive combinator for text filters | no _(renamed from v1's `Insensitive`)_ |
|
|
482
|
+
| `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 | — |
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
@DynamicMethod()
|
|
486
|
+
declare findByNameContainsIgnoreCase: (name: string) => Promise<User[]>;
|
|
487
|
+
|
|
488
|
+
@DynamicMethod()
|
|
489
|
+
declare findByAgeBetween: (age: [number, number]) => Promise<User[]>;
|
|
841
490
|
```
|
|
842
491
|
|
|
843
|
-
The `Optional` suffix can be added to any field to make the argument optional:
|
|
844
|
-
|
|
845
|
-
```ts
|
|
846
|
-
findByNameOptionalAndEmail // name is optional, email is required
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
---
|
|
850
|
-
|
|
851
492
|
### Logical operators
|
|
852
493
|
|
|
853
|
-
| Operator
|
|
854
|
-
|
|
|
855
|
-
| `And`
|
|
856
|
-
| `Or`
|
|
857
|
-
| `AND`
|
|
494
|
+
| Operator | Usage in the name | Example |
|
|
495
|
+
| -------- | ------------------------------- | --------------------------------------------------- |
|
|
496
|
+
| `And` | between two fields | `findOneByIdAndEmail` |
|
|
497
|
+
| `Or` | between two fields | `findByNameOrEmail` |
|
|
498
|
+
| `AND` | splits a final block into `AND` | `findByEmailOrNameANDActiveStatusAndAgeGreaterThan` |
|
|
858
499
|
|
|
859
|
-
`AND` (
|
|
500
|
+
`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`.
|
|
860
501
|
|
|
861
|
-
|
|
862
|
-
- All fields after `AND` are injected inside `AND: []`.
|
|
863
|
-
- After an `AND`, there can't be an `Or`.
|
|
502
|
+
### Relation filters
|
|
864
503
|
|
|
865
|
-
|
|
504
|
+
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).
|
|
866
505
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
506
|
+
| Suffix | Meaning | Restriction |
|
|
507
|
+
| -------------- | ----------------------------------------------- | ---------------------------------------------------------- |
|
|
508
|
+
| `Some` | at least one related record matches | to-many relations only |
|
|
509
|
+
| `SomeField` | filters within the related records | to-many relations only |
|
|
510
|
+
| `Every` | every related record matches | to-many relations only (needs `Field` to be a real filter) |
|
|
511
|
+
| `EveryField` | filters within the related records | to-many relations only |
|
|
512
|
+
| `None` | no related record matches | to-many relations only |
|
|
513
|
+
| `NoneField` | filters within the related records | to-many relations only |
|
|
514
|
+
| `With` | related record exists | to-one relations only |
|
|
515
|
+
| `WithField` | filters a field within the related record | to-one relations only |
|
|
516
|
+
| `Without` | related record does not exist | to-one relations only |
|
|
517
|
+
| `WithoutField` | negated filter on a field of the related record | to-one relations only |
|
|
874
518
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
await userRepository.findByEmailOrNameANDActiveStatusAndAgeGreaterThan("john@email.com", "John", true, 17)
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
Generates (`findOneByIdAndEmail`):
|
|
519
|
+
```typescript
|
|
520
|
+
@DynamicMethod()
|
|
521
|
+
declare findByAddressWithCityStartsWithIgnoreCase: (city: string) => Promise<User[]>;
|
|
882
522
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
id: 1,
|
|
886
|
-
email: "john@email.com"
|
|
887
|
-
}
|
|
523
|
+
@DynamicMethod()
|
|
524
|
+
declare findByProductsSome: () => Promise<User[]>;
|
|
888
525
|
```
|
|
889
526
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
```ts
|
|
893
|
-
{
|
|
894
|
-
OR: [
|
|
895
|
-
{ name: "John" },
|
|
896
|
-
{ email: "john@email.com" }
|
|
897
|
-
]
|
|
898
|
-
}
|
|
899
|
-
```
|
|
527
|
+
### Ordering, pagination and distinct
|
|
900
528
|
|
|
901
|
-
|
|
529
|
+
| Suffix | Effect |
|
|
530
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
531
|
+
| `Paginated` | Injects a `pagination` argument (`{ limit?, offset? }`) as the **penultimate** parameter (before the optional `MethodOptions`). |
|
|
532
|
+
| `Ordered` | Injects an `order: Ordering<T>` argument as the **penultimate** parameter (before the optional `MethodOptions`). |
|
|
533
|
+
| `OrderedAndPaginated` | Injects `order` as the antepenultimate, then `pagination` as the penultimate — both before `MethodOptions`. |
|
|
534
|
+
| `PaginatedAndOrdered` | Injects `pagination` as the antepenultimate, then `order` as the penultimate — both before `MethodOptions`. |
|
|
535
|
+
| `OrderBy<Field>Asc` / `OrderBy<Field>Desc` | **New in v2.** Bakes a fixed ordering directly into the method name — chain fields with `And` (e.g. `OrderByCreatedAtAscAndNameDesc`). No `order` argument needed. |
|
|
536
|
+
| `Distinct<Field>And<Field>...` | Bakes fixed `distinct` fields directly into the method name (only valid on `findBy`/`findWhere`-family methods). |
|
|
537
|
+
| `IgnoreConflicts` | On `createMany`/`createManyReturning`, skips records that would violate a unique constraint instead of throwing. _(Renamed from v1's `SkipDuplicates`.)_ |
|
|
902
538
|
|
|
903
|
-
|
|
904
|
-
{
|
|
905
|
-
OR: [
|
|
906
|
-
{ id: 1 },
|
|
907
|
-
{
|
|
908
|
-
email: "john@email.com",
|
|
909
|
-
name: "John"
|
|
910
|
-
}
|
|
911
|
-
]
|
|
912
|
-
}
|
|
913
|
-
```
|
|
539
|
+
> ⚠️ **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).
|
|
914
540
|
|
|
915
|
-
|
|
541
|
+
```typescript
|
|
542
|
+
// Paginated: pagination is the penultimate param (before MethodOptions)
|
|
543
|
+
@DynamicMethod()
|
|
544
|
+
declare findByActiveOrderByCreatedAtDescPaginated:
|
|
545
|
+
(active: boolean, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
|
|
916
546
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
{ name: "John" }
|
|
922
|
-
],
|
|
923
|
-
AND: [
|
|
924
|
-
{ activeStatus: true },
|
|
925
|
-
{ age: { gt: 17 } }
|
|
926
|
-
]
|
|
927
|
-
}
|
|
928
|
-
```
|
|
547
|
+
// OrderedAndPaginated: order, then pagination, then MethodOptions
|
|
548
|
+
@DynamicMethod()
|
|
549
|
+
declare findByNameContainsIgnoreCaseOrderedAndPaginated:
|
|
550
|
+
(name: string, order: Ordering<User>, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
|
|
929
551
|
|
|
930
|
-
|
|
552
|
+
@DynamicMethod()
|
|
553
|
+
declare createManyIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<{ count: number }>;
|
|
931
554
|
|
|
932
|
-
|
|
555
|
+
// createManyReturning: same as createMany, but returns the created records
|
|
556
|
+
@DynamicMethod()
|
|
557
|
+
declare createManyReturningIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<User[]>;
|
|
933
558
|
|
|
934
|
-
|
|
559
|
+
// findOne with no filter (equivalent to findOneOrThrow with no filter, but returns null instead of throwing)
|
|
560
|
+
@DynamicMethod()
|
|
561
|
+
declare findOne: (options?: MethodOptions<User>) => Promise<User | null>;
|
|
562
|
+
```
|
|
935
563
|
|
|
936
|
-
>
|
|
564
|
+
> ⚠️ **Precedence between `Distinct` and `OrderBy`:** when both are used in the same method name, **`Distinct` must come before `OrderBy`**:
|
|
937
565
|
>
|
|
938
|
-
>
|
|
939
|
-
>
|
|
940
|
-
>
|
|
941
|
-
>
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
| `Some` | `some: {}` | Relation has *some* record |
|
|
946
|
-
| `SomeField` | `some.field` | Filters within the relation's records |
|
|
947
|
-
| `EveryField` | `every.field` | Filters within the relation's records |
|
|
948
|
-
| `None` | `none: {}` | Relation has *no* records |
|
|
949
|
-
| `NoneField` | `none.field` | Filters within the relation's records |
|
|
950
|
-
| `With` | `is: {}` | Relation exists (not null) |
|
|
951
|
-
| `WithField` | `is.field` | Filters a field within the relation |
|
|
952
|
-
| `Without` | `isNot: {}` | Relation doesn't exist (is null) |
|
|
953
|
-
| `WithoutField` | `isNot.field` | Filters a field within the relation with negation |
|
|
954
|
-
|
|
955
|
-
Considering `user` with a to-one relation `profile` and a to-many relation `posts`:
|
|
956
|
-
|
|
957
|
-
```ts
|
|
958
|
-
methods: {
|
|
959
|
-
// to-many (posts)
|
|
960
|
-
findByPostsSome: { map: true }, // has at least one post
|
|
961
|
-
findByPostsSomeTitle: { map: true }, // has at least one post with that title
|
|
962
|
-
findByPostsEveryPublishedIsTrue:{ map: true }, // all posts are published
|
|
963
|
-
findByPostsNone: { map: true }, // has no posts
|
|
964
|
-
findByPostsNoneTitle: { map: true }, // no post has that title
|
|
965
|
-
|
|
966
|
-
// to-one (profile)
|
|
967
|
-
findByProfileWith: { map: true }, // has a profile (not null)
|
|
968
|
-
findByProfileWithBio: { map: true }, // has a profile with that bio
|
|
969
|
-
findByProfileWithout: { map: true }, // has no profile (is null)
|
|
970
|
-
findByProfileWithoutBio: { map: true }, // has a profile, but with a different bio than the one provided
|
|
971
|
-
}
|
|
566
|
+
> ```typescript
|
|
567
|
+
> @DynamicMethod()
|
|
568
|
+
> declare findByActiveDistinctNameOrderByCreatedAtDesc:
|
|
569
|
+
> (active: boolean) => Promise<User[]>;
|
|
570
|
+
> ```
|
|
571
|
+
>
|
|
572
|
+
> Putting `OrderBy` before `Distinct` (e.g. `findByActiveOrderByCreatedAtDescDistinctName`) is not a valid pattern and won't be parsed as expected.
|
|
972
573
|
|
|
973
|
-
|
|
974
|
-
await userRepository.findByPostsSomeTitle("My first post");
|
|
975
|
-
await userRepository.findByPostsEveryPublishedIsTrue();
|
|
976
|
-
await userRepository.findByPostsNone();
|
|
977
|
-
await userRepository.findByPostsNoneTitle("Draft");
|
|
574
|
+
### Decorator options
|
|
978
575
|
|
|
979
|
-
|
|
980
|
-
await userRepository.findByProfileWithBio("Hello, world!");
|
|
981
|
-
await userRepository.findByProfileWithout();
|
|
982
|
-
await userRepository.findByProfileWithoutBio("Old bio");
|
|
983
|
-
```
|
|
576
|
+
`@DynamicMethod<T>(options?)` accepts:
|
|
984
577
|
|
|
985
|
-
|
|
578
|
+
| Option | Type | Description |
|
|
579
|
+
| ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
580
|
+
| `proxyTo` | `string` | Redirects the method's logic to another valid dynamic-method pattern — useful for names that don't follow the naming convention. |
|
|
581
|
+
| `injectOrdering` | `Ordering<T>` | Fixed ordering automatically injected, overriding the repository's `defaultOrdering`. |
|
|
986
582
|
|
|
987
|
-
```
|
|
988
|
-
{
|
|
989
|
-
|
|
990
|
-
some: { title: "My first post" }
|
|
991
|
-
}
|
|
992
|
-
}
|
|
583
|
+
```typescript
|
|
584
|
+
@DynamicMethod<User>({ injectOrdering: { createdAt: "desc" } })
|
|
585
|
+
declare findByStatus: (status: string) => Promise<User[]>;
|
|
993
586
|
```
|
|
994
587
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
```ts
|
|
998
|
-
{
|
|
999
|
-
posts: {
|
|
1000
|
-
every: { published: true }
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
```
|
|
588
|
+
---
|
|
1004
589
|
|
|
1005
|
-
|
|
590
|
+
## Query methods (raw SQL)
|
|
1006
591
|
|
|
1007
|
-
|
|
1008
|
-
{
|
|
1009
|
-
profile: {
|
|
1010
|
-
is: { bio: "Hello, world!" }
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
```
|
|
592
|
+
`@QueryMethod` bypasses the name-parsing engine entirely and executes a raw SQL statement through the adapter's `query()` method. Use `$1`, `$2`, ... placeholders — never interpolate values directly into the SQL string.
|
|
1014
593
|
|
|
1015
|
-
|
|
594
|
+
```typescript
|
|
595
|
+
class UserRepository extends VSRepository<User, string> {
|
|
596
|
+
@QueryMethod('SELECT * FROM "user" WHERE email = $1')
|
|
597
|
+
declare findByEmailRaw: (arg: QueryMethodArg<[email: string]>) => Promise<User[]>;
|
|
1016
598
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
profile: {
|
|
1020
|
-
isNot: {}
|
|
1021
|
-
}
|
|
599
|
+
@QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
|
|
600
|
+
declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
|
|
1022
601
|
}
|
|
1023
602
|
```
|
|
1024
603
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
### Pagination and ordering suffixes
|
|
1030
|
-
|
|
1031
|
-
Applied at the **end** of the method name, they automatically inject the pagination and ordering arguments.
|
|
1032
|
-
|
|
1033
|
-
| Suffix | Additional arguments |
|
|
1034
|
-
| ------------------------ | -------------------------------|
|
|
1035
|
-
| `Paginated` | `(pagination)` |
|
|
1036
|
-
| `Ordered` | `(order)` |
|
|
1037
|
-
| `OrderedAndPaginated` | `(order, pagination)` |
|
|
1038
|
-
| `PaginatedAndOrdered` | `(pagination, order)` |
|
|
1039
|
-
|
|
1040
|
-
For `createMany` and `createManyAndReturn`, the `SkipDuplicates` suffix is available:
|
|
1041
|
-
|
|
1042
|
-
| Suffix | Effect |
|
|
1043
|
-
| ------------------- | ------------------------------------------ |
|
|
1044
|
-
| `SkipDuplicates` | Skips duplicate records during insertion |
|
|
1045
|
-
|
|
1046
|
-
---
|
|
1047
|
-
|
|
1048
|
-
### Distinct
|
|
604
|
+
| Option | Type | Default | Description |
|
|
605
|
+
| ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
606
|
+
| `modifying` | `boolean` | `false` | When `true`, runs as `INSERT`/`UPDATE`/`DELETE` and the method resolves to the number of affected rows. When `false`, runs as a read query and resolves to the declared return type. |
|
|
1049
607
|
|
|
1050
|
-
|
|
608
|
+
Query methods accept `{ args, db? }` at the call site — `db` lets them participate in a `transaction()` block just like base and dynamic methods.
|
|
1051
609
|
|
|
1052
|
-
|
|
610
|
+
### Ad-hoc raw queries with `query()`
|
|
1053
611
|
|
|
1054
|
-
|
|
1055
|
-
methods: {
|
|
1056
|
-
// Returns unique users combining "age" and "role" (no field filter)
|
|
1057
|
-
findManyDistinctAgeAndRole: { map: true },
|
|
612
|
+
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 goes through the same adapter's `query()` implementation under the hood:
|
|
1058
613
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
// Distinct combined with a field filter (name) — filters by name and then applies distinct on role
|
|
1063
|
-
findManyByNameDistinctRole: { map: true },
|
|
1064
|
-
},
|
|
614
|
+
```typescript
|
|
615
|
+
query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
|
|
1065
616
|
```
|
|
1066
617
|
|
|
1067
|
-
```
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// The pagination argument still works normally
|
|
1072
|
-
await userRepository.findManyDistinctNamePaginated({ take: 10, skip: 0 });
|
|
618
|
+
```typescript
|
|
619
|
+
const users = await userRepository.query<User[]>('SELECT * FROM "user" WHERE email = $1', {
|
|
620
|
+
args: ["maria@email.com"],
|
|
621
|
+
});
|
|
1073
622
|
|
|
1074
|
-
|
|
1075
|
-
|
|
623
|
+
const affectedRows = await userRepository.query<number>(
|
|
624
|
+
'UPDATE "user" SET active = true WHERE id = $1',
|
|
625
|
+
{ args: ["123"], modifying: true },
|
|
626
|
+
);
|
|
1076
627
|
```
|
|
1077
628
|
|
|
1078
|
-
|
|
629
|
+
| Option | Type | Default | Description |
|
|
630
|
+
| ----------- | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
631
|
+
| `args` | `any[]` | `undefined` | Positional parameters injected into `$1`, `$2`, ... placeholders. Never interpolate values directly into the SQL string. |
|
|
632
|
+
| `db` | `any` | Repository's default client | Database client or transaction to run this query in. |
|
|
633
|
+
| `modifying` | `boolean` | `false` | When `true`, treats the statement as `INSERT`/`UPDATE`/`DELETE`. |
|
|
1079
634
|
|
|
1080
|
-
|
|
635
|
+
Just like base, dynamic and query methods, `query()` accepts `db` in `options` to participate in a `transaction()` block.
|
|
1081
636
|
|
|
1082
637
|
---
|
|
1083
638
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
Each entry in `methods` accepts the following options:
|
|
639
|
+
## Transactions
|
|
1087
640
|
|
|
1088
|
-
|
|
1089
|
-
| --------------------- | ---------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
1090
|
-
| `map` | `boolean` | — | **Required.** Defines whether the method will be exposed on the repository. |
|
|
1091
|
-
| `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combines with `requiredWhere`. `overwrite` ignores `requiredWhere`. |
|
|
1092
|
-
| `selectModel` | `keyof SelectModels \| false` | — | Overrides `defaultSelectModel` for this method. |
|
|
1093
|
-
| `fbMode` | `'one'` \| `'list'` | `'list'` | (**Deprecated. Use `findOneBy`**) Only for `findBy`. `'one'` returns `T \| null`; `'list'` returns `T[]`. |
|
|
1094
|
-
| `proxyTo` | `Valid method pattern` | — | Delegates the logic to another valid method pattern. |
|
|
1095
|
-
| `pushWhere` | `WhereModel<M>` | — | Extra `where` added to the query in addition to `requiredWhere`. |
|
|
1096
|
-
| `injectOrdering` | `OrderingModel<M>` | — | Fixed ordering automatically injected into the query. |
|
|
1097
|
-
| `injectPagination` | `PaginationModel<M>` | — | Fixed pagination automatically injected into the query. |
|
|
1098
|
-
| `query` | `{ value: string; modifying?: boolean }` | — | Turns the method into a **Query Method** (raw SQL). Ignores every other option above — see [Query Methods](#query-methods). |
|
|
641
|
+
All methods (base, dynamic, and query) accept `options.db` to participate in a shared transaction:
|
|
1099
642
|
|
|
1100
|
-
|
|
643
|
+
```typescript
|
|
644
|
+
await userRepository.transaction(async tx => {
|
|
645
|
+
const user = await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
|
|
1101
646
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
pkName: "id",
|
|
1108
|
-
methods: {
|
|
1109
|
-
aggregate: { map: true },
|
|
1110
|
-
groupBy: { map: true },
|
|
1111
|
-
},
|
|
1112
|
-
}).build(prisma);
|
|
647
|
+
await userLogsRepository.save(
|
|
648
|
+
{ action: "User created", data: { userId: user.id } },
|
|
649
|
+
{ db: tx },
|
|
650
|
+
);
|
|
651
|
+
});
|
|
1113
652
|
```
|
|
1114
653
|
|
|
1115
|
-
|
|
1116
|
-
> These methods must have exactly these names (`aggregate` and `groupBy`).
|
|
1117
|
-
> Unlike the other dynamic methods, they receive native Prisma arguments and **ignore** the `selectModels`, `pushWhere`, and `requiredWhere` configurations.
|
|
1118
|
-
|
|
1119
|
-
---
|
|
1120
|
-
|
|
1121
|
-
### Query Methods
|
|
654
|
+
Different repositories can share the same transaction as long as their adapters point to the same underlying ORM connection.
|
|
1122
655
|
|
|
1123
|
-
|
|
656
|
+
`transaction()` accepts an optional `VSRepoTransactionOptions` as its second argument:
|
|
1124
657
|
|
|
1125
|
-
|
|
658
|
+
```typescript
|
|
659
|
+
import { TransactionIsolationLevel } from "vsrepo";
|
|
1126
660
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
```ts
|
|
1131
|
-
const userRepository = setupVSRepo<User, "user">()({
|
|
1132
|
-
tableName: "user",
|
|
1133
|
-
pkName: "id",
|
|
1134
|
-
methods: {
|
|
1135
|
-
// Read query method (non-modifying)
|
|
1136
|
-
findActiveUsersRaw: {
|
|
1137
|
-
map: true,
|
|
1138
|
-
query: {
|
|
1139
|
-
value: 'SELECT * FROM "user" WHERE active = $1',
|
|
1140
|
-
},
|
|
1141
|
-
},
|
|
1142
|
-
|
|
1143
|
-
// Write query method (modifying: true)
|
|
1144
|
-
deactivateUsersOlderThanRaw: {
|
|
1145
|
-
map: true,
|
|
1146
|
-
query: {
|
|
1147
|
-
value: 'UPDATE "user" SET active = false WHERE "createdAt" < $1',
|
|
1148
|
-
modifying: true,
|
|
1149
|
-
},
|
|
661
|
+
await userRepository.transaction(
|
|
662
|
+
async tx => {
|
|
663
|
+
await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
|
|
1150
664
|
},
|
|
1151
|
-
|
|
1152
|
-
|
|
665
|
+
{ isolationLevel: TransactionIsolationLevel.SERIALIZABLE, timeoutMs: 5000 },
|
|
666
|
+
);
|
|
1153
667
|
```
|
|
1154
668
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
```ts
|
|
1160
|
-
// Non-modifying: returns 'any' by default, but accepts a generic to
|
|
1161
|
-
// infer/assert the return type right at the call site
|
|
1162
|
-
const activeUsers = await userRepository.findActiveUsersRaw<User[]>({
|
|
1163
|
-
args: [true],
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
// Modifying: always returns 'number' (count of affected rows)
|
|
1167
|
-
const affected = await userRepository.deactivateUsersOlderThanRaw({
|
|
1168
|
-
args: [new Date("2024-01-01")],
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
// Participating in a transaction, via 'db'
|
|
1172
|
-
await userRepository.prisma.$transaction(async (tx) => {
|
|
1173
|
-
await userRepository.deactivateUsersOlderThanRaw({
|
|
1174
|
-
args: [new Date("2024-01-01")],
|
|
1175
|
-
db: tx,
|
|
1176
|
-
});
|
|
1177
|
-
});
|
|
1178
|
-
```
|
|
669
|
+
| Option | Type | Description |
|
|
670
|
+
| ----------------- | -------------------------- | -------------------------------------------------------------------------------- |
|
|
671
|
+
| `isolationLevel` | `TransactionIsolationLevel` | Isolation level to use for the transaction. Defaults to the underlying ORM's default. |
|
|
672
|
+
| `timeoutMs` | `number` | Maximum time (in ms) the transaction is allowed to run before being aborted. |
|
|
1179
673
|
|
|
1180
|
-
|
|
1181
|
-
| ------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1182
|
-
| `value` | `string` | — | **Required.** Raw SQL to execute. Use `$1`, `$2`, ... for the placeholders of the values in `args`. |
|
|
1183
|
-
| `modifying` | `boolean` | `false` | When `true`, executes via `$executeRawUnsafe` and the method always resolves to `number`. When `false`, executes via `$queryRawUnsafe` and the method resolves to `TReturn` (`any` by default, inferable via a generic at the call site). |
|
|
1184
|
-
|
|
1185
|
-
> [!NOTE]
|
|
1186
|
-
> Unlike the other dynamic methods, Query Methods **completely ignore** `selectModels`, `requiredWhere`, `pushWhere`, `whereType`, `injectOrdering`, `injectPagination`, and `proxyTo` — none of that applies, since there's no name parsing or `where`/`select` assembly by VSRepository. Free-form method names (outside the `findBy`, `updateBy`, etc. patterns) also **don't** require `proxyTo`.
|
|
1187
|
-
|
|
1188
|
-
The same functionality is available in the class-based approach via the `@QueryMethod` decorator — see [README-DynamicRepo.md](./README-DynamicRepo.md#the-querymethod-decorator).
|
|
674
|
+
`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.
|
|
1189
675
|
|
|
1190
676
|
---
|
|
1191
677
|
|
|
1192
|
-
##
|
|
1193
|
-
|
|
1194
|
-
Configure relations so that `save` and `patch` manage them automatically (`saveList` and `patchList` also manage relations automatically).
|
|
1195
|
-
|
|
1196
|
-
```ts
|
|
1197
|
-
import type { Prisma } from "../../generated/prisma/client";
|
|
678
|
+
## Utility types
|
|
1198
679
|
|
|
1199
|
-
|
|
1200
|
-
include: { profile: true; posts: true };
|
|
1201
|
-
}>;
|
|
680
|
+
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:
|
|
1202
681
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
682
|
+
```typescript
|
|
683
|
+
import type {
|
|
684
|
+
MethodOptions,
|
|
685
|
+
RestrictMethodOptions,
|
|
686
|
+
Pagination,
|
|
687
|
+
Ordering,
|
|
688
|
+
OrderByField,
|
|
689
|
+
SortDirection,
|
|
690
|
+
SeeMode,
|
|
691
|
+
DeepPartial,
|
|
692
|
+
CountResult,
|
|
693
|
+
QueryMethodArg,
|
|
694
|
+
KeysOfType,
|
|
695
|
+
NumericKeys,
|
|
696
|
+
NumericLike,
|
|
697
|
+
DecimalLike,
|
|
698
|
+
Primitive,
|
|
699
|
+
VSRepoWhere,
|
|
700
|
+
VSRepoOrmTypes,
|
|
701
|
+
VSRepoTransactionOptions,
|
|
702
|
+
TransactionIsolationLevel,
|
|
703
|
+
} from "vsrepo";
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
| Type | Description | Used by |
|
|
707
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
708
|
+
| `MethodOptions<T, K>` | Options accepted as the last argument of most base and dynamic methods: `select`, `relations`, `see`, `db`. | [Base methods](#base-methods), [Dynamic methods](#dynamic-methods). |
|
|
709
|
+
| `RestrictMethodOptions<T, K>` | Narrowed `MethodOptions<T, K>` exposing only `see`/`db` — used by methods that don't shape/return an `Entity` (`total`, `has`, `sum`, `average`, `min`, `max`, `removeList`, `softRemoveList`, `restoreList`). | [Base methods](#base-methods), [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
|
|
710
|
+
| `Pagination` | `{ limit?, offset? }` accepted by `getAll` and by `Paginated` dynamic methods. | [Base methods](#base-methods), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
|
|
711
|
+
| `Ordering<T>` / `OrderByField<T>` / `SortDirection` | Ordering shape accepted by `getAll`, `defaultOrdering` and `injectOrdering`, and by `Ordered` dynamic methods. A single object or a chained array; nested objects order to-one relations. | [Constructor options](#constructor-options), [Decorator options](#decorator-options), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
|
|
712
|
+
| `SeeMode` | `"active" \| "removed" \| "all"` — controls visibility of soft-deleted records. | [Soft-delete](#soft-delete). |
|
|
713
|
+
| `DeepPartial<T>` | Recursively makes every property of `T` optional, including nested objects and array elements. | `save`, `saveList`, `patch`, `merge`, and every write method on `VSRepoAdapter`. |
|
|
714
|
+
| `CountResult` | `{ count: number }` — the shape returned by batch operations. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
|
|
715
|
+
| `QueryMethodArg<T>` | `{ args?: T, db? }` — positional SQL parameters (`$1`, `$2`, ...) and transaction client for `@QueryMethod`. | [Query methods (raw SQL)](#query-methods-raw-sql). |
|
|
716
|
+
| `KeysOfType<T, K>` | Extracts the keys of `T` whose value type is assignable to `K`. | Constrains `pkName` in [Constructor options](#constructor-options) to fields of the entity matching the configured primary-key type. |
|
|
717
|
+
| `NumericKeys<T>` | Extracts the keys of `T` whose (non-nullable) value type is assignable to `NumericLike`. Nullable numeric fields (`number \| null`) are included. | Constrains `field` in [Atomic and aggregate methods](#atomic-and-aggregate-methods) (`increment`, `sum`, etc). |
|
|
718
|
+
| `NumericLike` | `number \| bigint \| DecimalLike`. | [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
|
|
719
|
+
| `DecimalLike` | Structural shape of an arbitrary-precision decimal value (`{ toNumber(): number; decimalPlaces(): number }`), matching e.g. Prisma's `Prisma.Decimal` without importing it directly. | [Which fields are eligible](#which-fields-are-eligible). |
|
|
720
|
+
| `Primitive` | Union of scalar types (`string \| number \| boolean \| bigint \| symbol \| undefined \| null \| Date`) treated as leaves — not relations — when walking an entity's shape. | Used by `Ordering<T>` to tell scalar fields apart from relation fields. |
|
|
721
|
+
| `VSRepoWhere<T>` | ORM-agnostic filter type accepted by `*Where` dynamic methods (e.g. `findWhere`, `findOneWhere`, `updateWhere`). Supports field filters, logical operators (`AND`/`OR`/`NOT`), and relation filters. | [`findWhere`, `findOneWhere` and other `*Where` prefixes](#available-prefixes). |
|
|
722
|
+
| `VSRepoOrmTypes` | `{ dbClient; dbTransaction }` — describes your ORM's client/transaction types. Passed as the third generic to `VSRepository<Entity, PKType, OrmTypes>` to type `getDbClient()`, `transaction()` and the `db` option instead of `any`. | [Creating a repository](#creating-a-repository). |
|
|
723
|
+
| `VSRepoTransactionOptions` | `{ isolationLevel?, timeoutMs? }` — options accepted as the second argument of `transaction()`. | [Transactions](#transactions). |
|
|
724
|
+
| `TransactionIsolationLevel` | Enum of standard SQL isolation levels (`READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`) accepted by `VSRepoTransactionOptions.isolationLevel`. | [Transactions](#transactions). |
|
|
725
|
+
|
|
726
|
+
### `DeepPartial<T>`
|
|
727
|
+
|
|
728
|
+
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:
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
type User = { id: string; name: string; address: { city: string; zip: string } };
|
|
732
|
+
|
|
733
|
+
const patch: DeepPartial<User> = {
|
|
734
|
+
address: { city: "São Paulo" }, // zip can be omitted; city keeps its type
|
|
735
|
+
};
|
|
1206
736
|
|
|
1207
|
-
|
|
1208
|
-
profile: {
|
|
1209
|
-
pk: "id",
|
|
1210
|
-
mode: "oto",
|
|
1211
|
-
restriction: "set",
|
|
1212
|
-
},
|
|
1213
|
-
posts: {
|
|
1214
|
-
pk: "id",
|
|
1215
|
-
mode: "otm",
|
|
1216
|
-
restriction: "add",
|
|
1217
|
-
},
|
|
1218
|
-
},
|
|
1219
|
-
}).build(prisma);
|
|
737
|
+
await userRepository.patch(id, patch);
|
|
1220
738
|
```
|
|
1221
739
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
| Mode | Relation |
|
|
1225
|
-
| ----- | -------------- |
|
|
1226
|
-
| `oto` | one-to-one |
|
|
1227
|
-
| `otm` | one-to-many |
|
|
1228
|
-
| `mto` | many-to-one |
|
|
1229
|
-
| `mtm` | many-to-many |
|
|
740
|
+
### `KeysOfType<T, K>`
|
|
1230
741
|
|
|
1231
|
-
|
|
742
|
+
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:
|
|
1232
743
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
744
|
+
```typescript
|
|
745
|
+
type User = { id: string; age: number; name: string };
|
|
746
|
+
type StringKeys = KeysOfType<User, string>; // "id" | "name"
|
|
747
|
+
```
|
|
1237
748
|
|
|
1238
|
-
|
|
1239
|
-
> **`set` means different things depending on the relation's `mode` — and this can cause data loss if you're not careful.**
|
|
1240
|
-
>
|
|
1241
|
-
> In relations where the related record **belongs** to the parent record (`oto` and `otm`), "removing the ones that weren't sent" means **deleting the record from the database** (`delete`/`deleteMany`). In relations where the related record is **independent** (`mto` and `mtm`), "removing" just means **unlinking** (`disconnect`/`set: []`) — the related record continues to exist in the database, it just stops pointing to the parent (or being in the join table).
|
|
1242
|
-
>
|
|
1243
|
-
> | Mode | `restriction: "set"` when an item is omitted | Does the item continue to exist in the database? |
|
|
1244
|
-
> | ----- | ------------------------------------------------ | ----------------------------------------------------- |
|
|
1245
|
-
> | `oto` | Passing `null` in the field → **deletes** the related record (`delete: true`) | No |
|
|
1246
|
-
> | `otm` | Items outside the sent list → **deleted** (`deleteMany` with `notIn`) | No |
|
|
1247
|
-
> | `mto` | Passing `null` in the field (with `nullable: true`) → **unlinks** (`disconnect: true`) | Yes |
|
|
1248
|
-
> | `mtm` | Items outside the sent list → **unlinked** from the join table (`set: []`) | Yes |
|
|
1249
|
-
>
|
|
1250
|
-
> Practical example: if `posts` is `otm` with `restriction: "set"`, a `save`/`patch` that sends the user with only 2 of the 5 existing posts will **delete the other 3 posts from the database**, not just unlink them from the user. If the expected behavior is just to unlink without deleting, use `restriction: "add"` (which never removes anything) and handle removal manually.
|
|
749
|
+
### `Ordering<T>`
|
|
1251
750
|
|
|
1252
|
-
|
|
751
|
+
Accepts either a single ordering object or an array of them, applied in the order they're declared:
|
|
1253
752
|
|
|
1254
|
-
|
|
753
|
+
```typescript
|
|
754
|
+
const order: Ordering<User> = { createdAt: "desc" };
|
|
755
|
+
const chained: Ordering<User> = [{ name: "asc" }, { createdAt: "desc" }];
|
|
1255
756
|
|
|
1256
|
-
|
|
1257
|
-
relations: {
|
|
1258
|
-
category: {
|
|
1259
|
-
pk: "id",
|
|
1260
|
-
mode: "mto",
|
|
1261
|
-
restriction: "set",
|
|
1262
|
-
nullable: true, // allows passing null to unlink
|
|
1263
|
-
},
|
|
1264
|
-
}
|
|
757
|
+
await userRepository.getAll({ order: chained });
|
|
1265
758
|
```
|
|
1266
759
|
|
|
1267
760
|
---
|
|
1268
761
|
|
|
1269
|
-
##
|
|
762
|
+
## Writing your own adapter
|
|
763
|
+
|
|
764
|
+
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:
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
export abstract class VSRepoAdapter<T> {
|
|
768
|
+
abstract runInTransaction<R>(
|
|
769
|
+
fn: (tx: any) => Promise<R>,
|
|
770
|
+
options?: VSRepoTransactionOptions,
|
|
771
|
+
): Promise<R>;
|
|
772
|
+
abstract getDbClient(): any;
|
|
773
|
+
abstract query<T = any>(query: string, options?: AdapterQueryOptions): Promise<T>;
|
|
774
|
+
abstract findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T | null>;
|
|
775
|
+
abstract findOneOrThrow(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
776
|
+
abstract findMany(
|
|
777
|
+
where: VSRepoWhere<T>,
|
|
778
|
+
options?: AdapterMethodOptions<T> & { distinct?: (keyof T)[] },
|
|
779
|
+
): Promise<T[]>;
|
|
780
|
+
abstract save(obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
781
|
+
abstract saveMany(objs: DeepPartial<T>[], options?: AdapterMethodOptions<T>): Promise<T[]>;
|
|
782
|
+
abstract create(objs: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
783
|
+
abstract createMany(
|
|
784
|
+
objs: DeepPartial<T>[],
|
|
785
|
+
options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
|
|
786
|
+
): Promise<CountResult>;
|
|
787
|
+
abstract createManyReturning(
|
|
788
|
+
objs: DeepPartial<T>[],
|
|
789
|
+
options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
|
|
790
|
+
): Promise<T[]>;
|
|
791
|
+
abstract delete(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
792
|
+
abstract deleteMany(
|
|
793
|
+
where: VSRepoWhere<T>,
|
|
794
|
+
options?: AdapterMethodOptions<T>,
|
|
795
|
+
): Promise<CountResult>;
|
|
796
|
+
abstract deleteManyReturning(
|
|
797
|
+
where: VSRepoWhere<T>,
|
|
798
|
+
options?: AdapterMethodOptions<T>,
|
|
799
|
+
): Promise<T[]>;
|
|
800
|
+
abstract update(
|
|
801
|
+
where: VSRepoWhere<T>,
|
|
802
|
+
obj: DeepPartial<T>,
|
|
803
|
+
options?: AdapterMethodOptions<T>,
|
|
804
|
+
): Promise<T>;
|
|
805
|
+
abstract updateMany(
|
|
806
|
+
where: VSRepoWhere<T>,
|
|
807
|
+
obj: DeepPartial<T>,
|
|
808
|
+
options?: AdapterMethodOptions<T>,
|
|
809
|
+
): Promise<CountResult>;
|
|
810
|
+
abstract updateManyReturning(
|
|
811
|
+
where: VSRepoWhere<T>,
|
|
812
|
+
obj: DeepPartial<T>,
|
|
813
|
+
options?: AdapterMethodOptions<T>,
|
|
814
|
+
): Promise<T[]>;
|
|
815
|
+
abstract count(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number>;
|
|
816
|
+
abstract exists(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<boolean>;
|
|
817
|
+
abstract merge<K>(
|
|
818
|
+
where: VSRepoWhere<T>,
|
|
819
|
+
obj: DeepPartial<T>,
|
|
820
|
+
options?: AdapterMethodOptions<T>,
|
|
821
|
+
): Promise<K & T>;
|
|
822
|
+
abstract upsert(
|
|
823
|
+
where: VSRepoWhere<T>,
|
|
824
|
+
create: DeepPartial<T>,
|
|
825
|
+
update: DeepPartial<T>,
|
|
826
|
+
options?: AdapterMethodOptions<T>,
|
|
827
|
+
): Promise<T>;
|
|
828
|
+
|
|
829
|
+
abstract incrementOne<K extends NumericKeys<T>>(
|
|
830
|
+
field: K,
|
|
831
|
+
value: NonNullable<T[K]>,
|
|
832
|
+
where: VSRepoWhere<T>,
|
|
833
|
+
options?: AdapterMethodOptions<T>,
|
|
834
|
+
): Promise<T>;
|
|
835
|
+
abstract decrementOne<K extends NumericKeys<T>>(
|
|
836
|
+
field: K,
|
|
837
|
+
value: NonNullable<T[K]>,
|
|
838
|
+
where: VSRepoWhere<T>,
|
|
839
|
+
options?: AdapterMethodOptions<T>,
|
|
840
|
+
): Promise<T>;
|
|
841
|
+
abstract multiplyOne<K extends NumericKeys<T>>(
|
|
842
|
+
field: K,
|
|
843
|
+
value: NonNullable<T[K]>,
|
|
844
|
+
where: VSRepoWhere<T>,
|
|
845
|
+
options?: AdapterMethodOptions<T>,
|
|
846
|
+
): Promise<T>;
|
|
847
|
+
abstract divideOne<K extends NumericKeys<T>>(
|
|
848
|
+
field: K,
|
|
849
|
+
value: NonNullable<T[K]>,
|
|
850
|
+
where: VSRepoWhere<T>,
|
|
851
|
+
options?: AdapterMethodOptions<T>,
|
|
852
|
+
): Promise<T>;
|
|
853
|
+
abstract sum(
|
|
854
|
+
field: NumericKeys<T>,
|
|
855
|
+
where?: VSRepoWhere<T>,
|
|
856
|
+
options?: AdapterMethodOptions<T>,
|
|
857
|
+
): Promise<number | null>;
|
|
858
|
+
abstract average(
|
|
859
|
+
field: NumericKeys<T>,
|
|
860
|
+
where?: VSRepoWhere<T>,
|
|
861
|
+
options?: AdapterMethodOptions<T>,
|
|
862
|
+
): Promise<number | null>;
|
|
863
|
+
abstract min(
|
|
864
|
+
field: NumericKeys<T>,
|
|
865
|
+
where?: VSRepoWhere<T>,
|
|
866
|
+
options?: AdapterMethodOptions<T>,
|
|
867
|
+
): Promise<number | null>;
|
|
868
|
+
abstract max(
|
|
869
|
+
field: NumericKeys<T>,
|
|
870
|
+
where?: VSRepoWhere<T>,
|
|
871
|
+
options?: AdapterMethodOptions<T>,
|
|
872
|
+
): Promise<number | null>;
|
|
873
|
+
}
|
|
874
|
+
```
|
|
1270
875
|
|
|
1271
|
-
|
|
876
|
+
`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.
|
|
1272
877
|
|
|
1273
|
-
|
|
1274
|
-
await userRepository.prisma.$transaction(async (tx) => {
|
|
1275
|
-
const user = await userRepository.save(
|
|
1276
|
-
{ name: "Mary", email: "mary@email.com", password: "password" },
|
|
1277
|
-
{ db: tx }
|
|
1278
|
-
);
|
|
878
|
+
### Logging from your adapter
|
|
1279
879
|
|
|
1280
|
-
|
|
1281
|
-
{ action: "User registration", data: { registeredUser: user.id } },
|
|
1282
|
-
{ db: tx }
|
|
1283
|
-
);
|
|
1284
|
-
});
|
|
1285
|
-
```
|
|
880
|
+
`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:
|
|
1286
881
|
|
|
1287
|
-
|
|
882
|
+
```typescript
|
|
883
|
+
import { VSLogger, VSLogLevel } from "vsrepo";
|
|
1288
884
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
// CORRECT: tx is a DbTransaction
|
|
1292
|
-
const registeredUsers = await userRepository.saveList([{ name: "Mary" }, { name: "Lucas" }], { db: tx });
|
|
885
|
+
export class MyOrmAdapter<T> extends VSRepoAdapter<T> {
|
|
886
|
+
private readonly logger = new VSLogger(VSLogLevel.WARN, "MyOrmAdapterLogger");
|
|
1293
887
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
888
|
+
async findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>) {
|
|
889
|
+
const start = this.logger.startPerformLog("adapter findOne");
|
|
890
|
+
try {
|
|
891
|
+
// ... talk to the ORM ...
|
|
892
|
+
this.logger.endPerformLog(start);
|
|
893
|
+
return result;
|
|
894
|
+
} catch (err) {
|
|
895
|
+
this.logger.endPerformLog(start);
|
|
896
|
+
this.logger.logError("adapter findOne failed", err);
|
|
897
|
+
throw err;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
1299
901
|
```
|
|
1300
902
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
pkName: "id",
|
|
1309
|
-
methods: {
|
|
1310
|
-
findOneByEmailEndsWith: { map: true },
|
|
1311
|
-
},
|
|
1312
|
-
})
|
|
1313
|
-
.build(prisma)
|
|
1314
|
-
.extend((repo) => ({
|
|
1315
|
-
findActiveByDomain: async (domain: string) => {
|
|
1316
|
-
return repo.findOneByEmailEndsWith(`@${domain}`);
|
|
1317
|
-
},
|
|
903
|
+
| Method | Description |
|
|
904
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
905
|
+
| `new VSLogger(logLevel, name, slowThresholdMs?)` | Creates a logger; `name` prefixes every line, `slowThresholdMs` defaults to 300. |
|
|
906
|
+
| `logDebug/logInfo/logWarn(text, obj?)` | Logs at the given level if `logLevel` allows it; `obj` is appended as pretty-printed JSON. |
|
|
907
|
+
| `logError(text, err?)` | Logs at `ERROR`; if `err` is an `Error`, only `name`/`message`/`stack`/`cause` are logged. |
|
|
908
|
+
| `startPerformLog(operation)` / `endPerformLog(data)` | Bracket a block to log its duration, escalating to `WARN` if it exceeds `slowThresholdMs`. |
|
|
909
|
+
| `getLogLevel()` | Returns the logger's configured `VSLogLevel`. |
|
|
1318
910
|
|
|
1319
|
-
|
|
1320
|
-
return repo.patchList(ids.map(id => [id, { active: true }]));
|
|
1321
|
-
},
|
|
1322
|
-
}));
|
|
1323
|
-
```
|
|
911
|
+
This is purely a convenience for adapter authors — nothing in the core requires your adapter to use it.
|
|
1324
912
|
|
|
1325
913
|
---
|
|
1326
914
|
|
|
1327
915
|
## Error handling
|
|
1328
916
|
|
|
1329
|
-
|
|
917
|
+
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.
|
|
1330
918
|
|
|
1331
|
-
```
|
|
1332
|
-
import { VSRepoError
|
|
919
|
+
```typescript
|
|
920
|
+
import { VSRepoError } from "vsrepo";
|
|
1333
921
|
|
|
1334
922
|
try {
|
|
1335
|
-
|
|
923
|
+
await userRepository.get(id);
|
|
1336
924
|
} catch (error) {
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
console.error("Repository error:", error.message);
|
|
1341
|
-
} else {
|
|
1342
|
-
console.error("Error:", error.message)
|
|
1343
|
-
}
|
|
925
|
+
if (error instanceof VSRepoError) {
|
|
926
|
+
console.error(`[${error.type}] ${error.message}`);
|
|
927
|
+
}
|
|
1344
928
|
}
|
|
1345
929
|
```
|
|
1346
930
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
|
1350
|
-
|
|
|
1351
|
-
| `
|
|
1352
|
-
| `
|
|
1353
|
-
| `
|
|
1354
|
-
| `
|
|
1355
|
-
| `VSRepoRuntimeError` | Runtime error during an operation |
|
|
1356
|
-
|
|
1357
|
-
`VSRepoRuntimeError` has a `code: VSRepoRuntimeErrorCode` property for programmatic identification, instead of having to parse the (human-readable, and possibly localized) message:
|
|
1358
|
-
|
|
1359
|
-
| Code | Meaning |
|
|
1360
|
-
| ---- | ------- |
|
|
1361
|
-
| `"65706"` | A required argument is missing or has an invalid shape — e.g. a missing `pk`, a `pks`/`objs`/`tuples` that isn't an array, or an `options`/`obj` that isn't a valid object. |
|
|
1362
|
-
| `"20727"` | No record was found for the provided primary key (`getOrThrow` when fetching the base record). |
|
|
1363
|
-
| `"67542"` | Validation (zod) of a method's `options`, or of a `@QueryMethod` argument, failed. |
|
|
1364
|
-
| `"91868"` | A relation passed to `save`/`patch`/`merge` has an invalid shape for the configured `mode`/`restriction` (e.g. `null` on a `mtm`/`otm` relation, or an array on a `oto`/`mto` relation). |
|
|
1365
|
-
| `"48670"` | A dynamic method (`config.methods`) was called with fewer positional arguments than its `where` fields require. |
|
|
1366
|
-
|
|
1367
|
-
---
|
|
1368
|
-
|
|
1369
|
-
## Utility types
|
|
1370
|
-
|
|
1371
|
-
### Client types
|
|
1372
|
-
|
|
1373
|
-
```ts
|
|
1374
|
-
import type { DbClient, DbTransaction, ClientOrTransaction } from "../../generated/vsrepo";
|
|
1375
|
-
|
|
1376
|
-
type DbClient = PrismaClient;
|
|
1377
|
-
type DbTransaction = Prisma.TransactionClient;
|
|
1378
|
-
type ClientOrTransaction = DbClient | DbTransaction;
|
|
1379
|
-
```
|
|
1380
|
-
|
|
1381
|
-
### Soft-delete visibility type
|
|
1382
|
-
|
|
1383
|
-
```ts
|
|
1384
|
-
import type { SeeMode } from "../../generated/vsrepo";
|
|
1385
|
-
|
|
1386
|
-
type SeeMode = "active" | "removed" | "all";
|
|
1387
|
-
```
|
|
1388
|
-
|
|
1389
|
-
### Types derived from the Prisma model
|
|
1390
|
-
|
|
1391
|
-
```ts
|
|
1392
|
-
import type {
|
|
1393
|
-
SelectModel,
|
|
1394
|
-
SelectModels,
|
|
1395
|
-
IncludeModel,
|
|
1396
|
-
IncludeModels,
|
|
1397
|
-
WhereModel,
|
|
1398
|
-
OrderingModel,
|
|
1399
|
-
PaginationModel,
|
|
1400
|
-
ModelUpsertInput,
|
|
1401
|
-
PrismaModelInputs,
|
|
1402
|
-
} from "../../generated/vsrepo";
|
|
1403
|
-
```
|
|
1404
|
-
|
|
1405
|
-
### Method options types
|
|
1406
|
-
|
|
1407
|
-
```ts
|
|
1408
|
-
import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
|
|
1409
|
-
|
|
1410
|
-
// MethodOptions<S, IM> — options passed into the repository's methods
|
|
1411
|
-
type Opts = MethodOptions<"public" | "minimal", "withPosts">;
|
|
1412
|
-
|
|
1413
|
-
// MethodOptionsModel<TRepo> — derived from a configured VSRepository instance
|
|
1414
|
-
const userVSRepo = setupVSRepo<User, "user">()(config);
|
|
1415
|
-
type OptsModel = MethodOptionsModel<typeof userVSRepo>;
|
|
1416
|
-
```
|
|
1417
|
-
|
|
1418
|
-
> The second parameter of `MethodOptions` (`IM`) represents the valid keys of `includeModels`. When provided, `selectModel` and `includeModel` become mutually exclusive in the type — it's not possible to pass both in the same call.
|
|
1419
|
-
>
|
|
1420
|
-
> `MethodOptions` also accepts two further generic parameters, `RI` and `RS`, for typing raw `include` and raw `select` respectively (both default to `never`, meaning they're not accepted unless explicitly typed): `MethodOptions<"public", "withPosts", "user", IncludeModel<"user">, SelectModel<"user">>`. `MethodOptionsModel`, derived directly from a configured repository, does not expose `RI`/`RS` — use `MethodOptions` directly if you need to type raw `include`/`select` options.
|
|
931
|
+
| `VSRepoErrorType` | Raised when |
|
|
932
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
933
|
+
| `DECORATOR` | Invalid arguments were passed to `@DynamicMethod` or `@QueryMethod`. |
|
|
934
|
+
| `RESOLVER` | The library failed to resolve a dynamic/query method's configuration into a callable method (e.g. an unknown method name). |
|
|
935
|
+
| `DYNAMIC` | A resolved dynamic method failed at runtime (e.g. missing arguments). |
|
|
936
|
+
| `VALIDATOR` | Invalid method options or arguments were detected during validation. |
|
|
937
|
+
| `BASE` | Invalid usage of a base method (`get`, `save`, `remove`, etc). |
|
|
938
|
+
| `ADAPTER` | A `VSRepoAdapter` failed while talking to the underlying ORM/database — always thrown as `VSRepoAdapterError`. |
|
|
1421
939
|
|
|
1422
|
-
###
|
|
940
|
+
### `VSRepoAdapterError` and `AdapterErrorCode`
|
|
1423
941
|
|
|
1424
|
-
|
|
1425
|
-
import type {
|
|
1426
|
-
MethodConfig,
|
|
1427
|
-
RepoConfig,
|
|
1428
|
-
BuildConfig,
|
|
1429
|
-
RepositoryRelations,
|
|
1430
|
-
ExtractRelationConfig,
|
|
1431
|
-
} from "../../generated/vsrepo";
|
|
1432
|
-
```
|
|
942
|
+
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:
|
|
1433
943
|
|
|
1434
|
-
|
|
944
|
+
```typescript
|
|
945
|
+
import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
|
|
1435
946
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
```
|
|
1442
|
-
|
|
1443
|
-
`RepositoryOf` accepts three parameters:
|
|
947
|
+
try {
|
|
948
|
+
await userRepository.save({ name: "Maria" });
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error instanceof VSRepoAdapterError) {
|
|
951
|
+
console.error(`[${error.code}] ${error.message}`, error.originalError);
|
|
1444
952
|
|
|
1445
|
-
|
|
1446
|
-
|
|
953
|
+
if (error.code === AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
|
|
954
|
+
// handle a duplicate key, e.g. return a friendly message
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
1447
958
|
```
|
|
1448
959
|
|
|
1449
|
-
|
|
960
|
+
| Property | Type | Description |
|
|
961
|
+
| --------------- | ------------------ | ----------------------------------------------------------------------------------- |
|
|
962
|
+
| `code` | `AdapterErrorCode` | Stable, adapter-agnostic code classifying the failure. |
|
|
963
|
+
| `originalError` | `unknown` | The raw error (or `null`/`undefined`) thrown by the underlying ORM/database driver. |
|
|
964
|
+
| `message` | `string` | Human-readable description of the adapter failure. |
|
|
965
|
+
| `type` | `VSRepoErrorType` | Always `VSRepoErrorType.ADAPTER`. |
|
|
966
|
+
| `cause` | `unknown` | Optional root cause the error was chained from. |
|
|
967
|
+
|
|
968
|
+
Adapter implementations construct it directly when mapping an ORM failure:
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
|
|
972
|
+
|
|
973
|
+
throw new VSRepoAdapterError(
|
|
974
|
+
"user creation failed",
|
|
975
|
+
AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
|
|
976
|
+
originalError, // raw DB/driver error
|
|
977
|
+
);
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
#### `AdapterErrorCode`
|
|
981
|
+
|
|
982
|
+
`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:
|
|
983
|
+
|
|
984
|
+
```typescript
|
|
985
|
+
import { AdapterErrorCode } from "vsrepo";
|
|
986
|
+
|
|
987
|
+
console.log(AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION); // "UNIQUE_CONSTRAINT_VIOLATION"
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
| Code | Meaning |
|
|
991
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
992
|
+
| `UNKNOWN` | Unclassified/unknown error; the fallback when no more specific code matches. |
|
|
993
|
+
| `MISSING_DB_CLIENT` | Database client (or connection pool) not provided or could not be resolved. |
|
|
994
|
+
| `CONNECTION_FAILED` | Could not reach/connect to the database, or an established connection was lost/terminated. |
|
|
995
|
+
| `CONNECTION_POOL_EXHAUSTED` | Connection pool exhausted/depleted — no connection available, all busy or the limit was reached. |
|
|
996
|
+
| `TIMEOUT` | Database did not respond in time; a query exceeded its allowed timeout. |
|
|
997
|
+
| `UNIQUE_CONSTRAINT_VIOLATION` | Unique constraint (duplicate key) violated. E.g. Postgres/SQLite `23505`, MySQL `1062`. |
|
|
998
|
+
| `FOREIGN_KEY_VIOLATION` | Foreign key constraint violated (referenced row missing). |
|
|
999
|
+
| `NOT_NULL_VIOLATION` | NOT NULL constraint violated. |
|
|
1000
|
+
| `CHECK_VIOLATION` | CHECK constraint violated. |
|
|
1001
|
+
| `CONSTRAINT_VIOLATION` | General integrity/constraint violation not covered by a more specific code. |
|
|
1002
|
+
| `NOT_FOUND` | Requested record not found (e.g. a `findOneOrThrow`-style operation). |
|
|
1003
|
+
| `INVALID_DATA` | Field value invalid for its type/length, or a required value is missing. |
|
|
1004
|
+
| `VALUE_TOO_LONG` | Provided value exceeds the column/field length limit. |
|
|
1005
|
+
| `CONVERSION_ERROR` | Value could not be converted/cast to the target type. E.g. Postgres `22P02`, MySQL `1366`. |
|
|
1006
|
+
| `INVALID_QUERY` | SQL query/stored procedure is malformed or invalid. |
|
|
1007
|
+
| `TABLE_OR_COLUMN_NOT_FOUND` | Referenced table/column/relation does not exist. |
|
|
1008
|
+
| `DEADLOCK` | Operation aborted by a lock timeout or deadlock between concurrent transactions. |
|
|
1009
|
+
| `LOCK_TIMEOUT` | Could not acquire a required database lock in time. |
|
|
1010
|
+
| `LOCKED` | Record is locked and cannot be modified. |
|
|
1011
|
+
| `ACCESS_DENIED` | Current user/role does not have permission for the operation. |
|
|
1012
|
+
| `INVALID_CREDENTIALS` | Invalid connection credentials (host/user/password). |
|
|
1013
|
+
| `ROW_NOT_ALLOWED` | Authenticated user does not own the record / row-level security rejected it. |
|
|
1014
|
+
| `MODEL_NOT_FOUND` | Entity/model or table not defined/mapped in the ORM, or the adapter lacks model metadata to build the query. |
|
|
1015
|
+
| `FIELD_NOT_FOUND` | Field/column name in the data or `where` does not exist on the entity/model. |
|
|
1016
|
+
| `TRANSACTION_CLOSED` | Transaction used after it was committed/rolled back. |
|
|
1017
|
+
| `TRANSACTION_ALREADY_STARTED` | A nested transaction could not be opened (e.g. nested `transaction()` calls). |
|
|
1018
|
+
| `TRANSACTION_CONFLICT` | Transaction failed to commit and was rolled back. |
|
|
1019
|
+
| `TRANSACTION_NOT_STARTED` | No active transaction when one was required. |
|
|
1020
|
+
| `CONNECTION_CLOSED` | Connection closed/terminated while a transaction or query was in progress. |
|
|
1021
|
+
| `INVALID_PARTIAL` | `merge`/`upsert`/`update` received a partial object that is invalid or missing required keys. |
|
|
1022
|
+
| `NOT_SUPPORTED` | Unsupported feature/operation requested from the adapter (e.g. raw `query()` not supported). |
|
|
1023
|
+
| `INVALID_ADAPTER_CONFIG` | Adapter configuration invalid or incomplete (missing required options, or options with an invalid type/value). |
|
|
1024
|
+
| `INTERNAL` | Internal adapter bug or unrecoverable state; should rarely be used — prefer a more specific code. |
|
|
1025
|
+
|
|
1026
|
+
#### `VSRepoError` vs. raw ORM errors
|
|
1027
|
+
|
|
1028
|
+
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.
|
|
1450
1029
|
|
|
1451
|
-
|
|
1452
|
-
import type { SaveObject, PatchObject } from "../../generated/vsrepo";
|
|
1030
|
+
---
|
|
1453
1031
|
|
|
1454
|
-
|
|
1455
|
-
tableName: "user",
|
|
1456
|
-
pkName: "id",
|
|
1457
|
-
relations: {
|
|
1458
|
-
profile: { pk: "id", mode: "oto", restriction: "set" },
|
|
1459
|
-
},
|
|
1460
|
-
});
|
|
1032
|
+
## Logging
|
|
1461
1033
|
|
|
1462
|
-
|
|
1463
|
-
type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
|
|
1464
|
-
```
|
|
1034
|
+
Every repository has an internal logger, configured via `logLevel` and `logSlowThresholdMs` on the constructor options:
|
|
1465
1035
|
|
|
1466
|
-
|
|
1036
|
+
```typescript
|
|
1037
|
+
import { VSLogLevel } from "vsrepo";
|
|
1467
1038
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
setupVSRepo<TPayload, TTableName>()({
|
|
1474
|
-
tableName: Uncapitalize<M>; // Table name in Prisma
|
|
1475
|
-
pkName: keyof T; // Primary key name
|
|
1476
|
-
softRemovekName?: keyof T & string; // DateTime field for soft-delete
|
|
1477
|
-
selectModels?: SelectModels<M>; // Named data projections (select)
|
|
1478
|
-
defaultSelectModel?: keyof SM; // Select applied by default
|
|
1479
|
-
includeModels?: IncludeModels<M>; // Named data projections (include) — no default, only in the call
|
|
1480
|
-
requiredWhere?: WhereModel<M>; // Always-applied filters
|
|
1481
|
-
defaultOrdering?: OrderingModel<M>; // Default ordering for queries without Ordered/injectOrdering
|
|
1482
|
-
relations?: RepositoryRelations<T>; // Relation configuration
|
|
1483
|
-
methods?: Record<string, MethodConfig<M, SM>>; // Dynamic methods
|
|
1039
|
+
super({
|
|
1040
|
+
pkName: "id",
|
|
1041
|
+
adapter,
|
|
1042
|
+
logLevel: VSLogLevel.DEBUG,
|
|
1043
|
+
logSlowThresholdMs: 200,
|
|
1484
1044
|
});
|
|
1485
1045
|
```
|
|
1486
1046
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
baseMethods?: {
|
|
1494
|
-
// Methods that can use a defaultSelect
|
|
1495
|
-
get?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1496
|
-
getOrThrow?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1497
|
-
getList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1498
|
-
remove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1499
|
-
save?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1500
|
-
saveList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1501
|
-
patch?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1502
|
-
patchList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1503
|
-
merge?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1504
|
-
getAll?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1505
|
-
softRemove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1506
|
-
restore?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1507
|
-
|
|
1508
|
-
// Methods that do NOT accept defaultSelect
|
|
1509
|
-
removeList?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
1510
|
-
softRemoveList?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
1511
|
-
restoreList?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
1512
|
-
total?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
1513
|
-
has?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
1514
|
-
};
|
|
1515
|
-
});
|
|
1516
|
-
```
|
|
1047
|
+
| Level | Meaning |
|
|
1048
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------- |
|
|
1049
|
+
| `DEBUG` | Verbose internal details, including every resolved query — very useful for debugging dynamic methods. |
|
|
1050
|
+
| `INFO` | High-level lifecycle events, such as repository initialization. |
|
|
1051
|
+
| `WARN` (default) | Recoverable issues and slow operations (see `logSlowThresholdMs`, defaults to 300ms). |
|
|
1052
|
+
| `ERROR` | Failures raised while executing an operation. |
|
|
1517
1053
|
|
|
1518
|
-
|
|
1054
|
+
---
|
|
1519
1055
|
|
|
1520
|
-
|
|
1521
|
-
repo.extend((repo) => ({
|
|
1522
|
-
myMethod: () => { ... }
|
|
1523
|
-
}));
|
|
1524
|
-
```
|
|
1056
|
+
## Development
|
|
1525
1057
|
|
|
1526
|
-
|
|
1058
|
+
The v2 core is built and packed from this branch as a standard npm package:
|
|
1527
1059
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
```text
|
|
1533
|
-
examples/
|
|
1534
|
-
├── prisma.ts # PrismaClient instance used by the examples
|
|
1535
|
-
├── repositories.ts # Repository configuration (User, Address, Product) with setupVSRepo
|
|
1536
|
-
└── tests/
|
|
1537
|
-
├── base-methods.test.ts # Base methods: get, save, patch, remove, getAll, total, has...
|
|
1538
|
-
├── relations.test.ts # How to configure and use relations in save/patch and in filters
|
|
1539
|
-
├── required-where.test.ts # How requiredWhere is automatically applied to queries
|
|
1540
|
-
├── dynamic-methods.test.ts # Prefixes, field filters, logical operators, and pagination/ordering
|
|
1541
|
-
├── transactions.test.ts # Transactions with options.db and instance access via repository.prisma
|
|
1542
|
-
├── soft-delete.test.ts # Soft-delete: softRemove, softRemoveList, restore, restoreList and SeeMode
|
|
1543
|
-
└── batch-methods.test.ts # Batch operations: getList, saveList, patchList and merge
|
|
1544
|
-
```
|
|
1060
|
+
```bash
|
|
1061
|
+
# 1. Install dependencies
|
|
1062
|
+
pnpm install
|
|
1545
1063
|
|
|
1546
|
-
|
|
1064
|
+
# 2. Compile the TypeScript sources into dist/ (removes a previous dist/ first)
|
|
1065
|
+
pnpm build
|
|
1547
1066
|
|
|
1548
|
-
|
|
1067
|
+
# 3. (Optional) Inspect what would be published without writing a tarball
|
|
1068
|
+
npm pack --dry-run
|
|
1549
1069
|
|
|
1550
|
-
|
|
1070
|
+
# 4. Produce the installable tarball (runs `prepack` -> `pnpm build` automatically)
|
|
1071
|
+
npm pack
|
|
1551
1072
|
|
|
1552
|
-
|
|
1073
|
+
# 5. Consume it locally in another project
|
|
1074
|
+
npm install ../path/to/vsrepo-1.4.0.tgz
|
|
1075
|
+
```
|
|
1553
1076
|
|
|
1554
|
-
|
|
1555
|
-
2. Create a new branch with your change: `git checkout -b fixing-bug`.
|
|
1556
|
-
3. Push to your branch: `git push origin fixing-bug`.
|
|
1557
|
-
4. Open a **Pull Request**.
|
|
1077
|
+
Notes:
|
|
1558
1078
|
|
|
1559
|
-
|
|
1079
|
+
- `pnpm build` runs `tsc -p tsconfig.build.json`, which outputs the compiled JS and generated type declarations into `dist/` with `rootDir: src`.
|
|
1080
|
+
- The published package contains **only** the `dist/` folder plus the READMEs and `LICENSE` (see `files` in `package.json`). The adapters will live in their own `@vsrepo/*-adapter` packages.
|
|
1081
|
+
- The core is ORM-agnostic and has no `@prisma/client` peer dependency.
|
|
1560
1082
|
|
|
1561
1083
|
---
|
|
1562
1084
|
|
|
1563
1085
|
## Requirements
|
|
1564
1086
|
|
|
1565
|
-
- Node.js 18+
|
|
1566
|
-
-
|
|
1567
|
-
- TypeScript (optional, but strongly recommended)
|
|
1568
|
-
- `"moduleResolution": "bundler"` or `"nodenext"` in tsconfig
|
|
1569
|
-
|
|
1570
|
-
Recommended `tsconfig.json`:
|
|
1087
|
+
- Node.js 18+
|
|
1088
|
+
- TypeScript, with **legacy/experimental decorators** enabled (required by `@DynamicMethod`/`@QueryMethod`):
|
|
1571
1089
|
|
|
1572
1090
|
```json
|
|
1573
1091
|
{
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
"moduleResolution": "NodeNext",
|
|
1578
|
-
"strict": true,
|
|
1579
|
-
"skipLibCheck": true,
|
|
1580
|
-
"lib": ["ES2020"]
|
|
1581
|
-
}
|
|
1092
|
+
"compilerOptions": {
|
|
1093
|
+
"experimentalDecorators": true
|
|
1094
|
+
}
|
|
1582
1095
|
}
|
|
1583
1096
|
```
|
|
1584
1097
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
## Troubleshooting
|
|
1588
|
-
|
|
1589
|
-
**Generic types not inferred** — Check that `strict: true` and `moduleResolution: "bundler"` or `"nodenext"` are set in `tsconfig.json`.
|
|
1590
|
-
|
|
1591
|
-
**Dynamic method doesn't exist at runtime** — The field referenced in the method name must exist in the Prisma model. E.g.: `findByEmail` requires the model to have an `email` field.
|
|
1592
|
-
|
|
1593
|
-
**`proxyTo` required** — Names outside the standard patterns (e.g. `searchByEmail`) aren't parsed directly. Use `proxyTo: "findByEmail"` in these cases.
|
|
1098
|
+
- `reflect-metadata` (bundled as a dependency, imported internally — you don't need to import it yourself)
|
|
1099
|
+
- 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
|
|
1594
1100
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
**`selectModel`, `includeModel` and `include` together in the same call** — Not allowed. Only one of the three can be provided per call: if `includeModel` or `include` is provided, the `select` (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
|
|
1598
|
-
|
|
1599
|
-
**`includeModel` doesn't appear as a default repository option** — This is expected. Unlike `defaultSelectModel`, there's no `defaultIncludeModel`/`defaultInclude`. An `includeModel` can only be set in the method call, via `options.includeModel`. A raw, ad hoc include can be set via `options.include`, without needing to be registered in `includeModels`.
|
|
1101
|
+
---
|
|
1600
1102
|
|
|
1601
|
-
|
|
1103
|
+
## Contributing
|
|
1602
1104
|
|
|
1603
|
-
|
|
1105
|
+
Contributions are welcome, especially towards finishing the Prisma and TypeORM adapters! (**[GitHub repository](https://github.com/jaobrabo123/VSRepository)**):
|
|
1604
1106
|
|
|
1605
|
-
|
|
1107
|
+
1. **Fork** the project.
|
|
1108
|
+
2. Create a branch off `v2` for your change: `git checkout -b v2-my-change`.
|
|
1109
|
+
3. Push your branch: `git push origin v2-my-change`.
|
|
1110
|
+
4. Open a **Pull Request** against `v2`.
|
|
1606
1111
|
|
|
1607
|
-
|
|
1112
|
+
To report issues or suggest features, open an **Issue**.
|