nestjs-typeorm3-kit 0.1.7 → 0.2.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/LICENSE +21 -0
- package/README.md +803 -162
- package/dist/common/decorator/index.d.ts.map +1 -1
- package/dist/common/decorator/index.js +1 -5
- package/dist/common/decorator/index.js.map +1 -1
- package/dist/controller/decorator/index.d.ts.map +1 -1
- package/dist/controller/decorator/index.js +66 -117
- package/dist/controller/decorator/index.js.map +1 -1
- package/dist/helpers/index.d.ts +4 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +81 -11
- package/dist/helpers/index.js.map +1 -1
- package/dist/module/decorator/index.d.ts +1 -1
- package/dist/module/decorator/index.d.ts.map +1 -1
- package/dist/module/decorator/index.js +188 -19
- package/dist/module/decorator/index.js.map +1 -1
- package/dist/repository/config/constants.d.ts +1 -0
- package/dist/repository/config/constants.d.ts.map +1 -1
- package/dist/repository/config/constants.js +2 -1
- package/dist/repository/config/constants.js.map +1 -1
- package/dist/repository/config/types.d.ts +2 -2
- package/dist/repository/config/types.d.ts.map +1 -1
- package/dist/repository/config/types.js +34 -5
- package/dist/repository/config/types.js.map +1 -1
- package/dist/repository/config/utils.d.ts +1 -1
- package/dist/repository/config/utils.d.ts.map +1 -1
- package/dist/repository/config/utils.js +12 -13
- package/dist/repository/config/utils.js.map +1 -1
- package/dist/repository/decorator/index.d.ts +2 -2
- package/dist/repository/decorator/index.d.ts.map +1 -1
- package/dist/repository/decorator/index.js +1 -1
- package/dist/repository/decorator/index.js.map +1 -1
- package/dist/repository/module/index.d.ts +2 -5
- package/dist/repository/module/index.d.ts.map +1 -1
- package/dist/repository/module/index.js +23 -87
- package/dist/repository/module/index.js.map +1 -1
- package/dist/transaction/decorator/index.d.ts +5 -3
- package/dist/transaction/decorator/index.d.ts.map +1 -1
- package/dist/transaction/decorator/index.js +20 -19
- package/dist/transaction/decorator/index.js.map +1 -1
- package/dist/transaction/setup/index.d.ts +8 -3
- package/dist/transaction/setup/index.d.ts.map +1 -1
- package/dist/transaction/setup/index.js +19 -5
- package/dist/transaction/setup/index.js.map +1 -1
- package/dist/validate/decorator/index.d.ts.map +1 -1
- package/dist/validate/decorator/index.js +6 -4
- package/dist/validate/decorator/index.js.map +1 -1
- package/dist/vendor/transactional/common/constants.d.ts +6 -0
- package/dist/vendor/transactional/common/constants.d.ts.map +1 -0
- package/dist/vendor/transactional/common/constants.js +9 -0
- package/dist/vendor/transactional/common/constants.js.map +1 -0
- package/dist/vendor/transactional/common/index.d.ts +26 -0
- package/dist/vendor/transactional/common/index.d.ts.map +1 -0
- package/dist/vendor/transactional/common/index.js +136 -0
- package/dist/vendor/transactional/common/index.js.map +1 -0
- package/dist/vendor/transactional/decorators/transactional.d.ts +3 -0
- package/dist/vendor/transactional/decorators/transactional.d.ts.map +1 -0
- package/dist/vendor/transactional/decorators/transactional.js +20 -0
- package/dist/vendor/transactional/decorators/transactional.js.map +1 -0
- package/dist/vendor/transactional/enums/isolation-level.d.ts +7 -0
- package/dist/vendor/transactional/enums/isolation-level.d.ts.map +1 -0
- package/dist/vendor/transactional/enums/isolation-level.js +11 -0
- package/dist/vendor/transactional/enums/isolation-level.js.map +1 -0
- package/dist/vendor/transactional/enums/propagation.d.ts +10 -0
- package/dist/vendor/transactional/enums/propagation.d.ts.map +1 -0
- package/dist/vendor/transactional/enums/propagation.js +14 -0
- package/dist/vendor/transactional/enums/propagation.js.map +1 -0
- package/dist/vendor/transactional/enums/storage-driver.d.ts +5 -0
- package/dist/vendor/transactional/enums/storage-driver.d.ts.map +1 -0
- package/dist/vendor/transactional/enums/storage-driver.js +9 -0
- package/dist/vendor/transactional/enums/storage-driver.js.map +1 -0
- package/dist/vendor/transactional/errors/transactional.d.ts +4 -0
- package/dist/vendor/transactional/errors/transactional.d.ts.map +1 -0
- package/dist/vendor/transactional/errors/transactional.js +11 -0
- package/dist/vendor/transactional/errors/transactional.js.map +1 -0
- package/dist/vendor/transactional/errors/typeorm-updated-patch.d.ts +5 -0
- package/dist/vendor/transactional/errors/typeorm-updated-patch.d.ts.map +1 -0
- package/dist/vendor/transactional/errors/typeorm-updated-patch.js +11 -0
- package/dist/vendor/transactional/errors/typeorm-updated-patch.js.map +1 -0
- package/dist/vendor/transactional/hooks/index.d.ts +11 -0
- package/dist/vendor/transactional/hooks/index.d.ts.map +1 -0
- package/dist/vendor/transactional/hooks/index.js +79 -0
- package/dist/vendor/transactional/hooks/index.js.map +1 -0
- package/dist/vendor/transactional/index.d.ts +10 -0
- package/dist/vendor/transactional/index.d.ts.map +1 -0
- package/dist/vendor/transactional/index.js +27 -0
- package/dist/vendor/transactional/index.js.map +1 -0
- package/dist/vendor/transactional/storage/driver/async-local-storage/index.d.ts +9 -0
- package/dist/vendor/transactional/storage/driver/async-local-storage/index.d.ts.map +1 -0
- package/dist/vendor/transactional/storage/driver/async-local-storage/index.js +25 -0
- package/dist/vendor/transactional/storage/driver/async-local-storage/index.js.map +1 -0
- package/dist/vendor/transactional/storage/driver/interface.d.ts +10 -0
- package/dist/vendor/transactional/storage/driver/interface.d.ts.map +1 -0
- package/dist/vendor/transactional/storage/driver/interface.js +3 -0
- package/dist/vendor/transactional/storage/driver/interface.js.map +1 -0
- package/dist/vendor/transactional/storage/index.d.ts +3 -0
- package/dist/vendor/transactional/storage/index.d.ts.map +1 -0
- package/dist/vendor/transactional/storage/index.js +6 -0
- package/dist/vendor/transactional/storage/index.js.map +1 -0
- package/dist/vendor/transactional/storage/storage.d.ts +7 -0
- package/dist/vendor/transactional/storage/storage.d.ts.map +1 -0
- package/dist/vendor/transactional/storage/storage.js +21 -0
- package/dist/vendor/transactional/storage/storage.js.map +1 -0
- package/dist/vendor/transactional/transactions/nested-scope.d.ts +12 -0
- package/dist/vendor/transactional/transactions/nested-scope.d.ts.map +1 -0
- package/dist/vendor/transactional/transactions/nested-scope.js +42 -0
- package/dist/vendor/transactional/transactions/nested-scope.js.map +1 -0
- package/dist/vendor/transactional/transactions/run-in-transaction.d.ts +3 -0
- package/dist/vendor/transactional/transactions/run-in-transaction.d.ts.map +1 -0
- package/dist/vendor/transactional/transactions/run-in-transaction.js +9 -0
- package/dist/vendor/transactional/transactions/run-in-transaction.js.map +1 -0
- package/dist/vendor/transactional/transactions/wrap-in-transaction.d.ts +11 -0
- package/dist/vendor/transactional/transactions/wrap-in-transaction.d.ts.map +1 -0
- package/dist/vendor/transactional/transactions/wrap-in-transaction.js +109 -0
- package/dist/vendor/transactional/transactions/wrap-in-transaction.js.map +1 -0
- package/dist/vendor/transactional/utils/index.d.ts +3 -0
- package/dist/vendor/transactional/utils/index.d.ts.map +1 -0
- package/dist/vendor/transactional/utils/index.js +12 -0
- package/dist/vendor/transactional/utils/index.js.map +1 -0
- package/package.json +28 -25
package/README.md
CHANGED
|
@@ -1,253 +1,894 @@
|
|
|
1
1
|
# nestjs-typeorm3-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://www.npmjs.com/package/nestjs-typeorm3-kit"><img alt="npm version" src="https://img.shields.io/npm/v/nestjs-typeorm3-kit?color=cb3837&label=npm" /></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/nestjs-typeorm3-kit"><img alt="npm downloads" src="https://img.shields.io/npm/dm/nestjs-typeorm3-kit?color=0a7bbb" /></a>
|
|
6
|
+
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/nestjs-typeorm3-kit.svg?color=informational" /></a>
|
|
7
|
+
<img alt="Node" src="https://img.shields.io/badge/node-%3E%3D16.4-43853d?logo=node.js&logoColor=white" />
|
|
8
|
+
<img alt="Bun" src="https://img.shields.io/badge/bun-supported-f9f1e1?logo=bun&logoColor=black" />
|
|
9
|
+
<img alt="TypeORM" src="https://img.shields.io/badge/typeorm-0.3.x-e83524" />
|
|
10
|
+
<img alt="NestJS" src="https://img.shields.io/badge/nestjs-8%E2%80%9312-ea2845?logo=nestjs&logoColor=white" />
|
|
11
|
+
<img alt="Types" src="https://img.shields.io/npm/types/nestjs-typeorm3-kit?color=3178c6&logo=typescript&logoColor=white" />
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
A NestJS toolkit for **TypeORM 0.3.x** that brings back the ergonomic `@EntityRepository` pattern, adds first-class multi-DataSource support, decorator-based transactions, Swagger helpers, and a safer `findOne`.
|
|
15
|
+
|
|
16
|
+
This package is also a **wrapper fork of [`typeorm-transactional`](https://github.com/Aliheym/typeorm-transactional) `v0.5.0`** — the transactional layer is vendored inside the package with `cls-hooked` removed, so transactions work on both Node.js and Bun without installing anything extra.
|
|
17
|
+
|
|
18
|
+
> 📚 **Looking for deep dives?** The [`docs/`](./docs) folder has bilingual
|
|
19
|
+
> (EN / VI) guides for every feature with full option references and
|
|
20
|
+
> common pitfalls.
|
|
21
|
+
|
|
22
|
+
## Highlights
|
|
23
|
+
|
|
24
|
+
- 🧩 `@DefEntityRepository` + `@InjectRepo` — repository pattern with multi-DataSource support
|
|
25
|
+
- 🔁 `@DefTransaction` — decorator-based transactions, works on Node **and** Bun
|
|
26
|
+
- 🛡️ `RepositoryWrapper` — hardens `findOne` against TypeORM's "first row on undefined where" footgun
|
|
27
|
+
- 🎯 `@DefController`, `@DefGet/Post/Put/Patch/Delete` — less boilerplate, auto Swagger metadata
|
|
28
|
+
- 🌳 `@ChildModule` — nested route prefixes + auto `ApiTags`
|
|
29
|
+
- 📜 `configSwaggerDocument` — merges `class-validator` metadata into the OpenAPI doc
|
|
30
|
+
- ⚡ `lazyLoadClasses` — auto-discover controllers/services by file suffix
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Table of contents
|
|
35
|
+
|
|
36
|
+
1. [Installation](#1-installation)
|
|
37
|
+
2. [Quick start](#2-quick-start)
|
|
38
|
+
3. [Tutorial — build a multi-DataSource app step by step](#3-tutorial--build-a-multi-datasource-app-step-by-step)
|
|
39
|
+
- [3.1 Set up DataSources](#31-set-up-datasources)
|
|
40
|
+
- [3.2 Write your first repository](#32-write-your-first-repository)
|
|
41
|
+
- [3.3 Wire the repository module](#33-wire-the-repository-module)
|
|
42
|
+
- [3.4 Inject the repository into a service](#34-inject-the-repository-into-a-service)
|
|
43
|
+
- [3.5 Add a controller](#35-add-a-controller)
|
|
44
|
+
- [3.6 Group modules with `@ChildModule`](#36-group-modules-with-childmodule)
|
|
45
|
+
- [3.7 Add transactions](#37-add-transactions)
|
|
46
|
+
- [3.8 Enable Swagger](#38-enable-swagger)
|
|
47
|
+
4. [Relationship to `typeorm-transactional`](#4-relationship-to-typeorm-transactional)
|
|
48
|
+
5. [Repository API](#5-repository-api)
|
|
49
|
+
6. [Transactions](#6-transactions)
|
|
50
|
+
7. [Controller & module helpers](#7-controller--module-helpers)
|
|
51
|
+
8. [Swagger helper](#8-swagger-helper)
|
|
52
|
+
9. [Validation decorators](#9-validation-decorators)
|
|
53
|
+
10. [Utilities](#10-utilities)
|
|
54
|
+
11. [Full example](#11-full-example)
|
|
55
|
+
12. [Testing](#12-testing)
|
|
56
|
+
13. [Compatibility](#13-compatibility)
|
|
57
|
+
14. [Troubleshooting](#14-troubleshooting)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 1. Installation
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary><b>npm</b></summary>
|
|
4
65
|
|
|
5
|
-
|
|
66
|
+
```bash
|
|
67
|
+
npm install nestjs-typeorm3-kit
|
|
68
|
+
```
|
|
6
69
|
|
|
7
|
-
|
|
70
|
+
</details>
|
|
8
71
|
|
|
9
|
-
|
|
72
|
+
<details open>
|
|
73
|
+
<summary><b>yarn</b></summary>
|
|
10
74
|
|
|
11
75
|
```bash
|
|
12
|
-
|
|
76
|
+
yarn add nestjs-typeorm3-kit
|
|
13
77
|
```
|
|
14
78
|
|
|
15
|
-
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary><b>pnpm</b></summary>
|
|
16
83
|
|
|
17
84
|
```bash
|
|
18
|
-
|
|
85
|
+
pnpm add nestjs-typeorm3-kit
|
|
19
86
|
```
|
|
20
87
|
|
|
21
|
-
|
|
88
|
+
</details>
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary><b>bun</b></summary>
|
|
22
92
|
|
|
23
93
|
```bash
|
|
24
|
-
|
|
94
|
+
bun add nestjs-typeorm3-kit
|
|
25
95
|
```
|
|
26
96
|
|
|
27
|
-
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
> Starting from **v0.2.0**, `typeorm-transactional` is vendored inside the package — no separate install needed.
|
|
100
|
+
|
|
101
|
+
**Peer dependencies** (install if you don't have them):
|
|
28
102
|
|
|
29
103
|
```bash
|
|
30
|
-
|
|
104
|
+
npm install @nestjs/common @nestjs/core @nestjs/typeorm @nestjs/swagger typeorm reflect-metadata rxjs class-validator class-transformer class-validator-jsonschema libphonenumber-js
|
|
31
105
|
```
|
|
32
106
|
|
|
33
|
-
|
|
107
|
+
**Runtime requirement:** Node.js **>= 16.4** or **Bun** for the transactional layer; **NestJS 12 requires Node.js >= 20.19**. The vendored transactional layer uses Node's built-in `AsyncLocalStorage` — there is no `cls-hooked` fallback.
|
|
34
108
|
|
|
35
|
-
|
|
109
|
+
---
|
|
36
110
|
|
|
37
|
-
|
|
111
|
+
## 2. Quick start
|
|
38
112
|
|
|
39
|
-
|
|
40
|
-
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
113
|
+
A 30-second peek. The full tutorial in [section 3](#3-tutorial--build-a-multi-datasource-app-step-by-step) walks through the same code with explanations.
|
|
41
114
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
retryDelay: 1000,
|
|
55
|
-
}),
|
|
56
|
-
dataSourceFactory: async (options: DataSourceOptions) => {
|
|
57
|
-
if (!options) {
|
|
58
|
-
throw new Error("Invalid options passed");
|
|
59
|
-
}
|
|
60
|
-
return addTransactionalDataSource({
|
|
61
|
-
dataSource: new DataSource(options),
|
|
62
|
-
name: PRIMARY_CONNECTION,
|
|
63
|
-
});
|
|
64
|
-
},
|
|
65
|
-
});
|
|
115
|
+
```ts
|
|
116
|
+
// main.ts
|
|
117
|
+
import { NestFactory } from "@nestjs/core";
|
|
118
|
+
import { setupTransactionContext } from "nestjs-typeorm3-kit";
|
|
119
|
+
import { AppModule } from "./app.module";
|
|
120
|
+
|
|
121
|
+
async function bootstrap() {
|
|
122
|
+
setupTransactionContext(); // MUST run before NestFactory.create
|
|
123
|
+
const app = await NestFactory.create(AppModule);
|
|
124
|
+
await app.listen(3000);
|
|
125
|
+
}
|
|
126
|
+
bootstrap();
|
|
66
127
|
```
|
|
67
128
|
|
|
68
|
-
|
|
129
|
+
```ts
|
|
130
|
+
// book.repo.ts
|
|
131
|
+
import { Repository } from "typeorm";
|
|
132
|
+
import { DefEntityRepository } from "nestjs-typeorm3-kit";
|
|
133
|
+
import { Book } from "./book.entity";
|
|
69
134
|
|
|
70
|
-
|
|
135
|
+
@DefEntityRepository(Book)
|
|
136
|
+
export class BookRepo extends Repository<Book> {}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
// app.module.ts
|
|
71
141
|
import { Module } from "@nestjs/common";
|
|
72
|
-
import {
|
|
142
|
+
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
143
|
+
import { DataSource, DataSourceOptions } from "typeorm";
|
|
144
|
+
import { addTransactionalDataSource, DefRepositoryModule } from "nestjs-typeorm3-kit";
|
|
73
145
|
|
|
74
|
-
import { PRIMARY_CONNECTION } from "~/common/constants";
|
|
75
|
-
import { DefRepositoryModule } from "nestjs-typeorm3-kit";
|
|
76
146
|
@Module({
|
|
77
147
|
imports: [
|
|
78
|
-
|
|
79
|
-
useFactory: () => ({
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
148
|
+
TypeOrmModule.forRootAsync({
|
|
149
|
+
useFactory: () => ({ type: "postgres", /* ... */ entities: [Book] }),
|
|
150
|
+
// @DefTransaction only works on DataSources registered here
|
|
151
|
+
dataSourceFactory: async (options) =>
|
|
152
|
+
addTransactionalDataSource(new DataSource(options as DataSourceOptions)),
|
|
83
153
|
}),
|
|
154
|
+
DefRepositoryModule.forFeature([BookRepo]),
|
|
84
155
|
],
|
|
85
|
-
|
|
156
|
+
providers: [BookService],
|
|
86
157
|
})
|
|
87
|
-
export class
|
|
158
|
+
export class AppModule {}
|
|
88
159
|
```
|
|
89
160
|
|
|
90
|
-
|
|
161
|
+
```ts
|
|
162
|
+
// book.service.ts
|
|
163
|
+
import { Injectable } from "@nestjs/common";
|
|
164
|
+
import { DefTransaction, InjectRepo } from "nestjs-typeorm3-kit";
|
|
165
|
+
|
|
166
|
+
@Injectable()
|
|
167
|
+
export class BookService {
|
|
168
|
+
constructor(@InjectRepo(BookRepo) private readonly repo: BookRepo) {}
|
|
91
169
|
|
|
92
|
-
|
|
170
|
+
@DefTransaction()
|
|
171
|
+
create(title: string) {
|
|
172
|
+
return this.repo.save({ title });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 3. Tutorial — build a multi-DataSource app step by step
|
|
180
|
+
|
|
181
|
+
This walkthrough builds a small Nest app with **two Postgres databases**, repositories, a service that writes across both inside a transaction, a controller, and Swagger. By the end you'll have used every major feature of the kit.
|
|
182
|
+
|
|
183
|
+
### Architecture at a glance
|
|
184
|
+
|
|
185
|
+
```mermaid
|
|
186
|
+
flowchart LR
|
|
187
|
+
Client[HTTP Client] --> Ctrl[BookController]
|
|
188
|
+
Ctrl --> Svc[BookService]
|
|
189
|
+
Svc -- "@InjectRepo(BookRepo)" --> BookRepo
|
|
190
|
+
Svc -- "@InjectRepo(LogRepo, 'secondary')" --> LogRepo
|
|
191
|
+
BookRepo --> Primary[(Primary DB)]
|
|
192
|
+
LogRepo --> Secondary[(Secondary DB)]
|
|
193
|
+
Svc -. "@DefTransaction()" .-> TxCtx[AsyncLocalStorage<br/>transaction context]
|
|
194
|
+
TxCtx --> Primary
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 3.1 Set up DataSources
|
|
198
|
+
|
|
199
|
+
Register each DataSource with `TypeOrmModule` **and** wrap it in `addTransactionalDataSource` so `@DefTransaction` can see it.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// databases.ts
|
|
203
|
+
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
204
|
+
import { DataSource, DataSourceOptions } from "typeorm";
|
|
205
|
+
import { addTransactionalDataSource } from "nestjs-typeorm3-kit";
|
|
206
|
+
|
|
207
|
+
const PRIMARY = "default";
|
|
208
|
+
const SECONDARY = "secondary";
|
|
209
|
+
|
|
210
|
+
export const primaryDb = TypeOrmModule.forRootAsync({
|
|
211
|
+
name: PRIMARY,
|
|
212
|
+
useFactory: () => ({ type: "postgres", /* ... */ }),
|
|
213
|
+
dataSourceFactory: async (options) =>
|
|
214
|
+
addTransactionalDataSource({
|
|
215
|
+
name: PRIMARY,
|
|
216
|
+
dataSource: new DataSource(options as DataSourceOptions),
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
export const secondaryDb = TypeOrmModule.forRootAsync({
|
|
221
|
+
name: SECONDARY,
|
|
222
|
+
// repeat the name here: @nestjs/typeorm reads it from these options on shutdown
|
|
223
|
+
useFactory: () => ({ name: SECONDARY, type: "postgres", /* ... */ }),
|
|
224
|
+
dataSourceFactory: async (options) =>
|
|
225
|
+
addTransactionalDataSource({
|
|
226
|
+
name: SECONDARY,
|
|
227
|
+
dataSource: new DataSource(options as DataSourceOptions),
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> 💡 `addTransactionalDataSource` is re-exported from `nestjs-typeorm3-kit` so you don't need a second import from `typeorm-transactional`.
|
|
233
|
+
|
|
234
|
+
### 3.2 Write your first repository
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// book.entity.ts
|
|
238
|
+
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
|
239
|
+
|
|
240
|
+
@Entity()
|
|
241
|
+
export class Book {
|
|
242
|
+
@PrimaryGeneratedColumn() id!: number;
|
|
243
|
+
@Column() title!: string;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// book.repo.ts
|
|
249
|
+
import { DefEntityRepository, RepositoryWrapper } from "nestjs-typeorm3-kit";
|
|
250
|
+
import { Book } from "./book.entity";
|
|
251
|
+
|
|
252
|
+
@DefEntityRepository(Book)
|
|
253
|
+
export class BookRepo extends RepositoryWrapper<Book> {
|
|
254
|
+
findByTitle(title: string) {
|
|
255
|
+
return this.findOne({ where: { title } });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
> ✨ Extending `RepositoryWrapper` instead of plain `Repository` makes `findOne` return `null` (instead of a random first row) when the `where` clause has any `undefined` value. See [`RepositoryWrapper`](#repositorywrapper).
|
|
261
|
+
|
|
262
|
+
### 3.3 Wire the repository module
|
|
263
|
+
|
|
264
|
+
`DefRepositoryModule` auto-discovers all `*.repo.{ts,js}` files that have a `@DefEntityRepository` decorator and registers them as providers.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// primary-repo.module.ts
|
|
93
268
|
import { Module } from "@nestjs/common";
|
|
94
269
|
import { join } from "path";
|
|
95
|
-
|
|
96
|
-
import { PRIMARY_CONNECTION } from "~/common/constants";
|
|
97
270
|
import { DefRepositoryModule } from "nestjs-typeorm3-kit";
|
|
271
|
+
|
|
98
272
|
@Module({
|
|
99
273
|
imports: [
|
|
100
|
-
DefRepositoryModule.
|
|
274
|
+
DefRepositoryModule.forRootAsync({
|
|
275
|
+
useFactory: () => ({
|
|
276
|
+
globPattern: join(__dirname, "./**/*.repo.{ts,js}"),
|
|
277
|
+
dataSource: "default",
|
|
278
|
+
}),
|
|
279
|
+
}),
|
|
101
280
|
],
|
|
102
281
|
exports: [DefRepositoryModule],
|
|
103
282
|
})
|
|
104
283
|
export class PrimaryRepoModule {}
|
|
105
284
|
```
|
|
106
285
|
|
|
107
|
-
|
|
286
|
+
Repeat with `dataSource: "secondary"` for the second connection.
|
|
108
287
|
|
|
109
|
-
|
|
110
|
-
import { Injectable } from "@nestjs/common";
|
|
111
|
-
import { DefTransaction, InjectRepo } from "nestjs-typeorm3-kit";
|
|
112
|
-
import { PhotoRepo } from "~/domains/primary/photo/photo.repo";
|
|
113
|
-
import { BookRepo } from "~/domains/primary/book/book.repo";
|
|
288
|
+
### 3.4 Inject the repository into a service
|
|
114
289
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@Injectable()
|
|
118
|
-
export class DemoService {
|
|
119
|
-
constructor(
|
|
120
|
-
readonly photoRepo: PhotoRepo,
|
|
121
|
-
@InjectRepo(BookRepo, PRIMARY_CONNECTION)
|
|
122
|
-
readonly bookRepo: BookRepo
|
|
123
|
-
) {}
|
|
290
|
+
For the **default** connection, plain constructor injection works.
|
|
291
|
+
For a **named** connection, use `@InjectRepo(Repo, connectionName)`.
|
|
124
292
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
293
|
+
```ts
|
|
294
|
+
// book.service.ts
|
|
295
|
+
import { Injectable } from "@nestjs/common";
|
|
296
|
+
import { InjectRepo } from "nestjs-typeorm3-kit";
|
|
297
|
+
import { BookRepo } from "./book.repo";
|
|
298
|
+
import { LogRepo } from "./log.repo";
|
|
130
299
|
|
|
131
|
-
// secondary connection
|
|
132
300
|
@Injectable()
|
|
133
|
-
export class
|
|
301
|
+
export class BookService {
|
|
134
302
|
constructor(
|
|
135
|
-
|
|
136
|
-
readonly
|
|
137
|
-
@InjectRepo(DataLogRepo, SECONDARY_CONNECTION)
|
|
138
|
-
readonly dataLogRepo: DataLogRepo
|
|
303
|
+
readonly bookRepo: BookRepo, // default connection
|
|
304
|
+
@InjectRepo(LogRepo, "secondary") readonly logRepo: LogRepo, // secondary
|
|
139
305
|
) {}
|
|
140
306
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return this.photoRepo.save(body);
|
|
307
|
+
list() {
|
|
308
|
+
return this.bookRepo.find();
|
|
144
309
|
}
|
|
145
310
|
}
|
|
146
311
|
```
|
|
147
312
|
|
|
148
|
-
|
|
313
|
+
### 3.5 Add a controller
|
|
149
314
|
|
|
150
|
-
```
|
|
151
|
-
|
|
315
|
+
```ts
|
|
316
|
+
// book.controller.ts
|
|
317
|
+
import { Body, Param, Query } from "@nestjs/common";
|
|
318
|
+
import { DefController, DefGet, DefPost } from "nestjs-typeorm3-kit";
|
|
319
|
+
import { BookService } from "./book.service";
|
|
152
320
|
|
|
153
|
-
@
|
|
154
|
-
export class
|
|
155
|
-
constructor(private readonly
|
|
321
|
+
@DefController("books")
|
|
322
|
+
export class BookController {
|
|
323
|
+
constructor(private readonly svc: BookService) {}
|
|
156
324
|
|
|
157
|
-
@DefGet()
|
|
158
|
-
list(@Query()
|
|
159
|
-
return this.
|
|
325
|
+
@DefGet("", { summary: "List all books" })
|
|
326
|
+
list(@Query() q: any) {
|
|
327
|
+
return this.svc.list();
|
|
160
328
|
}
|
|
161
329
|
|
|
162
|
-
@DefPost()
|
|
163
|
-
create(@Body() body:
|
|
164
|
-
return this.
|
|
330
|
+
@DefPost("", { summary: "Create a book" })
|
|
331
|
+
create(@Body() body: { title: string }) {
|
|
332
|
+
return this.svc.create(body.title);
|
|
165
333
|
}
|
|
166
334
|
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`@DefGet/Post/...` bundle `@Get/@Post`, `@HttpCode`, `@ApiOperation`, `@ApiResponse` (and optionally `@ApiBody`) into one call.
|
|
338
|
+
|
|
339
|
+
### 3.6 Group modules with `@ChildModule`
|
|
340
|
+
|
|
341
|
+
`@ChildModule` mounts the module under a route prefix and auto-tags its controllers in Swagger.
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// client.module.ts
|
|
345
|
+
import { ChildModule } from "nestjs-typeorm3-kit";
|
|
167
346
|
|
|
168
|
-
// child module suport prefix route and API TAG swagger
|
|
169
347
|
@ChildModule({
|
|
170
|
-
prefix:
|
|
348
|
+
prefix: "client",
|
|
171
349
|
imports: [PrimaryRepoModule, SecondaryRepoModule],
|
|
172
|
-
|
|
173
|
-
|
|
350
|
+
controllers: [BookController],
|
|
351
|
+
providers: [BookService],
|
|
174
352
|
})
|
|
175
|
-
export class ClientModule
|
|
176
|
-
|
|
353
|
+
export class ClientModule {}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Resulting routes: `GET /client/books`, `POST /client/books`. Swagger tag: `[Client] BookController`.
|
|
357
|
+
|
|
358
|
+
### 3.7 Add transactions
|
|
359
|
+
|
|
360
|
+
Decorate any service method with `@DefTransaction()`. Without `connectionName` it opens a transaction on the **default** DataSource only: `bookRepo` writes inside it, while `logRepo` (on `secondary`) writes outside it. Two databases cannot share one SQL transaction — see [Multi-DataSource transactions](./docs/en/transaction.md#multi-datasource-transactions).
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
// book.service.ts (continued)
|
|
364
|
+
import { DefTransaction } from "nestjs-typeorm3-kit";
|
|
365
|
+
|
|
366
|
+
@DefTransaction()
|
|
367
|
+
async create(title: string) {
|
|
368
|
+
const book = await this.bookRepo.save({ title });
|
|
369
|
+
// If this throws, the bookRepo.save above is rolled back too.
|
|
370
|
+
// The log row itself is not transactional: it lives on another DataSource.
|
|
371
|
+
await this.logRepo.save({ action: "book.created", refId: book.id });
|
|
372
|
+
return book;
|
|
177
373
|
}
|
|
178
|
-
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### How propagation works
|
|
377
|
+
|
|
378
|
+
```mermaid
|
|
379
|
+
flowchart TD
|
|
380
|
+
Call[Method call] --> HasTx{In a transaction<br/>already?}
|
|
381
|
+
HasTx -- "REQUIRED (default)" --> Join1[Join existing] & New1[Start new]
|
|
382
|
+
HasTx -- "REQUIRES_NEW" --> Suspend[Suspend outer<br/>and start fresh]
|
|
383
|
+
HasTx -- "NESTED" --> Save[Savepoint inside outer]
|
|
384
|
+
HasTx -- "MANDATORY" --> Throw1{No tx? throw}
|
|
385
|
+
HasTx -- "NEVER" --> Throw2{Has tx? throw}
|
|
386
|
+
HasTx -- "SUPPORTS" --> Pass[Run with or without]
|
|
387
|
+
HasTx -- "NOT_SUPPORTED" --> Pause[Pause tx, run plain]
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Pass `propagation`, `isoLevel`, and `connectionName` to target a specific DataSource:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
@DefTransaction({
|
|
394
|
+
connectionName: "secondary",
|
|
395
|
+
propagation: Propagation.REQUIRES_NEW,
|
|
396
|
+
isoLevel: "SERIALIZABLE",
|
|
397
|
+
})
|
|
398
|
+
async audit(payload: AuditPayload) { /* ... */ }
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### 3.8 Enable Swagger
|
|
402
|
+
|
|
403
|
+
```ts
|
|
179
404
|
// main.ts
|
|
180
405
|
import { NestFactory } from "@nestjs/core";
|
|
181
|
-
import { AppModule } from "./app.module";
|
|
182
406
|
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
|
183
|
-
import {
|
|
184
|
-
import { SchemasObject } from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
|
|
407
|
+
import { configSwaggerDocument, setupTransactionContext } from "nestjs-typeorm3-kit";
|
|
185
408
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
409
|
+
async function bootstrap() {
|
|
410
|
+
setupTransactionContext();
|
|
411
|
+
const app = await NestFactory.create(AppModule);
|
|
189
412
|
|
|
190
|
-
const configSwagger = (app: INestApplication) => {
|
|
191
413
|
const options = new DocumentBuilder()
|
|
192
|
-
.setTitle("
|
|
193
|
-
.
|
|
194
|
-
.
|
|
195
|
-
.addSecurity("bearer", {
|
|
196
|
-
type: "http",
|
|
197
|
-
scheme: "bearer",
|
|
198
|
-
bearerFormat: "JWT",
|
|
199
|
-
})
|
|
414
|
+
.setTitle("My API")
|
|
415
|
+
.setVersion("1.0")
|
|
416
|
+
.addBearerAuth()
|
|
200
417
|
.build();
|
|
201
418
|
|
|
202
|
-
const document = SwaggerModule.createDocument(app
|
|
203
|
-
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Creating all the swagger schemas based on the class-validator decorators
|
|
207
|
-
const metadatas = (getFromContainer(MetadataStorage) as any)
|
|
208
|
-
.validationMetadatas;
|
|
209
|
-
const targetSchemas = document.components.schemas || {};
|
|
210
|
-
const schemasBinding = validationMetadatasToSchemas(metadatas) || {};
|
|
211
|
-
|
|
212
|
-
Object.keys(schemasBinding).forEach((key) => {
|
|
213
|
-
const value = schemasBinding[key] as SchemasObject;
|
|
214
|
-
if (!targetSchemas[key]) {
|
|
215
|
-
Object.assign(targetSchemas, { key: value });
|
|
216
|
-
} else {
|
|
217
|
-
const targetValue = targetSchemas[key] as SchemasObject;
|
|
218
|
-
|
|
219
|
-
Object.assign(targetValue.properties, value.properties);
|
|
220
|
-
targetValue.required = value.required;
|
|
221
|
-
Object.assign(targetSchemas, { key: targetValue });
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
document.components.schemas = Object.assign({}, targetSchemas);
|
|
225
|
-
SwaggerModule.setup("swagger", app as any, document);
|
|
226
|
-
};
|
|
419
|
+
const document = SwaggerModule.createDocument(app, options);
|
|
420
|
+
configSwaggerDocument(app, document, "swagger"); // → GET /swagger
|
|
227
421
|
|
|
228
|
-
|
|
229
|
-
setupTransactionContext();
|
|
230
|
-
const app = await NestFactory.create(AppModule);
|
|
231
|
-
const port = process.env.PORT || 3000;
|
|
232
|
-
app.enableCors({ origin: "*" });
|
|
233
|
-
configSwagger(app);
|
|
234
|
-
await app.listen(port);
|
|
235
|
-
console.log(
|
|
236
|
-
`Server start on port ${port}. Open http://localhost:${port} to see results`
|
|
237
|
-
);
|
|
238
|
-
console.log(`API DOCUMENT Open http://localhost:${port}/swagger`);
|
|
239
|
-
console.log(`API DOCUMENT JSON Open http://localhost:${port}/swagger-json`);
|
|
240
|
-
console.log("TIMEZONE: ", process.env.TZ);
|
|
422
|
+
await app.listen(3000);
|
|
241
423
|
}
|
|
242
424
|
bootstrap();
|
|
243
425
|
```
|
|
244
426
|
|
|
427
|
+
That's a full app. The rest of the README is the API reference.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## 4. Relationship to `typeorm-transactional`
|
|
432
|
+
|
|
433
|
+
`nestjs-typeorm3-kit` is a **wrapper fork** of [`typeorm-transactional`](https://github.com/Aliheym/typeorm-transactional). Concretely:
|
|
434
|
+
|
|
435
|
+
- **Vendored:** the entire `typeorm-transactional@v0.5.0` source lives in [`src/vendor/transactional/`](./src/vendor/transactional). The MIT [LICENSE](./src/vendor/transactional/LICENSE) (Copyright © Ohad David) is preserved verbatim.
|
|
436
|
+
- **Wrapped:** `setupTransactionContext` and `@DefTransaction` are thin wrappers over the vendored `initializeTransactionalContext` and `@Transactional` decorators.
|
|
437
|
+
- **Re-exported:** `Transactional`, `Propagation`, `IsolationLevel`, `StorageDriver`, `addTransactionalDataSource`, `runInTransaction`, `wrapInTransaction`, `runOnTransactionCommit`, `runOnTransactionRollback`, `runOnTransactionComplete`, `TransactionalError`, `getDataSourceByName`, `deleteDataSourceByName` are all re-exported from `nestjs-typeorm3-kit` so you only need one import path.
|
|
438
|
+
|
|
439
|
+
<details>
|
|
440
|
+
<summary><b>What changed vs. upstream</b></summary>
|
|
441
|
+
|
|
442
|
+
| Area | Upstream `v0.5.0` | This fork (v0.2.0+) |
|
|
443
|
+
| ----------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- |
|
|
444
|
+
| Storage drivers | `cls-hooked` + `AsyncLocalStorage` | Only `AsyncLocalStorage` |
|
|
445
|
+
| Runtime dependencies | `cls-hooked`, `@types/cls-hooked`, `semver` | None |
|
|
446
|
+
| Bun support | ❌ broken (`async_hooks` incomplete) | ✅ works |
|
|
447
|
+
| Node requirement | `>= 14` | `>= 16.4` |
|
|
448
|
+
| Public API (decorators, hooks, propagation, isolation)| — | **Identical** |
|
|
449
|
+
| `initializeTransactionalContext` options | `{ maxHookHandlers, storageDriver }` | `{ maxHookHandlers }` — `storageDriver` removed (only one driver) |
|
|
450
|
+
| `StorageDriver` enum values | `AUTO`, `CLS_HOOKED`, `ASYNC_LOCAL_STORAGE` | `AUTO` (alias), `ASYNC_LOCAL_STORAGE` |
|
|
451
|
+
|
|
452
|
+
</details>
|
|
453
|
+
|
|
454
|
+
<details>
|
|
455
|
+
<summary><b>Migrating from <code>typeorm-transactional</code></b></summary>
|
|
456
|
+
|
|
457
|
+
```diff
|
|
458
|
+
- import { Transactional, Propagation, addTransactionalDataSource } from 'typeorm-transactional';
|
|
459
|
+
+ import { Transactional, Propagation, addTransactionalDataSource } from 'nestjs-typeorm3-kit';
|
|
460
|
+
|
|
461
|
+
- import { initializeTransactionalContext, StorageDriver } from 'typeorm-transactional';
|
|
462
|
+
- initializeTransactionalContext({ storageDriver: StorageDriver.CLS_HOOKED });
|
|
463
|
+
+ import { setupTransactionContext } from 'nestjs-typeorm3-kit';
|
|
464
|
+
+ setupTransactionContext();
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Then **uninstall** `typeorm-transactional` — it is no longer needed.
|
|
468
|
+
|
|
469
|
+
</details>
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## 5. Repository API
|
|
474
|
+
|
|
475
|
+
### `@DefEntityRepository`
|
|
476
|
+
|
|
477
|
+
Marks a class as a TypeORM repository bound to an entity. Your class should extend `Repository<Entity>` or `RepositoryWrapper<Entity>`.
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
@DefEntityRepository(Book)
|
|
481
|
+
export class BookRepo extends Repository<Book> {
|
|
482
|
+
findPublished() { return this.find({ where: { published: true } }); }
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Under the hood the decorator stores the entity on the class via `Reflect.defineMetadata`. `DefRepositoryModule` reads that metadata to wire up providers.
|
|
487
|
+
|
|
488
|
+
### `DefRepositoryModule`
|
|
489
|
+
|
|
490
|
+
Registers repository providers for one or more DataSources. Three styles:
|
|
491
|
+
|
|
492
|
+
#### `forFeature(repositories, dataSource?)`
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
DefRepositoryModule.forFeature([BookRepo, AuthorRepo]);
|
|
496
|
+
DefRepositoryModule.forFeature([LogRepo], "secondary");
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
#### `forRoot({ globPattern, dataSource? })`
|
|
500
|
+
|
|
501
|
+
Returns a `Promise<DynamicModule>`. Use `forRootAsync` when the options come from a factory or options class, or the module needs extra `imports`.
|
|
502
|
+
|
|
503
|
+
#### `forRootAsync({ useFactory | useClass })`
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
DefRepositoryModule.forRootAsync({
|
|
507
|
+
useFactory: async () => ({
|
|
508
|
+
globPattern: join(__dirname, "./**/*.repo.{ts,js}"),
|
|
509
|
+
dataSource: "default",
|
|
510
|
+
}),
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Repository discovery and provider tokens are resolved **before Nest bootstraps**.
|
|
515
|
+
`useFactory` runs once without injected arguments; `useClass` constructs a
|
|
516
|
+
dependency-free options factory once. `inject` and `useExisting` are not
|
|
517
|
+
supported: Nest providers do not exist yet when the glob must be scanned.
|
|
518
|
+
`imports` remains available for the resulting dynamic module.
|
|
519
|
+
|
|
520
|
+
<details>
|
|
521
|
+
<summary><b><code>DefRepositoryModuleOptions</code></b></summary>
|
|
522
|
+
|
|
523
|
+
| Field | Type | Required | Description |
|
|
524
|
+
| ------------- | ------------------------------------------- | :------: | ------------------------------------------------------------------------- |
|
|
525
|
+
| `globPattern` | `string` | ✔ | Absolute glob used to discover `*.repo.ts` / `*.repo.js` files. |
|
|
526
|
+
| `dataSource` | `string \| DataSource \| DataSourceOptions` | | DataSource name. Defaults to `"default"`. Determines the injection token. |
|
|
527
|
+
|
|
528
|
+
</details>
|
|
529
|
+
|
|
530
|
+
### `@InjectRepo`
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
constructor(
|
|
534
|
+
@InjectRepo(BookRepo) readonly bookRepo: BookRepo, // default
|
|
535
|
+
@InjectRepo(LogRepo, "secondary") readonly logRepo: LogRepo, // named
|
|
536
|
+
@InjectRepo(AuditRepo, opts) readonly auditRepo: AuditRepo, // by DataSourceOptions
|
|
537
|
+
) {}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
For the default connection you can also use plain constructor injection (`readonly repo: BookRepo`).
|
|
541
|
+
|
|
542
|
+
### `RepositoryWrapper`
|
|
543
|
+
|
|
544
|
+
`TypeOrmRepository.findOne({ where: { id: undefined } })` silently strips `undefined` and returns the **first row of the table** — a footgun that can cause data leaks or auth bypasses.
|
|
545
|
+
|
|
546
|
+
`RepositoryWrapper` overrides `findOne` to return `null` when:
|
|
547
|
+
|
|
548
|
+
- `options` is missing or not an object
|
|
549
|
+
- `options.where` is falsy or not an object
|
|
550
|
+
- `where` is empty (`{}`, `[]`, or an OR branch with `{}`)
|
|
551
|
+
- any mapped column or relation/embedded predicate in `where` has an `undefined` value or an empty nested condition
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
export class BaseRepo<Entity> extends RepositoryWrapper<Entity> {}
|
|
555
|
+
|
|
556
|
+
@DefEntityRepository(Book)
|
|
557
|
+
export class BookRepo extends BaseRepo<Book> {}
|
|
558
|
+
|
|
559
|
+
await bookRepo.findOne({ where: { id: undefined } }); // → null (safe)
|
|
560
|
+
await bookRepo.findOne({ where: {} }); // → null (no filter)
|
|
561
|
+
await bookRepo.findOne({ where: { author: { id: undefined } } }); // → null
|
|
562
|
+
await bookRepo.findOne({ where: { id: 1 } }); // → Book | null
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## 6. Transactions
|
|
568
|
+
|
|
569
|
+
### `setupTransactionContext`
|
|
570
|
+
|
|
571
|
+
Must be called in `main.ts` **before** `NestFactory.create`. Initialises the vendored transactional layer.
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
setupTransactionContext();
|
|
575
|
+
// or with options:
|
|
576
|
+
setupTransactionContext({ maxHookHandlers: 50 });
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
<details>
|
|
580
|
+
<summary><b>Options</b></summary>
|
|
581
|
+
|
|
582
|
+
| Field | Type | Default | Description |
|
|
583
|
+
| ----------------- | -------- | ------- | ---------------------------------------------------- |
|
|
584
|
+
| `maxHookHandlers` | `number` | `10` | Hooks of one type per transaction before Node warns about a possible leak (not a hard limit). |
|
|
585
|
+
|
|
586
|
+
</details>
|
|
587
|
+
|
|
588
|
+
#### Runtime support
|
|
589
|
+
|
|
590
|
+
| Runtime | Supported | Why |
|
|
591
|
+
| ----------------- | :--------: | -------------------------------------------------------------------------------------------------------------------- |
|
|
592
|
+
| Node.js >= 16.4 | ✅ | `AsyncLocalStorage` is native. |
|
|
593
|
+
| Bun | ✅ | The vendored fork dropped `cls-hooked` (which Bun can't run) — only `AsyncLocalStorage` is used. |
|
|
594
|
+
| Node.js < 16.4 | ❌ | Upgrade Node, or stay on `nestjs-typeorm3-kit@0.1.x`. |
|
|
595
|
+
|
|
596
|
+
### `@DefTransaction`
|
|
597
|
+
|
|
598
|
+
Decorator-based transactions — wraps the vendored `@Transactional`.
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
import { DefTransaction, IsolationLevel, Propagation } from "nestjs-typeorm3-kit";
|
|
602
|
+
|
|
603
|
+
class BookService {
|
|
604
|
+
@DefTransaction() async create(dto: CreateBookDto) { /* ... */ }
|
|
605
|
+
|
|
606
|
+
@DefTransaction({ connectionName: "secondary" })
|
|
607
|
+
async audit(dto: AuditDto) { /* ... */ }
|
|
608
|
+
|
|
609
|
+
@DefTransaction({
|
|
610
|
+
propagation: Propagation.REQUIRES_NEW,
|
|
611
|
+
isolationLevel: IsolationLevel.READ_COMMITTED,
|
|
612
|
+
})
|
|
613
|
+
async complex() { /* ... */ }
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
<details>
|
|
618
|
+
<summary><b>Options</b></summary>
|
|
619
|
+
|
|
620
|
+
| Field | Type | Description |
|
|
621
|
+
| ---------------- | ------------------------------------------------------------------------------- | -------------------------------------- |
|
|
622
|
+
| `connectionName` | `string \| (() => string \| undefined)` | DataSource name. |
|
|
623
|
+
| `propagation` | `Propagation` | Propagation strategy. |
|
|
624
|
+
| `isolationLevel` | `IsolationLevel` or its enum key | SQL transaction isolation level. |
|
|
625
|
+
| `isoLevel` | `"READ_UNCOMMITTED" \| "READ_COMMITTED" \| "REPEATABLE_READ" \| "SERIALIZABLE"` | Legacy alias for `isolationLevel`. |
|
|
626
|
+
|
|
627
|
+
</details>
|
|
628
|
+
|
|
629
|
+
Parallel `NESTED` calls on the same transaction are serialized at the
|
|
630
|
+
savepoint boundary; a failed child cannot roll back a sibling's write.
|
|
631
|
+
Transactions on different DataSources remain independent.
|
|
632
|
+
|
|
633
|
+
#### Transaction hooks
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
import { runOnTransactionCommit, runOnTransactionRollback, runOnTransactionComplete } from "nestjs-typeorm3-kit";
|
|
637
|
+
|
|
638
|
+
@DefTransaction()
|
|
639
|
+
async create(dto: CreateBookDto) {
|
|
640
|
+
const book = await this.repo.save(dto);
|
|
641
|
+
runOnTransactionCommit(() => this.events.emit("book.created", book));
|
|
642
|
+
runOnTransactionRollback((err) => this.logger.warn("rolled back", err));
|
|
643
|
+
return book;
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## 7. Controller & module helpers
|
|
650
|
+
|
|
651
|
+
### `@DefController` and method decorators
|
|
652
|
+
|
|
653
|
+
`@DefController` is like `@Controller` but also applies `@ApiBearerAuth()`. Each method decorator bundles `@Get/Post/...`, `@HttpCode`, `@ApiOperation`, `@ApiResponse`, optionally `@ApiBody`.
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
@DefController("books")
|
|
657
|
+
export class BookController {
|
|
658
|
+
@DefGet("", { summary: "List books", responseType: [BookDto] })
|
|
659
|
+
list(@Query() q: ListQuery) { /* ... */ }
|
|
660
|
+
|
|
661
|
+
@DefPost("", { bodyType: CreateBookDto, responseType: BookDto })
|
|
662
|
+
create(@Body() dto: CreateBookDto) { /* ... */ }
|
|
663
|
+
|
|
664
|
+
@DefPatch(":id", { bodyType: UpdateBookDto })
|
|
665
|
+
update(@Param("id") id: string, @Body() dto: UpdateBookDto) { /* ... */ }
|
|
666
|
+
|
|
667
|
+
@DefDelete(":id", { statusCode: 204 })
|
|
668
|
+
remove(@Param("id") id: string) { /* ... */ }
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
<details>
|
|
673
|
+
<summary><b>Method options</b></summary>
|
|
674
|
+
|
|
675
|
+
| Field | Type | Default |
|
|
676
|
+
| -------------- | ---------------------------------------- | --------------- |
|
|
677
|
+
| `summary` | `string` | `""` |
|
|
678
|
+
| `statusCode` | `HttpStatus` | `HttpStatus.OK` |
|
|
679
|
+
| `responseType` | `Type \| Function \| [Function] \| string` | `undefined` |
|
|
680
|
+
| `bodyType`\* | `Type \| Function \| [Function] \| string` | `undefined` |
|
|
681
|
+
|
|
682
|
+
\* `bodyType` is only on `DefPost`, `DefPut`, `DefPatch`, `DefDelete`.
|
|
683
|
+
|
|
684
|
+
</details>
|
|
685
|
+
|
|
686
|
+
### `@ChildModule`
|
|
687
|
+
|
|
688
|
+
Mounts a module under a route prefix, auto-tags its controllers in Swagger as `[PascalPrefix] ControllerName`, and propagates the prefix down to imported modules.
|
|
689
|
+
|
|
690
|
+
```ts
|
|
691
|
+
@ChildModule({
|
|
692
|
+
prefix: "client",
|
|
693
|
+
imports: [BookRepoModule],
|
|
694
|
+
controllers: [BookController],
|
|
695
|
+
providers: [BookService],
|
|
696
|
+
})
|
|
697
|
+
export class ClientModule {}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
An imported child can be mounted under different parent prefixes. Each mount
|
|
701
|
+
has its own module instance and providers; the child class's original route
|
|
702
|
+
metadata is not overwritten.
|
|
703
|
+
|
|
704
|
+
When selecting Swagger routes with `include: [ChildModuleClass]`, use
|
|
705
|
+
`createScopedSwaggerDocument` instead of `SwaggerModule.createDocument` to
|
|
706
|
+
include every prefixed mount of that public module class.
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
## 8. Swagger helper
|
|
711
|
+
|
|
712
|
+
### `createScopedSwaggerDocument(app, config, options?)` and `configSwaggerDocument`
|
|
713
|
+
|
|
714
|
+
`createScopedSwaggerDocument` expands `include` to prefixed child mounts. With
|
|
715
|
+
no `include`, it behaves like `SwaggerModule.createDocument`. Then
|
|
716
|
+
`configSwaggerDocument` merges `class-validator` metadata into
|
|
717
|
+
`components.schemas` without replacing Swagger's existing property definitions
|
|
718
|
+
and mounts the Swagger UI.
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
const document = createScopedSwaggerDocument(app, options, {
|
|
722
|
+
include: [ClientModule],
|
|
723
|
+
});
|
|
724
|
+
configSwaggerDocument(app, document, "swagger"); // → GET /swagger
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
> 🐛 **Historical note:** versions before `0.2.0` had a bug where every schema was merged under the literal key `"key"` due to a missing computed property name. Use the helper instead of copy-pasting it.
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## 9. Validation decorators
|
|
732
|
+
|
|
733
|
+
### `@IsNumber({ required? })`
|
|
734
|
+
|
|
735
|
+
Coerces to number. Optional by default — `{ required: true }` makes it required.
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
class Query {
|
|
739
|
+
@IsNumber() page?: number;
|
|
740
|
+
@IsNumber({ required: true }) limit!: number;
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### `@IsPhoneNumber(region, options?)`
|
|
745
|
+
|
|
746
|
+
Casts to `string` and delegates to `class-validator`'s `IsPhoneNumber`.
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
class Dto {
|
|
750
|
+
@IsPhoneNumber("VN") phone!: string;
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### `@IsLongerThan(otherProperty, options?)`
|
|
755
|
+
|
|
756
|
+
Passes when the decorated string is strictly longer than another property on the same object.
|
|
757
|
+
|
|
758
|
+
```ts
|
|
759
|
+
class ResetPasswordDto {
|
|
760
|
+
oldPassword!: string;
|
|
761
|
+
@IsLongerThan("oldPassword") newPassword!: string;
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## 10. Utilities
|
|
768
|
+
|
|
769
|
+
### `lazyLoadClasses(baseDir, suffixes, additional?)`
|
|
770
|
+
|
|
771
|
+
Scans `baseDir`'s immediate subdirectories for files matching the given suffixes (e.g. `.controller`, `.service`) and returns the first exported class from each.
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
@Module({
|
|
775
|
+
controllers: lazyLoadClasses(__dirname, [".controller"]),
|
|
776
|
+
providers: lazyLoadClasses(__dirname, [".service"]),
|
|
777
|
+
})
|
|
778
|
+
export class ClientModule {}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
> Assumes one exported class per file.
|
|
782
|
+
|
|
783
|
+
### `addTransactionalDataSource` (re-export)
|
|
784
|
+
|
|
785
|
+
Re-exported from the vendored `typeorm-transactional` for convenience.
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## 11. Full example
|
|
790
|
+
|
|
791
|
+
A runnable Nest project lives in [`./example`](./example) — two Postgres connections, `ChildModule`, `@DefTransaction` across connections, Swagger wired up end-to-end.
|
|
792
|
+
|
|
793
|
+
```bash
|
|
794
|
+
cd example
|
|
795
|
+
yarn install
|
|
796
|
+
yarn start:dev
|
|
797
|
+
# http://localhost:3000/swagger
|
|
798
|
+
```
|
|
799
|
+
|
|
245
800
|

|
|
246
801
|
|
|
247
|
-
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## 12. Testing
|
|
805
|
+
|
|
806
|
+
The package ships with two test configurations:
|
|
807
|
+
|
|
808
|
+
- **Unit tests** (`yarn test`) — fast, no external dependencies (in-memory `sql.js`). They live next to the source as `src/**/*.spec.ts`, with fixtures in `__fixtures__/`, and are excluded from the build by `tsconfig.build.json`. Run `yarn test --coverage` for a coverage report.
|
|
809
|
+
- **Integration tests** (`yarn test:integration`) — spin up the Nest module against a real Postgres (see `tests/src/app.module.ts`). Requires a Postgres instance matching the configured host/port.
|
|
810
|
+
|
|
811
|
+
```bash
|
|
812
|
+
yarn test # unit tests
|
|
813
|
+
yarn test:integration # integration tests (needs Postgres)
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
## 13. Compatibility
|
|
819
|
+
|
|
820
|
+
**Runtime:** Node.js `>= 16.4` or Bun for the package itself; NestJS 12 and `@nestjs/typeorm` 12 require Node.js `>= 20.19`.
|
|
821
|
+
|
|
822
|
+
<details>
|
|
823
|
+
<summary><b>Peer dependency matrix</b></summary>
|
|
824
|
+
|
|
825
|
+
| Peer dependency | Range |
|
|
826
|
+
| ----------------------------- | ------------------ |
|
|
827
|
+
| `@nestjs/common`, `core` | `>= 8.0.0 < 13` |
|
|
828
|
+
| `@nestjs/typeorm` | `>= 9.0.0 < 13` |
|
|
829
|
+
| `@nestjs/swagger` | `>= 5.0.0 < 13` |
|
|
830
|
+
| `typeorm` | `>= 0.3.0 < 1` |
|
|
831
|
+
| `reflect-metadata` | `>= 0.1.13 < 0.3` |
|
|
832
|
+
| `rxjs` | `>= 7.2.0 < 8` |
|
|
833
|
+
| `class-validator-jsonschema` | `^5.0.2` |
|
|
834
|
+
|
|
835
|
+
</details>
|
|
836
|
+
|
|
837
|
+
`fast-glob` is installed as a regular dependency. Swagger and JSON-schema are peers because public decorator and helper exports import them at module load, even when the application only uses repositories.
|
|
838
|
+
|
|
839
|
+
Developed against NestJS v12 / TypeORM 0.3.x. NestJS v8–v11 remain in the declared range, but exercise your application's integration tests before upgrading. Use a matching `@nestjs/typeorm` major for the NestJS version in your application.
|
|
840
|
+
|
|
841
|
+
Since v0.2.0, `typeorm-transactional` is **vendored**; the original package is no longer a runtime or peer dependency.
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
## 14. Troubleshooting
|
|
846
|
+
|
|
847
|
+
<details>
|
|
848
|
+
<summary><b><code>UnknownDependencyException: Nest can't resolve dependencies of ...</code></b></summary>
|
|
849
|
+
|
|
850
|
+
You forgot to import the `DefRepositoryModule`-wrapping module, or the repository's `@DefEntityRepository` decorator is missing. Also make sure the `globPattern` actually matches your file names (the module only picks up classes decorated with `@DefEntityRepository`).
|
|
851
|
+
|
|
852
|
+
</details>
|
|
853
|
+
|
|
854
|
+
<details>
|
|
855
|
+
<summary><b><code>No storage driver defined in your app</code> / <code>No data sources defined in your app</code></b></summary>
|
|
856
|
+
|
|
857
|
+
- `No storage driver defined …` — `setupTransactionContext()` was not called, or ran too late. It must run **before** `NestFactory.create(AppModule)`.
|
|
858
|
+
- `No data sources defined …` — the DataSource named by `connectionName` (default `"default"`) was never passed to `addTransactionalDataSource(...)`.
|
|
859
|
+
|
|
860
|
+
</details>
|
|
861
|
+
|
|
862
|
+
<details>
|
|
863
|
+
<summary><b>Transactions silently lose context on Bun</b></summary>
|
|
864
|
+
|
|
865
|
+
Upgrade to **v0.2.0+**. The vendored fork dropped `cls-hooked` entirely and uses `AsyncLocalStorage` only, which works on Bun.
|
|
866
|
+
|
|
867
|
+
</details>
|
|
868
|
+
|
|
869
|
+
<details>
|
|
870
|
+
<summary><b><code>findOne</code> returns <code>null</code> for a query I expect to work</b></summary>
|
|
871
|
+
|
|
872
|
+
That's the `RepositoryWrapper` guard at work — one of the values passed in `where` is `undefined`. Either provide a real value or an `IsNull()`/`Not()` clause. If you need raw TypeORM behaviour, use `Repository` instead of `RepositoryWrapper`.
|
|
873
|
+
|
|
874
|
+
</details>
|
|
875
|
+
|
|
876
|
+
<details>
|
|
877
|
+
<summary><b>Swagger schemas merged into a key literally called <code>"key"</code></b></summary>
|
|
878
|
+
|
|
879
|
+
Upgrade to `nestjs-typeorm3-kit >= 0.2.0` and call `configSwaggerDocument(app, document)` instead of copy-pasting the legacy snippet.
|
|
880
|
+
|
|
881
|
+
</details>
|
|
882
|
+
|
|
883
|
+
<details>
|
|
884
|
+
<summary><b>Repositories not picked up by the glob pattern</b></summary>
|
|
885
|
+
|
|
886
|
+
Paths in the glob **must be absolute** (use `join(__dirname, '...')`). On Windows the module rewrites `\` → `/` automatically.
|
|
887
|
+
|
|
888
|
+
</details>
|
|
248
889
|
|
|
249
|
-
|
|
890
|
+
---
|
|
250
891
|
|
|
251
|
-
|
|
892
|
+
## License
|
|
252
893
|
|
|
253
|
-
[
|
|
894
|
+
MIT © [x302502](mailto:thanhluan.bkhn@gmail.com)
|