vsrepo 1.4.1 → 2.0.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 +723 -1338
- package/README.pt-BR.md +729 -1341
- package/dist/VSRepoAdapter.d.ts +71 -0
- package/dist/VSRepoAdapter.js +18 -0
- package/dist/VSRepository.d.ts +135 -1201
- package/dist/VSRepository.js +273 -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 +34 -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 +33 -0
- package/dist/internal/validators/vsrepo.validator.js +160 -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/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/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 +6 -0
- package/dist/types/utils/query-method-arg.type.d.ts +27 -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-options.type.d.ts +34 -0
- package/dist/types/vsrepo/vsrepo-orm-types.type.d.ts +17 -0
- package/dist/types/vsrepo/vsrepo-pretty-where.type.d.ts +7 -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 -536
- 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/deep-partial.type.js} +0 -0
- /package/dist/{internal/resolvers/types/prisma-args.type.js → types/utils/keys-of-type.type.js} +0 -0
- /package/dist/{internal/resolvers/types/repository-build-instance.type.js → types/utils/methods-options.type.js} +0 -0
- /package/dist/{internal/resolvers/types/resolve-db-and-prisma-args-data.type.js → types/utils/ordering.type.js} +0 -0
- /package/dist/{internal/validation/types → types/utils}/pagination.type.js +0 -0
- /package/dist/{internal/resolvers/types/ugly-where.type.js → types/utils/perform-data.type.js} +0 -0
- /package/dist/{internal/validation/types/base-methods.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 → types/utils}/see-mode.type.js +0 -0
- /package/dist/{internal/validation/types/build-config.type.js → types/vsrepo/vsrepo-args.type.js} +0 -0
- /package/dist/{internal/validation/types/constructor-config.type.js → types/vsrepo/vsrepo-method.type.js} +0 -0
- /package/dist/{internal/validation/types/method-options.type.js → types/vsrepo/vsrepo-options.type.js} +0 -0
- /package/dist/{internal/validation/types/method.type.js → types/vsrepo/vsrepo-orm-types.type.js} +0 -0
- /package/dist/{internal/validation/types/relation.type.js → types/vsrepo/vsrepo-pretty-where.type.js} +0 -0
package/README.md
CHANGED
|
@@ -9,139 +9,124 @@
|
|
|
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](./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
|
-
- [Merge](#merge)
|
|
43
|
-
- [Configuring the base methods](#configuring-the-base-methods)
|
|
44
|
-
- [Select Models](#select-models)
|
|
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
|
+
- [`select` and `relations`](#select-and-relations)
|
|
51
43
|
- [Dynamic methods](#dynamic-methods)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- [Query Methods](#query-methods)
|
|
61
|
-
- [Relations in save](#relations-in-save)
|
|
44
|
+
- [Available prefixes](#available-prefixes)
|
|
45
|
+
- [Field filters](#field-filters)
|
|
46
|
+
- [Logical operators](#logical-operators)
|
|
47
|
+
- [Relation filters](#relation-filters)
|
|
48
|
+
- [Ordering, pagination and distinct](#ordering-pagination-and-distinct)
|
|
49
|
+
- [Decorator options](#decorator-options)
|
|
50
|
+
- [Query methods (raw SQL)](#query-methods-raw-sql)
|
|
51
|
+
- [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query)
|
|
62
52
|
- [Transactions](#transactions)
|
|
63
|
-
- [Extending a repository](#extending-a-repository)
|
|
64
|
-
- [Error handling](#error-handling)
|
|
65
53
|
- [Utility types](#utility-types)
|
|
66
|
-
- [
|
|
67
|
-
- [
|
|
68
|
-
- [
|
|
54
|
+
- [Writing your own adapter](#writing-your-own-adapter)
|
|
55
|
+
- [Error handling](#error-handling)
|
|
56
|
+
- [`VSRepoAdapterError` and `AdapterErrorCode`](#vsrepoadaptererror-and-adaptererrorcode)
|
|
57
|
+
- [Logging](#logging)
|
|
58
|
+
- [Development](#development)
|
|
69
59
|
- [Requirements](#requirements)
|
|
70
|
-
- [
|
|
60
|
+
- [Contributing](#contributing)
|
|
71
61
|
|
|
72
62
|
---
|
|
73
63
|
|
|
74
|
-
##
|
|
64
|
+
## What changed from v1
|
|
75
65
|
|
|
76
|
-
|
|
77
|
-
npm i vsrepo @prisma/client
|
|
78
|
-
```
|
|
66
|
+
If you're coming from the [v1](./v1) code/docs, here's the short version. See each linked section for details.
|
|
79
67
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
68
|
+
| Area | v1 | v2 |
|
|
69
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
70
|
+
| 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 |
|
|
71
|
+
| Defining a repository | Functional `setupVSRepo<T, M>()({...}).build(prisma)`, **or** a `DynamicRepository` class | A single **class-based** API: `extends VSRepository<Entity, PKType, OrmTypes>` |
|
|
72
|
+
| Dynamic methods | `methods: { findByEmail: { map: true } }` config object | `@DynamicMethod()` decorator on a `declare` field |
|
|
73
|
+
| Data projections | Named, reusable `selectModels` + `defaultSelectModel` | Ad-hoc `select`/`relations` passed per call (no named models) |
|
|
74
|
+
| Eager loading | `include`/`includeModels` (Prisma-specific) | ORM-agnostic `relations` option |
|
|
75
|
+
| Global filters | `requiredWhere` (any arbitrary filter, always applied) | **Removed**; Now it only accepts `softRemoveKey` + `see: "active" \| "removed" \| "all"` |
|
|
76
|
+
| Case-insensitive filter suffix | `Insensitive` | `IgnoreCase` |
|
|
77
|
+
| 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 |
|
|
78
|
+
| Duplicate handling on `createMany` | `SkipDuplicates` suffix | `IgnoreConflicts` suffix |
|
|
79
|
+
| `aggregate` / `groupBy` | Supported (Prisma-native passthrough) | **Not implemented yet** |
|
|
80
|
+
| 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 |
|
|
81
|
+
| Debug logging | `showWorking: true` boolean | `logLevel: VSLogLevel` (`DEBUG`/`INFO`/`WARN`/`ERROR`) + `logSlowThresholdMs` for slow-query warnings |
|
|
82
|
+
| `vsrepo generate` CLI (type generation step) | Required before use | Not part of the v2 core — types come directly from your entity/ORM types |
|
|
83
|
+
| 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
84
|
|
|
86
85
|
---
|
|
87
86
|
|
|
88
|
-
##
|
|
89
|
-
|
|
90
|
-
VSRepository needs to know the real path of your Prisma Client to generate the typings correctly.
|
|
87
|
+
## Adapter status
|
|
91
88
|
|
|
92
|
-
|
|
93
|
-
npx vsrepo generate
|
|
94
|
-
```
|
|
89
|
+
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:
|
|
95
90
|
|
|
96
|
-
|
|
91
|
+
- `@vsrepo/prisma7-adapter`
|
|
92
|
+
- `@vsrepo/prisma8-adapter`
|
|
93
|
+
- `@vsrepo/typeorm-adapter`
|
|
94
|
+
- `@vsrepo/drizzle-adapter`
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
npx vsrepo generate \
|
|
100
|
-
--output generated/vsrepo \
|
|
101
|
-
--prisma generated/prisma
|
|
102
|
-
```
|
|
96
|
+
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.
|
|
103
97
|
|
|
104
|
-
|
|
98
|
+
| Adapter | Status |
|
|
99
|
+
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Released** — published to npm, implements the entire `VSRepoAdapter` contract (CRUD, relations, transactions, `merge`, logging) with tests; see [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) for source and docs. |
|
|
101
|
+
| 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. |
|
|
102
|
+
| 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. |
|
|
103
|
+
| 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. |
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
| ---------- | ----- | -------------------- |
|
|
108
|
-
| `--output` | `-o` | `generated/vsrepo` |
|
|
109
|
-
| `--prisma` | `-p` | `generated/prisma` |
|
|
105
|
+
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.
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
```text
|
|
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
|
-
```
|
|
107
|
+
---
|
|
123
108
|
|
|
124
|
-
|
|
109
|
+
## Installation
|
|
125
110
|
|
|
126
|
-
|
|
127
|
-
// CORRECT ✅
|
|
128
|
-
import { setupVSRepo } from "../../generated/vsrepo";
|
|
111
|
+
v2 is installed as the core package plus one adapter package for your ORM, for example:
|
|
129
112
|
|
|
130
|
-
|
|
131
|
-
|
|
113
|
+
```bash
|
|
114
|
+
npm i vsrepo @vsrepo/prisma7-adapter
|
|
132
115
|
```
|
|
133
116
|
|
|
117
|
+
> `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)).
|
|
118
|
+
|
|
134
119
|
---
|
|
135
120
|
|
|
136
121
|
## Basic usage
|
|
137
122
|
|
|
138
|
-
###
|
|
123
|
+
### Implementing/choosing an adapter
|
|
139
124
|
|
|
140
|
-
```
|
|
125
|
+
```typescript
|
|
141
126
|
// src/configs/db.ts
|
|
142
|
-
import { PrismaClient } from
|
|
143
|
-
import { PrismaPg } from
|
|
144
|
-
import
|
|
127
|
+
import { PrismaClient } from "../../generated/prisma/client";
|
|
128
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
129
|
+
import "dotenv/config";
|
|
145
130
|
|
|
146
131
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
|
147
132
|
const prisma = new PrismaClient({ adapter });
|
|
@@ -151,1457 +136,857 @@ export default prisma;
|
|
|
151
136
|
|
|
152
137
|
### Creating a repository
|
|
153
138
|
|
|
154
|
-
```
|
|
155
|
-
// src/repositories/
|
|
139
|
+
```typescript
|
|
140
|
+
// src/repositories/user.repository.ts
|
|
141
|
+
import { VSRepository, DynamicMethod } from "vsrepo";
|
|
142
|
+
import { VSRepoPrisma7Adapter } from "@vsrepo/prisma7-adapter";
|
|
156
143
|
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
|
-
```
|
|
171
|
-
|
|
172
|
-
### Using the repository
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
import userRepository from "./repositories/userRepository";
|
|
176
|
-
|
|
177
|
-
const user = await userRepository.save({
|
|
178
|
-
name: "John",
|
|
179
|
-
email: "john@email.com",
|
|
180
|
-
password: "password",
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const found = await userRepository.get(user.id);
|
|
184
|
-
const all = await userRepository.getAll();
|
|
185
|
-
|
|
186
|
-
user.name = "John Smith";
|
|
187
|
-
|
|
188
|
-
await userRepository.save(user);
|
|
189
|
-
await userRepository.remove(user.id);
|
|
190
|
-
```
|
|
191
|
-
|
|
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
|
-
---
|
|
144
|
+
import type { UserGetPayload } from "../../generated/prisma/models";
|
|
201
145
|
|
|
202
|
-
|
|
146
|
+
type User = UserGetPayload<{ include: { address: true } }>;
|
|
203
147
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
import { PrismaService } from "../../database/prisma.service";
|
|
212
|
-
import { UserGetPayload } from "../../../generated/prisma/models";
|
|
213
|
-
import { setupVSRepo } from "../../../generated/vsrepo";
|
|
214
|
-
|
|
215
|
-
const userVSRepo = setupVSRepo<
|
|
216
|
-
UserGetPayload<{ include: { profile: true } }>,
|
|
217
|
-
"User"
|
|
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
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const setupUserRepository = (prisma: PrismaService) => {
|
|
258
|
-
return userVSRepo.build(prisma);
|
|
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
|
-
```
|
|
279
|
-
|
|
280
|
-
### Registering the provider in the module
|
|
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 {}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Using the repository in a service
|
|
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
|
-
}
|
|
319
|
-
|
|
320
|
-
async createUser(data: { email: string; password: string; name: string }) {
|
|
321
|
-
return this.userRepository.save({
|
|
322
|
-
email: data.email,
|
|
323
|
-
password: data.password,
|
|
324
|
-
name: data.name,
|
|
148
|
+
class UserRepository extends VSRepository<User, string> {
|
|
149
|
+
constructor() {
|
|
150
|
+
super({
|
|
151
|
+
pkName: "id",
|
|
152
|
+
adapter: new VSRepoPrisma7Adapter<User>(prisma, { tableName: "user", pkName: "id" }),
|
|
153
|
+
softRemoveKey: "deletedAt",
|
|
154
|
+
defaultOrdering: { createdAt: "desc" },
|
|
325
155
|
});
|
|
326
156
|
}
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
157
|
|
|
330
|
-
|
|
158
|
+
@DynamicMethod()
|
|
159
|
+
declare findByEmail: (email: string) => Promise<User[]>;
|
|
331
160
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
- ✅ Repository reuse across multiple services
|
|
336
|
-
- ✅ Transaction support via `PrismaService`
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
## Base methods
|
|
341
|
-
|
|
342
|
-
When calling `.build(prisma)`, the base methods below are automatically made available:
|
|
343
|
-
|
|
344
|
-
| Method | Description |
|
|
345
|
-
| ------------------------ | -------------------------------------------------------------------------------------------------------------|
|
|
346
|
-
| `get(pk)` | Fetches a record by its primary key |
|
|
347
|
-
| `getOrThrow(pk)` | Fetches a record by its primary key; throws `VSRepoRuntimeError` (code `"20727"`) if not found |
|
|
348
|
-
| `getList(pks)` | Fetches multiple records from a list of primary keys |
|
|
349
|
-
| `save(obj)` | Creates or updates — if the object has a `pk` it performs an `upsert`, otherwise a `create` |
|
|
350
|
-
| `saveList(objs)` | Saves an array of objects in a single automatic transaction |
|
|
351
|
-
| `patch(pk, obj)` | Partially updates a record by its primary key |
|
|
352
|
-
| `patchList(tuples)` | Partially updates multiple records via an array of `[pk, obj]` tuples in an automatic transaction |
|
|
353
|
-
| `merge(pk, obj)` | Fetches a record and deep merges it in memory — **does not persist**, returns the merged object |
|
|
354
|
-
| `remove(pk)` | Removes a record by its primary key |
|
|
355
|
-
| `removeList(pks)` | Removes several records by a list of primary keys — returns `{ count }` |
|
|
356
|
-
| `getAll()` | Returns all records (accepts `pagination` and `order` in `options`) |
|
|
357
|
-
| `total()` | Returns the total number of records |
|
|
358
|
-
| `has(pk)` | Checks whether a record exists by its primary key — returns `boolean` |
|
|
359
|
-
|
|
360
|
-
All of them accept `options` as the last argument.
|
|
361
|
-
|
|
362
|
-
### Soft-delete
|
|
363
|
-
|
|
364
|
-
When `softRemovekName` is configured on the repository, the following additional methods become available:
|
|
365
|
-
|
|
366
|
-
| Method | Description |
|
|
367
|
-
| -------------------------- | ---------------------------------------------------------------------------------- |
|
|
368
|
-
| `softRemove(pk)` | Marks a record as removed by filling `softRemovekName` with the current date |
|
|
369
|
-
| `softRemoveList(pks)` | Marks multiple records as removed in batch — returns `{ count }` |
|
|
370
|
-
| `restore(pk)` | Restores a soft-deleted record, clearing the `softRemovekName` field |
|
|
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
|
-
]);
|
|
161
|
+
@DynamicMethod()
|
|
162
|
+
declare findOneByEmail: (email: string) => Promise<User | null>;
|
|
163
|
+
}
|
|
396
164
|
|
|
397
|
-
|
|
398
|
-
const updated = await userRepository.patchList([
|
|
399
|
-
[1, { active: false }],
|
|
400
|
-
[2, { name: "New Name" }],
|
|
401
|
-
]);
|
|
165
|
+
export default new UserRepository();
|
|
402
166
|
```
|
|
403
167
|
|
|
404
|
-
|
|
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
|
-
```
|
|
168
|
+
> 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.
|
|
412
169
|
|
|
413
|
-
|
|
170
|
+
> **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`:
|
|
171
|
+
> ```typescript
|
|
172
|
+
> type PrismaOrmTypes = { dbClient: PrismaClient; dbTransaction: Prisma.TransactionClient };
|
|
173
|
+
>
|
|
174
|
+
> class UserRepository extends VSRepository<User, string, PrismaOrmTypes> {
|
|
175
|
+
> // getDbClient() now returns PrismaClient, and transaction(fn) types `tx` as Prisma.TransactionClient
|
|
176
|
+
> }
|
|
177
|
+
> ```
|
|
178
|
+
> If omitted, it defaults to `VSRepoOrmTypes` (`dbClient`/`dbTransaction` both `any`).
|
|
414
179
|
|
|
415
|
-
|
|
180
|
+
### Using the repository
|
|
416
181
|
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
// existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
|
|
182
|
+
```typescript
|
|
183
|
+
import userRepository from "./repositories/user.repository";
|
|
420
184
|
|
|
421
|
-
const
|
|
422
|
-
|
|
185
|
+
const user = await userRepository.save({
|
|
186
|
+
name: "Joao",
|
|
187
|
+
email: "joao@email.com",
|
|
188
|
+
password: "password",
|
|
423
189
|
});
|
|
424
|
-
// merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
|
|
425
190
|
|
|
426
|
-
|
|
427
|
-
await userRepository.
|
|
428
|
-
|
|
191
|
+
const found = await userRepository.get(user.id);
|
|
192
|
+
const all = await userRepository.getAll();
|
|
193
|
+
const byEmail = await userRepository.findByEmail("joao@email.com");
|
|
429
194
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
**Merging to-many relations (`otm`/`mtm`) is done by PK, not by simple concatenation.** For to-one relations (`oto`/`mto`), `merge` performs a regular deep merge of the object. For to-many relations, each item in the sent array is matched against the existing item that has the same PK (defined in `relations[key].pk`): if the PK matches, the two objects are merged together; if it doesn't match (a new item with no counterpart), it's simply added to the list. Existing items that don't appear in the sent array are kept.
|
|
433
|
-
|
|
434
|
-
```ts
|
|
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
|
-
],
|
|
449
|
-
});
|
|
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
|
-
// }
|
|
195
|
+
await userRepository.patch(user.id, { name: "Joao Pedro" });
|
|
196
|
+
await userRepository.remove(user.id);
|
|
458
197
|
```
|
|
459
198
|
|
|
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`.
|
|
465
|
-
|
|
466
|
-
```ts
|
|
467
|
-
userVSRepo.build(prisma, {
|
|
468
|
-
// Shows VSRepository's internal logs on the console (built queries, detected prefix,
|
|
469
|
-
// applied filters, etc). Great for debugging dynamic methods. Default = false.
|
|
470
|
-
showWorking: true,
|
|
199
|
+
---
|
|
471
200
|
|
|
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,
|
|
201
|
+
## Constructor options
|
|
477
202
|
|
|
478
|
-
|
|
479
|
-
// Overrides the `defaultSelectModel` from setupVSRepo for this method only.
|
|
480
|
-
defaultSelect: "public",
|
|
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
|
-
});
|
|
511
|
-
```
|
|
203
|
+
`VSRepoOptions<T, K>`, passed to `super(...)` inside your repository's constructor:
|
|
512
204
|
|
|
513
|
-
|
|
205
|
+
| Option | Type | Description |
|
|
206
|
+
| -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
|
207
|
+
| `adapter` | `VSRepoAdapter<T>` | **Required.** The adapter instance that translates repository calls into calls against the underlying ORM/database. |
|
|
208
|
+
| `pkName` | `keyof T` | **Required.** Name of the field that represents the entity's primary key. |
|
|
209
|
+
| `softRemoveKey` | `keyof T` | Optional. When set, enables `softRemove`, `softRemoveList`, `restore` and `restoreList`. |
|
|
210
|
+
| `defaultOrdering` | `Ordering<T>` | Optional. Default ordering applied automatically to queries that accept `order`, unless overridden per call. |
|
|
211
|
+
| `logLevel` | `VSLogLevel` | Optional. Minimum severity printed by the internal logger. Defaults to `VSLogLevel.WARN`. |
|
|
212
|
+
| `logSlowThresholdMs` | `number` | Optional. Duration (ms) above which a finished operation is logged as `WARN` instead of `DEBUG`. Defaults to 300ms. |
|
|
514
213
|
|
|
515
214
|
---
|
|
516
215
|
|
|
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`.
|
|
216
|
+
## Base methods
|
|
531
217
|
|
|
532
|
-
|
|
218
|
+
Available automatically on every `VSRepository` subclass:
|
|
533
219
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
220
|
+
| Method | Description |
|
|
221
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
222
|
+
| `get(pk, options?)` | Fetches a record by primary key. |
|
|
223
|
+
| `getOrThrow(pk, options?)` | Fetches a record by primary key, throwing if not found. |
|
|
224
|
+
| `getList(pks, options?)` | Fetches multiple records by a list of primary keys. |
|
|
225
|
+
| `getAll(options?)` | Fetches all records; accepts `pagination` and `order` in `options`. |
|
|
226
|
+
| `save(obj, options?)` | Creates or updates (upsert) a single record. |
|
|
227
|
+
| `saveList(objs, options?)` | Creates or updates (upsert) multiple records in one call. |
|
|
228
|
+
| `patch(pk, obj, options?)` | Partially updates a record by primary key. |
|
|
229
|
+
| `merge(pk, obj, options?)` | Fetches a record and returns it deep-merged, in memory, with the given object — does **not** persist anything. |
|
|
230
|
+
| `remove(pk, options?)` | Deletes a record by primary key. |
|
|
231
|
+
| `removeList(pks, options?)` | Deletes multiple records by primary key, returning `{ count }`. |
|
|
232
|
+
| `total(options?)` | Returns the total number of records. |
|
|
233
|
+
| `has(pk, options?)` | Checks whether a record exists, returning `boolean`. |
|
|
234
|
+
| `transaction(fn, options?)` | Runs `fn` inside a native transaction of the underlying ORM. |
|
|
235
|
+
| `getDbClient()` | Returns the underlying ORM client instance used outside of transactions. |
|
|
236
|
+
| `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). |
|
|
537
237
|
|
|
538
|
-
|
|
238
|
+
All of the above (except `transaction`, `query`, and `getDbClient`, which accept their own options or none at all) accept a `MethodOptions<Entity, OrmTypes>` object as their last argument (`select`, `relations`, `see`, `db`).
|
|
539
239
|
|
|
540
|
-
|
|
541
|
-
const fullUser = await userRepository.get(id, { selectModel: false });
|
|
542
|
-
```
|
|
240
|
+
---
|
|
543
241
|
|
|
544
|
-
|
|
242
|
+
## Soft-delete
|
|
545
243
|
|
|
546
|
-
|
|
244
|
+
Soft-delete is now a **first-class, built-in concept**. Configure `softRemoveKey` once on the repository:
|
|
547
245
|
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
|
|
246
|
+
```typescript
|
|
247
|
+
super({
|
|
248
|
+
pkName: "id",
|
|
249
|
+
adapter,
|
|
250
|
+
softRemoveKey: "deletedAt",
|
|
551
251
|
});
|
|
552
252
|
```
|
|
553
253
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
**Rules and behavior:**
|
|
254
|
+
This unlocks four extra methods:
|
|
557
255
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
256
|
+
| Method | Effect |
|
|
257
|
+
| ------------------------------- | ------------------------------------- |
|
|
258
|
+
| `softRemove(pk, options?)` | Sets `deletedAt` to the current date. |
|
|
259
|
+
| `softRemoveList(pks, options?)` | Same, in batch — returns `{ count }`. |
|
|
260
|
+
| `restore(pk, options?)` | Sets `deletedAt` back to `null`. |
|
|
261
|
+
| `restoreList(pks, options?)` | Same, in batch — returns `{ count }`. |
|
|
561
262
|
|
|
562
|
-
|
|
563
|
-
// CORRECT ✅ — raw select only
|
|
564
|
-
await userRepository.get(id, { select: { id: true, name: true } });
|
|
263
|
+
Every other method accepts a `see` option controlling visibility of soft-deleted rows:
|
|
565
264
|
|
|
566
|
-
|
|
567
|
-
await userRepository.
|
|
568
|
-
await userRepository.
|
|
265
|
+
```typescript
|
|
266
|
+
await userRepository.getAll({ see: "active" }); // default — only non-deleted records
|
|
267
|
+
await userRepository.getAll({ see: "removed" }); // only soft-deleted records
|
|
268
|
+
await userRepository.getAll({ see: "all" }); // everything, ignoring soft-delete
|
|
569
269
|
```
|
|
570
270
|
|
|
571
|
-
> **When to use `selectModel` vs. `select`:** prefer `selectModel` for projections reused across multiple calls (defined once in `selectModels`); use `select` for specific, occasional projections that don't need a name.
|
|
572
|
-
|
|
573
271
|
---
|
|
574
272
|
|
|
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);
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
**Using an `includeModel` in the call:**
|
|
595
|
-
|
|
596
|
-
```ts
|
|
597
|
-
const user = await userRepository.get(id, { includeModel: "withPosts" });
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
In this case, the default `select` (`selectModels`/`defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
|
|
273
|
+
## `select` and `relations`
|
|
601
274
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
- **Can only be passed in the method call**, via `options.includeModel`. There's no `defaultIncludeModel` or `defaultInclude` — there's no way to configure a default `includeModel` on the repository, unlike what happens with `defaultSelectModel`.
|
|
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.
|
|
606
|
-
|
|
607
|
-
```ts
|
|
608
|
-
// CORRECT ✅ — includeModel only
|
|
609
|
-
await userRepository.get(id, { includeModel: "withPosts" });
|
|
610
|
-
|
|
611
|
-
// CORRECT ✅ — selectModel only
|
|
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
|
-
```
|
|
275
|
+
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:
|
|
617
276
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
Besides `includeModel` (named, pre-configured in `includeModels`), you can pass a raw Prisma `include` directly in the call, without registering it beforehand on the repository.
|
|
621
|
-
|
|
622
|
-
```ts
|
|
277
|
+
```typescript
|
|
623
278
|
const user = await userRepository.get(id, {
|
|
624
|
-
|
|
279
|
+
select: { id: true, name: true, address: { city: true } },
|
|
625
280
|
});
|
|
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
281
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
695
|
-
- The method uses the `Ordered`, `OrderedAndPaginated`, or `PaginatedAndOrdered` suffix — in these cases the `order` argument passed in the call takes priority.
|
|
696
|
-
- The dynamic method has `injectOrdering` configured — the method's fixed ordering takes precedence.
|
|
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
|
-
}
|
|
282
|
+
const userWithAddress = await userRepository.get(id, {
|
|
283
|
+
relations: { address: true },
|
|
284
|
+
});
|
|
707
285
|
```
|
|
708
286
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
## `see` option
|
|
287
|
+
- `select` mirrors the entity's shape: scalar fields take a `boolean`; relation fields take a `boolean` or a nested `select`.
|
|
288
|
+
- `relations` eagerly loads related records; each relation field takes a `boolean` or a nested `relations` object.
|
|
289
|
+
- Whether `select` and `relations` can be combined depends on the adapter (see below).
|
|
714
290
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
>
|
|
291
|
+
> ⚠️ **Adapter-dependent behavior for `relations`:**
|
|
292
|
+
>
|
|
293
|
+
> The core only forwards `MethodOptions.select` and `MethodOptions.relations` to the adapter — each adapter decides how to translate them to the underlying ORM:
|
|
294
|
+
>
|
|
295
|
+
> - **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`:
|
|
296
|
+
> ```typescript
|
|
297
|
+
> // TypeORM: select alone is NOT enough
|
|
298
|
+
> await userRepository.get(id, {
|
|
299
|
+
> select: { id: true, address: { city: true } },
|
|
300
|
+
> relations: { address: true }, // ← required in TypeORM
|
|
301
|
+
> });
|
|
302
|
+
> ```
|
|
303
|
+
> - **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:
|
|
304
|
+
> ```typescript
|
|
305
|
+
> // Prisma7: relations is ignored when select exists
|
|
306
|
+
> await userRepository.get(id, {
|
|
307
|
+
> select: { id: true, name: true },
|
|
308
|
+
> relations: { address: true }, // ← ignored, include = undefined
|
|
309
|
+
> });
|
|
310
|
+
> ```
|
|
311
|
+
>
|
|
312
|
+
> Custom adapters may map `relations` differently — consult the adapter's documentation for the exact semantics.
|
|
735
313
|
|
|
736
314
|
---
|
|
737
315
|
|
|
738
316
|
## Dynamic methods
|
|
739
317
|
|
|
740
|
-
Dynamic methods are
|
|
741
|
-
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
318
|
+
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.
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
class UserRepository extends VSRepository<User, string> {
|
|
322
|
+
@DynamicMethod()
|
|
323
|
+
declare findByEmail: (email: string) => Promise<User[]>;
|
|
324
|
+
|
|
325
|
+
@DynamicMethod()
|
|
326
|
+
declare findOneByEmail: (email: string) => Promise<User | null>;
|
|
327
|
+
|
|
328
|
+
@DynamicMethod()
|
|
329
|
+
declare updateById: (id: string, data: DeepPartial<User>) => Promise<User>;
|
|
330
|
+
|
|
331
|
+
// Where-based: VSRepoWhere<T> as the first param, pagination penultimate, MethodOptions last
|
|
332
|
+
@DynamicMethod()
|
|
333
|
+
declare findWherePaginated: (
|
|
334
|
+
where: VSRepoWhere<User>,
|
|
335
|
+
pagination: Pagination,
|
|
336
|
+
options?: MethodOptions<User>,
|
|
337
|
+
) => Promise<User[]>;
|
|
338
|
+
|
|
339
|
+
// OrderedAndPaginated: field filters, then order, then pagination, then MethodOptions
|
|
340
|
+
@DynamicMethod()
|
|
341
|
+
declare findByNameIgnoreCaseOrAgeBetweenOrderByCreatedAtAscPaginated: (
|
|
342
|
+
name: string,
|
|
343
|
+
age: [number, number],
|
|
344
|
+
order: Ordering<User>,
|
|
345
|
+
pagination: Pagination,
|
|
346
|
+
options?: MethodOptions<User>,
|
|
347
|
+
) => Promise<User[]>;
|
|
748
348
|
}
|
|
749
349
|
```
|
|
750
350
|
|
|
751
|
-
---
|
|
752
|
-
|
|
753
351
|
### Available prefixes
|
|
754
352
|
|
|
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
|
-
---
|
|
353
|
+
| Prefix | Adapter method | Notes |
|
|
354
|
+
| -------------------------- | --------------------- | ---------------------------------------------------------------------------------------- |
|
|
355
|
+
| `findBy` | `findMany` | Field filters follow the prefix. |
|
|
356
|
+
| `findOneBy` | `findOne` | Field filters follow the prefix; single result. |
|
|
357
|
+
| `findOneOrThrowBy` | `findOneOrThrow` | Throws if no record is found. |
|
|
358
|
+
| `findOneOrThrow` | `findOneOrThrow` | No field filters; applies only soft-delete/`see`. |
|
|
359
|
+
| `findOneOrThrowWhere` | `findOneOrThrow` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
360
|
+
| `findWhere` | `findMany` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
361
|
+
| `findOneWhere` | `findOne` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
362
|
+
| `findOne` | `findOne` | No field filters; applies only soft-delete/`see`. |
|
|
363
|
+
| `countBy` | `count` | Field filters follow the prefix. |
|
|
364
|
+
| `countWhere` | `count` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
365
|
+
| `count` | `count` | No field filters. |
|
|
366
|
+
| `existsBy` | `exists` | Returns `boolean`. |
|
|
367
|
+
| `existsWhere` | `exists` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
368
|
+
| `create` | `create` | Receives `data` as argument. |
|
|
369
|
+
| `createMany` | `createMany` | Receives `data[]` as argument; supports `IgnoreConflicts`. |
|
|
370
|
+
| `createManyReturning` | `createManyReturning` | Receives `data[]` as argument; supports `IgnoreConflicts`; returns the created records (`T[]`) instead of `CountResult`. |
|
|
371
|
+
| `updateBy` | `update` | Field filters + `data` as argument. |
|
|
372
|
+
| `updateWhere` | `update` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
|
|
373
|
+
| `updateManyBy` | `updateMany` | Field filters + `data`. |
|
|
374
|
+
| `updateManyWhere` | `updateMany` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
|
|
375
|
+
| `updateManyReturningBy` | `updateManyReturning` | Field filters + `data`; returns updated records. |
|
|
376
|
+
| `updateManyReturningWhere` | `updateManyReturning` | Receives a `VSRepoWhere<T>` as the first argument, then `data`; returns updated records. |
|
|
377
|
+
| `upsertBy` | `upsert` | Field filters + `create`/`update` payloads. |
|
|
378
|
+
| `upsertWhere` | `upsert` | Receives a `VSRepoWhere<T>` as the first argument, then `create`/`update` payloads. |
|
|
379
|
+
| `deleteBy` | `delete` | Field filters follow the prefix. |
|
|
380
|
+
| `deleteWhere` | `delete` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
381
|
+
| `deleteManyBy` | `deleteMany` | Field filters follow the prefix. |
|
|
382
|
+
| `deleteManyWhere` | `deleteMany` | Receives a `VSRepoWhere<T>` as the first argument. |
|
|
383
|
+
| `deleteManyReturningBy` | `deleteManyReturning` | Field filters follow the prefix; returns deleted records. |
|
|
384
|
+
| `deleteManyReturningWhere` | `deleteManyReturning` | Receives a `VSRepoWhere<T>` as the first argument; returns deleted records. |
|
|
385
|
+
|
|
386
|
+
> `aggregate` and `groupBy` are **not implemented yet** in v2 (they existed in v1). This is planned but not currently available.
|
|
792
387
|
|
|
793
388
|
### Field filters
|
|
794
389
|
|
|
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")]);
|
|
841
|
-
```
|
|
842
|
-
|
|
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
|
|
390
|
+
Applied as suffixes to the field name inside the method (same idea as v1, one renamed suffix):
|
|
391
|
+
|
|
392
|
+
| Suffix | Meaning | Argument |
|
|
393
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
394
|
+
| _(none)_ | equality (`=`) | yes |
|
|
395
|
+
| `Not` | negation | yes |
|
|
396
|
+
| `In` | is one of | yes (array) |
|
|
397
|
+
| `NotIn` | is none of | yes (array) |
|
|
398
|
+
| `Contains` | substring match | yes |
|
|
399
|
+
| `NotContains` | negated substring match | yes |
|
|
400
|
+
| `StartsWith` | prefix match | yes |
|
|
401
|
+
| `NotStartsWith` | negated prefix match | yes |
|
|
402
|
+
| `EndsWith` | suffix match | yes |
|
|
403
|
+
| `NotEndsWith` | negated suffix match | yes |
|
|
404
|
+
| `GreaterThan` | `>` | yes |
|
|
405
|
+
| `GreaterThanEqual` | `>=` | yes |
|
|
406
|
+
| `LessThan` | `<` | yes |
|
|
407
|
+
| `LessThanEqual` | `<=` | yes |
|
|
408
|
+
| `Between` | inclusive range | yes (`[min, max]` tuple) |
|
|
409
|
+
| `NotBetween` | outside an inclusive range | yes (`[min, max]` tuple) |
|
|
410
|
+
| `IsNull` | field is `null` | no |
|
|
411
|
+
| `IsNotNull` | field is not `null` | no |
|
|
412
|
+
| `IsTrue` | field is `true` | no |
|
|
413
|
+
| `IsFalse` | field is `false` | no |
|
|
414
|
+
| `IgnoreCase` | case-insensitive combinator for text filters | no _(renamed from v1's `Insensitive`)_ |
|
|
415
|
+
| `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 | — |
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
@DynamicMethod()
|
|
419
|
+
declare findByNameContainsIgnoreCase: (name: string) => Promise<User[]>;
|
|
420
|
+
|
|
421
|
+
@DynamicMethod()
|
|
422
|
+
declare findByAgeBetween: (age: [number, number]) => Promise<User[]>;
|
|
847
423
|
```
|
|
848
424
|
|
|
849
|
-
---
|
|
850
|
-
|
|
851
425
|
### Logical operators
|
|
852
426
|
|
|
853
|
-
| Operator
|
|
854
|
-
|
|
|
855
|
-
| `And`
|
|
856
|
-
| `Or`
|
|
857
|
-
| `AND`
|
|
858
|
-
|
|
859
|
-
`AND` (in caps) has a specific rule:
|
|
427
|
+
| Operator | Usage in the name | Example |
|
|
428
|
+
| -------- | ------------------------------- | --------------------------------------------------- |
|
|
429
|
+
| `And` | between two fields | `findOneByIdAndEmail` |
|
|
430
|
+
| `Or` | between two fields | `findByNameOrEmail` |
|
|
431
|
+
| `AND` | splits a final block into `AND` | `findByEmailOrNameANDActiveStatusAndAgeGreaterThan` |
|
|
860
432
|
|
|
861
|
-
|
|
862
|
-
- All fields after `AND` are injected inside `AND: []`.
|
|
863
|
-
- After an `AND`, there can't be an `Or`.
|
|
433
|
+
`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`.
|
|
864
434
|
|
|
865
|
-
|
|
435
|
+
### Relation filters
|
|
866
436
|
|
|
867
|
-
|
|
868
|
-
methods: {
|
|
869
|
-
findOneByIdAndEmail: { map: true },
|
|
870
|
-
findByNameOrEmail: { map: true },
|
|
871
|
-
findFirstByIdOrEmailAndName: { map: true },
|
|
872
|
-
findByEmailOrNameANDActiveStatusAndAgeGreaterThan: { map: true }
|
|
873
|
-
}
|
|
437
|
+
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).
|
|
874
438
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
439
|
+
| Suffix | Meaning | Restriction |
|
|
440
|
+
| -------------- | ----------------------------------------------- | ---------------------------------------------------------- |
|
|
441
|
+
| `Some` | at least one related record matches | to-many relations only |
|
|
442
|
+
| `SomeField` | filters within the related records | to-many relations only |
|
|
443
|
+
| `Every` | every related record matches | to-many relations only (needs `Field` to be a real filter) |
|
|
444
|
+
| `EveryField` | filters within the related records | to-many relations only |
|
|
445
|
+
| `None` | no related record matches | to-many relations only |
|
|
446
|
+
| `NoneField` | filters within the related records | to-many relations only |
|
|
447
|
+
| `With` | related record exists | to-one relations only |
|
|
448
|
+
| `WithField` | filters a field within the related record | to-one relations only |
|
|
449
|
+
| `Without` | related record does not exist | to-one relations only |
|
|
450
|
+
| `WithoutField` | negated filter on a field of the related record | to-one relations only |
|
|
880
451
|
|
|
881
|
-
|
|
452
|
+
```typescript
|
|
453
|
+
@DynamicMethod()
|
|
454
|
+
declare findByAddressWithCityStartsWithIgnoreCase: (city: string) => Promise<User[]>;
|
|
882
455
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
id: 1,
|
|
886
|
-
email: "john@email.com"
|
|
887
|
-
}
|
|
456
|
+
@DynamicMethod()
|
|
457
|
+
declare findByProductsSome: () => Promise<User[]>;
|
|
888
458
|
```
|
|
889
459
|
|
|
890
|
-
|
|
460
|
+
### Ordering, pagination and distinct
|
|
891
461
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
462
|
+
| Suffix | Effect |
|
|
463
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
464
|
+
| `Paginated` | Injects a `pagination` argument (`{ limit?, offset? }`) as the **penultimate** parameter (before the optional `MethodOptions`). |
|
|
465
|
+
| `Ordered` | Injects an `order: Ordering<T>` argument as the **penultimate** parameter (before the optional `MethodOptions`). |
|
|
466
|
+
| `OrderedAndPaginated` | Injects `order` as the antepenultimate, then `pagination` as the penultimate — both before `MethodOptions`. |
|
|
467
|
+
| `PaginatedAndOrdered` | Injects `pagination` as the antepenultimate, then `order` as the penultimate — both before `MethodOptions`. |
|
|
468
|
+
| `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. |
|
|
469
|
+
| `Distinct<Field>And<Field>...` | Bakes fixed `distinct` fields directly into the method name (only valid on `findBy`/`findWhere`-family methods). |
|
|
470
|
+
| `IgnoreConflicts` | On `createMany`/`createManyReturning`, skips records that would violate a unique constraint instead of throwing. _(Renamed from v1's `SkipDuplicates`.)_ |
|
|
900
471
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
```ts
|
|
904
|
-
{
|
|
905
|
-
OR: [
|
|
906
|
-
{ id: 1 },
|
|
907
|
-
{
|
|
908
|
-
email: "john@email.com",
|
|
909
|
-
name: "John"
|
|
910
|
-
}
|
|
911
|
-
]
|
|
912
|
-
}
|
|
913
|
-
```
|
|
472
|
+
> ⚠️ **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
473
|
|
|
915
|
-
|
|
474
|
+
```typescript
|
|
475
|
+
// Paginated: pagination is the penultimate param (before MethodOptions)
|
|
476
|
+
@DynamicMethod()
|
|
477
|
+
declare findByActiveOrderByCreatedAtDescPaginated:
|
|
478
|
+
(active: boolean, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
|
|
916
479
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
{ name: "John" }
|
|
922
|
-
],
|
|
923
|
-
AND: [
|
|
924
|
-
{ activeStatus: true },
|
|
925
|
-
{ age: { gt: 17 } }
|
|
926
|
-
]
|
|
927
|
-
}
|
|
928
|
-
```
|
|
480
|
+
// OrderedAndPaginated: order, then pagination, then MethodOptions
|
|
481
|
+
@DynamicMethod()
|
|
482
|
+
declare findByNameContainsIgnoreCaseOrderedAndPaginated:
|
|
483
|
+
(name: string, order: Ordering<User>, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
|
|
929
484
|
|
|
930
|
-
|
|
485
|
+
@DynamicMethod()
|
|
486
|
+
declare createManyIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<{ count: number }>;
|
|
931
487
|
|
|
932
|
-
|
|
488
|
+
// createManyReturning: same as createMany, but returns the created records
|
|
489
|
+
@DynamicMethod()
|
|
490
|
+
declare createManyReturningIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<User[]>;
|
|
933
491
|
|
|
934
|
-
|
|
492
|
+
// findOne with no filter (equivalent to findOneOrThrow with no filter, but returns null instead of throwing)
|
|
493
|
+
@DynamicMethod()
|
|
494
|
+
declare findOne: (options?: MethodOptions<User>) => Promise<User | null>;
|
|
495
|
+
```
|
|
935
496
|
|
|
936
|
-
>
|
|
497
|
+
> ⚠️ **Precedence between `Distinct` and `OrderBy`:** when both are used in the same method name, **`Distinct` must come before `OrderBy`**:
|
|
937
498
|
>
|
|
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
|
-
}
|
|
499
|
+
> ```typescript
|
|
500
|
+
> @DynamicMethod()
|
|
501
|
+
> declare findByActiveDistinctNameOrderByCreatedAtDesc:
|
|
502
|
+
> (active: boolean) => Promise<User[]>;
|
|
503
|
+
> ```
|
|
504
|
+
>
|
|
505
|
+
> Putting `OrderBy` before `Distinct` (e.g. `findByActiveOrderByCreatedAtDescDistinctName`) is not a valid pattern and won't be parsed as expected.
|
|
972
506
|
|
|
973
|
-
|
|
974
|
-
await userRepository.findByPostsSomeTitle("My first post");
|
|
975
|
-
await userRepository.findByPostsEveryPublishedIsTrue();
|
|
976
|
-
await userRepository.findByPostsNone();
|
|
977
|
-
await userRepository.findByPostsNoneTitle("Draft");
|
|
507
|
+
### Decorator options
|
|
978
508
|
|
|
979
|
-
|
|
980
|
-
await userRepository.findByProfileWithBio("Hello, world!");
|
|
981
|
-
await userRepository.findByProfileWithout();
|
|
982
|
-
await userRepository.findByProfileWithoutBio("Old bio");
|
|
983
|
-
```
|
|
509
|
+
`@DynamicMethod<T>(options?)` accepts:
|
|
984
510
|
|
|
985
|
-
|
|
511
|
+
| Option | Type | Description |
|
|
512
|
+
| ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
513
|
+
| `proxyTo` | `string` | Redirects the method's logic to another valid dynamic-method pattern — useful for names that don't follow the naming convention. |
|
|
514
|
+
| `injectOrdering` | `Ordering<T>` | Fixed ordering automatically injected, overriding the repository's `defaultOrdering`. |
|
|
986
515
|
|
|
987
|
-
```
|
|
988
|
-
{
|
|
989
|
-
|
|
990
|
-
some: { title: "My first post" }
|
|
991
|
-
}
|
|
992
|
-
}
|
|
516
|
+
```typescript
|
|
517
|
+
@DynamicMethod<User>({ injectOrdering: { createdAt: "desc" } })
|
|
518
|
+
declare findByStatus: (status: string) => Promise<User[]>;
|
|
993
519
|
```
|
|
994
520
|
|
|
995
|
-
|
|
521
|
+
---
|
|
996
522
|
|
|
997
|
-
|
|
998
|
-
{
|
|
999
|
-
posts: {
|
|
1000
|
-
every: { published: true }
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
```
|
|
523
|
+
## Query methods (raw SQL)
|
|
1004
524
|
|
|
1005
|
-
|
|
525
|
+
`@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.
|
|
1006
526
|
|
|
1007
|
-
```
|
|
1008
|
-
{
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
Generates (`findByProfileWithout`):
|
|
527
|
+
```typescript
|
|
528
|
+
class UserRepository extends VSRepository<User, string> {
|
|
529
|
+
@QueryMethod('SELECT * FROM "user" WHERE email = $1')
|
|
530
|
+
declare findByEmailRaw: (arg: QueryMethodArg<[email: string]>) => Promise<User[]>;
|
|
1016
531
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
profile: {
|
|
1020
|
-
isNot: {}
|
|
1021
|
-
}
|
|
532
|
+
@QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
|
|
533
|
+
declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
|
|
1022
534
|
}
|
|
1023
535
|
```
|
|
1024
536
|
|
|
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)` |
|
|
537
|
+
| Option | Type | Default | Description |
|
|
538
|
+
| ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
539
|
+
| `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. |
|
|
1039
540
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
| Suffix | Effect |
|
|
1043
|
-
| ------------------- | ------------------------------------------ |
|
|
1044
|
-
| `SkipDuplicates` | Skips duplicate records during insertion |
|
|
1045
|
-
|
|
1046
|
-
---
|
|
541
|
+
Query methods accept `{ args, db? }` at the call site — `db` lets them participate in a `transaction()` block just like base and dynamic methods.
|
|
1047
542
|
|
|
1048
|
-
###
|
|
543
|
+
### Ad-hoc raw queries with `query()`
|
|
1049
544
|
|
|
1050
|
-
|
|
545
|
+
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:
|
|
1051
546
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
```ts
|
|
1055
|
-
methods: {
|
|
1056
|
-
// Returns unique users combining "age" and "role" (no field filter)
|
|
1057
|
-
findManyDistinctAgeAndRole: { map: true },
|
|
1058
|
-
|
|
1059
|
-
// Distinct combined with the Paginated suffix
|
|
1060
|
-
findManyDistinctNamePaginated: { map: true },
|
|
1061
|
-
|
|
1062
|
-
// Distinct combined with a field filter (name) — filters by name and then applies distinct on role
|
|
1063
|
-
findManyByNameDistinctRole: { map: true },
|
|
1064
|
-
},
|
|
547
|
+
```typescript
|
|
548
|
+
query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
|
|
1065
549
|
```
|
|
1066
550
|
|
|
1067
|
-
```
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// The pagination argument still works normally
|
|
1072
|
-
await userRepository.findManyDistinctNamePaginated({ take: 10, skip: 0 });
|
|
551
|
+
```typescript
|
|
552
|
+
const users = await userRepository.query<User[]>('SELECT * FROM "user" WHERE email = $1', {
|
|
553
|
+
args: ["maria@email.com"],
|
|
554
|
+
});
|
|
1073
555
|
|
|
1074
|
-
|
|
1075
|
-
|
|
556
|
+
const affectedRows = await userRepository.query<number>(
|
|
557
|
+
'UPDATE "user" SET active = true WHERE id = $1',
|
|
558
|
+
{ args: ["123"], modifying: true },
|
|
559
|
+
);
|
|
1076
560
|
```
|
|
1077
561
|
|
|
1078
|
-
|
|
562
|
+
| Option | Type | Default | Description |
|
|
563
|
+
| ----------- | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
564
|
+
| `args` | `any[]` | `undefined` | Positional parameters injected into `$1`, `$2`, ... placeholders. Never interpolate values directly into the SQL string. |
|
|
565
|
+
| `db` | `any` | Repository's default client | Database client or transaction to run this query in. |
|
|
566
|
+
| `modifying` | `boolean` | `false` | When `true`, treats the statement as `INSERT`/`UPDATE`/`DELETE`. |
|
|
1079
567
|
|
|
1080
|
-
|
|
568
|
+
Just like base, dynamic and query methods, `query()` accepts `db` in `options` to participate in a `transaction()` block.
|
|
1081
569
|
|
|
1082
570
|
---
|
|
1083
571
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
Each entry in `methods` accepts the following options:
|
|
1087
|
-
|
|
1088
|
-
| Option | Type | Default | Description |
|
|
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). |
|
|
1099
|
-
|
|
1100
|
-
---
|
|
1101
|
-
|
|
1102
|
-
### Aggregate and GroupBy
|
|
1103
|
-
|
|
1104
|
-
```ts
|
|
1105
|
-
const userRepository = setupVSRepo<User, "user">()(({
|
|
1106
|
-
tableName: "user",
|
|
1107
|
-
pkName: "id",
|
|
1108
|
-
methods: {
|
|
1109
|
-
aggregate: { map: true },
|
|
1110
|
-
groupBy: { map: true },
|
|
1111
|
-
},
|
|
1112
|
-
}).build(prisma);
|
|
1113
|
-
```
|
|
1114
|
-
|
|
1115
|
-
> [!NOTE]
|
|
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
|
|
1122
|
-
|
|
1123
|
-
Query Methods let a method run **raw SQL** directly, completely bypassing the dynamic-method name parser. They're useful for complex queries (heavy joins, CTEs, database-specific functions) that aren't practical to express with the standard prefixes/suffixes.
|
|
1124
|
-
|
|
1125
|
-
Internally, VSRepository executes the SQL through Prisma using `$queryRawUnsafe` (for reads) or `$executeRawUnsafe` (for writes), and the values in the `args` array are passed as **positional parameters** (`$1`, `$2`, ...) — the same prepared-statement technique Prisma itself uses. This means the values are never concatenated into the SQL string, which is what actually prevents SQL injection.
|
|
1126
|
-
|
|
1127
|
-
> [!WARNING]
|
|
1128
|
-
> `$1`, `$2`, ... in your SQL must always represent **values** (data parameters), never column names, table names, or dynamic SQL fragments. Identifier names (columns/tables) can't be passed as a positional parameter — if your method needs to vary those, build the SQL from a fixed, known set of options in your own code, never from untrusted input.
|
|
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
|
-
},
|
|
1150
|
-
},
|
|
1151
|
-
},
|
|
1152
|
-
}).build(prisma);
|
|
1153
|
-
```
|
|
1154
|
-
|
|
1155
|
-
**Calling a Query Method:**
|
|
572
|
+
## Transactions
|
|
1156
573
|
|
|
1157
|
-
|
|
574
|
+
All methods (base, dynamic, and query) accept `options.db` to participate in a shared transaction:
|
|
1158
575
|
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const activeUsers = await userRepository.findActiveUsersRaw<User[]>({
|
|
1163
|
-
args: [true],
|
|
1164
|
-
});
|
|
576
|
+
```typescript
|
|
577
|
+
await userRepository.transaction(async tx => {
|
|
578
|
+
const user = await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
|
|
1165
579
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
});
|
|
580
|
+
await userLogsRepository.save(
|
|
581
|
+
{ action: "User created", data: { userId: user.id } },
|
|
582
|
+
{ db: tx },
|
|
583
|
+
);
|
|
1177
584
|
});
|
|
1178
585
|
```
|
|
1179
586
|
|
|
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).
|
|
1189
|
-
|
|
1190
|
-
---
|
|
587
|
+
Different repositories can share the same transaction as long as their adapters point to the same underlying ORM connection.
|
|
1191
588
|
|
|
1192
|
-
|
|
589
|
+
`transaction()` accepts an optional `VSRepoTransactionOptions` as its second argument:
|
|
1193
590
|
|
|
1194
|
-
|
|
591
|
+
```typescript
|
|
592
|
+
import { TransactionIsolationLevel } from "vsrepo";
|
|
1195
593
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
type User = Prisma.userGetPayload<{
|
|
1200
|
-
include: { profile: true; posts: true };
|
|
1201
|
-
}>;
|
|
1202
|
-
|
|
1203
|
-
const userRepository = setupVSRepo<User, "user">()(({
|
|
1204
|
-
tableName: "user",
|
|
1205
|
-
pkName: "id",
|
|
1206
|
-
|
|
1207
|
-
relations: {
|
|
1208
|
-
profile: {
|
|
1209
|
-
pk: "id",
|
|
1210
|
-
mode: "oto",
|
|
1211
|
-
restriction: "set",
|
|
594
|
+
await userRepository.transaction(
|
|
595
|
+
async tx => {
|
|
596
|
+
await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
|
|
1212
597
|
},
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
mode: "otm",
|
|
1216
|
-
restriction: "add",
|
|
1217
|
-
},
|
|
1218
|
-
},
|
|
1219
|
-
}).build(prisma);
|
|
598
|
+
{ isolationLevel: TransactionIsolationLevel.SERIALIZABLE, timeoutMs: 5000 },
|
|
599
|
+
);
|
|
1220
600
|
```
|
|
1221
601
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
|
1225
|
-
|
|
|
1226
|
-
| `oto` | one-to-one |
|
|
1227
|
-
| `otm` | one-to-many |
|
|
1228
|
-
| `mto` | many-to-one |
|
|
1229
|
-
| `mtm` | many-to-many |
|
|
602
|
+
| Option | Type | Description |
|
|
603
|
+
| ----------------- | -------------------------- | -------------------------------------------------------------------------------- |
|
|
604
|
+
| `isolationLevel` | `TransactionIsolationLevel` | Isolation level to use for the transaction. Defaults to the underlying ORM's default. |
|
|
605
|
+
| `timeoutMs` | `number` | Maximum time (in ms) the transaction is allowed to run before being aborted. |
|
|
1230
606
|
|
|
1231
|
-
|
|
607
|
+
`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.
|
|
1232
608
|
|
|
1233
|
-
|
|
1234
|
-
| ------------ | ------------------------------------------------------ |
|
|
1235
|
-
| `set` | Fully replaces (removes the ones that weren't sent) |
|
|
1236
|
-
| `add` | Adds/updates without removing existing ones |
|
|
609
|
+
---
|
|
1237
610
|
|
|
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.
|
|
611
|
+
## Utility types
|
|
1251
612
|
|
|
1252
|
-
|
|
613
|
+
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:
|
|
1253
614
|
|
|
1254
|
-
|
|
615
|
+
```typescript
|
|
616
|
+
import type {
|
|
617
|
+
MethodOptions,
|
|
618
|
+
Pagination,
|
|
619
|
+
Ordering,
|
|
620
|
+
OrderByField,
|
|
621
|
+
SortDirection,
|
|
622
|
+
SeeMode,
|
|
623
|
+
DeepPartial,
|
|
624
|
+
CountResult,
|
|
625
|
+
QueryMethodArg,
|
|
626
|
+
KeysOfType,
|
|
627
|
+
Primitive,
|
|
628
|
+
VSRepoWhere,
|
|
629
|
+
VSRepoOrmTypes,
|
|
630
|
+
VSRepoTransactionOptions,
|
|
631
|
+
TransactionIsolationLevel,
|
|
632
|
+
} from "vsrepo";
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
| Type | Description | Used by |
|
|
636
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
637
|
+
| `MethodOptions<T, K>` | Options accepted as the last argument of every base and dynamic method: `select`, `relations`, `see`, `db`. | [Base methods](#base-methods), [Dynamic methods](#dynamic-methods). |
|
|
638
|
+
| `Pagination` | `{ limit?, offset? }` accepted by `getAll` and by `Paginated` dynamic methods. | [Base methods](#base-methods), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
|
|
639
|
+
| `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). |
|
|
640
|
+
| `SeeMode` | `"active" \| "removed" \| "all"` — controls visibility of soft-deleted records. | [Soft-delete](#soft-delete). |
|
|
641
|
+
| `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`. |
|
|
642
|
+
| `CountResult` | `{ count: number }` — the shape returned by batch operations. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
|
|
643
|
+
| `QueryMethodArg<T>` | `{ args?: T, db? }` — positional SQL parameters (`$1`, `$2`, ...) and transaction client for `@QueryMethod`. | [Query methods (raw SQL)](#query-methods-raw-sql). |
|
|
644
|
+
| `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. |
|
|
645
|
+
| `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. |
|
|
646
|
+
| `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). |
|
|
647
|
+
| `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). |
|
|
648
|
+
| `VSRepoTransactionOptions` | `{ isolationLevel?, timeoutMs? }` — options accepted as the second argument of `transaction()`. | [Transactions](#transactions). |
|
|
649
|
+
| `TransactionIsolationLevel` | Enum of standard SQL isolation levels (`READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`) accepted by `VSRepoTransactionOptions.isolationLevel`. | [Transactions](#transactions). |
|
|
650
|
+
|
|
651
|
+
### `DeepPartial<T>`
|
|
652
|
+
|
|
653
|
+
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:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
type User = { id: string; name: string; address: { city: string; zip: string } };
|
|
657
|
+
|
|
658
|
+
const patch: DeepPartial<User> = {
|
|
659
|
+
address: { city: "São Paulo" }, // zip can be omitted; city keeps its type
|
|
660
|
+
};
|
|
1255
661
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
662
|
+
await userRepository.patch(id, patch);
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### `KeysOfType<T, K>`
|
|
666
|
+
|
|
667
|
+
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:
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
type User = { id: string; age: number; name: string };
|
|
671
|
+
type StringKeys = KeysOfType<User, string>; // "id" | "name"
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### `Ordering<T>`
|
|
675
|
+
|
|
676
|
+
Accepts either a single ordering object or an array of them, applied in the order they're declared:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
const order: Ordering<User> = { createdAt: "desc" };
|
|
680
|
+
const chained: Ordering<User> = [{ name: "asc" }, { createdAt: "desc" }];
|
|
681
|
+
|
|
682
|
+
await userRepository.getAll({ order: chained });
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## Writing your own adapter
|
|
688
|
+
|
|
689
|
+
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:
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
export abstract class VSRepoAdapter<T> {
|
|
693
|
+
abstract runInTransaction<R>(
|
|
694
|
+
fn: (tx: any) => Promise<R>,
|
|
695
|
+
options?: VSRepoTransactionOptions,
|
|
696
|
+
): Promise<R>;
|
|
697
|
+
abstract getDbClient(): any;
|
|
698
|
+
abstract query<T = any>(query: string, options?: AdapterQueryOptions): Promise<T>;
|
|
699
|
+
abstract findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T | null>;
|
|
700
|
+
abstract findOneOrThrow(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
701
|
+
abstract findMany(
|
|
702
|
+
where: VSRepoWhere<T>,
|
|
703
|
+
options?: AdapterMethodOptions<T> & { distinct?: (keyof T)[] },
|
|
704
|
+
): Promise<T[]>;
|
|
705
|
+
abstract save(obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
706
|
+
abstract saveMany(objs: DeepPartial<T>[], options?: AdapterMethodOptions<T>): Promise<T[]>;
|
|
707
|
+
abstract create(objs: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
708
|
+
abstract createMany(
|
|
709
|
+
objs: DeepPartial<T>[],
|
|
710
|
+
options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
|
|
711
|
+
): Promise<CountResult>;
|
|
712
|
+
abstract createManyReturning(
|
|
713
|
+
objs: DeepPartial<T>[],
|
|
714
|
+
options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
|
|
715
|
+
): Promise<T[]>;
|
|
716
|
+
abstract delete(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
|
|
717
|
+
abstract deleteMany(
|
|
718
|
+
where: VSRepoWhere<T>,
|
|
719
|
+
options?: AdapterMethodOptions<T>,
|
|
720
|
+
): Promise<CountResult>;
|
|
721
|
+
abstract deleteManyReturning(
|
|
722
|
+
where: VSRepoWhere<T>,
|
|
723
|
+
options?: AdapterMethodOptions<T>,
|
|
724
|
+
): Promise<T[]>;
|
|
725
|
+
abstract update(
|
|
726
|
+
where: VSRepoWhere<T>,
|
|
727
|
+
obj: DeepPartial<T>,
|
|
728
|
+
options?: AdapterMethodOptions<T>,
|
|
729
|
+
): Promise<T>;
|
|
730
|
+
abstract updateMany(
|
|
731
|
+
where: VSRepoWhere<T>,
|
|
732
|
+
obj: DeepPartial<T>,
|
|
733
|
+
options?: AdapterMethodOptions<T>,
|
|
734
|
+
): Promise<CountResult>;
|
|
735
|
+
abstract updateManyReturning(
|
|
736
|
+
where: VSRepoWhere<T>,
|
|
737
|
+
obj: DeepPartial<T>,
|
|
738
|
+
options?: AdapterMethodOptions<T>,
|
|
739
|
+
): Promise<T[]>;
|
|
740
|
+
abstract count(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number>;
|
|
741
|
+
abstract exists(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<boolean>;
|
|
742
|
+
abstract merge<K>(
|
|
743
|
+
where: VSRepoWhere<T>,
|
|
744
|
+
obj: DeepPartial<T>,
|
|
745
|
+
options?: AdapterMethodOptions<T>,
|
|
746
|
+
): Promise<K & T>;
|
|
747
|
+
abstract upsert(
|
|
748
|
+
where: VSRepoWhere<T>,
|
|
749
|
+
create: DeepPartial<T>,
|
|
750
|
+
update: DeepPartial<T>,
|
|
751
|
+
options?: AdapterMethodOptions<T>,
|
|
752
|
+
): Promise<T>;
|
|
1264
753
|
}
|
|
1265
754
|
```
|
|
1266
755
|
|
|
1267
|
-
|
|
756
|
+
`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.
|
|
1268
757
|
|
|
1269
|
-
|
|
758
|
+
### Logging from your adapter
|
|
1270
759
|
|
|
1271
|
-
|
|
760
|
+
`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:
|
|
1272
761
|
|
|
1273
|
-
```
|
|
1274
|
-
|
|
1275
|
-
const user = await userRepository.save(
|
|
1276
|
-
{ name: "Mary", email: "mary@email.com", password: "password" },
|
|
1277
|
-
{ db: tx }
|
|
1278
|
-
);
|
|
762
|
+
```typescript
|
|
763
|
+
import { VSLogger, VSLogLevel } from "vsrepo";
|
|
1279
764
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
{ db: tx }
|
|
1283
|
-
);
|
|
1284
|
-
});
|
|
1285
|
-
```
|
|
765
|
+
export class MyOrmAdapter<T> extends VSRepoAdapter<T> {
|
|
766
|
+
private readonly logger = new VSLogger(VSLogLevel.WARN, "MyOrmAdapterLogger");
|
|
1286
767
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
768
|
+
async findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>) {
|
|
769
|
+
const start = this.logger.startPerformLog("adapter findOne");
|
|
770
|
+
try {
|
|
771
|
+
// ... talk to the ORM ...
|
|
772
|
+
this.logger.endPerformLog(start);
|
|
773
|
+
return result;
|
|
774
|
+
} catch (err) {
|
|
775
|
+
this.logger.endPerformLog(start);
|
|
776
|
+
this.logger.logError("adapter findOne failed", err);
|
|
777
|
+
throw err;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
1299
781
|
```
|
|
1300
782
|
|
|
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
|
-
},
|
|
783
|
+
| Method | Description |
|
|
784
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
785
|
+
| `new VSLogger(logLevel, name, slowThresholdMs?)` | Creates a logger; `name` prefixes every line, `slowThresholdMs` defaults to 300. |
|
|
786
|
+
| `logDebug/logInfo/logWarn(text, obj?)` | Logs at the given level if `logLevel` allows it; `obj` is appended as pretty-printed JSON. |
|
|
787
|
+
| `logError(text, err?)` | Logs at `ERROR`; if `err` is an `Error`, only `name`/`message`/`stack`/`cause` are logged. |
|
|
788
|
+
| `startPerformLog(operation)` / `endPerformLog(data)` | Bracket a block to log its duration, escalating to `WARN` if it exceeds `slowThresholdMs`. |
|
|
789
|
+
| `getLogLevel()` | Returns the logger's configured `VSLogLevel`. |
|
|
1318
790
|
|
|
1319
|
-
|
|
1320
|
-
return repo.patchList(ids.map(id => [id, { active: true }]));
|
|
1321
|
-
},
|
|
1322
|
-
}));
|
|
1323
|
-
```
|
|
791
|
+
This is purely a convenience for adapter authors — nothing in the core requires your adapter to use it.
|
|
1324
792
|
|
|
1325
793
|
---
|
|
1326
794
|
|
|
1327
795
|
## Error handling
|
|
1328
796
|
|
|
1329
|
-
|
|
797
|
+
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
798
|
|
|
1331
|
-
```
|
|
1332
|
-
import { VSRepoError
|
|
799
|
+
```typescript
|
|
800
|
+
import { VSRepoError } from "vsrepo";
|
|
1333
801
|
|
|
1334
802
|
try {
|
|
1335
|
-
|
|
803
|
+
await userRepository.get(id);
|
|
1336
804
|
} catch (error) {
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
console.error("Repository error:", error.message);
|
|
1341
|
-
} else {
|
|
1342
|
-
console.error("Error:", error.message)
|
|
1343
|
-
}
|
|
805
|
+
if (error instanceof VSRepoError) {
|
|
806
|
+
console.error(`[${error.type}] ${error.message}`);
|
|
807
|
+
}
|
|
1344
808
|
}
|
|
1345
809
|
```
|
|
1346
810
|
|
|
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";
|
|
811
|
+
| `VSRepoErrorType` | Raised when |
|
|
812
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
813
|
+
| `DECORATOR` | Invalid arguments were passed to `@DynamicMethod` or `@QueryMethod`. |
|
|
814
|
+
| `RESOLVER` | The library failed to resolve a dynamic/query method's configuration into a callable method (e.g. an unknown method name). |
|
|
815
|
+
| `DYNAMIC` | A resolved dynamic method failed at runtime (e.g. missing arguments). |
|
|
816
|
+
| `VALIDATOR` | Invalid method options or arguments were detected during validation. |
|
|
817
|
+
| `BASE` | Invalid usage of a base method (`get`, `save`, `remove`, etc). |
|
|
818
|
+
| `ADAPTER` | A `VSRepoAdapter` failed while talking to the underlying ORM/database — always thrown as `VSRepoAdapterError`. |
|
|
1375
819
|
|
|
1376
|
-
|
|
1377
|
-
type DbTransaction = Prisma.TransactionClient;
|
|
1378
|
-
type ClientOrTransaction = DbClient | DbTransaction;
|
|
1379
|
-
```
|
|
1380
|
-
|
|
1381
|
-
### Soft-delete visibility type
|
|
820
|
+
### `VSRepoAdapterError` and `AdapterErrorCode`
|
|
1382
821
|
|
|
1383
|
-
|
|
1384
|
-
import type { SeeMode } from "../../generated/vsrepo";
|
|
822
|
+
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:
|
|
1385
823
|
|
|
1386
|
-
|
|
1387
|
-
|
|
824
|
+
```typescript
|
|
825
|
+
import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
|
|
1388
826
|
|
|
1389
|
-
|
|
827
|
+
try {
|
|
828
|
+
await userRepository.save({ name: "Maria" });
|
|
829
|
+
} catch (error) {
|
|
830
|
+
if (error instanceof VSRepoAdapterError) {
|
|
831
|
+
console.error(`[${error.code}] ${error.message}`, error.originalError);
|
|
1390
832
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
IncludeModels,
|
|
1397
|
-
WhereModel,
|
|
1398
|
-
OrderingModel,
|
|
1399
|
-
PaginationModel,
|
|
1400
|
-
ModelUpsertInput,
|
|
1401
|
-
PrismaModelInputs,
|
|
1402
|
-
} from "../../generated/vsrepo";
|
|
833
|
+
if (error.code === AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
|
|
834
|
+
// handle a duplicate key, e.g. return a friendly message
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
1403
838
|
```
|
|
1404
839
|
|
|
1405
|
-
|
|
840
|
+
| Property | Type | Description |
|
|
841
|
+
| --------------- | ------------------ | ----------------------------------------------------------------------------------- |
|
|
842
|
+
| `code` | `AdapterErrorCode` | Stable, adapter-agnostic code classifying the failure. |
|
|
843
|
+
| `originalError` | `unknown` | The raw error (or `null`/`undefined`) thrown by the underlying ORM/database driver. |
|
|
844
|
+
| `message` | `string` | Human-readable description of the adapter failure. |
|
|
845
|
+
| `type` | `VSRepoErrorType` | Always `VSRepoErrorType.ADAPTER`. |
|
|
846
|
+
| `cause` | `unknown` | Optional root cause the error was chained from. |
|
|
1406
847
|
|
|
1407
|
-
|
|
1408
|
-
import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
|
|
848
|
+
Adapter implementations construct it directly when mapping an ORM failure:
|
|
1409
849
|
|
|
1410
|
-
|
|
1411
|
-
|
|
850
|
+
```typescript
|
|
851
|
+
import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
|
|
1412
852
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
853
|
+
throw new VSRepoAdapterError(
|
|
854
|
+
"user creation failed",
|
|
855
|
+
AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
|
|
856
|
+
originalError, // raw DB/driver error
|
|
857
|
+
);
|
|
1416
858
|
```
|
|
1417
859
|
|
|
1418
|
-
|
|
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.
|
|
1421
|
-
|
|
1422
|
-
### Configuration types
|
|
860
|
+
#### `AdapterErrorCode`
|
|
1423
861
|
|
|
1424
|
-
|
|
1425
|
-
import type {
|
|
1426
|
-
MethodConfig,
|
|
1427
|
-
RepoConfig,
|
|
1428
|
-
BuildConfig,
|
|
1429
|
-
RepositoryRelations,
|
|
1430
|
-
ExtractRelationConfig,
|
|
1431
|
-
} from "../../generated/vsrepo";
|
|
1432
|
-
```
|
|
1433
|
-
|
|
1434
|
-
### Built repository type
|
|
862
|
+
`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:
|
|
1435
863
|
|
|
1436
|
-
```
|
|
1437
|
-
import
|
|
864
|
+
```typescript
|
|
865
|
+
import { AdapterErrorCode } from "vsrepo";
|
|
1438
866
|
|
|
1439
|
-
|
|
1440
|
-
type UserRepository = RepositoryOf<typeof userVSRepo>;
|
|
867
|
+
console.log(AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION); // "UNIQUE_CONSTRAINT_VIOLATION"
|
|
1441
868
|
```
|
|
1442
869
|
|
|
1443
|
-
|
|
870
|
+
| Code | Meaning |
|
|
871
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
872
|
+
| `UNKNOWN` | Unclassified/unknown error; the fallback when no more specific code matches. |
|
|
873
|
+
| `MISSING_DB_CLIENT` | Database client (or connection pool) not provided or could not be resolved. |
|
|
874
|
+
| `CONNECTION_FAILED` | Could not reach/connect to the database, or an established connection was lost/terminated. |
|
|
875
|
+
| `CONNECTION_POOL_EXHAUSTED` | Connection pool exhausted/depleted — no connection available, all busy or the limit was reached. |
|
|
876
|
+
| `TIMEOUT` | Database did not respond in time; a query exceeded its allowed timeout. |
|
|
877
|
+
| `UNIQUE_CONSTRAINT_VIOLATION` | Unique constraint (duplicate key) violated. E.g. Postgres/SQLite `23505`, MySQL `1062`. |
|
|
878
|
+
| `FOREIGN_KEY_VIOLATION` | Foreign key constraint violated (referenced row missing). |
|
|
879
|
+
| `NOT_NULL_VIOLATION` | NOT NULL constraint violated. |
|
|
880
|
+
| `CHECK_VIOLATION` | CHECK constraint violated. |
|
|
881
|
+
| `CONSTRAINT_VIOLATION` | General integrity/constraint violation not covered by a more specific code. |
|
|
882
|
+
| `NOT_FOUND` | Requested record not found (e.g. a `findOneOrThrow`-style operation). |
|
|
883
|
+
| `INVALID_DATA` | Field value invalid for its type/length, or a required value is missing. |
|
|
884
|
+
| `VALUE_TOO_LONG` | Provided value exceeds the column/field length limit. |
|
|
885
|
+
| `CONVERSION_ERROR` | Value could not be converted/cast to the target type. E.g. Postgres `22P02`, MySQL `1366`. |
|
|
886
|
+
| `INVALID_QUERY` | SQL query/stored procedure is malformed or invalid. |
|
|
887
|
+
| `TABLE_OR_COLUMN_NOT_FOUND` | Referenced table/column/relation does not exist. |
|
|
888
|
+
| `DEADLOCK` | Operation aborted by a lock timeout or deadlock between concurrent transactions. |
|
|
889
|
+
| `LOCK_TIMEOUT` | Could not acquire a required database lock in time. |
|
|
890
|
+
| `LOCKED` | Record is locked and cannot be modified. |
|
|
891
|
+
| `ACCESS_DENIED` | Current user/role does not have permission for the operation. |
|
|
892
|
+
| `INVALID_CREDENTIALS` | Invalid connection credentials (host/user/password). |
|
|
893
|
+
| `ROW_NOT_ALLOWED` | Authenticated user does not own the record / row-level security rejected it. |
|
|
894
|
+
| `MODEL_NOT_FOUND` | Entity/model or table not defined/mapped in the ORM, or the adapter lacks model metadata to build the query. |
|
|
895
|
+
| `FIELD_NOT_FOUND` | Field/column name in the data or `where` does not exist on the entity/model. |
|
|
896
|
+
| `TRANSACTION_CLOSED` | Transaction used after it was committed/rolled back. |
|
|
897
|
+
| `TRANSACTION_ALREADY_STARTED` | A nested transaction could not be opened (e.g. nested `transaction()` calls). |
|
|
898
|
+
| `TRANSACTION_CONFLICT` | Transaction failed to commit and was rolled back. |
|
|
899
|
+
| `TRANSACTION_NOT_STARTED` | No active transaction when one was required. |
|
|
900
|
+
| `CONNECTION_CLOSED` | Connection closed/terminated while a transaction or query was in progress. |
|
|
901
|
+
| `INVALID_PARTIAL` | `merge`/`upsert`/`update` received a partial object that is invalid or missing required keys. |
|
|
902
|
+
| `NOT_SUPPORTED` | Unsupported feature/operation requested from the adapter (e.g. raw `query()` not supported). |
|
|
903
|
+
| `INVALID_ADAPTER_CONFIG` | Adapter configuration invalid or incomplete (missing required options, or options with an invalid type/value). |
|
|
904
|
+
| `INTERNAL` | Internal adapter bug or unrecoverable state; should rarely be used — prefer a more specific code. |
|
|
1444
905
|
|
|
1445
|
-
|
|
1446
|
-
type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
|
|
1447
|
-
```
|
|
906
|
+
#### `VSRepoError` vs. raw ORM errors
|
|
1448
907
|
|
|
1449
|
-
|
|
908
|
+
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
909
|
|
|
1451
|
-
|
|
1452
|
-
import type { SaveObject, PatchObject } from "../../generated/vsrepo";
|
|
910
|
+
---
|
|
1453
911
|
|
|
1454
|
-
|
|
1455
|
-
tableName: "user",
|
|
1456
|
-
pkName: "id",
|
|
1457
|
-
relations: {
|
|
1458
|
-
profile: { pk: "id", mode: "oto", restriction: "set" },
|
|
1459
|
-
},
|
|
1460
|
-
});
|
|
912
|
+
## Logging
|
|
1461
913
|
|
|
1462
|
-
|
|
1463
|
-
type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
|
|
1464
|
-
```
|
|
914
|
+
Every repository has an internal logger, configured via `logLevel` and `logSlowThresholdMs` on the constructor options:
|
|
1465
915
|
|
|
1466
|
-
|
|
916
|
+
```typescript
|
|
917
|
+
import { VSLogLevel } from "vsrepo";
|
|
1467
918
|
|
|
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
|
|
919
|
+
super({
|
|
920
|
+
pkName: "id",
|
|
921
|
+
adapter,
|
|
922
|
+
logLevel: VSLogLevel.DEBUG,
|
|
923
|
+
logSlowThresholdMs: 200,
|
|
1484
924
|
});
|
|
1485
925
|
```
|
|
1486
926
|
|
|
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
|
-
```
|
|
927
|
+
| Level | Meaning |
|
|
928
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------- |
|
|
929
|
+
| `DEBUG` | Verbose internal details, including every resolved query — very useful for debugging dynamic methods. |
|
|
930
|
+
| `INFO` | High-level lifecycle events, such as repository initialization. |
|
|
931
|
+
| `WARN` (default) | Recoverable issues and slow operations (see `logSlowThresholdMs`, defaults to 300ms). |
|
|
932
|
+
| `ERROR` | Failures raised while executing an operation. |
|
|
1517
933
|
|
|
1518
|
-
|
|
934
|
+
---
|
|
1519
935
|
|
|
1520
|
-
|
|
1521
|
-
repo.extend((repo) => ({
|
|
1522
|
-
myMethod: () => { ... }
|
|
1523
|
-
}));
|
|
1524
|
-
```
|
|
936
|
+
## Development
|
|
1525
937
|
|
|
1526
|
-
|
|
938
|
+
The v2 core is built and packed from this branch as a standard npm package:
|
|
1527
939
|
|
|
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
|
-
```
|
|
940
|
+
```bash
|
|
941
|
+
# 1. Install dependencies
|
|
942
|
+
pnpm install
|
|
1545
943
|
|
|
1546
|
-
|
|
944
|
+
# 2. Compile the TypeScript sources into dist/ (removes a previous dist/ first)
|
|
945
|
+
pnpm build
|
|
1547
946
|
|
|
1548
|
-
|
|
947
|
+
# 3. (Optional) Inspect what would be published without writing a tarball
|
|
948
|
+
npm pack --dry-run
|
|
1549
949
|
|
|
1550
|
-
|
|
950
|
+
# 4. Produce the installable tarball (runs `prepack` -> `pnpm build` automatically)
|
|
951
|
+
npm pack
|
|
1551
952
|
|
|
1552
|
-
|
|
953
|
+
# 5. Consume it locally in another project
|
|
954
|
+
npm install ../path/to/vsrepo-1.4.0.tgz
|
|
955
|
+
```
|
|
1553
956
|
|
|
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**.
|
|
957
|
+
Notes:
|
|
1558
958
|
|
|
1559
|
-
|
|
959
|
+
- `pnpm build` runs `tsc -p tsconfig.build.json`, which outputs the compiled JS and generated type declarations into `dist/` with `rootDir: src`.
|
|
960
|
+
- The published package contains **only** the `dist/` folder plus the READMEs and `LICENSE` (see `files` in `package.json`). Source, tests, the `v1/` folder and `generated/` are **not** shipped — the adapters will live in their own `@vsrepo/*-adapter` packages.
|
|
961
|
+
- The core is ORM-agnostic and has no `@prisma/client` peer dependency.
|
|
1560
962
|
|
|
1561
963
|
---
|
|
1562
964
|
|
|
1563
965
|
## Requirements
|
|
1564
966
|
|
|
1565
|
-
- Node.js 18+
|
|
1566
|
-
-
|
|
1567
|
-
- TypeScript (optional, but strongly recommended)
|
|
1568
|
-
- `"moduleResolution": "bundler"` or `"nodenext"` in tsconfig
|
|
1569
|
-
|
|
1570
|
-
Recommended `tsconfig.json`:
|
|
967
|
+
- Node.js 18+
|
|
968
|
+
- TypeScript, with **legacy/experimental decorators** enabled (required by `@DynamicMethod`/`@QueryMethod`):
|
|
1571
969
|
|
|
1572
970
|
```json
|
|
1573
971
|
{
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
"moduleResolution": "NodeNext",
|
|
1578
|
-
"strict": true,
|
|
1579
|
-
"skipLibCheck": true,
|
|
1580
|
-
"lib": ["ES2020"]
|
|
1581
|
-
}
|
|
972
|
+
"compilerOptions": {
|
|
973
|
+
"experimentalDecorators": true
|
|
974
|
+
}
|
|
1582
975
|
}
|
|
1583
976
|
```
|
|
1584
977
|
|
|
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.
|
|
978
|
+
- `reflect-metadata` (bundled as a dependency, imported internally — you don't need to import it yourself)
|
|
979
|
+
- 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
980
|
|
|
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`.
|
|
981
|
+
---
|
|
1600
982
|
|
|
1601
|
-
|
|
983
|
+
## Contributing
|
|
1602
984
|
|
|
1603
|
-
|
|
985
|
+
Contributions are welcome, especially towards finishing the Prisma and TypeORM adapters! (**[GitHub repository](https://github.com/jaobrabo123/VSRepository)**):
|
|
1604
986
|
|
|
1605
|
-
|
|
987
|
+
1. **Fork** the project.
|
|
988
|
+
2. Create a branch off `v2` for your change: `git checkout -b v2-my-change`.
|
|
989
|
+
3. Push your branch: `git push origin v2-my-change`.
|
|
990
|
+
4. Open a **Pull Request** against `v2`.
|
|
1606
991
|
|
|
1607
|
-
|
|
992
|
+
To report issues or suggest features, open an **Issue**.
|