@fluojs/mongoose 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -11,6 +11,8 @@
11
11
  - [빠른 시작](#빠른-시작)
12
12
  - [라이프사이클과 종료](#라이프사이클과-종료)
13
13
  - [공통 패턴](#공통-패턴)
14
+ - [서비스 트랜잭션 경계 (@Transaction)](#서비스-트랜잭션-경계-transaction)
15
+ - [수동 트랜잭션과 currentSession()](#수동-트랜잭션과-currentsession)
14
16
  - [공개 API](#공개-api)
15
17
  - [관련 패키지](#관련-패키지)
16
18
  - [예제 소스](#예제-소스)
@@ -26,7 +28,7 @@ pnpm add mongoose
26
28
 
27
29
  - Mongoose를 나머지 애플리케이션과 같은 DI 및 라이프사이클 모델에 연결하고 싶을 때.
28
30
  - 모든 서비스에서 MongoDB 세션과 트랜잭션을 임시 배관 코드 없이 하나의 wrapper로 다루고 싶을 때.
29
- - 요청 범위 트랜잭션을 interceptor로 명시적으로 켜고 싶을 때.
31
+ - 애플리케이션이 이미 concrete Mongoose connection을 생성·구성하고 있고, fluo가 그 ownership을 대체하지 않고 관측하기를 원할 때.
30
32
 
31
33
  ## 빠른 시작
32
34
 
@@ -54,77 +56,103 @@ class AppModule {}
54
56
 
55
57
  ## 라이프사이클과 종료
56
58
 
57
- `MongooseModule`은 `MongooseConnection`을 fluo 애플리케이션 라이프사이클에 등록합니다. 이 패키지는 원본 Mongoose 연결을 직접 생성하거나 소유하지 않습니다. 애플리케이션 종료 시 외부 연결을 닫아야 한다면 `dispose` 훅을 전달하세요.
59
+ `MongooseModule`은 `MongooseConnection`을 fluo 애플리케이션 라이프사이클에 등록합니다. 이 패키지는 원본 Mongoose 연결을 직접 생성하거나 소유하지 않습니다. `connection`에는 concrete Mongoose connection object/function을 전달하고, 연결 문자열, pool, plugin, model compilation ownership은 애플리케이션에 남겨두며, 애플리케이션 종료 시 외부 연결을 닫아야 한다면 `dispose` 훅을 전달하세요.
58
60
 
59
- 종료 과정은 트랜잭션 정리 순서를 보존하며, 종료가 시작된 뒤에는 수동 또는 요청 범위 트랜잭션 경계를 거부합니다.
61
+ 종료 절차는 트랜잭션 정리 순서를 보존하고, 종료가 시작된 뒤에는 새로운 수동 또는 요청 단위 트랜잭션 경계를 거부합니다.
60
62
 
61
- 1. 열려 있는 요청 범위 트랜잭션은 `Application shutdown interrupted an open request transaction.` 오류로 abort됩니다.
63
+ 1. 열린 요청 단위 트랜잭션은 `Application shutdown interrupted an open request transaction.`으로 abort됩니다.
62
64
  2. 활성 ambient session은 transaction callback과 session cleanup이 settle될 때까지 추적됩니다.
63
65
  3. 해당 Mongoose 세션은 `abortTransaction()`과 `endSession()` 정리를 끝냅니다.
64
66
  4. 설정한 `dispose(connection)` 훅은 활성 요청 트랜잭션과 ambient session scope가 모두 settled된 뒤에만 실행됩니다.
65
67
 
66
- `createMongoosePlatformStatusSnapshot(...)`은 트래픽 처리 중에는 `ready`, 요청 트랜잭션 drain 중에는 `shutting-down`, dispose 완료 뒤에는 `stopped`를 보고합니다. 상태 details에는 `sessionStrategy`, `transactionContext: 'als'`, 활성 요청/session 개수, 리소스 소유권, strict/session 지원 진단이 포함됩니다. 수동 `transaction()`도 요청 범위 트랜잭션과 같은 명시적 세션 계약을 사용하므로, 트랜잭션에 참여해야 하는 Mongoose 모델 작업에는 repository 코드가 `conn.currentSession()`을 전달해야 합니다. 감싼 Mongoose 연결이 `connection.transaction(...)`을 노출하면 fluo는 Mongoose 자체 ambient-session scope를 보존하기 위해 API에 transaction boundary를 위임하면서도 같은 session을 `currentSession()`으로 노출합니다. 요청 범위 트랜잭션은 session을 획득하는 동안과 위임된 `connection.transaction(...)` 작업을 시작하는 동안에도 request `AbortSignal`을 관찰하므로, 요청 취소가 사용자 callback 실행 전에 이러한 시작 단계를 중단할 수 있습니다.
68
+ `MongooseConnection.createPlatformStatusSnapshot()`과 export된 low-level `createMongoosePlatformStatusSnapshot(...)` helper는 serving 중에는 `ready`, 요청 트랜잭션을 drain하는 shutdown 중에는 `shutting-down`, dispose hook 완료 후에는 `stopped`를 보고합니다. status details에는 `sessionStrategy`, `transactionContext: 'als'`, 활성 요청/세션 수, 리소스 소유권, strict/session 지원 진단이 포함됩니다. 수동 `transaction()` 호출과 서비스 `@Transaction()` 메서드는 같은 ambient session을 `conn.model(...)`에 노출합니다. 지원되는 facade 메서드(`create`, `find`, `findOne`, `aggregate`, `bulkWrite`)는 해당 세션을 자동으로 첨부합니다. 자동 세션 주입은 `MongooseConnection.model(...)` wrapper 메서드에만 scope되며, `conn.current()`가 반환하는 raw `connection.model(...)` cache/compile 경로를 교체하거나 변형하지 않습니다. 지원되지 않는 model 메서드, `doc.save()`, 외부 유틸리티에 명시적 세션 배관이 필요할 때는 `conn.currentSession()`을 사용하세요. 래핑된 Mongoose connection이 `connection.transaction(...)`을 제공하면 fluo는 Mongoose 자체 ambient-session scope를 보존하면서 동일한 세션을 `currentSession()`으로 노출하도록 해당 API에 트랜잭션 경계를 위임합니다. 요청 단위 트랜잭션은 세션을 획득하는 동안과 위임된 `connection.transaction(...)` 작업을 시작하는 동안 request `AbortSignal`을 관찰하므로, request cancellation은 사용자 callback 실행되기 전의 startup phase를 중단할 수 있습니다.
69
+
67
70
  기존 수동 `transaction(...)` boundary 안에서 열린 중첩 `requestTransaction(...)` 호출은 ambient session을 재사용하고 `details.activeRequestTransactions`에 계속 표시되며, 종료 중에 abort되어 바깥 수동 transaction이 `dispose(connection)` 실행 전에 rollback할 수 있습니다.
68
71
 
69
72
  ## 공통 패턴
70
73
 
71
- ### `MongooseConnection`을 통한 연결 접근
74
+ ### 서비스 트랜잭션 경계 (@Transaction)
72
75
 
73
- `MongooseConnection` 래퍼는 기본 Mongoose 연결에 대한 접근을 제공합니다.
76
+ `@Transaction()` 데코레이터는 서비스 레이어에서 트랜잭션 경계를 정의하는 권장 방법입니다. 이 데코레이터가 적용된 메서드 내부에서 발생하는 모든 리포지토리 호출은 동일한 MongoDB 세션을 공유합니다.
74
77
 
75
- ```typescript
76
- import { MongooseConnection } from '@fluojs/mongoose';
78
+ ```ts
79
+ import { Transaction } from '@fluojs/mongoose';
80
+ import { UserRepository } from './user.repository';
81
+
82
+ export class UserService {
83
+ constructor(private readonly repo: UserRepository) {}
84
+
85
+ @Transaction()
86
+ async onboardUser(dto: CreateUserDto) {
87
+ const user = await this.repo.create(dto);
88
+ await this.repo.initProfile(user._id);
89
+ return user;
90
+ }
91
+ }
77
92
 
78
93
  export class UserRepository {
79
94
  constructor(private readonly conn: MongooseConnection) {}
80
95
 
81
- async findById(id: string) {
82
- const User = this.conn.current().model('User');
83
- return User.findById(id);
96
+ async create(data: any) {
97
+ // @Transaction() 내부에서 conn.model()은 세션 인지형 facade를 반환합니다.
98
+ // create, find, findOne, aggregate, bulkWrite 등의 작업은
99
+ // 자동으로 활성 트랜잭션에 참여합니다.
100
+ return this.conn.model('User').create(data);
101
+ }
102
+
103
+ async initProfile(userId: any) {
104
+ return this.conn.model('Profile').create({ userId });
84
105
  }
85
106
  }
86
107
  ```
87
108
 
88
- ### 수동 트랜잭션과 세션
109
+ `@Transaction()` 메서드 호출은 재진입(reentrant)이 가능합니다. 데코레이터가 적용된 메서드가 다른 데코레이터 적용 메서드를 호출하더라도 하나의 동일한 MongoDB 세션 안에서 실행됩니다. 참고로 v1에서 `doc.save()`는 자동으로 세션을 주입하지 않으므로, 자동 트랜잭션 참여가 필요하다면 지원되는 facade 작업(`model.create()`, `model.find()`, `model.findOne()`, `model.aggregate()`, `model.bulkWrite()`)을 사용하세요.
89
110
 
90
- `conn.transaction()`으로 세션 경계를 만들고, Mongoose 모델 작업에는 세션을 명시적으로 전달합니다.
111
+ ### 수동 트랜잭션과 currentSession()
91
112
 
92
- ```typescript
93
- await this.conn.transaction(async () => {
94
- const session = this.conn.currentSession();
95
- const User = this.conn.current().model('User');
96
-
97
- // 작업에 세션을 명시적으로 전달
98
- await User.create([{ name: 'Ada' }], { session });
99
- });
100
- ```
101
-
102
- 감싼 연결이 `startSession()`을 구현하지 않으면 트랜잭션은 기본적으로 직접 실행으로 fallback합니다. fallback 대신 예외를 던지려면 `strictTransactions: true`를 설정합니다. 이때 오류 메시지는 `Transaction not supported: Mongoose connection does not implement startSession.`입니다.
113
+ `MongooseConnection`은 활성 MongoDB 세션에 접근하기 위한 `currentSession()`과 루트 연결 handle에 접근하기 위한 `current()` 메서드를 제공합니다. 외부 유틸리티에 세션을 전달하거나 복잡한 수동 처리가 필요한 경우 escape hatch로 사용하세요.
103
114
 
104
- Fluo는 Mongoose operation option을 다시 쓰지 않습니다. 모델 호출이 명시적인 `{ session }`을 전달하면 그 option은 그대로 유지되며, 생략한 경우 fluo가 session을 자동 부착한다고 가정하면 안 됩니다. 같은 session에서 병렬 작업이나 중첩 transaction 기대치는 보수적으로 유지하세요. 중첩된 `MongooseConnection.transaction(...)` 호출은 같은 session에 두 번째 MongoDB transaction을 여는 대신 활성 boundary를 재사용합니다.
115
+ ```ts
116
+ import { MongooseConnection } from '@fluojs/mongoose';
105
117
 
106
- ### 요청 범위 트랜잭션
118
+ export class AdvancedRepository {
119
+ constructor(private readonly conn: MongooseConnection) {}
107
120
 
108
- 컨트롤러나 메서드에 `MongooseTransactionInterceptor`를 적용하면 전체 요청을 MongoDB 세션으로 감쌉니다.
121
+ async customOperation() {
122
+ const session = this.conn.currentSession();
123
+ const User = this.conn.current().model('User');
124
+
125
+ // 명시적으로 세션 전달
126
+ return User.find({ status: 'active' }).session(session || null);
127
+ }
128
+ }
129
+ ```
109
130
 
110
- ```typescript
111
- import { UseInterceptors } from '@fluojs/http';
112
- import { MongooseTransactionInterceptor } from '@fluojs/mongoose';
131
+ 수동 트랜잭션 블록에는 `conn.transaction()`을 사용하세요:
113
132
 
114
- @UseInterceptors(MongooseTransactionInterceptor)
115
- class UserController {}
133
+ ```ts
134
+ await this.conn.transaction(async () => {
135
+ const User = this.conn.model('User');
136
+ await User.create([{ name: 'Ada' }]);
137
+ });
116
138
  ```
117
139
 
118
- HTTP interceptor 밖에서 같은 request-aware transaction boundary가 필요하다면 `MongooseConnection.requestTransaction(...)`을 직접 사용할 있습니다. 중첩된 service transaction은 활성 session boundary를 재사용하며, 수동 transaction 안에서 열린 중첩 request boundary도 request abort와 shutdown tracking에 참여합니다.
140
+ 래핑된 연결이 `connection.transaction(...)`을 구현하고 있다면 fluo는 이를 엄격한 트랜잭션 경계로 취급합니다. 그렇지 않고 `startSession()`이 없는 경우 트랜잭션은 기본값(`strictTransactions: false`)에서 callback 직접 실행으로 fail-open합니다. 모드는 local fake나 staged migration에는 유용하지만 rollback 원자성은 제공하지 않습니다. MongoDB transaction 보장이 필요한 production 흐름에서는 `strictTransactions: true`를 설정하세요. 그러면 transaction 지원 누락이 readiness `not-ready`와 helper 예외로 드러납니다.
141
+
142
+ 지원되는 facade 메서드에서 fluo는 기존 Mongoose 작업 옵션을 보존하고 올바른 options 인자에 ambient `{ session }`만 병합합니다. 활성 트랜잭션 내부에서 명시적으로 `{ session: null }`을 전달하거나 다른 세션 객체를 사용하면, 의도치 않은 트랜잭션 탈출을 방지하기 위해 세션 충돌 에러를 발생시킵니다.
119
143
 
120
144
  ## 공개 API
121
145
 
122
146
  - `MongooseModule.forRoot(options)` / `MongooseModule.forRootAsync(options)`
123
147
  - `MongooseConnection`
124
- - `MongooseTransactionInterceptor`
148
+ - `MongooseConnection.createPlatformStatusSnapshot()` — platform observability surface를 위해 health/readiness, resource ownership, 활성 request/session drain 수, strict transaction 지원 진단을 보고합니다.
149
+ - `MongooseConnection.model(name, ...args)` — 트랜잭션 밖에서는 raw model을 반환하고, 활성 트랜잭션 안에서는 underlying Mongoose connection을 변형하지 않으면서 `create`, `find`, `findOne`, `aggregate`, `bulkWrite`에 세션을 주입하는 facade를 반환합니다.
150
+ - `Transaction`
125
151
  - `MONGOOSE_CONNECTION`, `MONGOOSE_DISPOSE`, `MONGOOSE_OPTIONS`
126
152
  - `createMongooseProviders(options)` — 호환성/수동 composition helper입니다. 애플리케이션-facing 등록에서는 module export와 provider visibility가 문서화된 namespace facade와 맞도록 `MongooseModule.forRoot(...)` 또는 `MongooseModule.forRootAsync(...)`를 우선 사용하세요.
127
153
  - `createMongoosePlatformStatusSnapshot(...)`
154
+ - sync 및 async 등록 모두에서 `connection`은 실제 object/function handle이어야 하며, 누락된 handle은 모듈 등록 또는 async bootstrap 중 거부됩니다.
155
+ - `Transaction`은 서비스 계층 세션 트랜잭션 경계를 위한 표준 TC39 method decorator입니다. 기본적으로 `this.conn`, 데코레이터가 적용된 인스턴스 자체, 또는 하나의 고유한 중첩 `this.*.conn` collaborator를 resolve합니다. `MongooseConnection`이 다른 필드에 있거나 resolution이 모호하다면 accessor를 전달하세요.
128
156
 
129
157
  ### 관련 export 타입
130
158
 
@@ -138,7 +166,7 @@ HTTP interceptor 밖에서 같은 request-aware transaction boundary가 필요
138
166
  ## 관련 패키지
139
167
 
140
168
  - `@fluojs/runtime`: 애플리케이션 라이프사이클 및 종료 훅을 관리합니다.
141
- - `@fluojs/http`: 인터셉터 시스템을 제공합니다.
169
+ - `@fluojs/http`: 명시적 `requestTransaction(...)` 경계와 함께 사용할 수 있는 요청 라이프사이클 primitive를 제공합니다.
142
170
  - `@fluojs/prisma` / `@fluojs/drizzle`: 대안 데이터베이스 통합 모듈입니다.
143
171
 
144
172
  ## 예제 소스
package/README.md CHANGED
@@ -11,6 +11,8 @@ Mongoose integration for fluo with session-aware transaction handling and lifecy
11
11
  - [Quick Start](#quick-start)
12
12
  - [Lifecycle and Shutdown](#lifecycle-and-shutdown)
13
13
  - [Common Patterns](#common-patterns)
14
+ - [Service Transaction Boundary (@Transaction)](#service-transaction-boundary-transaction)
15
+ - [Manual Transactions and currentSession()](#manual-transactions-and-currentsession)
14
16
  - [Public API](#public-api)
15
17
  - [Related Packages](#related-packages)
16
18
  - [Example Sources](#example-sources)
@@ -26,7 +28,8 @@ pnpm add mongoose
26
28
 
27
29
  - when Mongoose should plug into the same DI and application lifecycle as the rest of the app
28
30
  - when MongoDB sessions and transactions need one shared wrapper instead of ad hoc session plumbing in every service
29
- - when request-scoped transactions should be opt-in through an interceptor
31
+ - when request-scoped transactions need explicit `requestTransaction(...)` boundaries
32
+ - when an application already creates and configures its concrete Mongoose connection and wants fluo to observe, not replace, that ownership
30
33
 
31
34
  ## Quick Start
32
35
 
@@ -52,7 +55,7 @@ class AppModule {}
52
55
 
53
56
  ## Lifecycle and Shutdown
54
57
 
55
- `MongooseModule` registers `MongooseConnection` with the fluo application lifecycle. The package does not create or own the raw Mongoose connection for you; pass a `dispose` hook when the application should close that external connection during shutdown.
58
+ `MongooseModule` registers `MongooseConnection` with the fluo application lifecycle. The package does not create or own the raw Mongoose connection for you; pass a concrete Mongoose connection object/function as `connection`, keep connection-string, pool, plugin, and model compilation ownership in the application, and provide a `dispose` hook when the application should close that external connection during shutdown.
56
59
 
57
60
  Shutdown preserves transaction cleanup order and rejects new manual or request-scoped transaction boundaries once shutdown begins:
58
61
 
@@ -61,61 +64,93 @@ Shutdown preserves transaction cleanup order and rejects new manual or request-s
61
64
  3. Their Mongoose sessions finish `abortTransaction()` and `endSession()` cleanup.
62
65
  4. The configured `dispose(connection)` hook runs only after active request transactions and ambient session scopes have settled.
63
66
 
64
- `createMongoosePlatformStatusSnapshot(...)` reports `ready` while serving traffic, `shutting-down` while request transactions are draining, and `stopped` after the dispose hook completes. The status details include `sessionStrategy`, `transactionContext: 'als'`, active request/session counts, resource ownership, and strict/session support diagnostics. Manual `transaction()` calls still use the same explicit-session contract as request-scoped transactions: repository code must pass `conn.currentSession()` into Mongoose model operations that participate in the transaction. If the wrapped Mongoose connection exposes `connection.transaction(...)`, fluo delegates the transaction boundary to that API so Mongoose's own ambient-session scope is preserved while still exposing the same session through `currentSession()`. Request-scoped transactions observe the request `AbortSignal` while acquiring sessions and while starting delegated `connection.transaction(...)` work, so request cancellation can interrupt those startup phases before user callbacks run.
67
+ `MongooseConnection.createPlatformStatusSnapshot()` and the exported low-level `createMongoosePlatformStatusSnapshot(...)` helper report `ready` while serving traffic, `shutting-down` while request transactions are draining, and `stopped` after the dispose hook completes. The status details include `sessionStrategy`, `transactionContext: 'als'`, active request/session counts, resource ownership, and strict/session support diagnostics. Manual `transaction()` calls and service `@Transaction()` methods expose the same ambient session to `conn.model(...)`; supported facade methods (`create`, `find`, `findOne`, `aggregate`, and `bulkWrite`) automatically attach that session. Automatic session injection is scoped to the `MongooseConnection.model(...)` wrapper method and does not replace or mutate the raw `connection.model(...)` cache/compile path returned by `conn.current()`. Use `conn.currentSession()` for unsupported model methods, `doc.save()`, or external utilities that need explicit session plumbing. If the wrapped Mongoose connection exposes `connection.transaction(...)`, fluo delegates the transaction boundary to that API so Mongoose's own ambient-session scope is preserved while still exposing the same session through `currentSession()`. Request-scoped transactions observe the request `AbortSignal` while acquiring sessions and while starting delegated `connection.transaction(...)` work, so request cancellation can interrupt those startup phases before user callbacks run.
65
68
  Nested `requestTransaction(...)` calls opened inside an existing manual `transaction(...)` boundary reuse the ambient session, stay visible in `details.activeRequestTransactions`, and are aborted during shutdown so the outer manual transaction can roll back before `dispose(connection)` runs.
66
69
 
67
70
  ## Common Patterns
68
71
 
69
- ### Access the connection through `MongooseConnection`
72
+ ### Service Transaction Boundary (@Transaction)
73
+
74
+ The `@Transaction()` decorator is the recommended way to define transaction boundaries in your service layer. It ensures that all repository calls made within the decorated method share the same MongoDB session.
70
75
 
71
76
  ```ts
72
- import { MongooseConnection } from '@fluojs/mongoose';
77
+ import { Transaction } from '@fluojs/mongoose';
78
+ import { UserRepository } from './user.repository';
79
+
80
+ export class UserService {
81
+ constructor(private readonly repo: UserRepository) {}
82
+
83
+ @Transaction()
84
+ async onboardUser(dto: CreateUserDto) {
85
+ const user = await this.repo.create(dto);
86
+ await this.repo.initProfile(user._id);
87
+ return user;
88
+ }
89
+ }
73
90
 
74
91
  export class UserRepository {
75
92
  constructor(private readonly conn: MongooseConnection) {}
76
93
 
77
- async findById(id: string) {
78
- const User = this.conn.current().model('User');
79
- return User.findById(id);
94
+ async create(data: any) {
95
+ // model() returns a session-aware facade inside @Transaction().
96
+ // Operations like create, find, findOne, aggregate, and bulkWrite
97
+ // automatically participate in the ambient transaction.
98
+ return this.conn.model('User').create(data);
99
+ }
100
+
101
+ async initProfile(userId: any) {
102
+ return this.conn.model('Profile').create({ userId });
80
103
  }
81
104
  }
82
105
  ```
83
106
 
84
- ### Manual transactions still need explicit sessions
107
+ Calls to `@Transaction()` methods are reentrant. If a decorated method calls another decorated method, they share the same underlying MongoDB session. Note that `doc.save()` is not automatically session-aware in v1; use the supported facade operations (`model.create()`, `model.find()`, `model.findOne()`, `model.aggregate()`, or `model.bulkWrite()`) for automatic transaction participation.
85
108
 
86
- ```ts
87
- await this.conn.transaction(async () => {
88
- const session = this.conn.currentSession();
89
- const User = this.conn.current().model('User');
109
+ ### Manual Transactions and currentSession()
90
110
 
91
- await User.create([{ name: 'Ada' }], { session });
92
- });
93
- ```
111
+ The `MongooseConnection` provides `currentSession()` to access the ambient MongoDB session and `current()` to access the root connection handle. Use these as escape hatches when you need to pass sessions to external utilities or perform advanced manual plumbing.
94
112
 
95
- If the wrapped connection does not implement `startSession()`, transactions fall back to direct execution by default. Set `strictTransactions: true` to throw `Transaction not supported: Mongoose connection does not implement startSession.` instead of falling back.
113
+ ```ts
114
+ import { MongooseConnection } from '@fluojs/mongoose';
115
+
116
+ export class AdvancedRepository {
117
+ constructor(private readonly conn: MongooseConnection) {}
96
118
 
97
- Fluo never rewrites Mongoose operation options. If a model call passes an explicit `{ session }`, that option is left intact; if it omits one, repositories should not assume fluo will attach a session for them. Keep same-session parallel work and nested transaction expectations conservative: nested `MongooseConnection.transaction(...)` calls reuse the active boundary rather than opening a second MongoDB transaction on the same session.
119
+ async customOperation() {
120
+ const session = this.conn.currentSession();
121
+ const User = this.conn.current().model('User');
122
+
123
+ // Explicitly passing the session
124
+ return User.find({ status: 'active' }).session(session || null);
125
+ }
126
+ }
127
+ ```
98
128
 
99
- ### Request-scoped transactions
129
+ Use `conn.transaction()` for manual transaction blocks:
100
130
 
101
131
  ```ts
102
- import { UseInterceptors } from '@fluojs/http';
103
- import { MongooseTransactionInterceptor } from '@fluojs/mongoose';
104
-
105
- @UseInterceptors(MongooseTransactionInterceptor)
106
- class UserController {}
132
+ await this.conn.transaction(async () => {
133
+ const User = this.conn.model('User');
134
+ await User.create([{ name: 'Ada' }]);
135
+ });
107
136
  ```
108
137
 
109
- Use `MongooseConnection.requestTransaction(...)` directly when you need the same request-aware transaction boundary outside an HTTP interceptor. Nested service transactions reuse the active session boundary, and nested request boundaries opened inside a manual transaction still participate in request abort and shutdown tracking.
138
+ If the wrapped connection implements `connection.transaction(...)`, fluo treats that as the strict transaction boundary. Otherwise, when the connection does not implement `startSession()`, transactions use fail-open direct callback execution by default (`strictTransactions: false`), which is useful for local fakes and staged migrations but provides no rollback atomicity. Set `strictTransactions: true` for production flows that require MongoDB transaction guarantees; missing transaction support then makes readiness `not-ready` and causes transaction helpers to throw.
139
+
140
+ For supported facade methods, fluo preserves existing Mongoose operation options and only merges the ambient `{ session }` into the correct options argument. If a model call passes an explicit `{ session: null }` or a different session object inside an ambient transaction, fluo throws a session conflict error to prevent accidental transaction escapes.
110
141
 
111
142
  ## Public API
112
143
 
113
144
  - `MongooseModule.forRoot(options)` / `MongooseModule.forRootAsync(options)`
114
145
  - `MongooseConnection`
115
- - `MongooseTransactionInterceptor`
146
+ - `MongooseConnection.createPlatformStatusSnapshot()` — reports health/readiness, resource ownership, active request/session drain counts, and strict transaction support diagnostics for platform observability surfaces.
147
+ - `MongooseConnection.model(name, ...args)` — returns the raw model outside transactions or a session-aware facade for `create`, `find`, `findOne`, `aggregate`, and `bulkWrite` inside an active transaction without mutating the underlying Mongoose connection.
148
+ - `Transaction`
116
149
  - `MONGOOSE_CONNECTION`, `MONGOOSE_DISPOSE`, `MONGOOSE_OPTIONS`
117
150
  - `createMongooseProviders(options)` — compatibility/manual composition helper; prefer `MongooseModule.forRoot(...)` or `MongooseModule.forRootAsync(...)` for application-facing registration so module exports and provider visibility stay aligned.
118
151
  - `createMongoosePlatformStatusSnapshot(...)`
152
+ - `connection` must be a concrete object/function handle for both sync and async registration; missing handles are rejected during module registration or async bootstrap.
153
+ - `Transaction` is a standard TC39 method decorator for service-layer session transaction boundaries. It resolves `this.conn`, the decorated instance itself, or one unique nested `this.*.conn` collaborator by default; pass an accessor when the `MongooseConnection` lives under a different field or resolution would be ambiguous.
119
154
 
120
155
  ### Related exported types
121
156
 
@@ -129,7 +164,7 @@ Use `MongooseConnection.requestTransaction(...)` directly when you need the same
129
164
  ## Related Packages
130
165
 
131
166
  - `@fluojs/runtime`: manages startup and shutdown hooks
132
- - `@fluojs/http`: provides the interceptor chain for request transactions
167
+ - `@fluojs/http`: provides request lifecycle primitives that can be paired with explicit `requestTransaction(...)` boundaries
133
168
  - `@fluojs/prisma` and `@fluojs/drizzle`: alternate database integrations with different transaction models
134
169
 
135
170
  ## Example Sources
@@ -3,6 +3,7 @@ import type { MongooseConnectionLike, MongooseHandleProvider, MongooseSessionLik
3
3
  type MongooseRuntimeOptions = {
4
4
  strictTransactions: boolean;
5
5
  };
6
+ type MongooseModelLike = Record<PropertyKey, unknown>;
6
7
  /**
7
8
  * Session-aware Mongoose wrapper that integrates request scoping and shutdown handling with the Fluo runtime.
8
9
  *
@@ -39,6 +40,14 @@ export declare class MongooseConnection<TConnection extends MongooseConnectionLi
39
40
  * @returns The ambient session inside a transaction boundary, or `undefined` outside one.
40
41
  */
41
42
  currentSession(): MongooseSessionLike | undefined;
43
+ /**
44
+ * Returns a model from the root connection, injecting the ambient transaction session into conservative operations.
45
+ *
46
+ * @param name Model name passed to the underlying Mongoose connection.
47
+ * @param args Additional model resolver arguments forwarded unchanged.
48
+ * @returns The real model outside transactions, or a model facade inside an active transaction boundary.
49
+ */
50
+ model(name: string, ...args: unknown[]): MongooseModelLike;
42
51
  /** Aborts active request transactions, waits for settlement, then runs the optional dispose hook. */
43
52
  onApplicationShutdown(): Promise<void>;
44
53
  /** Produces the shared persistence status snapshot for platform diagnostics surfaces. */
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAS7D,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAuBpB,KAAK,sBAAsB,GAAG;IAC5B,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAmBF;;;;GAIG;AACH,qBACa,kBAAkB,CAAC,WAAW,SAAS,sBAAsB,GAAG,sBAAsB,CACjG,YAAW,sBAAsB,CAAC,WAAW,CAAC,EAAE,qBAAqB;IAQnE,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IARpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgD;IACzE,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAuC;IACjF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiC;IAChE,OAAO,CAAC,cAAc,CAAkD;gBAGrD,UAAU,EAAE,WAAW,EACvB,OAAO,CAAC,GAAE,CAAC,UAAU,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,aAAA,EAC3D,iBAAiB,GAAE,sBAAsD;IAG5F;;;;;;;;;OASG;IACH,OAAO,IAAI,WAAW;IAItB;;;;;;;;;OASG;IACH,cAAc,IAAI,mBAAmB,GAAG,SAAS;IAIjD,qGAAqG;IAC/F,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5C,yFAAyF;IACzF,4BAA4B;IAY5B;;;;;;;;;;;;OAYG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAoBtD;;;;;;;;;;;OAWG;IACG,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IA+CnF,OAAO,CAAC,2BAA2B;IAMnC,OAAO,CAAC,kCAAkC;YAM5B,2BAA2B;YAc3B,wBAAwB;YA4BxB,wBAAwB;IActC,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,+BAA+B;YAIzB,cAAc;CAW7B"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAS7D,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAuBpB,KAAK,sBAAsB,GAAG;IAC5B,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,KAAK,iBAAiB,GAAG,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AAyJtD;;;;GAIG;AACH,qBACa,kBAAkB,CAAC,WAAW,SAAS,sBAAsB,GAAG,sBAAsB,CACjG,YAAW,sBAAsB,CAAC,WAAW,CAAC,EAAE,qBAAqB;IAQnE,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IARpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgD;IACzE,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAuC;IACjF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiC;IAChE,OAAO,CAAC,cAAc,CAAkD;gBAGrD,UAAU,EAAE,WAAW,EACvB,OAAO,CAAC,GAAE,CAAC,UAAU,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,aAAA,EAC3D,iBAAiB,GAAE,sBAAsD;IAG5F;;;;;;;;;OASG;IACH,OAAO,IAAI,WAAW;IAItB;;;;;;;;;OASG;IACH,cAAc,IAAI,mBAAmB,GAAG,SAAS;IAIjD;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,iBAAiB;IAa1D,qGAAqG;IAC/F,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5C,yFAAyF;IACzF,4BAA4B;IAY5B;;;;;;;;;;;;OAYG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAoBtD;;;;;;;;;;;OAWG;IACG,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IA+CnF,OAAO,CAAC,2BAA2B;IAMnC,OAAO,CAAC,kCAAkC;YAM5B,2BAA2B;YAc3B,wBAAwB;YA4BxB,wBAAwB;IActC,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,+BAA+B;YAIzB,cAAc;CAW7B"}
@@ -11,6 +11,95 @@ import { createMongoosePlatformStatusSnapshot } from './status.js';
11
11
  import { MONGOOSE_CONNECTION, MONGOOSE_DISPOSE, MONGOOSE_OPTIONS } from './tokens.js';
12
12
  const TRANSACTIONS_NOT_SUPPORTED_ERROR = 'Transaction not supported: Mongoose connection does not implement startSession.';
13
13
  const TRANSACTION_UNAVAILABLE_ERROR = 'Mongoose transactions are unavailable during application shutdown.';
14
+ const MODEL_OPERATIONS_WITH_OPTIONS = new Set(['aggregate', 'bulkWrite', 'create', 'find', 'findOne']);
15
+ const MODEL_OPERATIONS_WITH_PROJECTION = new Set(['find', 'findOne']);
16
+ const MONGOOSE_CREATE_OPTION_KEYS = new Set(['aggregateErrors', 'checkKeys', 'j', 'ordered', 'populate', 'safe', 'session', 'timestamps', 'validateBeforeSave', 'validateModifiedOnly', 'w', 'writeConcern', 'wtimeout']);
17
+ function hasExplicitSessionOption(value) {
18
+ return value !== null && typeof value === 'object' && 'session' in value;
19
+ }
20
+ function isObjectLike(value) {
21
+ return typeof value === 'object' && value !== null || typeof value === 'function';
22
+ }
23
+ function isMongooseCreateOptionsCandidate(value) {
24
+ if (!isObjectLike(value)) {
25
+ return false;
26
+ }
27
+ for (const key of MONGOOSE_CREATE_OPTION_KEYS) {
28
+ if (key in value) {
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ function isEmptyCreateOptionsCandidate(value) {
35
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0;
36
+ }
37
+ function resolveCreateOptionsIndex(operationArgs) {
38
+ if (operationArgs.length >= 2 && isMongooseCreateOptionsCandidate(operationArgs[operationArgs.length - 1])) {
39
+ return operationArgs.length - 1;
40
+ }
41
+ if (operationArgs.length === 2 && Array.isArray(operationArgs[0])) {
42
+ return 1;
43
+ }
44
+ if (operationArgs.length === 2 && isEmptyCreateOptionsCandidate(operationArgs[1])) {
45
+ return 1;
46
+ }
47
+ return operationArgs.length;
48
+ }
49
+ function resolveOptionsIndex(operation, operationArgs) {
50
+ if (operation === 'create') {
51
+ return resolveCreateOptionsIndex(operationArgs);
52
+ }
53
+ if (!MODEL_OPERATIONS_WITH_PROJECTION.has(operation)) {
54
+ return operationArgs.length > 1 ? 1 : operationArgs.length;
55
+ }
56
+ if (operationArgs.length >= 3) {
57
+ return 2;
58
+ }
59
+ if (operationArgs.length === 2 && hasExplicitSessionOption(operationArgs[1])) {
60
+ return 1;
61
+ }
62
+ if (operationArgs.length <= 1) {
63
+ return 2;
64
+ }
65
+ return operationArgs.length;
66
+ }
67
+ function resolveSessionOptions(opts, ambient) {
68
+ const options = opts && typeof opts === 'object' ? opts : {};
69
+ if (options.session === null) {
70
+ throw new Error('Explicit session: null conflicts with ambient transaction session');
71
+ }
72
+ if (options.session !== undefined && options.session !== ambient) {
73
+ throw new Error('Explicit session conflicts with ambient transaction session');
74
+ }
75
+ return {
76
+ ...options,
77
+ session: ambient
78
+ };
79
+ }
80
+ function createAmbientSessionModelFacade(model, ambient) {
81
+ return new Proxy(model, {
82
+ get(target, prop, receiver) {
83
+ const value = Reflect.get(target, prop, receiver);
84
+ if (!MODEL_OPERATIONS_WITH_OPTIONS.has(prop) || typeof value !== 'function') {
85
+ return value;
86
+ }
87
+ return (...args) => {
88
+ const operationArgs = [...args];
89
+ const optionsIndex = resolveOptionsIndex(prop, operationArgs);
90
+ operationArgs[optionsIndex] = resolveSessionOptions(operationArgs[optionsIndex], ambient);
91
+ return value.apply(target, operationArgs);
92
+ };
93
+ }
94
+ });
95
+ }
96
+ function resolveModelFactory(connection) {
97
+ if (!isObjectLike(connection)) {
98
+ return undefined;
99
+ }
100
+ const modelConnection = connection;
101
+ return modelConnection.model;
102
+ }
14
103
  async function executeSessionTransaction(session, fn) {
15
104
  try {
16
105
  await session.startTransaction();
@@ -77,6 +166,23 @@ class MongooseConnection {
77
166
  return this.sessions.getStore();
78
167
  }
79
168
 
169
+ /**
170
+ * Returns a model from the root connection, injecting the ambient transaction session into conservative operations.
171
+ *
172
+ * @param name Model name passed to the underlying Mongoose connection.
173
+ * @param args Additional model resolver arguments forwarded unchanged.
174
+ * @returns The real model outside transactions, or a model facade inside an active transaction boundary.
175
+ */
176
+ model(name, ...args) {
177
+ const modelFactory = resolveModelFactory(this.connection);
178
+ if (typeof modelFactory !== 'function') {
179
+ throw new Error('Mongoose connection does not implement model().');
180
+ }
181
+ const model = modelFactory.call(this.connection, name, ...args);
182
+ const ambient = this.currentSession();
183
+ return ambient ? createAmbientSessionModelFacade(model, ambient) : model;
184
+ }
185
+
80
186
  /** Aborts active request transactions, waits for settlement, then runs the optional dispose hook. */
81
187
  async onApplicationShutdown() {
82
188
  this.lifecycleState = 'shutting-down';
@@ -117,11 +223,11 @@ class MongooseConnection {
117
223
  * @returns The callback result after the session transaction finishes or the direct-execution fallback completes.
118
224
  */
119
225
  async transaction(fn) {
226
+ this.assertTransactionsAvailable();
120
227
  const currentSession = this.sessions.getStore();
121
228
  if (currentSession) {
122
229
  return fn();
123
230
  }
124
- this.assertTransactionsAvailable();
125
231
  if (typeof this.connection.transaction === 'function') {
126
232
  return this.runConnectionTransaction(fn);
127
233
  }
package/dist/module.d.ts CHANGED
@@ -30,7 +30,7 @@ export declare class MongooseModule {
30
30
  * Registers Mongoose providers from static options.
31
31
  *
32
32
  * @param options Mongoose module options with connection handle, optional dispose hook, and strict transaction mode.
33
- * @returns A module definition that exports `MongooseConnection` and `MongooseTransactionInterceptor`.
33
+ * @returns A module definition that exports `MongooseConnection`.
34
34
  */
35
35
  static forRoot<TConnection extends MongooseConnectionLike>(options: MongooseModuleOptions<TConnection>): ModuleType;
36
36
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAahF;;;;;;GAMG;AACH,MAAM,MAAM,0BAA0B,CAAC,WAAW,SAAS,sBAAsB,IAAI,kBAAkB,CACrG,IAAI,CAAC,qBAAqB,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CACnD,GAAG,IAAI,CAAC,qBAAqB,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAC;AAiEvD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,SAAS,sBAAsB,EAChF,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAC1C,QAAQ,EAAE,CAOZ;AA0BD;;GAEG;AACH,qBAAa,cAAc;IACzB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,WAAW,SAAS,sBAAsB,EAAE,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAAG,UAAU;IAInH;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,WAAW,SAAS,sBAAsB,EAC5D,OAAO,EAAE,0BAA0B,CAAC,WAAW,CAAC,GAC/C,UAAU;CAGd"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAahF;;;;;;GAMG;AACH,MAAM,MAAM,0BAA0B,CAAC,WAAW,SAAS,sBAAsB,IAAI,kBAAkB,CACrG,IAAI,CAAC,qBAAqB,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CACnD,GAAG,IAAI,CAAC,qBAAqB,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAC;AA2EvD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,SAAS,sBAAsB,EAChF,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAC1C,QAAQ,EAAE,CAOZ;AA0BD;;GAEG;AACH,qBAAa,cAAc;IACzB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,WAAW,SAAS,sBAAsB,EAAE,OAAO,EAAE,qBAAqB,CAAC,WAAW,CAAC,GAAG,UAAU;IAInH;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,WAAW,SAAS,sBAAsB,EAC5D,OAAO,EAAE,0BAA0B,CAAC,WAAW,CAAC,GAC/C,UAAU;CAGd"}
package/dist/module.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { defineModule } from '@fluojs/runtime';
2
2
  import { MongooseConnection } from './connection.js';
3
3
  import { MONGOOSE_CONNECTION, MONGOOSE_DISPOSE, MONGOOSE_OPTIONS } from './tokens.js';
4
- import { MongooseTransactionInterceptor } from './transaction.js';
5
4
 
6
5
  /**
7
6
  * Async registration options accepted by `MongooseModule.forRootAsync(...)`.
@@ -12,8 +11,14 @@ import { MongooseTransactionInterceptor } from './transaction.js';
12
11
  */
13
12
 
14
13
  const MONGOOSE_NORMALIZED_OPTIONS = Symbol('fluo.mongoose.normalized-options');
15
- const MONGOOSE_MODULE_EXPORTS = [MongooseConnection, MongooseTransactionInterceptor];
14
+ const MONGOOSE_MODULE_EXPORTS = [MongooseConnection];
15
+ function isObjectLike(value) {
16
+ return typeof value === 'object' && value !== null || typeof value === 'function';
17
+ }
16
18
  function normalizeMongooseModuleOptions(options) {
19
+ if (!isObjectLike(options.connection)) {
20
+ throw new Error('MongooseModule requires a connection option.');
21
+ }
17
22
  return {
18
23
  ...options,
19
24
  strictTransactions: options.strictTransactions ?? false
@@ -37,7 +42,7 @@ function createMongooseRuntimeProviders(normalizedOptionsProvider) {
37
42
  inject: [MONGOOSE_NORMALIZED_OPTIONS],
38
43
  provide: MONGOOSE_OPTIONS,
39
44
  useFactory: options => createRuntimeOptionsProviderValue(options.strictTransactions)
40
- }, MongooseConnection, MongooseTransactionInterceptor];
45
+ }, MongooseConnection];
41
46
  }
42
47
  function createMongooseProvidersAsync(options) {
43
48
  const factory = options.useFactory;
@@ -47,7 +52,10 @@ function createMongooseProvidersAsync(options) {
47
52
  scope: 'singleton',
48
53
  useFactory: async (...deps) => {
49
54
  const resolvedOptions = await factory(...deps);
50
- return normalizeMongooseModuleOptions(resolvedOptions);
55
+ return normalizeMongooseModuleOptions({
56
+ ...resolvedOptions,
57
+ global: options.global
58
+ });
51
59
  }
52
60
  };
53
61
  return createMongooseRuntimeProviders(normalizedOptionsProvider);
@@ -96,7 +104,7 @@ export class MongooseModule {
96
104
  * Registers Mongoose providers from static options.
97
105
  *
98
106
  * @param options Mongoose module options with connection handle, optional dispose hook, and strict transaction mode.
99
- * @returns A module definition that exports `MongooseConnection` and `MongooseTransactionInterceptor`.
107
+ * @returns A module definition that exports `MongooseConnection`.
100
108
  */
101
109
  static forRoot(options) {
102
110
  return buildMongooseModule(options);
@@ -1,25 +1,20 @@
1
- import type { Interceptor, InterceptorContext } from '@fluojs/http';
2
- import { MongooseConnection } from './connection.js';
3
- import type { MongooseConnectionLike } from './types.js';
1
+ type TransactionConnection = {
2
+ transaction<T>(fn: () => Promise<T>): Promise<T>;
3
+ };
4
+ type TransactionMethod<THost, TArgs extends unknown[], TResult> = (this: THost, ...args: TArgs) => Promise<TResult>;
4
5
  /**
5
- * HTTP interceptor that wraps each request in a Mongoose request transaction boundary.
6
+ * Wraps a service method in a `MongooseConnection.transaction(...)` boundary.
6
7
  *
7
8
  * @remarks
8
- * Pair this with repository/service code that reads `MongooseConnection.current()` and `currentSession()` so downstream
9
- * calls share the same request-scoped session.
9
+ * This is a TC39 standard method decorator. By default it uses `this.conn` when present, the decorated instance
10
+ * itself when it is transaction-capable, or one unique nested `this.*.conn` collaborator. Pass an accessor when the
11
+ * connection lives under a different field or more than one nested collaborator exposes a connection; the decorator
12
+ * does not bind arbitrary transaction-capable properties to avoid selecting the wrong persistence handle.
13
+ * Nested decorated calls reuse the ambient Mongoose session through `MongooseConnection.transaction(...)`.
14
+ *
15
+ * @param accessor Optional connection resolver for the decorated service instance.
16
+ * @returns A standard method decorator that executes the original method inside a Mongoose transaction.
10
17
  */
11
- export declare class MongooseTransactionInterceptor implements Interceptor {
12
- private readonly connection;
13
- constructor(connection: MongooseConnection<MongooseConnectionLike>);
14
- /**
15
- * Runs the downstream handler inside a Mongoose request transaction boundary.
16
- *
17
- * @param context Interceptor context that supplies the request abort signal.
18
- * @param next Downstream handler chain.
19
- * @returns The downstream handler result after the request transaction settles.
20
- */
21
- intercept(context: InterceptorContext, next: {
22
- handle(): Promise<unknown>;
23
- }): Promise<unknown>;
24
- }
18
+ export declare function Transaction<THost>(accessor?: (self: THost) => TransactionConnection): <TArgs extends unknown[], TResult>(value: TransactionMethod<THost, TArgs, TResult>, context: ClassMethodDecoratorContext<THost, TransactionMethod<THost, TArgs, TResult>>) => TransactionMethod<THost, TArgs, TResult>;
19
+ export {};
25
20
  //# sourceMappingURL=transaction.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;GAMG;AACH,qBACa,8BAA+B,YAAW,WAAW;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,kBAAkB,CAAC,sBAAsB,CAAC;IAEnF;;;;;;OAMG;IACG,SAAS,CAAC,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAGrG"}
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,KAAK,qBAAqB,GAAG;IAC3B,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAClD,CAAC;AAEF,KAAK,iBAAiB,CAAC,KAAK,EAAE,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,IAAI,CAChE,IAAI,EAAE,KAAK,EACX,GAAG,IAAI,EAAE,KAAK,KACX,OAAO,CAAC,OAAO,CAAC,CAAC;AA6DtB;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAC/B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,qBAAqB,GAChD,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EAClC,KAAK,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAC/C,OAAO,EAAE,2BAA2B,CAAC,KAAK,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,KAClF,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAW5C"}
@@ -1,39 +1,65 @@
1
- let _initClass;
2
- function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
3
- function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
4
- function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
5
- function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
- function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
- import { Inject } from '@fluojs/core';
8
- import { MongooseConnection } from './connection.js';
9
- let _MongooseTransactionI;
10
- /**
11
- * HTTP interceptor that wraps each request in a Mongoose request transaction boundary.
12
- *
13
- * @remarks
14
- * Pair this with repository/service code that reads `MongooseConnection.current()` and `currentSession()` so downstream
15
- * calls share the same request-scoped session.
16
- */
17
- class MongooseTransactionInterceptor {
18
- static {
19
- [_MongooseTransactionI, _initClass] = _applyDecs(this, [Inject(MongooseConnection)], []).c;
1
+ function isTransactionConnection(value) {
2
+ return (typeof value === 'object' && value !== null || typeof value === 'function') && typeof value.transaction === 'function';
3
+ }
4
+ function collectNestedConnCandidates(self) {
5
+ if ((typeof self !== 'object' || self === null) && typeof self !== 'function') {
6
+ return [];
20
7
  }
21
- constructor(connection) {
22
- this.connection = connection;
8
+ const candidates = new Set();
9
+ for (const value of Object.values(self)) {
10
+ if ((typeof value !== 'object' || value === null) && typeof value !== 'function') {
11
+ continue;
12
+ }
13
+ const nestedConn = value.conn;
14
+ if (isTransactionConnection(nestedConn)) {
15
+ candidates.add(nestedConn);
16
+ }
23
17
  }
24
-
25
- /**
26
- * Runs the downstream handler inside a Mongoose request transaction boundary.
27
- *
28
- * @param context Interceptor context that supplies the request abort signal.
29
- * @param next Downstream handler chain.
30
- * @returns The downstream handler result after the request transaction settles.
31
- */
32
- async intercept(context, next) {
33
- return this.connection.requestTransaction(async () => next.handle(), context.requestContext.request.signal);
18
+ return Array.from(candidates);
19
+ }
20
+ function resolveTransactionConnection(self, accessor) {
21
+ if (accessor) {
22
+ const connection = accessor(self);
23
+ if (isTransactionConnection(connection)) {
24
+ return connection;
25
+ }
26
+ throw new Error('Mongoose @Transaction() accessor did not return a transaction-capable connection.');
27
+ }
28
+ const fallbackHost = self;
29
+ if (isTransactionConnection(fallbackHost.conn)) {
30
+ return fallbackHost.conn;
34
31
  }
35
- static {
36
- _initClass();
32
+ if (isTransactionConnection(self)) {
33
+ return self;
37
34
  }
35
+ const nestedConnCandidates = collectNestedConnCandidates(self);
36
+ if (nestedConnCandidates.length === 1) {
37
+ return nestedConnCandidates[0];
38
+ }
39
+ if (nestedConnCandidates.length > 1) {
40
+ throw new Error('Mongoose @Transaction() found multiple nested this.*.conn candidates; pass an accessor.');
41
+ }
42
+ throw new Error('Mongoose @Transaction() could not resolve a transaction-capable connection from this.conn.');
38
43
  }
39
- export { _MongooseTransactionI as MongooseTransactionInterceptor };
44
+
45
+ /**
46
+ * Wraps a service method in a `MongooseConnection.transaction(...)` boundary.
47
+ *
48
+ * @remarks
49
+ * This is a TC39 standard method decorator. By default it uses `this.conn` when present, the decorated instance
50
+ * itself when it is transaction-capable, or one unique nested `this.*.conn` collaborator. Pass an accessor when the
51
+ * connection lives under a different field or more than one nested collaborator exposes a connection; the decorator
52
+ * does not bind arbitrary transaction-capable properties to avoid selecting the wrong persistence handle.
53
+ * Nested decorated calls reuse the ambient Mongoose session through `MongooseConnection.transaction(...)`.
54
+ *
55
+ * @param accessor Optional connection resolver for the decorated service instance.
56
+ * @returns A standard method decorator that executes the original method inside a Mongoose transaction.
57
+ */
58
+ export function Transaction(accessor) {
59
+ return function transactionDecorator(value, _context) {
60
+ return async function transactionWrappedMethod(...args) {
61
+ const connection = resolveTransactionConnection(this, accessor);
62
+ return connection.transaction(() => value.apply(this, args));
63
+ };
64
+ };
65
+ }
package/dist/types.d.ts CHANGED
@@ -48,6 +48,14 @@ export interface MongooseHandleProvider<TConnection extends MongooseConnectionLi
48
48
  current(): TConnection;
49
49
  /** Returns the ambient Mongoose session for the current async context, when one exists. */
50
50
  currentSession(): MongooseSessionLike | undefined;
51
+ /**
52
+ * Returns a Mongoose model handle, or a session-aware facade inside an active transaction.
53
+ *
54
+ * @param name Model name passed to the underlying Mongoose connection.
55
+ * @param args Additional model resolver arguments forwarded unchanged.
56
+ * @returns The root model outside transactions, or a model facade inside an active transaction boundary.
57
+ */
58
+ model(name: string, ...args: unknown[]): Record<PropertyKey, unknown>;
51
59
  /**
52
60
  * Opens a Mongoose session transaction boundary around `fn`.
53
61
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC9C,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IACvC,iBAAiB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IACxC,gBAAgB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IACvC,UAAU,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;CAClC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB,CAAC,WAAW,SAAS,sBAAsB,GAAG,sBAAsB;IACxG,kFAAkF;IAClF,UAAU,EAAE,WAAW,CAAC;IACxB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IAC1D,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB,CAAC,WAAW,SAAS,sBAAsB,GAAG,sBAAsB;IACzG,uFAAuF;IACvF,OAAO,IAAI,WAAW,CAAC;IACvB,2FAA2F;IAC3F,cAAc,IAAI,mBAAmB,GAAG,SAAS,CAAC;IAClD;;;;;OAKG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/E"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC9C,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IACvC,iBAAiB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IACxC,gBAAgB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IACvC,UAAU,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;CAClC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB,CAAC,WAAW,SAAS,sBAAsB,GAAG,sBAAsB;IACxG,kFAAkF;IAClF,UAAU,EAAE,WAAW,CAAC;IACxB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IAC1D,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB,CAAC,WAAW,SAAS,sBAAsB,GAAG,sBAAsB;IACzG,uFAAuF;IACvF,OAAO,IAAI,WAAW,CAAC;IACvB,2FAA2F;IAC3F,cAAc,IAAI,mBAAmB,GAAG,SAAS,CAAC;IAClD;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACtE;;;;;OAKG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/E"}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "transaction",
10
10
  "odm"
11
11
  ],
12
- "version": "1.0.4",
12
+ "version": "1.1.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -37,9 +37,9 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@fluojs/core": "^1.0.3",
40
- "@fluojs/http": "^1.1.0",
41
- "@fluojs/di": "^1.0.3",
42
- "@fluojs/runtime": "^1.1.1"
40
+ "@fluojs/http": "^1.1.2",
41
+ "@fluojs/di": "^1.1.0",
42
+ "@fluojs/runtime": "^1.1.8"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "mongoose": ">=7.0.0"