@nestjs-transactional/core 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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -0
  3. package/dist/bootstrap/transactional-methods.bootstrap.d.ts +42 -0
  4. package/dist/bootstrap/transactional-methods.bootstrap.js +129 -0
  5. package/dist/bootstrap/transactional-methods.bootstrap.js.map +1 -0
  6. package/dist/context/transaction-context-view.d.ts +38 -0
  7. package/dist/context/transaction-context-view.js +48 -0
  8. package/dist/context/transaction-context-view.js.map +1 -0
  9. package/dist/context/transaction.context.d.ts +114 -0
  10. package/dist/context/transaction.context.js +112 -0
  11. package/dist/context/transaction.context.js.map +1 -0
  12. package/dist/decorators/inject-decorators.d.ts +36 -0
  13. package/dist/decorators/inject-decorators.js +45 -0
  14. package/dist/decorators/inject-decorators.js.map +1 -0
  15. package/dist/decorators/transactional.decorator.d.ts +64 -0
  16. package/dist/decorators/transactional.decorator.js +82 -0
  17. package/dist/decorators/transactional.decorator.js.map +1 -0
  18. package/dist/index.d.ts +19 -0
  19. package/dist/index.js +35 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/interceptor/transactional.interceptor.d.ts +28 -0
  22. package/dist/interceptor/transactional.interceptor.js +56 -0
  23. package/dist/interceptor/transactional.interceptor.js.map +1 -0
  24. package/dist/internal/markers.d.ts +15 -0
  25. package/dist/internal/markers.js +18 -0
  26. package/dist/internal/markers.js.map +1 -0
  27. package/dist/manager/adapter.registry.d.ts +103 -0
  28. package/dist/manager/adapter.registry.js +179 -0
  29. package/dist/manager/adapter.registry.js.map +1 -0
  30. package/dist/manager/transaction.manager.d.ts +129 -0
  31. package/dist/manager/transaction.manager.js +412 -0
  32. package/dist/manager/transaction.manager.js.map +1 -0
  33. package/dist/module/transactional.module.d.ts +242 -0
  34. package/dist/module/transactional.module.js +374 -0
  35. package/dist/module/transactional.module.js.map +1 -0
  36. package/dist/observability/transaction-observer.d.ts +78 -0
  37. package/dist/observability/transaction-observer.js +18 -0
  38. package/dist/observability/transaction-observer.js.map +1 -0
  39. package/dist/testing/in-memory.adapter.d.ts +66 -0
  40. package/dist/testing/in-memory.adapter.js +83 -0
  41. package/dist/testing/in-memory.adapter.js.map +1 -0
  42. package/dist/testing/index.d.ts +2 -0
  43. package/dist/testing/index.js +18 -0
  44. package/dist/testing/index.js.map +1 -0
  45. package/dist/tokens/constants.d.ts +14 -0
  46. package/dist/tokens/constants.js +17 -0
  47. package/dist/tokens/constants.js.map +1 -0
  48. package/dist/tokens/index.d.ts +3 -0
  49. package/dist/tokens/index.js +11 -0
  50. package/dist/tokens/index.js.map +1 -0
  51. package/dist/tokens/token-utils.d.ts +52 -0
  52. package/dist/tokens/token-utils.js +67 -0
  53. package/dist/tokens/token-utils.js.map +1 -0
  54. package/dist/types/domain-event.d.ts +18 -0
  55. package/dist/types/domain-event.js +3 -0
  56. package/dist/types/domain-event.js.map +1 -0
  57. package/dist/types/errors.d.ts +50 -0
  58. package/dist/types/errors.js +61 -0
  59. package/dist/types/errors.js.map +1 -0
  60. package/dist/types/isolation.d.ts +11 -0
  61. package/dist/types/isolation.js +3 -0
  62. package/dist/types/isolation.js.map +1 -0
  63. package/dist/types/propagation.d.ts +63 -0
  64. package/dist/types/propagation.js +67 -0
  65. package/dist/types/propagation.js.map +1 -0
  66. package/dist/types/transaction-adapter.d.ts +72 -0
  67. package/dist/types/transaction-adapter.js +3 -0
  68. package/dist/types/transaction-adapter.js.map +1 -0
  69. package/dist/types/transaction-handle.d.ts +23 -0
  70. package/dist/types/transaction-handle.js +3 -0
  71. package/dist/types/transaction-handle.js.map +1 -0
  72. package/dist/types/transaction-options.d.ts +88 -0
  73. package/dist/types/transaction-options.js +3 -0
  74. package/dist/types/transaction-options.js.map +1 -0
  75. package/package.json +74 -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,265 @@
1
+ # @nestjs-transactional/core
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40nestjs-transactional%2Fcore/alpha?style=flat-square&label=npm)](https://www.npmjs.com/package/@nestjs-transactional/core)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/igorgolovanov/nestjs-transactional/blob/main/LICENSE)
5
+
6
+ Core primitives for declarative Spring-style transaction management
7
+ in NestJS.
8
+
9
+ ## Overview
10
+
11
+ The adapter-agnostic foundation of the `@nestjs-transactional` family:
12
+
13
+ - `TransactionContext` — `AsyncLocalStorage`-backed carrier that
14
+ propagates the active transaction across `await` boundaries.
15
+ - `TransactionManager` — runtime with the full Spring propagation
16
+ semantics (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, `SUPPORTS`,
17
+ `NOT_SUPPORTED`, `NEVER`, `MANDATORY`) plus `rollbackFor` /
18
+ `noRollbackFor` rules and before / after commit / rollback hooks.
19
+ - `@Transactional()`, `@ReadOnly()`, `@TransactionalOn(instance)`
20
+ decorators — metadata-only; runtime wrapping is performed by the
21
+ three coordinated mechanisms documented in
22
+ [ADR-005](../../docs/adr/005-method-wrapping-strategy.md).
23
+ - `TransactionalInterceptor` — wires `@Transactional` on controllers,
24
+ resolvers, gateways, and microservice handlers via `APP_INTERCEPTOR`.
25
+ - `TransactionalModule.forRoot` / `forRootAsync` — module wiring,
26
+ one call per dataSource (multi-`forRoot` pattern, see
27
+ [ADR-019](../../docs/adr/019-outbox-multi-forroot-pattern.md)).
28
+ - `TransactionAdapter<THandle>` SPI — the port for ORM-specific
29
+ adapters.
30
+ - `InMemoryTransactionAdapter` (via the `@nestjs-transactional/core/testing`
31
+ subpath) — drop-in adapter for unit tests.
32
+
33
+ This package does not depend on any concrete ORM. Install
34
+ `@nestjs-transactional/typeorm` for TypeORM integration, or implement
35
+ your own adapter against the `TransactionAdapter` interface.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pnpm add @nestjs-transactional/core reflect-metadata
41
+ ```
42
+
43
+ Load `reflect-metadata` once at the application entry point (same as
44
+ for NestJS itself).
45
+
46
+ ## Quick start
47
+
48
+ In typical use this package is imported via an integration package
49
+ (like `@nestjs-transactional/typeorm`) which registers the adapter
50
+ into the `AdapterRegistry` automatically. The minimal application
51
+ shape is:
52
+
53
+ ```ts
54
+ import { Module } from '@nestjs/common';
55
+ import { TransactionalModule } from '@nestjs-transactional/core';
56
+ import { TypeOrmTransactionalModule } from '@nestjs-transactional/typeorm';
57
+ // ...your TypeORM config
58
+
59
+ @Module({
60
+ imports: [
61
+ TypeOrmModule.forRoot({ /* ... */ }),
62
+
63
+ // Infrastructure-only forRoot — registers TransactionManager,
64
+ // AdapterRegistry, and the interceptor. No `adapter` here; the
65
+ // integration package below registers it.
66
+ TransactionalModule.forRoot({ isGlobal: true }),
67
+
68
+ // Integration package registers `TypeOrmTransactionAdapter`
69
+ // for the default dataSource.
70
+ TypeOrmTransactionalModule.forRoot(),
71
+ ],
72
+ })
73
+ export class AppModule {}
74
+ ```
75
+
76
+ `@Transactional()` on any controller handler, query handler, or
77
+ service method is then wrapped in a transaction automatically:
78
+
79
+ ```ts
80
+ import { Controller, Get, Param } from '@nestjs/common';
81
+ import { Transactional } from '@nestjs-transactional/core';
82
+
83
+ @Controller('orders')
84
+ export class OrdersController {
85
+ constructor(private readonly orders: OrdersService) {}
86
+
87
+ @Get(':id')
88
+ @Transactional()
89
+ async findOne(@Param('id') id: string) {
90
+ return this.orders.findById(id);
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### Direct adapter registration (custom backends)
96
+
97
+ When implementing a new `TransactionAdapter` (Prisma, Mongoose, ...),
98
+ pass it to `forRoot` directly:
99
+
100
+ ```ts
101
+ import { TransactionalModule, type TransactionAdapter } from '@nestjs-transactional/core';
102
+
103
+ const myAdapter: TransactionAdapter = /* ... */;
104
+
105
+ @Module({
106
+ imports: [
107
+ TransactionalModule.forRoot({
108
+ isGlobal: true,
109
+ adapter: myAdapter,
110
+ }),
111
+ ],
112
+ })
113
+ export class AppModule {}
114
+ ```
115
+
116
+ For multi-dataSource setups, call `forRoot` once per dataSource —
117
+ each call registers exactly one adapter under its dataSource name.
118
+
119
+ ## Decorator options
120
+
121
+ ```ts
122
+ import {
123
+ Transactional,
124
+ ReadOnly,
125
+ TransactionalOn,
126
+ PropagationMode,
127
+ } from '@nestjs-transactional/core';
128
+
129
+ class ReportsService {
130
+ // Explicit propagation + isolation.
131
+ @Transactional({
132
+ propagation: PropagationMode.REQUIRES_NEW,
133
+ isolation: 'SERIALIZABLE',
134
+ timeout: 10_000,
135
+ })
136
+ async rebuildReport() { /* ... */ }
137
+
138
+ // Shorthand for { readOnly: true }.
139
+ @ReadOnly()
140
+ async exportCsv() { /* ... */ }
141
+
142
+ // Rollback rules — commit on `ValidationError`, roll back on others.
143
+ @Transactional({ noRollbackFor: [ValidationError] })
144
+ async processBatch() { /* ... */ }
145
+
146
+ // Target a specific dataSource in multi-DataSource setups.
147
+ @TransactionalOn('billing')
148
+ async chargeCard() { /* ... */ }
149
+ }
150
+ ```
151
+
152
+ Propagation semantics:
153
+
154
+ | Mode | Active outer transaction | No outer transaction |
155
+ | --- | --- | --- |
156
+ | `REQUIRED` (default) | join | start new |
157
+ | `REQUIRES_NEW` | suspend + start new, then resume | start new |
158
+ | `NESTED` | run inside a savepoint | start new |
159
+ | `SUPPORTS` | join | run without transaction |
160
+ | `NOT_SUPPORTED` | suspend + run without transaction, then resume | run without transaction |
161
+ | `NEVER` | throw `IllegalTransactionStateError` | run without transaction |
162
+ | `MANDATORY` | join | throw `IllegalTransactionStateError` |
163
+
164
+ ## Async module configuration
165
+
166
+ ```ts
167
+ import { TransactionalModule } from '@nestjs-transactional/core';
168
+
169
+ @Module({
170
+ imports: [
171
+ TransactionalModule.forRootAsync({
172
+ imports: [ConfigModule],
173
+ inject: [ConfigService],
174
+ useFactory: (config: ConfigService) => ({
175
+ adapter: buildAdapterFromConfig(config),
176
+ }),
177
+ }),
178
+ ],
179
+ })
180
+ export class AppModule {}
181
+ ```
182
+
183
+ `isGlobal` and `registerInterceptor` remain static top-level flags —
184
+ they must be known at module definition time. The async factory
185
+ returns the per-call configuration (`adapter` and any other
186
+ runtime-resolved options).
187
+
188
+ ## Lifecycle hooks
189
+
190
+ Register hooks from inside a transactional method — they fire on the
191
+ current transaction:
192
+
193
+ ```ts
194
+ import { TransactionManager } from '@nestjs-transactional/core';
195
+
196
+ export class OrdersService {
197
+ constructor(private readonly manager: TransactionManager) {}
198
+
199
+ @Transactional()
200
+ async placeOrder(payload: PlaceOrderDto) {
201
+ const order = await this.orders.insert(payload);
202
+
203
+ this.manager.registerAfterCommit(async () => {
204
+ // Fires only after the adapter commits. Never on rollback.
205
+ await this.analytics.trackOrderPlaced(order.id);
206
+ });
207
+
208
+ this.manager.registerAfterRollback(async (error) => {
209
+ // Receives the error that caused the rollback.
210
+ await this.metrics.recordFailedOrder(order.id, error);
211
+ });
212
+
213
+ return order;
214
+ }
215
+ }
216
+ ```
217
+
218
+ Hook errors are caught and logged via NestJS `Logger` — they do not
219
+ affect the transaction outcome or prevent sibling hooks from running.
220
+
221
+ ## Testing
222
+
223
+ `InMemoryTransactionAdapter` from the `/testing` subpath gives
224
+ adapter-level observability without a real database:
225
+
226
+ ```ts
227
+ import { InMemoryTransactionAdapter } from '@nestjs-transactional/core/testing';
228
+ import { TransactionalModule } from '@nestjs-transactional/core';
229
+
230
+ const adapter = new InMemoryTransactionAdapter();
231
+
232
+ const moduleRef = await Test.createTestingModule({
233
+ imports: [
234
+ TransactionalModule.forRoot({ isGlobal: true, adapter }),
235
+ ],
236
+ }).compile();
237
+
238
+ // After exercising the code under test:
239
+ expect(adapter.committedTransactions).toHaveLength(1);
240
+ expect(adapter.rolledBackTransactions).toHaveLength(0);
241
+ expect(adapter.savepointsReleased).toHaveLength(0);
242
+ ```
243
+
244
+ `adapter.reset()` clears all observation arrays between tests when
245
+ you keep a single adapter instance across cases. For multi-DS test
246
+ setups, pass distinct dataSource names to the constructor:
247
+
248
+ ```ts
249
+ const billing = new InMemoryTransactionAdapter('billing');
250
+ const inventory = new InMemoryTransactionAdapter('inventory');
251
+ ```
252
+
253
+ ## Worked examples
254
+
255
+ - [`basic-transactional`](../../examples/basic-transactional) —
256
+ `@Transactional()` on a plain service.
257
+ - [`testing-patterns`](../../examples/testing-patterns) —
258
+ `InMemoryTransactionAdapter` from `core/testing` plus the outbox /
259
+ integration test layers.
260
+
261
+ Full catalogue: [examples/README.md](../../examples/README.md).
262
+
263
+ ## Status
264
+
265
+ Alpha. Public API may change between 0.x releases.
@@ -0,0 +1,42 @@
1
+ import 'reflect-metadata';
2
+ import { type OnApplicationBootstrap } from '@nestjs/common';
3
+ import { DiscoveryService, MetadataScanner } from '@nestjs/core';
4
+ import { TransactionManager } from '../manager/transaction.manager';
5
+ /**
6
+ * `OnApplicationBootstrap` service that wraps every `@Transactional()`
7
+ * method on a plain `@Injectable()` provider with
8
+ * `TransactionManager.run(...)`.
9
+ *
10
+ * This is the second of the three coordinated wrapping mechanisms
11
+ * described in ADR-005:
12
+ *
13
+ * 1. {@link import('../interceptor/transactional.interceptor').TransactionalInterceptor}
14
+ * — for controller / resolver / gateway request-boundary handlers.
15
+ * 2. This class — for regular `@Injectable` services.
16
+ * 3. `CqrsHandlerWrapper` (in `@nestjs-transactional/cqrs`) — for CQRS
17
+ * command / query / event handlers.
18
+ *
19
+ * Skip rules:
20
+ * - CQRS handler classes (detected via `@nestjs/cqrs` metadata keys) are
21
+ * left alone so their wrapping goes through `CqrsHandlerWrapper`.
22
+ * - Methods already marked with {@link WRAPPED_MARKER} are left alone
23
+ * (safety net against double-wrap).
24
+ *
25
+ * Metadata lookup priority: method-level `@Transactional` overrides
26
+ * class-level. Classes with only class-level `@Transactional` apply the
27
+ * same options to every method that doesn't carry its own metadata.
28
+ *
29
+ * Registered by `TransactionalModule.forRoot` unless the caller sets
30
+ * `registerMethodsBootstrap: false`.
31
+ */
32
+ export declare class TransactionalMethodsBootstrap implements OnApplicationBootstrap {
33
+ private readonly discovery;
34
+ private readonly metadataScanner;
35
+ private readonly manager;
36
+ private readonly logger;
37
+ constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner, manager: TransactionManager);
38
+ onApplicationBootstrap(): void;
39
+ private isCqrsHandler;
40
+ private wrapMethod;
41
+ }
42
+ //# sourceMappingURL=transactional-methods.bootstrap.d.ts.map
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var TransactionalMethodsBootstrap_1;
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.TransactionalMethodsBootstrap = void 0;
14
+ require("reflect-metadata");
15
+ const common_1 = require("@nestjs/common");
16
+ const core_1 = require("@nestjs/core");
17
+ const transactional_decorator_1 = require("../decorators/transactional.decorator");
18
+ const markers_1 = require("../internal/markers");
19
+ const transaction_manager_1 = require("../manager/transaction.manager");
20
+ /**
21
+ * Metadata keys set by `@nestjs/cqrs`'s handler decorators. Mirrored here
22
+ * so the bootstrap skips CQRS handler classes — they are wrapped by
23
+ * `CqrsHandlerWrapper` in `@nestjs-transactional/cqrs`, which knows how
24
+ * to apply per-kind defaults (read-only queries, etc.). Double-wrapping
25
+ * is additionally guarded via `WRAPPED_MARKER`.
26
+ */
27
+ const CQRS_HANDLER_KEYS = [
28
+ '__commandHandler__',
29
+ '__queryHandler__',
30
+ '__eventsHandler__',
31
+ ];
32
+ /**
33
+ * `OnApplicationBootstrap` service that wraps every `@Transactional()`
34
+ * method on a plain `@Injectable()` provider with
35
+ * `TransactionManager.run(...)`.
36
+ *
37
+ * This is the second of the three coordinated wrapping mechanisms
38
+ * described in ADR-005:
39
+ *
40
+ * 1. {@link import('../interceptor/transactional.interceptor').TransactionalInterceptor}
41
+ * — for controller / resolver / gateway request-boundary handlers.
42
+ * 2. This class — for regular `@Injectable` services.
43
+ * 3. `CqrsHandlerWrapper` (in `@nestjs-transactional/cqrs`) — for CQRS
44
+ * command / query / event handlers.
45
+ *
46
+ * Skip rules:
47
+ * - CQRS handler classes (detected via `@nestjs/cqrs` metadata keys) are
48
+ * left alone so their wrapping goes through `CqrsHandlerWrapper`.
49
+ * - Methods already marked with {@link WRAPPED_MARKER} are left alone
50
+ * (safety net against double-wrap).
51
+ *
52
+ * Metadata lookup priority: method-level `@Transactional` overrides
53
+ * class-level. Classes with only class-level `@Transactional` apply the
54
+ * same options to every method that doesn't carry its own metadata.
55
+ *
56
+ * Registered by `TransactionalModule.forRoot` unless the caller sets
57
+ * `registerMethodsBootstrap: false`.
58
+ */
59
+ let TransactionalMethodsBootstrap = TransactionalMethodsBootstrap_1 = class TransactionalMethodsBootstrap {
60
+ discovery;
61
+ metadataScanner;
62
+ manager;
63
+ logger = new common_1.Logger(TransactionalMethodsBootstrap_1.name);
64
+ constructor(discovery, metadataScanner, manager) {
65
+ this.discovery = discovery;
66
+ this.metadataScanner = metadataScanner;
67
+ this.manager = manager;
68
+ }
69
+ onApplicationBootstrap() {
70
+ const providers = this.discovery.getProviders();
71
+ let wrappedCount = 0;
72
+ for (const wrapper of providers) {
73
+ if (wrapper.instance === null || wrapper.instance === undefined)
74
+ continue;
75
+ if (typeof wrapper.metatype !== 'function')
76
+ continue;
77
+ const metatype = wrapper.metatype;
78
+ if (this.isCqrsHandler(metatype))
79
+ continue;
80
+ const instance = wrapper.instance;
81
+ const prototype = Object.getPrototypeOf(instance);
82
+ if (prototype === null)
83
+ continue;
84
+ const classMetadata = (0, transactional_decorator_1.getTransactionalMetadata)(metatype);
85
+ const methodNames = this.metadataScanner.getAllMethodNames(prototype);
86
+ const methods = prototype;
87
+ const host = instance;
88
+ for (const methodName of methodNames) {
89
+ const protoMethod = methods[methodName];
90
+ if (typeof protoMethod !== 'function')
91
+ continue;
92
+ const metadata = (0, transactional_decorator_1.getTransactionalMetadata)(protoMethod) ?? classMetadata;
93
+ if (metadata === undefined)
94
+ continue;
95
+ const currentMethod = host[methodName];
96
+ if (typeof currentMethod !== 'function')
97
+ continue;
98
+ if (Reflect.getMetadata(markers_1.WRAPPED_MARKER, currentMethod) === true)
99
+ continue;
100
+ if (this.wrapMethod(host, methodName, currentMethod, instance, metadata)) {
101
+ wrappedCount++;
102
+ }
103
+ }
104
+ }
105
+ this.logger.log(`Wrapped ${wrappedCount} @Transactional method${wrappedCount === 1 ? '' : 's'}`);
106
+ }
107
+ isCqrsHandler(metatype) {
108
+ return CQRS_HANDLER_KEYS.some((key) => Reflect.hasMetadata(key, metatype));
109
+ }
110
+ wrapMethod(host, methodName, currentMethod, instance, metadata) {
111
+ const boundOriginal = currentMethod.bind(instance);
112
+ const manager = this.manager;
113
+ const wrapped = (...args) => manager.run(metadata, () => Promise.resolve(boundOriginal(...args)));
114
+ Reflect.defineMetadata(markers_1.WRAPPED_MARKER, true, wrapped);
115
+ Reflect.defineMetadata(transactional_decorator_1.TRANSACTIONAL_METADATA, metadata, wrapped);
116
+ host[methodName] = wrapped;
117
+ this.logger.debug(`Wrapped ${instance.constructor.name}.${methodName} ` +
118
+ `(propagation=${metadata.propagation ?? 'REQUIRED'})`);
119
+ return true;
120
+ }
121
+ };
122
+ exports.TransactionalMethodsBootstrap = TransactionalMethodsBootstrap;
123
+ exports.TransactionalMethodsBootstrap = TransactionalMethodsBootstrap = TransactionalMethodsBootstrap_1 = __decorate([
124
+ (0, common_1.Injectable)(),
125
+ __metadata("design:paramtypes", [core_1.DiscoveryService,
126
+ core_1.MetadataScanner,
127
+ transaction_manager_1.TransactionManager])
128
+ ], TransactionalMethodsBootstrap);
129
+ //# sourceMappingURL=transactional-methods.bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transactional-methods.bootstrap.js","sourceRoot":"","sources":["../../src/bootstrap/transactional-methods.bootstrap.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,4BAA0B;AAE1B,2CAAiF;AACjF,uCAAiE;AAEjE,mFAI+C;AAC/C,iDAAqD;AACrD,wEAAoE;AAEpE;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAsB;IAC3C,oBAAoB;IACpB,kBAAkB;IAClB,mBAAmB;CACpB,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEI,IAAM,6BAA6B,qCAAnC,MAAM,6BAA6B;IAIrB;IACA;IACA;IALF,MAAM,GAAG,IAAI,eAAM,CAAC,+BAA6B,CAAC,IAAI,CAAC,CAAC;IAEzE,YACmB,SAA2B,EAC3B,eAAgC,EAChC,OAA2B;QAF3B,cAAS,GAAT,SAAS,CAAkB;QAC3B,oBAAe,GAAf,eAAe,CAAiB;QAChC,YAAO,GAAP,OAAO,CAAoB;IAC3C,CAAC;IAEJ,sBAAsB;QACpB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;QAChD,IAAI,YAAY,GAAG,CAAC,CAAC;QAErB,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;YAChC,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS;gBAAE,SAAS;YAC1E,IAAI,OAAO,OAAO,CAAC,QAAQ,KAAK,UAAU;gBAAE,SAAS;YAErD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAkB,CAAC;YAC5C,IAAI,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAE3C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAkB,CAAC;YAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAkB,CAAC;YACnE,IAAI,SAAS,KAAK,IAAI;gBAAE,SAAS;YAEjC,MAAM,aAAa,GAAG,IAAA,kDAAwB,EAAC,QAAQ,CAAC,CAAC;YACzD,MAAM,WAAW,GAAG,IAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YACtE,MAAM,OAAO,GAAG,SAAoC,CAAC;YACrD,MAAM,IAAI,GAAG,QAAmC,CAAC;YAEjD,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;gBACrC,MAAM,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;gBACxC,IAAI,OAAO,WAAW,KAAK,UAAU;oBAAE,SAAS;gBAEhD,MAAM,QAAQ,GAAG,IAAA,kDAAwB,EAAC,WAAW,CAAC,IAAI,aAAa,CAAC;gBACxE,IAAI,QAAQ,KAAK,SAAS;oBAAE,SAAS;gBAErC,MAAM,aAAa,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;gBACvC,IAAI,OAAO,aAAa,KAAK,UAAU;oBAAE,SAAS;gBAClD,IAAI,OAAO,CAAC,WAAW,CAAC,wBAAc,EAAE,aAAa,CAAC,KAAK,IAAI;oBAAE,SAAS;gBAE1E,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,aAAyB,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;oBACrF,YAAY,EAAE,CAAC;gBACjB,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CACb,WAAW,YAAY,yBAAyB,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAChF,CAAC;IACJ,CAAC;IAEO,aAAa,CAAC,QAAgB;QACpC,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC7E,CAAC;IAEO,UAAU,CAChB,IAA6B,EAC7B,UAAkB,EAClB,aAAuB,EACvB,QAAgB,EAChB,QAA+B;QAE/B,MAAM,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAE7B,MAAM,OAAO,GAAG,CAAC,GAAG,IAAe,EAAoB,EAAE,CACvD,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAEvE,OAAO,CAAC,cAAc,CAAC,wBAAc,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACtD,OAAO,CAAC,cAAc,CAAC,gDAAsB,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAClE,IAAI,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC;QAE3B,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,WAAW,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,UAAU,GAAG;YACnD,gBAAgB,QAAQ,CAAC,WAAW,IAAI,UAAU,GAAG,CACxD,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AA9EY,sEAA6B;wCAA7B,6BAA6B;IADzC,IAAA,mBAAU,GAAE;qCAKmB,uBAAgB;QACV,sBAAe;QACvB,wCAAkB;GANnC,6BAA6B,CA8EzC"}
@@ -0,0 +1,38 @@
1
+ import { type ActiveTransaction } from './transaction.context';
2
+ /**
3
+ * Per-dataSource read-only view over {@link TransactionContext}'s active
4
+ * transaction Map (DD-022). Bound to a single dataSource name at
5
+ * construction; consumers ask "is there an active transaction for *my*
6
+ * dataSource?" without typing the name twice.
7
+ *
8
+ * Mutations (set / remove) intentionally do NOT live on this surface —
9
+ * they remain on `TransactionContext` and are the domain of
10
+ * {@link TransactionManager} alone. The view is a thin lookup helper
11
+ * to keep the per-dataSource inject-decorator surface (DD-022)
12
+ * meaningful without duplicating the lifecycle API.
13
+ *
14
+ * Wired by `TransactionalModule.forRoot` under
15
+ * `getTransactionContextToken(dataSource)`. Inject via
16
+ * `@InjectTransactionContext(dataSource?)`.
17
+ */
18
+ export declare class TransactionContextView {
19
+ readonly dataSource: string;
20
+ /**
21
+ * @param dataSource - Public dataSource name this view is bound to.
22
+ * `'default'` for the single-adapter case.
23
+ */
24
+ constructor(dataSource: string);
25
+ /**
26
+ * Return the active transaction for the bound dataSource, or
27
+ * `undefined` if none is active. Equivalent to calling
28
+ * `TransactionContext.getActiveTransactionByDataSource(this.dataSource)`.
29
+ */
30
+ getActiveTransaction(): ActiveTransaction | undefined;
31
+ /**
32
+ * Predicate convenience over {@link getActiveTransaction}. Useful for
33
+ * smart-facade publishers (DD-024) that branch on whether the bound
34
+ * dataSource is the one currently running a transaction.
35
+ */
36
+ hasActiveTransaction(): boolean;
37
+ }
38
+ //# sourceMappingURL=transaction-context-view.d.ts.map
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransactionContextView = void 0;
4
+ const transaction_context_1 = require("./transaction.context");
5
+ /**
6
+ * Per-dataSource read-only view over {@link TransactionContext}'s active
7
+ * transaction Map (DD-022). Bound to a single dataSource name at
8
+ * construction; consumers ask "is there an active transaction for *my*
9
+ * dataSource?" without typing the name twice.
10
+ *
11
+ * Mutations (set / remove) intentionally do NOT live on this surface —
12
+ * they remain on `TransactionContext` and are the domain of
13
+ * {@link TransactionManager} alone. The view is a thin lookup helper
14
+ * to keep the per-dataSource inject-decorator surface (DD-022)
15
+ * meaningful without duplicating the lifecycle API.
16
+ *
17
+ * Wired by `TransactionalModule.forRoot` under
18
+ * `getTransactionContextToken(dataSource)`. Inject via
19
+ * `@InjectTransactionContext(dataSource?)`.
20
+ */
21
+ class TransactionContextView {
22
+ dataSource;
23
+ /**
24
+ * @param dataSource - Public dataSource name this view is bound to.
25
+ * `'default'` for the single-adapter case.
26
+ */
27
+ constructor(dataSource) {
28
+ this.dataSource = dataSource;
29
+ }
30
+ /**
31
+ * Return the active transaction for the bound dataSource, or
32
+ * `undefined` if none is active. Equivalent to calling
33
+ * `TransactionContext.getActiveTransactionByDataSource(this.dataSource)`.
34
+ */
35
+ getActiveTransaction() {
36
+ return transaction_context_1.TransactionContext.getActiveTransactionByDataSource(this.dataSource);
37
+ }
38
+ /**
39
+ * Predicate convenience over {@link getActiveTransaction}. Useful for
40
+ * smart-facade publishers (DD-024) that branch on whether the bound
41
+ * dataSource is the one currently running a transaction.
42
+ */
43
+ hasActiveTransaction() {
44
+ return this.getActiveTransaction() !== undefined;
45
+ }
46
+ }
47
+ exports.TransactionContextView = TransactionContextView;
48
+ //# sourceMappingURL=transaction-context-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-context-view.js","sourceRoot":"","sources":["../../src/context/transaction-context-view.ts"],"names":[],"mappings":";;;AAAA,+DAG+B;AAE/B;;;;;;;;;;;;;;;GAeG;AACH,MAAa,sBAAsB;IAKZ;IAJrB;;;OAGG;IACH,YAAqB,UAAkB;QAAlB,eAAU,GAAV,UAAU,CAAQ;IAAG,CAAC;IAE3C;;;;OAIG;IACH,oBAAoB;QAClB,OAAO,wCAAkB,CAAC,gCAAgC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9E,CAAC;IAED;;;;OAIG;IACH,oBAAoB;QAClB,OAAO,IAAI,CAAC,oBAAoB,EAAE,KAAK,SAAS,CAAC;IACnD,CAAC;CACF;AAxBD,wDAwBC"}
@@ -0,0 +1,114 @@
1
+ import type { TransactionHandle } from '../types/transaction-handle';
2
+ import type { TransactionOptions } from '../types/transaction-options';
3
+ /**
4
+ * A single transaction currently live on the async context. Adapters register
5
+ * one of these under their `adapterInstanceName` when they begin a transaction
6
+ * and remove it when the transaction ends. Hooks registered on this object
7
+ * are fired by the manager during commit / rollback.
8
+ */
9
+ export interface ActiveTransaction {
10
+ /** Adapter-specific opaque handle — cast by the adapter's own helpers. */
11
+ readonly handle: TransactionHandle;
12
+ /** Adapter type name (e.g. `'typeorm'`) — mirrors `handle.adapterName`. */
13
+ readonly adapterName: string;
14
+ /** Adapter instance name (e.g. `'primary'`, `'billing'`) under which it was registered. */
15
+ readonly adapterInstanceName: string;
16
+ /** Options the manager handed to the adapter when beginning this transaction. */
17
+ readonly options: TransactionOptions;
18
+ /** Wall-clock moment the transaction began, for observability. */
19
+ readonly startedAt: Date;
20
+ /** Hooks executed just before the adapter issues COMMIT. A throwing hook rolls the transaction back. */
21
+ readonly afterCommitHooks: (() => Promise<void>)[];
22
+ /** Hooks executed after a successful COMMIT. */
23
+ readonly afterRollbackHooks: ((error: unknown) => Promise<void>)[];
24
+ /** Hooks executed after a ROLLBACK; receive the error that caused the rollback. */
25
+ readonly beforeCommitHooks: (() => Promise<void>)[];
26
+ /** Correlation id inherited from the enclosing {@link TransactionContext} scope. */
27
+ readonly correlationId: string;
28
+ }
29
+ /**
30
+ * Per-scope state that lives on the async context. Created on the outermost
31
+ * {@link TransactionContext.run} call and reused by all nested ones, so that
32
+ * a single logical unit of work owns a single correlation id and a single
33
+ * registry of active transactions across adapters.
34
+ */
35
+ export interface TransactionContextStore {
36
+ /** Active transactions keyed by `adapterInstanceName`. */
37
+ readonly activeTransactions: Map<string, ActiveTransaction>;
38
+ /** Stable correlation id for the whole scope — set by the outermost run(). */
39
+ readonly correlationId: string;
40
+ /** Wall-clock moment the scope began. */
41
+ readonly startedAt: Date;
42
+ }
43
+ /**
44
+ * Thin façade over `AsyncLocalStorage` that carries the active
45
+ * {@link TransactionContextStore} across async boundaries. This is the
46
+ * foundation of the module — every decorator, interceptor, and adapter helper
47
+ * ultimately asks this class whether a transaction is live on the current
48
+ * async chain.
49
+ *
50
+ * **Internal Map key format**: `${adapterName}:${instanceName}` (composite)
51
+ * for historical reasons and cross-package compatibility — typeorm
52
+ * helpers, the CQRS dispatcher, and the outbox publisher all consume
53
+ * this format directly via {@link getActiveTransaction}. The composite
54
+ * key is also the format `TransactionManager` writes under.
55
+ *
56
+ * **Public dataSource-name access**:
57
+ * {@link getActiveTransactionByDataSource} provides dataSource-name
58
+ * lookup for Phase 14.2+ multi-adapter consumers — it scans the Map
59
+ * for the entry whose `adapterInstanceName === dataSource`. Both
60
+ * access patterns coexist; future cleanup is possible once
61
+ * cross-package consumers migrate to the dataSource-name lookup and
62
+ * no backwards-compatibility constraints remain.
63
+ */
64
+ export declare class TransactionContext {
65
+ /**
66
+ * Run `fn` inside a transaction context scope.
67
+ *
68
+ * - If there is no active store on the current async chain, a new store is
69
+ * created (empty active-transaction map, `correlationId`, current time)
70
+ * and installed for the duration of `fn`.
71
+ * - If there is already an active store, `fn` is executed directly — the
72
+ * existing store is reused. The `correlationId` argument is ignored in
73
+ * that case; the outermost scope owns the correlation id.
74
+ *
75
+ * Propagates the value resolved (or error thrown) by `fn`.
76
+ */
77
+ static run<T>(correlationId: string, fn: () => Promise<T>): Promise<T>;
78
+ /** Return the active store, or `undefined` if called outside any run() scope. */
79
+ static getStore(): TransactionContextStore | undefined;
80
+ /** Return the active transaction registered under `adapterInstanceName`, or `undefined`. */
81
+ static getActiveTransaction(adapterInstanceName: string): ActiveTransaction | undefined;
82
+ /**
83
+ * Return the active transaction whose adapter instance was
84
+ * registered under the given dataSource name (DD-020 / DD-023).
85
+ * Scans the active-transactions Map for the entry whose
86
+ * `adapterInstanceName === dataSource`.
87
+ *
88
+ * Returns the first match — there is exactly one active transaction
89
+ * per dataSource by construction (multiple adapters sharing the
90
+ * same dataSource name is rejected at the registry level by
91
+ * {@link AdapterRegistry.getByDataSource}).
92
+ *
93
+ * Used by {@link TransactionContextView} and by smart-facade
94
+ * publishers to answer "is there an active transaction for *my*
95
+ * dataSource?" without needing to know the adapter type that owns
96
+ * the dataSource.
97
+ */
98
+ static getActiveTransactionByDataSource(dataSource: string): ActiveTransaction | undefined;
99
+ /**
100
+ * Register `tx` under `adapterInstanceName` on the current store.
101
+ *
102
+ * @throws {IllegalTransactionStateError} If called outside of a run() scope.
103
+ * Adapters must not attempt to register a transaction without a
104
+ * surrounding context — doing so would leak the transaction state.
105
+ */
106
+ static setActiveTransaction(adapterInstanceName: string, tx: ActiveTransaction): void;
107
+ /**
108
+ * Remove the active transaction registered under `adapterInstanceName`.
109
+ * Idempotent: a no-op when no store is active or the instance is not
110
+ * registered, so that adapter cleanup paths can call it unconditionally.
111
+ */
112
+ static removeActiveTransaction(adapterInstanceName: string): void;
113
+ }
114
+ //# sourceMappingURL=transaction.context.d.ts.map