@nestjs-transactional/typeorm 1.0.0-alpha.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 +361 -0
- package/dist/adapter/typeorm.adapter.d.ts +39 -0
- package/dist/adapter/typeorm.adapter.js +79 -0
- package/dist/adapter/typeorm.adapter.js.map +1 -0
- package/dist/helpers/get-entity-manager.d.ts +27 -0
- package/dist/helpers/get-entity-manager.js +52 -0
- package/dist/helpers/get-entity-manager.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/module/typeorm-transactional.module.d.ts +157 -0
- package/dist/module/typeorm-transactional.module.js +289 -0
- package/dist/module/typeorm-transactional.module.js.map +1 -0
- package/dist/patching/data-source-patches.d.ts +48 -0
- package/dist/patching/data-source-patches.js +139 -0
- package/dist/patching/data-source-patches.js.map +1 -0
- package/dist/patching/entity-manager-patches.d.ts +31 -0
- package/dist/patching/entity-manager-patches.js +86 -0
- package/dist/patching/entity-manager-patches.js.map +1 -0
- package/dist/patching/index.d.ts +49 -0
- package/dist/patching/index.js +75 -0
- package/dist/patching/index.js.map +1 -0
- package/dist/patching/managed-registry.d.ts +59 -0
- package/dist/patching/managed-registry.js +112 -0
- package/dist/patching/managed-registry.js.map +1 -0
- package/dist/patching/repository-patches.d.ts +56 -0
- package/dist/patching/repository-patches.js +150 -0
- package/dist/patching/repository-patches.js.map +1 -0
- package/dist/patching/symbols.d.ts +53 -0
- package/dist/patching/symbols.js +56 -0
- package/dist/patching/symbols.js.map +1 -0
- package/dist/types/typeorm-transaction-handle.d.ts +17 -0
- package/dist/types/typeorm-transaction-handle.js +3 -0
- package/dist/types/typeorm-transaction-handle.js.map +1 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Golovanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# @nestjs-transactional/typeorm
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@nestjs-transactional/typeorm)
|
|
4
|
+
[](https://github.com/igorgolovanov/nestjs-transactional/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
TypeORM adapter for [`@nestjs-transactional/core`](../core).
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
- `TypeOrmTransactionAdapter` — implements the core
|
|
11
|
+
`TransactionAdapter` SPI over TypeORM's `DataSource`. Handles
|
|
12
|
+
BEGIN / COMMIT / ROLLBACK via `DataSource.transaction(...)` and
|
|
13
|
+
issues raw `SAVEPOINT` / `ROLLBACK TO SAVEPOINT` /
|
|
14
|
+
`RELEASE SAVEPOINT` SQL for nested transactions.
|
|
15
|
+
- **Transparent transactional repositories** —
|
|
16
|
+
`@InjectRepository(Entity)` instances,
|
|
17
|
+
`@InjectEntityManager() em.getRepository(E)`,
|
|
18
|
+
`@InjectDataSource() ds.manager.save(...)`, and
|
|
19
|
+
`ds.getRepository(E).save(...)` automatically dispatch through the
|
|
20
|
+
active `@Transactional()` scope's `EntityManager`. No
|
|
21
|
+
`getCurrentEntityManager()` boilerplate. Custom repositories via
|
|
22
|
+
`Repository.extend(...)` and `TreeRepository` work transparently.
|
|
23
|
+
See [Transparent transactional behaviour](#transparent-transactional-behaviour)
|
|
24
|
+
below.
|
|
25
|
+
- `getCurrentEntityManager(dataSource?, fallback?)` — escape-hatch
|
|
26
|
+
helper that returns the transaction-aware `EntityManager` from the
|
|
27
|
+
current async context (or falls back to `dataSource.manager`
|
|
28
|
+
outside a transaction). Mostly needed for the documented
|
|
29
|
+
limitations below; standard injection paths cover everything else.
|
|
30
|
+
- `isInTransaction(dataSource?)` — predicate for the current
|
|
31
|
+
context.
|
|
32
|
+
- `TypeOrmTransactionalModule.forRoot({ dataSource?, isDefault? })` —
|
|
33
|
+
NestJS dynamic module that activates the transparent patches and
|
|
34
|
+
registers an adapter with the core `AdapterRegistry`. The
|
|
35
|
+
`DataSource` itself resolves from DI under
|
|
36
|
+
`getDataSourceToken(dataSource)` — the same convention
|
|
37
|
+
`@nestjs/typeorm` uses for `@InjectRepository(E, dataSource)`.
|
|
38
|
+
- `TypeOrmTransactionalModule.forRootAsync({ useFactory, inject?, imports? })`
|
|
39
|
+
— async variant for `ConfigService`-driven setups. Registers via
|
|
40
|
+
`OnModuleInit` to defer DataSource resolution past `@nestjs/typeorm`'s
|
|
41
|
+
async DataSource provider settling
|
|
42
|
+
([Convention #22](../../docs/status/conventions.md)).
|
|
43
|
+
- Multi-DataSource: call `forRoot` once per dataSource. Mirrors
|
|
44
|
+
[`OutboxModule`](../outbox) (ADR-019) and
|
|
45
|
+
[`TransactionalModule`](../core).
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add @nestjs-transactional/typeorm @nestjs-transactional/core typeorm @nestjs/typeorm reflect-metadata
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
Minimal single-DataSource setup:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { Module } from '@nestjs/common';
|
|
59
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
60
|
+
import { TransactionalModule } from '@nestjs-transactional/core';
|
|
61
|
+
import { TypeOrmTransactionalModule } from '@nestjs-transactional/typeorm';
|
|
62
|
+
|
|
63
|
+
@Module({
|
|
64
|
+
imports: [
|
|
65
|
+
TypeOrmModule.forRoot({
|
|
66
|
+
type: 'postgres',
|
|
67
|
+
url: process.env.DATABASE_URL,
|
|
68
|
+
entities: [User],
|
|
69
|
+
synchronize: false,
|
|
70
|
+
}),
|
|
71
|
+
TypeOrmModule.forFeature([User]),
|
|
72
|
+
|
|
73
|
+
TransactionalModule.forRoot({ isGlobal: true }),
|
|
74
|
+
TypeOrmTransactionalModule.forRoot(),
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
export class AppModule {}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Import order matters** — `TransactionalModule.forRoot({ isGlobal: true })`
|
|
81
|
+
must be present (with `isGlobal`) so the `AdapterRegistry` is visible
|
|
82
|
+
inside `TypeOrmTransactionalModule`'s DI scope. The actual
|
|
83
|
+
`DataSource` is resolved via `@nestjs/typeorm`'s
|
|
84
|
+
`getDataSourceToken(name)` — `TypeOrmModule.forRoot(...)` registers
|
|
85
|
+
it globally, so `TypeOrmTransactionalModule.forRoot` finds it
|
|
86
|
+
automatically.
|
|
87
|
+
|
|
88
|
+
## Transparent transactional behaviour
|
|
89
|
+
|
|
90
|
+
Once the module is imported, every Repository reachable through the
|
|
91
|
+
standard `@nestjs/typeorm` injection paths automatically dispatches
|
|
92
|
+
through the active `@Transactional()` scope. No
|
|
93
|
+
`getCurrentEntityManager()` calls in user code:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { Injectable } from '@nestjs/common';
|
|
97
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
98
|
+
import { Transactional } from '@nestjs-transactional/core';
|
|
99
|
+
import { Repository } from 'typeorm';
|
|
100
|
+
import { Order } from './order.entity';
|
|
101
|
+
|
|
102
|
+
@Injectable()
|
|
103
|
+
export class OrderService {
|
|
104
|
+
constructor(
|
|
105
|
+
@InjectRepository(Order)
|
|
106
|
+
private readonly orderRepo: Repository<Order>,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
@Transactional()
|
|
110
|
+
async placeOrder(dto: PlaceOrderDto): Promise<Order> {
|
|
111
|
+
// `orderRepo.save(...)` automatically uses the transactional
|
|
112
|
+
// EntityManager. If the method throws, the save rolls back.
|
|
113
|
+
// Outside a @Transactional scope, the same call autocommits.
|
|
114
|
+
return this.orderRepo.save(dto);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Supported transparent patterns:
|
|
120
|
+
|
|
121
|
+
- `@InjectRepository(Entity) repo` — the headline case.
|
|
122
|
+
- `@InjectEntityManager() em.getRepository(E).save(...)`.
|
|
123
|
+
- `@InjectDataSource() ds.getRepository(E).save(...)`.
|
|
124
|
+
- `@InjectDataSource() ds.manager.save(Entity, ...)` (the patched
|
|
125
|
+
DataSource manager getter routes through the active EM).
|
|
126
|
+
- Custom repositories via `Repository.extend(...)`.
|
|
127
|
+
- `TreeRepository` and `MongoRepository` (inherit from `Repository`).
|
|
128
|
+
|
|
129
|
+
The mechanism is prototype-level patching modelled on the
|
|
130
|
+
`typeorm-transactional` library; patches install at module-load
|
|
131
|
+
time so they cover Repositories constructed by any DI factory,
|
|
132
|
+
regardless of resolution order.
|
|
133
|
+
|
|
134
|
+
### Documented limitations
|
|
135
|
+
|
|
136
|
+
Two patterns are NOT covered by the patches and require an escape
|
|
137
|
+
hatch:
|
|
138
|
+
|
|
139
|
+
1. **`@InjectEntityManager() em.save(Entity, ...)` direct call** is
|
|
140
|
+
NOT transactional. The patches cover `em.getRepository(E).save(...)`
|
|
141
|
+
(the typical pattern) but not direct method calls on the injected
|
|
142
|
+
`EntityManager`. Use the Repository pattern instead, or call
|
|
143
|
+
`getCurrentEntityManager()`:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
@Transactional()
|
|
147
|
+
async createUser(name: string) {
|
|
148
|
+
// Option A — Repository pattern (recommended).
|
|
149
|
+
return this.em.getRepository(User).save({ name });
|
|
150
|
+
|
|
151
|
+
// Option B — escape hatch.
|
|
152
|
+
// const em = getCurrentEntityManager();
|
|
153
|
+
// return em.save(User, { name });
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
2. **`BaseEntity` static methods** (`User.save(...)` etc.) are NOT
|
|
158
|
+
supported. The `BaseEntity.useDataSource(...)` API stores a
|
|
159
|
+
captured DataSource reference that bypasses the patches. Use the
|
|
160
|
+
Repository pattern.
|
|
161
|
+
|
|
162
|
+
The escape hatch:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { getCurrentEntityManager } from '@nestjs-transactional/typeorm';
|
|
166
|
+
|
|
167
|
+
@Injectable()
|
|
168
|
+
export class RawSqlService {
|
|
169
|
+
constructor(@InjectDataSource() private readonly ds: DataSource) {}
|
|
170
|
+
|
|
171
|
+
@Transactional()
|
|
172
|
+
async runRawSql() {
|
|
173
|
+
// Pass `ds` as fallback so the helper returns ds.manager when
|
|
174
|
+
// no transaction is active (autocommit). Inside a tx, returns
|
|
175
|
+
// the transactional EM.
|
|
176
|
+
const em = getCurrentEntityManager('default', this.ds);
|
|
177
|
+
await em.query(
|
|
178
|
+
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
|
|
179
|
+
[100, 1],
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Multi-DataSource
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
@Module({
|
|
189
|
+
imports: [
|
|
190
|
+
TypeOrmModule.forRoot({ name: 'default', /* ... */ }),
|
|
191
|
+
TypeOrmModule.forRoot({ name: 'billing', /* ... */ }),
|
|
192
|
+
|
|
193
|
+
TransactionalModule.forRoot({ isGlobal: true }),
|
|
194
|
+
TypeOrmTransactionalModule.forRoot({ isDefault: true }), // 'default'
|
|
195
|
+
TypeOrmTransactionalModule.forRoot({ dataSource: 'billing' }), // 'billing'
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
export class AppModule {}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Target a specific dataSource in a transactional method:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { Transactional } from '@nestjs-transactional/core';
|
|
205
|
+
|
|
206
|
+
@Injectable()
|
|
207
|
+
export class BillingService {
|
|
208
|
+
constructor(
|
|
209
|
+
@InjectRepository(Invoice, 'billing')
|
|
210
|
+
private readonly invoiceRepo: Repository<Invoice>,
|
|
211
|
+
) {}
|
|
212
|
+
|
|
213
|
+
@Transactional({ dataSource: 'billing' })
|
|
214
|
+
async chargeCard(/* ... */) {
|
|
215
|
+
// Repository is bound to 'billing' DS — saves go to billing.
|
|
216
|
+
return this.invoiceRepo.save(/* ... */);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Cross-DS isolation (DD-023)**: a Repository bound to dataSource A
|
|
222
|
+
inside a `@Transactional({ dataSource: 'B' })` method autocommits —
|
|
223
|
+
its patched `manager` getter looks up the active transaction for
|
|
224
|
+
dataSource A, finds none, and falls back to its captured original
|
|
225
|
+
manager. Distributed transactions across dataSources are explicitly
|
|
226
|
+
NOT supported; cross-DS atomicity goes through the outbox.
|
|
227
|
+
|
|
228
|
+
Each `forRoot` call registers its adapter under
|
|
229
|
+
`typeorm:${dataSource}` in the core `AdapterRegistry`.
|
|
230
|
+
`TransactionManager` routes based on `options.dataSource`.
|
|
231
|
+
|
|
232
|
+
## Async configuration
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
@Module({
|
|
236
|
+
imports: [
|
|
237
|
+
ConfigModule,
|
|
238
|
+
TypeOrmModule.forRootAsync({
|
|
239
|
+
inject: [ConfigService],
|
|
240
|
+
useFactory: (cfg: ConfigService) => ({
|
|
241
|
+
type: 'postgres',
|
|
242
|
+
url: cfg.get('DATABASE_URL'),
|
|
243
|
+
entities: [User],
|
|
244
|
+
}),
|
|
245
|
+
}),
|
|
246
|
+
|
|
247
|
+
TransactionalModule.forRoot({ isGlobal: true }),
|
|
248
|
+
TypeOrmTransactionalModule.forRootAsync({
|
|
249
|
+
imports: [ConfigModule],
|
|
250
|
+
inject: [ConfigService],
|
|
251
|
+
useFactory: (cfg: ConfigService) => ({
|
|
252
|
+
dataSource: cfg.get('DATA_SOURCE_NAME', 'default'),
|
|
253
|
+
isDefault: true,
|
|
254
|
+
}),
|
|
255
|
+
}),
|
|
256
|
+
],
|
|
257
|
+
})
|
|
258
|
+
export class AppModule {}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`forRootAsync` defers resolution of the dataSource name until the
|
|
262
|
+
factory runs. Per-DS DI tokens (`getTransactionalAdapterToken(ds)`)
|
|
263
|
+
are NOT registered in the async path because NestJS provider tokens
|
|
264
|
+
must be declared statically — if you need direct adapter injection
|
|
265
|
+
by per-DS token, use sync `forRoot({ dataSource })` instead.
|
|
266
|
+
|
|
267
|
+
The async path uses an `OnModuleInit`-driven registration class so
|
|
268
|
+
the DataSource resolves correctly even when paired with
|
|
269
|
+
`TypeOrmModule.forRootAsync` (whose own DataSource provider is
|
|
270
|
+
async). Pinned by
|
|
271
|
+
[`packages/typeorm/test/integration/forrootasync.integration.spec.ts`](test/integration/forrootasync.integration.spec.ts).
|
|
272
|
+
|
|
273
|
+
## Testing
|
|
274
|
+
|
|
275
|
+
### Unit tests — in-memory SQLite
|
|
276
|
+
|
|
277
|
+
For fast unit tests that don't need a real database, use TypeORM's
|
|
278
|
+
`sqljs` driver:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { DataSource } from 'typeorm';
|
|
282
|
+
import { TypeOrmTransactionAdapter } from '@nestjs-transactional/typeorm';
|
|
283
|
+
|
|
284
|
+
const ds = new DataSource({
|
|
285
|
+
type: 'sqljs',
|
|
286
|
+
synchronize: true,
|
|
287
|
+
entities: [YourEntity],
|
|
288
|
+
});
|
|
289
|
+
await ds.initialize();
|
|
290
|
+
|
|
291
|
+
const adapter = new TypeOrmTransactionAdapter(ds, 'default');
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Integration tests — testcontainers-node + real Postgres
|
|
295
|
+
|
|
296
|
+
Bundled helper for real Postgres integration:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import {
|
|
300
|
+
startPostgresContainer,
|
|
301
|
+
stopPostgresContainer,
|
|
302
|
+
createAdditionalDatabase,
|
|
303
|
+
} from '@nestjs-transactional/typeorm/test/setup-testcontainers';
|
|
304
|
+
|
|
305
|
+
let ctx;
|
|
306
|
+
beforeAll(async () => {
|
|
307
|
+
ctx = await startPostgresContainer({ entities: [User], synchronize: true });
|
|
308
|
+
});
|
|
309
|
+
afterAll(async () => {
|
|
310
|
+
await stopPostgresContainer(ctx);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Multi-DS: a second database inside the same container.
|
|
314
|
+
const secondary = await createAdditionalDatabase(ctx, 'billing_test', {
|
|
315
|
+
entities: [User],
|
|
316
|
+
synchronize: true,
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Run integration tests:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
pnpm --filter @nestjs-transactional/typeorm test:integration
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The bundled `docker-compose.yml` is for manual local use (`psql`
|
|
327
|
+
against a persistent instance). Testcontainers manages its own
|
|
328
|
+
containers and does not require compose.
|
|
329
|
+
|
|
330
|
+
## Savepoints and NESTED propagation
|
|
331
|
+
|
|
332
|
+
When a method uses `PropagationMode.NESTED` from inside an existing
|
|
333
|
+
TypeORM transaction, the adapter issues a `SAVEPOINT sp_<uuid-30>`
|
|
334
|
+
statement. Rollback rolls back to the savepoint; the outer
|
|
335
|
+
transaction continues. Savepoint names are at most 33 characters
|
|
336
|
+
long — valid on Postgres, MySQL, MariaDB, SQLite, and Oracle's
|
|
337
|
+
identifier limit.
|
|
338
|
+
|
|
339
|
+
## Worked examples
|
|
340
|
+
|
|
341
|
+
- [`basic-transactional`](../../examples/basic-transactional) —
|
|
342
|
+
`@Transactional()` on `@InjectRepository`, single DataSource.
|
|
343
|
+
Transparent repository showcase.
|
|
344
|
+
- [`multi-datasource-basic`](../../examples/multi-datasource-basic)
|
|
345
|
+
— two DataSources with `@Transactional({ dataSource })`, no
|
|
346
|
+
outbox.
|
|
347
|
+
- [`read-write-separation`](../../examples/read-write-separation) —
|
|
348
|
+
master + replica, only the master gets
|
|
349
|
+
`TypeOrmTransactionalModule`.
|
|
350
|
+
- [`async-config-from-environment`](../../examples/async-config-from-environment)
|
|
351
|
+
— `TypeOrmTransactionalModule.forRootAsync` end-to-end with
|
|
352
|
+
`ConfigService` + Joi profiles.
|
|
353
|
+
- [`e-commerce-orders`](../../examples/e-commerce-orders) —
|
|
354
|
+
three-DataSource flagship combining transparent repositories +
|
|
355
|
+
per-DS outbox + CQRS + REST + Kafka externalization.
|
|
356
|
+
|
|
357
|
+
Full catalogue: [examples/README.md](../../examples/README.md).
|
|
358
|
+
|
|
359
|
+
## Status
|
|
360
|
+
|
|
361
|
+
Alpha. Public API may change between 0.x releases.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TransactionAdapter, TransactionOptions } from '@nestjs-transactional/core';
|
|
2
|
+
import type { DataSource } from 'typeorm';
|
|
3
|
+
import type { TypeOrmTransactionHandle } from '../types/typeorm-transaction-handle';
|
|
4
|
+
/**
|
|
5
|
+
* TypeORM implementation of {@link TransactionAdapter}. Delegates begin /
|
|
6
|
+
* commit / rollback to `DataSource.transaction` and emits raw `SAVEPOINT`
|
|
7
|
+
* SQL for nested transactions through the transactional
|
|
8
|
+
* {@link EntityManager}.
|
|
9
|
+
*
|
|
10
|
+
* Only `isolation` from {@link TransactionOptions} is forwarded today.
|
|
11
|
+
* `readOnly` and `timeout` are accepted for forward compatibility but do
|
|
12
|
+
* not yet map to per-dialect statements.
|
|
13
|
+
*/
|
|
14
|
+
export declare class TypeOrmTransactionAdapter implements TransactionAdapter<TypeOrmTransactionHandle> {
|
|
15
|
+
private readonly dataSource;
|
|
16
|
+
/**
|
|
17
|
+
* Adapter instance name this adapter was created for (e.g.
|
|
18
|
+
* `'primary'`, `'billing'`). Exposed so observability / diagnostics
|
|
19
|
+
* can correlate events back to the registered instance.
|
|
20
|
+
*/
|
|
21
|
+
readonly instanceName: string;
|
|
22
|
+
readonly name = "typeorm";
|
|
23
|
+
constructor(dataSource: DataSource,
|
|
24
|
+
/**
|
|
25
|
+
* Adapter instance name this adapter was created for (e.g.
|
|
26
|
+
* `'primary'`, `'billing'`). Exposed so observability / diagnostics
|
|
27
|
+
* can correlate events back to the registered instance.
|
|
28
|
+
*/
|
|
29
|
+
instanceName: string);
|
|
30
|
+
/**
|
|
31
|
+
* Public dataSource name (DD-021). For TypeORM the dataSource name
|
|
32
|
+
* IS the adapter instance name — the constructor's `instanceName`
|
|
33
|
+
* argument is the single user-supplied identifier.
|
|
34
|
+
*/
|
|
35
|
+
get dataSourceName(): string;
|
|
36
|
+
runInTransaction<T>(options: TransactionOptions, fn: (handle: TypeOrmTransactionHandle) => Promise<T>): Promise<T>;
|
|
37
|
+
runInSavepoint<T>(parent: TypeOrmTransactionHandle, fn: (handle: TypeOrmTransactionHandle) => Promise<T>): Promise<T>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=typeorm.adapter.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TypeOrmTransactionAdapter = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
/**
|
|
6
|
+
* TypeORM implementation of {@link TransactionAdapter}. Delegates begin /
|
|
7
|
+
* commit / rollback to `DataSource.transaction` and emits raw `SAVEPOINT`
|
|
8
|
+
* SQL for nested transactions through the transactional
|
|
9
|
+
* {@link EntityManager}.
|
|
10
|
+
*
|
|
11
|
+
* Only `isolation` from {@link TransactionOptions} is forwarded today.
|
|
12
|
+
* `readOnly` and `timeout` are accepted for forward compatibility but do
|
|
13
|
+
* not yet map to per-dialect statements.
|
|
14
|
+
*/
|
|
15
|
+
class TypeOrmTransactionAdapter {
|
|
16
|
+
dataSource;
|
|
17
|
+
instanceName;
|
|
18
|
+
name = 'typeorm';
|
|
19
|
+
constructor(dataSource,
|
|
20
|
+
/**
|
|
21
|
+
* Adapter instance name this adapter was created for (e.g.
|
|
22
|
+
* `'primary'`, `'billing'`). Exposed so observability / diagnostics
|
|
23
|
+
* can correlate events back to the registered instance.
|
|
24
|
+
*/
|
|
25
|
+
instanceName) {
|
|
26
|
+
this.dataSource = dataSource;
|
|
27
|
+
this.instanceName = instanceName;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Public dataSource name (DD-021). For TypeORM the dataSource name
|
|
31
|
+
* IS the adapter instance name — the constructor's `instanceName`
|
|
32
|
+
* argument is the single user-supplied identifier.
|
|
33
|
+
*/
|
|
34
|
+
get dataSourceName() {
|
|
35
|
+
return this.instanceName;
|
|
36
|
+
}
|
|
37
|
+
async runInTransaction(options, fn) {
|
|
38
|
+
const isolation = mapIsolation(options.isolation);
|
|
39
|
+
const runner = async (entityManager) => {
|
|
40
|
+
const handle = {
|
|
41
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
42
|
+
adapterName: this.name,
|
|
43
|
+
entityManager,
|
|
44
|
+
};
|
|
45
|
+
return fn(handle);
|
|
46
|
+
};
|
|
47
|
+
if (isolation !== undefined) {
|
|
48
|
+
return this.dataSource.transaction(isolation, runner);
|
|
49
|
+
}
|
|
50
|
+
return this.dataSource.transaction(runner);
|
|
51
|
+
}
|
|
52
|
+
async runInSavepoint(parent, fn) {
|
|
53
|
+
const savepointName = `sp_${(0, node_crypto_1.randomUUID)().replace(/-/g, '_').substring(0, 30)}`;
|
|
54
|
+
await parent.entityManager.query(`SAVEPOINT ${savepointName}`);
|
|
55
|
+
try {
|
|
56
|
+
const result = await fn(parent);
|
|
57
|
+
await parent.entityManager.query(`RELEASE SAVEPOINT ${savepointName}`);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
await parent.entityManager.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.TypeOrmTransactionAdapter = TypeOrmTransactionAdapter;
|
|
67
|
+
/**
|
|
68
|
+
* Map the core's underscore-style {@link IsolationLevel} to TypeORM's
|
|
69
|
+
* space-separated string. Returns `undefined` when no level is set so the
|
|
70
|
+
* caller can invoke the `DataSource.transaction` overload that leaves the
|
|
71
|
+
* database default in place.
|
|
72
|
+
*/
|
|
73
|
+
function mapIsolation(level) {
|
|
74
|
+
if (level === undefined) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return level.replace(/_/g, ' ');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=typeorm.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typeorm.adapter.js","sourceRoot":"","sources":["../../src/adapter/typeorm.adapter.ts"],"names":[],"mappings":";;;AAAA,6CAAyC;AAsBzC;;;;;;;;;GASG;AACH,MAAa,yBAAyB;IAIjB;IAMR;IATF,IAAI,GAAG,SAAS,CAAC;IAE1B,YACmB,UAAsB;IACvC;;;;OAIG;IACM,YAAoB;QANZ,eAAU,GAAV,UAAU,CAAY;QAM9B,iBAAY,GAAZ,YAAY,CAAQ;IAC5B,CAAC;IAEJ;;;;OAIG;IACH,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,gBAAgB,CACpB,OAA2B,EAC3B,EAAoD;QAEpD,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAElD,MAAM,MAAM,GAAG,KAAK,EAAE,aAA4B,EAAc,EAAE;YAChE,MAAM,MAAM,GAA6B;gBACvC,EAAE,EAAE,IAAA,wBAAU,GAAE;gBAChB,WAAW,EAAE,IAAI,CAAC,IAAI;gBACtB,aAAa;aACd,CAAC;YACF,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC,CAAC;QAEF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,MAAgC,EAChC,EAAoD;QAEpD,MAAM,aAAa,GAAG,MAAM,IAAA,wBAAU,GAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAE/E,MAAM,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,aAAa,aAAa,EAAE,CAAC,CAAC;QAE/D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;YAChC,MAAM,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,qBAAqB,aAAa,EAAE,CAAC,CAAC;YACvE,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,yBAAyB,aAAa,EAAE,CAAC,CAAC;YAC3E,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;CACF;AA5DD,8DA4DC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,KAAiC;IACrD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAA0B,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DataSource, EntityManager } from 'typeorm';
|
|
2
|
+
/**
|
|
3
|
+
* Return the TypeORM {@link EntityManager} bound to the currently active
|
|
4
|
+
* transaction on the given adapter instance. Repositories and services
|
|
5
|
+
* call this to stay inside the surrounding `@Transactional` scope without
|
|
6
|
+
* having to thread the EntityManager through their arguments.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
* 1. If a transaction is active on `typeorm:${adapterInstance}`, return
|
|
10
|
+
* its EntityManager — every write goes through the transaction.
|
|
11
|
+
* 2. Otherwise, if `fallback` is provided, return `fallback.manager` —
|
|
12
|
+
* writes execute autocommit.
|
|
13
|
+
* 3. Otherwise throw {@link IllegalTransactionStateError}. Passing no
|
|
14
|
+
* fallback is a deliberate assertion that the caller MUST be inside
|
|
15
|
+
* a transaction.
|
|
16
|
+
*
|
|
17
|
+
* @param adapterInstance - Adapter instance name. Defaults to `'default'`.
|
|
18
|
+
* @param fallback - DataSource used when no transaction is active.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getCurrentEntityManager(adapterInstance?: string, fallback?: DataSource): EntityManager;
|
|
21
|
+
/**
|
|
22
|
+
* Predicate: is there an active TypeORM transaction on this adapter
|
|
23
|
+
* instance? Useful to guard side effects that must only fire inside a
|
|
24
|
+
* transaction.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isInTransaction(adapterInstance?: string): boolean;
|
|
27
|
+
//# sourceMappingURL=get-entity-manager.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCurrentEntityManager = getCurrentEntityManager;
|
|
4
|
+
exports.isInTransaction = isInTransaction;
|
|
5
|
+
const core_1 = require("@nestjs-transactional/core");
|
|
6
|
+
/**
|
|
7
|
+
* Compose the `TransactionContext` lookup key for the TypeORM adapter.
|
|
8
|
+
* Must match the `${adapterName}:${instanceName}` format that
|
|
9
|
+
* `TransactionManager` in core writes under. Keeping it private forces
|
|
10
|
+
* all helper call-sites to go through the same builder.
|
|
11
|
+
*/
|
|
12
|
+
function typeOrmContextKey(adapterInstance) {
|
|
13
|
+
return `typeorm:${adapterInstance}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Return the TypeORM {@link EntityManager} bound to the currently active
|
|
17
|
+
* transaction on the given adapter instance. Repositories and services
|
|
18
|
+
* call this to stay inside the surrounding `@Transactional` scope without
|
|
19
|
+
* having to thread the EntityManager through their arguments.
|
|
20
|
+
*
|
|
21
|
+
* Resolution order:
|
|
22
|
+
* 1. If a transaction is active on `typeorm:${adapterInstance}`, return
|
|
23
|
+
* its EntityManager — every write goes through the transaction.
|
|
24
|
+
* 2. Otherwise, if `fallback` is provided, return `fallback.manager` —
|
|
25
|
+
* writes execute autocommit.
|
|
26
|
+
* 3. Otherwise throw {@link IllegalTransactionStateError}. Passing no
|
|
27
|
+
* fallback is a deliberate assertion that the caller MUST be inside
|
|
28
|
+
* a transaction.
|
|
29
|
+
*
|
|
30
|
+
* @param adapterInstance - Adapter instance name. Defaults to `'default'`.
|
|
31
|
+
* @param fallback - DataSource used when no transaction is active.
|
|
32
|
+
*/
|
|
33
|
+
function getCurrentEntityManager(adapterInstance = 'default', fallback) {
|
|
34
|
+
const active = core_1.TransactionContext.getActiveTransaction(typeOrmContextKey(adapterInstance));
|
|
35
|
+
if (active !== undefined) {
|
|
36
|
+
return active.handle.entityManager;
|
|
37
|
+
}
|
|
38
|
+
if (fallback !== undefined) {
|
|
39
|
+
return fallback.manager;
|
|
40
|
+
}
|
|
41
|
+
throw new core_1.IllegalTransactionStateError(`No active transaction for 'typeorm:${adapterInstance}' and no fallback DataSource ` +
|
|
42
|
+
`provided. Either wrap the call with @Transactional() or pass a DataSource as fallback.`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Predicate: is there an active TypeORM transaction on this adapter
|
|
46
|
+
* instance? Useful to guard side effects that must only fire inside a
|
|
47
|
+
* transaction.
|
|
48
|
+
*/
|
|
49
|
+
function isInTransaction(adapterInstance = 'default') {
|
|
50
|
+
return core_1.TransactionContext.getActiveTransaction(typeOrmContextKey(adapterInstance)) !== undefined;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=get-entity-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-entity-manager.js","sourceRoot":"","sources":["../../src/helpers/get-entity-manager.ts"],"names":[],"mappings":";;AAiCA,0DAkBC;AAOD,0CAEC;AA5DD,qDAA8F;AAK9F;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,eAAuB;IAChD,OAAO,WAAW,eAAe,EAAE,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,uBAAuB,CACrC,eAAe,GAAG,SAAS,EAC3B,QAAqB;IAErB,MAAM,MAAM,GAAG,yBAAkB,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,eAAe,CAAC,CAAC,CAAC;IAE3F,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAQ,MAAM,CAAC,MAAmC,CAAC,aAAa,CAAC;IACnE,CAAC;IAED,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IAED,MAAM,IAAI,mCAA4B,CACpC,sCAAsC,eAAe,+BAA+B;QAClF,wFAAwF,CAC3F,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAAC,eAAe,GAAG,SAAS;IACzD,OAAO,yBAAkB,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,eAAe,CAAC,CAAC,KAAK,SAAS,CAAC;AACnG,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types/typeorm-transaction-handle"), exports);
|
|
18
|
+
__exportStar(require("./adapter/typeorm.adapter"), exports);
|
|
19
|
+
__exportStar(require("./helpers/get-entity-manager"), exports);
|
|
20
|
+
__exportStar(require("./module/typeorm-transactional.module"), exports);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,qEAAmD;AACnD,4DAA0C;AAC1C,+DAA6C;AAC7C,wEAAsD"}
|