@fluojs/mongoose 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @fluojs/mongoose
2
2
 
3
3
  <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
+ <!-- fluo-mongoose-contract: application-owned-connection, ambient-session-merge, preserves-operation-options, strict-fail-open, explicit-target -->
4
5
 
5
6
  세션 인지형 트랜잭션 처리와 라이프사이클 친화적인 외부 연결 관리를 제공하는 fluo용 Mongoose 통합 패키지입니다.
6
7
 
@@ -12,6 +13,8 @@
12
13
  - [라이프사이클과 종료](#라이프사이클과-종료)
13
14
  - [공통 패턴](#공통-패턴)
14
15
  - [서비스 트랜잭션 경계 (@Transaction)](#서비스-트랜잭션-경계-transaction)
16
+ - [기존 문서 저장](#기존-문서-저장)
17
+ - [요청 트랜잭션 인터셉터 호환성](#요청-트랜잭션-인터셉터-호환성)
15
18
  - [수동 트랜잭션과 currentSession()](#수동-트랜잭션과-currentsession)
16
19
  - [공개 API](#공개-api)
17
20
  - [관련 패키지](#관련-패키지)
@@ -28,8 +31,11 @@ pnpm add mongoose
28
31
 
29
32
  - Mongoose를 나머지 애플리케이션과 같은 DI 및 라이프사이클 모델에 연결하고 싶을 때.
30
33
  - 모든 서비스에서 MongoDB 세션과 트랜잭션을 임시 배관 코드 없이 하나의 wrapper로 다루고 싶을 때.
34
+ - 요청 단위 트랜잭션에 명시적 `requestTransaction(...)` 경계가 필요할 때.
31
35
  - 애플리케이션이 이미 concrete Mongoose connection을 생성·구성하고 있고, fluo가 그 ownership을 대체하지 않고 관측하기를 원할 때.
32
36
 
37
+ Root `@fluojs/mongoose` wrapper는 ambient transaction context에 Node.js `node:async_hooks`를 사용하며 패키지 자체의 지원 계약으로 Node.js `>=24.0.0 <27`을 요구합니다. Node 20 및 Node 22 host를 Node.js `>=24.0.0 <27`로 올리세요. Node 24 미만과 Node 27 이상은 지원하지 않습니다. 비 Node 런타임에서는 runtime-specific transaction-context adapter가 문서화되기 전까지 root wrapper를 import하지 말고 raw Mongoose-compatible handle을 애플리케이션 소유 provider 뒤에 등록하세요.
38
+
33
39
  ## 빠른 시작
34
40
 
35
41
  루트 모듈에 Mongoose 연결 인스턴스를 전달하여 `MongooseModule`을 등록합니다.
@@ -61,10 +67,12 @@ class AppModule {}
61
67
  종료 절차는 트랜잭션 정리 순서를 보존하고, 종료가 시작된 뒤에는 새로운 수동 또는 요청 단위 트랜잭션 경계를 거부합니다.
62
68
 
63
69
  1. 열린 요청 단위 트랜잭션은 `Application shutdown interrupted an open request transaction.`으로 abort됩니다.
64
- 2. 활성 ambient session transaction callback session cleanup이 settle될 때까지 추적됩니다.
65
- 3. 해당 Mongoose 세션은 `abortTransaction()`과 `endSession()` 정리를 끝냅니다.
70
+ 2. 활성 ambient session, 원본 request callback, fail-open 직접 실행 transaction callback 작업이 settle될 때까지 추적됩니다.
71
+ 3. 해당 Mongoose 세션은 시작된 callback이 settle된 뒤에만 `abortTransaction()`과 `endSession()` 정리를 끝냅니다.
66
72
  4. 설정한 `dispose(connection)` 훅은 활성 요청 트랜잭션과 ambient session scope가 모두 settled된 뒤에만 실행됩니다.
67
73
 
74
+ Request cancellation 또는 shutdown이 callback 시작 후 boundary를 abort하면, boundary는 abort 결과를 보존하되 원본 callback이 settle될 때까지 기다린 다음 rollback, session 종료, connection dispose를 진행합니다. 따라서 ALS-backed 작업이 이미 정리된 session이나 connection을 사용하며 계속 실행되지 않습니다.
75
+
68
76
  `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
77
 
70
78
  기존 수동 `transaction(...)` boundary 안에서 열린 중첩 `requestTransaction(...)` 호출은 ambient session을 재사용하고 `details.activeRequestTransactions`에 계속 표시되며, 종료 중에 abort되어 바깥 수동 transaction이 `dispose(connection)` 실행 전에 rollback할 수 있습니다.
@@ -76,37 +84,100 @@ class AppModule {}
76
84
  `@Transaction()` 데코레이터는 서비스 레이어에서 트랜잭션 경계를 정의하는 권장 방법입니다. 이 데코레이터가 적용된 메서드 내부에서 발생하는 모든 리포지토리 호출은 동일한 MongoDB 세션을 공유합니다.
77
85
 
78
86
  ```ts
79
- import { Transaction } from '@fluojs/mongoose';
80
- import { UserRepository } from './user.repository';
87
+ import { Inject } from '@fluojs/core';
88
+ import { MongooseConnection, Transaction, type MongooseModelFacade } from '@fluojs/mongoose';
89
+
90
+ type UserDocumentSaveOptions = {
91
+ readonly validateBeforeSave?: boolean;
92
+ readonly session?: object | null;
93
+ };
94
+ type UserDocument = {
95
+ readonly _id: string;
96
+ readonly name: string;
97
+ save(options?: UserDocumentSaveOptions): Promise<UserDocument>;
98
+ };
99
+ type UserCreateModel = MongooseModelFacade<Promise<readonly [UserDocument]>>;
100
+ type ProfileCreateModel = MongooseModelFacade<Promise<readonly { readonly userId: string }[]>>;
101
+
102
+ @Inject(MongooseConnection)
103
+ export class UserRepository {
104
+ constructor(private readonly conn: MongooseConnection) {}
105
+
106
+ async create(data: CreateUserDto) {
107
+ // @Transaction() 내부에서 conn.model()은 세션 인지형 facade를 반환합니다.
108
+ // create, find, findOne, aggregate, bulkWrite 등의 작업은
109
+ // 자동으로 활성 트랜잭션에 참여합니다.
110
+ return this.conn.model<UserCreateModel>('User').create([data]);
111
+ }
81
112
 
113
+ async initProfile(userId: string) {
114
+ return this.conn.model<ProfileCreateModel>('Profile').create([{ userId }]);
115
+ }
116
+ }
117
+
118
+ @Inject(UserRepository)
82
119
  export class UserService {
83
120
  constructor(private readonly repo: UserRepository) {}
84
121
 
85
122
  @Transaction()
86
123
  async onboardUser(dto: CreateUserDto) {
87
- const user = await this.repo.create(dto);
124
+ const [user] = await this.repo.create(dto);
88
125
  await this.repo.initProfile(user._id);
89
126
  return user;
90
127
  }
91
128
  }
129
+ ```
92
130
 
93
- export class UserRepository {
94
- constructor(private readonly conn: MongooseConnection) {}
131
+ `@Transaction()` 메서드 호출은 재진입(reentrant)이 가능합니다. 데코레이터가 적용된 메서드가 다른 데코레이터 적용 메서드를 호출하더라도 하나의 동일한 MongoDB 세션 안에서 실행됩니다. 참고로 v1에서 `doc.save()`는 자동으로 세션을 주입하지 않으므로, 자동 트랜잭션 참여가 필요하다면 지원되는 facade 작업(`model.create()`, `model.find()`, `model.findOne()`, `model.aggregate()`, `model.bulkWrite()`)을 사용하세요.
95
132
 
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);
133
+ `@Transaction()`은 `this.conn`, transaction-capable한 decorated instance 자체, 또는 하나뿐인 중첩 `this.*.conn` collaborator를 해석합니다. 임의의 connection field를 선택하지는 않습니다. 서비스가 여러 connection을 소유하거나 다른 field에 connection을 저장하는 경우에는 경계를 명시적으로 선택하세요.
134
+
135
+ ```ts
136
+ @Inject(MongooseConnection)
137
+ export class AnalyticsService {
138
+ constructor(private readonly analyticsConnection: MongooseConnection) {}
139
+
140
+ @Transaction((self: AnalyticsService) => self.analyticsConnection)
141
+ async rebuildReports() {
142
+ // 추론된 connection 대신 analyticsConnection을 사용합니다.
101
143
  }
144
+ }
145
+ ```
146
+
147
+ ### 기존 문서 저장
148
+
149
+ <!-- fluo-mongoose-save-document-contract: opt-in, active-session, save-compatible-document -->
150
+
151
+ 기존 Mongoose 문서를 활성 `@Transaction()`, `transaction()`, `requestTransaction()` 경계 안에서 저장해야 하면 opt-in `MongooseConnection.saveDocument(...)` helper를 사용하세요.
102
152
 
103
- async initProfile(userId: any) {
104
- return this.conn.model('Profile').create({ userId });
153
+ ```ts
154
+ @Transaction()
155
+ async rename(document: UserDocument) {
156
+ return this.conn.saveDocument(document, { validateBeforeSave: false });
157
+ }
158
+ ```
159
+
160
+ helper는 native Mongoose save option을 전달하고 ambient session을 붙인 뒤 동일한 document instance를 반환합니다. 활성 트랜잭션 밖에서는 fail-closed하며, 실수로 현재 트랜잭션을 벗어나지 않도록 `{ session: null }` 또는 다른 명시적 session을 거부합니다. document, prototype, model cache를 patch하지 않으므로 `doc.save()` 직접 호출은 계속 native Mongoose 동작이며 자동 session을 받지 않습니다.
161
+
162
+ ### 요청 트랜잭션 인터셉터 호환성
163
+
164
+ `MongooseTransactionInterceptor`는 기존 request-wide `@UseInterceptors(...)` boundary를 위한 deprecated 1.x 호환성 export로 복원되었습니다. `MongooseModule.forRoot(...)`와 `forRootAsync(...)`가 이 interceptor를 provider 및 export로 제공하며, `MongooseConnection.requestTransaction(...)`에 위임하고 request `AbortSignal`을 전달합니다.
165
+
166
+ ```ts
167
+ import { Controller, Post, UseInterceptors } from '@fluojs/http';
168
+ import { MongooseTransactionInterceptor } from '@fluojs/mongoose';
169
+
170
+ @Controller('/orders')
171
+ export class OrdersController {
172
+ @Post('/')
173
+ @UseInterceptors(MongooseTransactionInterceptor)
174
+ createOrder() {
175
+ return this.orders.create();
105
176
  }
106
177
  }
107
178
  ```
108
179
 
109
- `@Transaction()` 메서드 호출은 재진입(reentrant) 가능합니다. 데코레이터가 적용된 메서드가 다른 데코레이터 적용 메서드를 호출하더라도 하나의 동일한 MongoDB 세션 안에서 실행됩니다. 참고로 v1에서 `doc.save()`는 자동으로 세션을 주입하지 않으므로, 자동 트랜잭션 참여가 필요하다면 지원되는 facade 작업(`model.create()`, `model.find()`, `model.findOne()`, `model.aggregate()`, `model.bulkWrite()`)을 사용하세요.
180
+ 비즈니스 작업에는 서비스 계층 `@Transaction()`을 우선 사용하세요. 기존 request-wide boundary를 migration하는 동안에만 interceptor를 유지하고, request orchestration에서 경계를 명시해야 한다면 `requestTransaction(...)` 직접 호출로 교체하세요.
110
181
 
111
182
  ### 수동 트랜잭션과 currentSession()
112
183
 
@@ -137,17 +208,19 @@ await this.conn.transaction(async () => {
137
208
  });
138
209
  ```
139
210
 
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 예외로 드러납니다.
211
+ 래핑된 연결이 `connection.transaction(...)`을 구현하고 있다면 fluo는 이를 엄격한 트랜잭션 경계로 취급합니다. 그렇지 않고 `startSession()`이 없는 경우 트랜잭션은 기본값(`strictTransactions: false`)에서 callback 직접 실행으로 fail-open합니다. 이 모드는 local fake나 staged migration에는 유용하지만 rollback 원자성은 제공하지 않습니다. 열린 fail-open 수동 `transaction(...)` callback도 종료 중에 drain되므로 `dispose(connection)`은 해당 callback이 settle된 뒤 실행됩니다. MongoDB transaction 보장이 필요한 production 흐름에서는 `strictTransactions: true`를 설정하세요. 그러면 transaction 지원 누락이 readiness `not-ready`와 helper 예외로 드러납니다.
141
212
 
142
- 지원되는 facade 메서드에서 fluo는 기존 Mongoose 작업 옵션을 보존하고 올바른 options 인자에 ambient `{ session }`만 병합합니다. 활성 트랜잭션 내부에서 명시적으로 `{ session: null }`을 전달하거나 다른 세션 객체를 사용하면, 의도치 않은 트랜잭션 탈출을 방지하기 위해 세션 충돌 에러를 발생시킵니다.
213
+ 지원되는 facade 메서드에서 fluo는 기존 Mongoose 작업 옵션을 보존하고 올바른 options 인자에 ambient `{ session }`만 병합합니다. `create(...)`는 Mongoose의 array overload인 `create([docs], options?)`를 통해서만 session을 주입합니다. Positional `create(docA, docB)` 인자는 마지막 문서에 `timestamps` 같은 option-like field가 있어도 그대로 전달되며 자동 session 주입을 받지 않습니다. 트랜잭션 참여가 필요하면 array overload를 사용하세요. 활성 트랜잭션 내부에서 명시적으로 `{ session: null }`을 전달하거나 다른 세션 객체를 사용하면, `findOne(filter, projection, options)`의 세 번째 options 인자를 포함해 의도치 않은 트랜잭션 탈출을 방지하는 세션 충돌 에러를 발생시킵니다. Repository code에서 typed operation result가 필요하면 result-specialized `MongooseModelFacade`를 `model<TModel>(...)` 타입 인자로 전달하세요.
143
214
 
144
215
  ## 공개 API
145
216
 
217
+ - `MongooseConnection.saveDocument(document, options?)` — native save option과 document identity를 보존하면서 현재 트랜잭션 session으로 기존 문서를 명시적으로 저장합니다.
146
218
  - `MongooseModule.forRoot(options)` / `MongooseModule.forRootAsync(options)`
147
219
  - `MongooseConnection`
148
220
  - `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를 반환합니다.
221
+ - `MongooseConnection.model<TModel>(name, ...args)` — 트랜잭션 밖에서는 callable하고 result-specializable한 `MongooseModelFacade`를 반환하고, 활성 트랜잭션 안에서는 underlying Mongoose connection을 변형하지 않으면서 `create`, `find`, `findOne`, `aggregate`, `bulkWrite`에 세션을 주입하는 버전을 반환합니다.
150
222
  - `Transaction`
223
+ - `MongooseTransactionInterceptor` — deprecated request-wide 호환성 interceptor입니다. 새 코드에서는 서비스 `@Transaction()` 또는 명시적 `requestTransaction(...)`을 우선 사용하세요.
151
224
  - `MONGOOSE_CONNECTION`, `MONGOOSE_DISPOSE`, `MONGOOSE_OPTIONS`
152
225
  - `createMongooseProviders(options)` — 호환성/수동 composition helper입니다. 애플리케이션-facing 등록에서는 module export와 provider visibility가 문서화된 namespace facade와 맞도록 `MongooseModule.forRoot(...)` 또는 `MongooseModule.forRootAsync(...)`를 우선 사용하세요.
153
226
  - `createMongoosePlatformStatusSnapshot(...)`
@@ -160,6 +233,7 @@ await this.conn.transaction(async () => {
160
233
  - `MongooseAsyncModuleOptions<TConnection>`
161
234
  - `MongooseConnectionLike`
162
235
  - `MongooseSessionLike`
236
+ - `MongooseModelFacade`
163
237
  - `MongooseHandleProvider`
164
238
  - `MongoosePlatformStatusSnapshotInput`
165
239
 
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @fluojs/mongoose
2
2
 
3
3
  <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+ <!-- fluo-mongoose-contract: application-owned-connection, ambient-session-merge, preserves-operation-options, strict-fail-open, explicit-target -->
4
5
 
5
6
  Mongoose integration for fluo with session-aware transaction handling and lifecycle-friendly connection management.
6
7
 
@@ -12,6 +13,8 @@ Mongoose integration for fluo with session-aware transaction handling and lifecy
12
13
  - [Lifecycle and Shutdown](#lifecycle-and-shutdown)
13
14
  - [Common Patterns](#common-patterns)
14
15
  - [Service Transaction Boundary (@Transaction)](#service-transaction-boundary-transaction)
16
+ - [Saving an Existing Document](#saving-an-existing-document)
17
+ - [Request Transaction Interceptor Compatibility](#request-transaction-interceptor-compatibility)
15
18
  - [Manual Transactions and currentSession()](#manual-transactions-and-currentsession)
16
19
  - [Public API](#public-api)
17
20
  - [Related Packages](#related-packages)
@@ -31,6 +34,8 @@ pnpm add mongoose
31
34
  - when request-scoped transactions need explicit `requestTransaction(...)` boundaries
32
35
  - when an application already creates and configures its concrete Mongoose connection and wants fluo to observe, not replace, that ownership
33
36
 
37
+ The root `@fluojs/mongoose` wrapper uses Node.js `node:async_hooks` for ambient transaction context and requires Node.js `>=24.0.0 <27` as its package-owned support contract. Upgrade Node 20 and Node 22 hosts to Node.js `>=24.0.0 <27`; Node versions below 24 and Node 27+ are unsupported. For non-Node runtimes, register raw Mongoose-compatible handles behind application-owned providers instead of importing the root wrapper until a runtime-specific transaction-context adapter is documented.
38
+
34
39
  ## Quick Start
35
40
 
36
41
  ```ts
@@ -60,10 +65,12 @@ class AppModule {}
60
65
  Shutdown preserves transaction cleanup order and rejects new manual or request-scoped transaction boundaries once shutdown begins:
61
66
 
62
67
  1. Open request-scoped transactions are aborted with `Application shutdown interrupted an open request transaction.`
63
- 2. Active ambient sessions are tracked until their transaction callback and session cleanup settle.
64
- 3. Their Mongoose sessions finish `abortTransaction()` and `endSession()` cleanup.
68
+ 2. Active ambient sessions, original request callbacks, and fail-open direct-execution transaction callbacks are tracked until their work settles.
69
+ 3. Their Mongoose sessions finish `abortTransaction()` and `endSession()` cleanup only after started callbacks settle.
65
70
  4. The configured `dispose(connection)` hook runs only after active request transactions and ambient session scopes have settled.
66
71
 
72
+ When request cancellation or shutdown aborts a boundary after its callback has started, the boundary preserves the abort result but waits for the original callback to settle before rolling back, ending the session, or disposing the connection. This prevents ALS-backed work from continuing against an already-cleaned-up session or connection.
73
+
67
74
  `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.
68
75
  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.
69
76
 
@@ -74,37 +81,100 @@ Nested `requestTransaction(...)` calls opened inside an existing manual `transac
74
81
  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.
75
82
 
76
83
  ```ts
77
- import { Transaction } from '@fluojs/mongoose';
78
- import { UserRepository } from './user.repository';
84
+ import { Inject } from '@fluojs/core';
85
+ import { MongooseConnection, Transaction, type MongooseModelFacade } from '@fluojs/mongoose';
86
+
87
+ type UserDocumentSaveOptions = {
88
+ readonly validateBeforeSave?: boolean;
89
+ readonly session?: object | null;
90
+ };
91
+ type UserDocument = {
92
+ readonly _id: string;
93
+ readonly name: string;
94
+ save(options?: UserDocumentSaveOptions): Promise<UserDocument>;
95
+ };
96
+ type UserCreateModel = MongooseModelFacade<Promise<readonly [UserDocument]>>;
97
+ type ProfileCreateModel = MongooseModelFacade<Promise<readonly { readonly userId: string }[]>>;
98
+
99
+ @Inject(MongooseConnection)
100
+ export class UserRepository {
101
+ constructor(private readonly conn: MongooseConnection) {}
102
+
103
+ async create(data: CreateUserDto) {
104
+ // model() returns a session-aware facade inside @Transaction().
105
+ // Operations like create, find, findOne, aggregate, and bulkWrite
106
+ // automatically participate in the ambient transaction.
107
+ return this.conn.model<UserCreateModel>('User').create([data]);
108
+ }
79
109
 
110
+ async initProfile(userId: string) {
111
+ return this.conn.model<ProfileCreateModel>('Profile').create([{ userId }]);
112
+ }
113
+ }
114
+
115
+ @Inject(UserRepository)
80
116
  export class UserService {
81
117
  constructor(private readonly repo: UserRepository) {}
82
118
 
83
119
  @Transaction()
84
120
  async onboardUser(dto: CreateUserDto) {
85
- const user = await this.repo.create(dto);
121
+ const [user] = await this.repo.create(dto);
86
122
  await this.repo.initProfile(user._id);
87
123
  return user;
88
124
  }
89
125
  }
126
+ ```
90
127
 
91
- export class UserRepository {
92
- constructor(private readonly conn: MongooseConnection) {}
128
+ 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.
93
129
 
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);
130
+ `@Transaction()` resolves `this.conn`, the decorated instance when it is transaction-capable, or one unique nested `this.*.conn` collaborator. It does not select arbitrary connection fields. When a service owns multiple connections or stores its connection elsewhere, select the boundary explicitly:
131
+
132
+ ```ts
133
+ @Inject(MongooseConnection)
134
+ export class AnalyticsService {
135
+ constructor(private readonly analyticsConnection: MongooseConnection) {}
136
+
137
+ @Transaction((self: AnalyticsService) => self.analyticsConnection)
138
+ async rebuildReports() {
139
+ // Uses analyticsConnection rather than an inferred connection.
99
140
  }
141
+ }
142
+ ```
143
+
144
+ ### Saving an Existing Document
145
+
146
+ <!-- fluo-mongoose-save-document-contract: opt-in, active-session, save-compatible-document -->
147
+
148
+ Use the opt-in `MongooseConnection.saveDocument(...)` helper when an existing Mongoose document must save inside an active `@Transaction()`, `transaction()`, or `requestTransaction()` boundary:
100
149
 
101
- async initProfile(userId: any) {
102
- return this.conn.model('Profile').create({ userId });
150
+ ```ts
151
+ @Transaction()
152
+ async rename(document: UserDocument) {
153
+ return this.conn.saveDocument(document, { validateBeforeSave: false });
154
+ }
155
+ ```
156
+
157
+ The helper forwards native Mongoose save options, attaches the ambient session, and returns the same document instance. It fails closed outside an active transaction and rejects `{ session: null }` or a different explicit session so a save cannot leave the current transaction accidentally. It never patches documents, prototypes, or model caches: calling `doc.save()` directly remains native Mongoose behavior and does not receive an automatic session.
158
+
159
+ ### Request Transaction Interceptor Compatibility
160
+
161
+ `MongooseTransactionInterceptor` is restored as a deprecated 1.x compatibility export for existing request-wide `@UseInterceptors(...)` boundaries. `MongooseModule.forRoot(...)` and `forRootAsync(...)` provide and export it. It delegates to `MongooseConnection.requestTransaction(...)` and forwards the request `AbortSignal`.
162
+
163
+ ```ts
164
+ import { Controller, Post, UseInterceptors } from '@fluojs/http';
165
+ import { MongooseTransactionInterceptor } from '@fluojs/mongoose';
166
+
167
+ @Controller('/orders')
168
+ export class OrdersController {
169
+ @Post('/')
170
+ @UseInterceptors(MongooseTransactionInterceptor)
171
+ createOrder() {
172
+ return this.orders.create();
103
173
  }
104
174
  }
105
175
  ```
106
176
 
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.
177
+ Prefer service-layer `@Transaction()` for new business operations. Keep this interceptor only while migrating existing request-wide boundaries, or replace it with an explicit `requestTransaction(...)` call when request orchestration must make the boundary visible.
108
178
 
109
179
  ### Manual Transactions and currentSession()
110
180
 
@@ -135,17 +205,19 @@ await this.conn.transaction(async () => {
135
205
  });
136
206
  ```
137
207
 
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.
208
+ 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. Open fail-open manual `transaction(...)` callbacks still drain during shutdown before `dispose(connection)` runs. 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
209
 
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.
210
+ For supported facade methods, fluo preserves existing Mongoose operation options and only merges the ambient `{ session }` into the correct options argument. `create(...)` injects the session only through Mongoose's array overload, `create([docs], options?)`. Positional `create(docA, docB)` arguments are forwarded unchanged—even when the last document contains option-like fields such as `timestamps`—and therefore do not receive automatic session injection. Use the array overload for transaction participation. If a model call passes an explicit `{ session: null }` or a different session object inside an ambient transaction, including the third options argument of `findOne(filter, projection, options)`, fluo throws a session conflict error to prevent accidental transaction escapes. Pass a result-specialized `MongooseModelFacade` as the `model<TModel>(...)` type argument when repository code needs typed operation results.
141
211
 
142
212
  ## Public API
143
213
 
214
+ - `MongooseConnection.saveDocument(document, options?)` — explicitly saves an existing document with the current transaction session while preserving native save options and document identity.
144
215
  - `MongooseModule.forRoot(options)` / `MongooseModule.forRootAsync(options)`
145
216
  - `MongooseConnection`
146
217
  - `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.
218
+ - `MongooseConnection.model<TModel>(name, ...args)` — returns the callable, result-specializable `MongooseModelFacade` outside transactions or a session-aware version for `create`, `find`, `findOne`, `aggregate`, and `bulkWrite` inside an active transaction without mutating the underlying Mongoose connection.
148
219
  - `Transaction`
220
+ - `MongooseTransactionInterceptor` — deprecated request-wide compatibility interceptor; prefer service `@Transaction()` or explicit `requestTransaction(...)` in new code.
149
221
  - `MONGOOSE_CONNECTION`, `MONGOOSE_DISPOSE`, `MONGOOSE_OPTIONS`
150
222
  - `createMongooseProviders(options)` — compatibility/manual composition helper; prefer `MongooseModule.forRoot(...)` or `MongooseModule.forRootAsync(...)` for application-facing registration so module exports and provider visibility stay aligned.
151
223
  - `createMongoosePlatformStatusSnapshot(...)`
@@ -158,6 +230,7 @@ For supported facade methods, fluo preserves existing Mongoose operation options
158
230
  - `MongooseAsyncModuleOptions<TConnection>`
159
231
  - `MongooseConnectionLike`
160
232
  - `MongooseSessionLike`
233
+ - `MongooseModelFacade`
161
234
  - `MongooseHandleProvider`
162
235
  - `MongoosePlatformStatusSnapshotInput`
163
236
 
@@ -1,9 +1,8 @@
1
1
  import type { OnApplicationShutdown } from '@fluojs/runtime';
2
- import type { MongooseConnectionLike, MongooseHandleProvider, MongooseSessionLike } from './types.js';
2
+ import type { MongooseConnectionLike, MongooseHandleProvider, MongooseModelFacade, MongooseSessionLike } from './types.js';
3
3
  type MongooseRuntimeOptions = {
4
4
  strictTransactions: boolean;
5
5
  };
6
- type MongooseModelLike = Record<PropertyKey, unknown>;
7
6
  /**
8
7
  * Session-aware Mongoose wrapper that integrates request scoping and shutdown handling with the Fluo runtime.
9
8
  *
@@ -16,6 +15,7 @@ export declare class MongooseConnection<TConnection extends MongooseConnectionLi
16
15
  private readonly sessions;
17
16
  private readonly activeRequestTransactions;
18
17
  private readonly activeSessions;
18
+ private readonly activeTransactionCallbacks;
19
19
  private lifecycleState;
20
20
  constructor(connection: TConnection, dispose?: ((connection: TConnection) => Promise<void> | void) | undefined, connectionOptions?: MongooseRuntimeOptions);
21
21
  /**
@@ -40,14 +40,31 @@ export declare class MongooseConnection<TConnection extends MongooseConnectionLi
40
40
  * @returns The ambient session inside a transaction boundary, or `undefined` outside one.
41
41
  */
42
42
  currentSession(): MongooseSessionLike | undefined;
43
+ /**
44
+ * Saves a Mongoose document with the active transaction session.
45
+ *
46
+ * This opt-in helper preserves the document instance and forwards caller-provided save options.
47
+ * It does not patch document instances, prototypes, or model caches.
48
+ *
49
+ * @typeParam TSaveOptions Options accepted by the document's native `save()` method.
50
+ * @typeParam TDocument Mongoose document-like value being saved.
51
+ * @param document Existing Mongoose document to save.
52
+ * @param options Native Mongoose save options to preserve while attaching the ambient session.
53
+ * @returns The same document instance after its native save operation completes.
54
+ * @throws When called outside an active transaction or with a conflicting explicit session.
55
+ */
56
+ saveDocument<TSaveOptions extends object, TDocument extends {
57
+ save(options?: TSaveOptions): Promise<TDocument>;
58
+ }>(document: TDocument, options?: TSaveOptions): Promise<TDocument>;
43
59
  /**
44
60
  * Returns a model from the root connection, injecting the ambient transaction session into conservative operations.
45
61
  *
62
+ * @typeParam TModel Consumer-defined facade result contract for the wrapped model.
46
63
  * @param name Model name passed to the underlying Mongoose connection.
47
64
  * @param args Additional model resolver arguments forwarded unchanged.
48
65
  * @returns The real model outside transactions, or a model facade inside an active transaction boundary.
49
66
  */
50
- model(name: string, ...args: unknown[]): MongooseModelLike;
67
+ model<TModel extends MongooseModelFacade = MongooseModelFacade>(name: string, ...args: unknown[]): TModel;
51
68
  /** Aborts active request transactions, waits for settlement, then runs the optional dispose hook. */
52
69
  onApplicationShutdown(): Promise<void>;
53
70
  /** Produces the shared persistence status snapshot for platform diagnostics surfaces. */
@@ -82,9 +99,11 @@ export declare class MongooseConnection<TConnection extends MongooseConnectionLi
82
99
  private assertTransactionsAvailable;
83
100
  private assertRequestTransactionsAvailable;
84
101
  private runManualSessionTransaction;
102
+ private runDirectTransaction;
85
103
  private resolveSessionForRequest;
86
104
  private runConnectionTransaction;
87
105
  private trackActiveSession;
106
+ private trackActiveTransactionCallback;
88
107
  private trackActiveRequestTransaction;
89
108
  private untrackActiveRequestTransaction;
90
109
  private resolveSession;
@@ -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;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"}
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,EACnB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAqCpB,KAAK,sBAAsB,GAAG;IAC5B,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AA0IF;;;;GAIG;AACH,qBACa,kBAAkB,CAAC,WAAW,SAAS,sBAAsB,GAAG,sBAAsB,CACjG,YAAW,sBAAsB,CAAC,WAAW,CAAC,EAAE,qBAAqB;IASnE,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IATpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgD;IACzE,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAuC;IACjF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiC;IAChE,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAwC;IACnF,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;;;;;;;;;;;;OAYG;IACG,YAAY,CAChB,YAAY,SAAS,MAAM,EAC3B,SAAS,SAAS;QAAE,IAAI,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;KAAE,EACtE,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC;IASlE;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,SAAS,mBAAmB,GAAG,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM;IAczG,qGAAqG;IAC/F,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5C,yFAAyF;IACzF,4BAA4B;IAY5B;;;;;;;;;;;;OAYG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA6BtD;;;;;;;;;;;OAWG;IACG,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IAwDnF,OAAO,CAAC,2BAA2B;IAMnC,OAAO,CAAC,kCAAkC;YAM5B,2BAA2B;YAmB3B,oBAAoB;YAQpB,wBAAwB;YA4BxB,wBAAwB;IActC,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,8BAA8B;IAkBtC,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,+BAA+B;YAIzB,cAAc;CAW7B"}
@@ -13,52 +13,31 @@ const TRANSACTIONS_NOT_SUPPORTED_ERROR = 'Transaction not supported: Mongoose co
13
13
  const TRANSACTION_UNAVAILABLE_ERROR = 'Mongoose transactions are unavailable during application shutdown.';
14
14
  const MODEL_OPERATIONS_WITH_OPTIONS = new Set(['aggregate', 'bulkWrite', 'create', 'find', 'findOne']);
15
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
16
  function isObjectLike(value) {
21
17
  return typeof value === 'object' && value !== null || typeof value === 'function';
22
18
  }
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
19
  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])) {
20
+ if (Array.isArray(operationArgs[0])) {
42
21
  return 1;
43
22
  }
44
- if (operationArgs.length === 2 && isEmptyCreateOptionsCandidate(operationArgs[1])) {
45
- return 1;
46
- }
47
- return operationArgs.length;
23
+ return undefined;
24
+ }
25
+ function resolveAggregateOptionsIndex() {
26
+ return 1;
48
27
  }
49
28
  function resolveOptionsIndex(operation, operationArgs) {
50
29
  if (operation === 'create') {
51
30
  return resolveCreateOptionsIndex(operationArgs);
52
31
  }
32
+ if (operation === 'aggregate') {
33
+ return resolveAggregateOptionsIndex();
34
+ }
53
35
  if (!MODEL_OPERATIONS_WITH_PROJECTION.has(operation)) {
54
36
  return operationArgs.length > 1 ? 1 : operationArgs.length;
55
37
  }
56
38
  if (operationArgs.length >= 3) {
57
39
  return 2;
58
40
  }
59
- if (operationArgs.length === 2 && hasExplicitSessionOption(operationArgs[1])) {
60
- return 1;
61
- }
62
41
  if (operationArgs.length <= 1) {
63
42
  return 2;
64
43
  }
@@ -87,12 +66,29 @@ function createAmbientSessionModelFacade(model, ambient) {
87
66
  return (...args) => {
88
67
  const operationArgs = [...args];
89
68
  const optionsIndex = resolveOptionsIndex(prop, operationArgs);
69
+ if (optionsIndex === undefined) {
70
+ return value.apply(target, operationArgs);
71
+ }
90
72
  operationArgs[optionsIndex] = resolveSessionOptions(operationArgs[optionsIndex], ambient);
91
73
  return value.apply(target, operationArgs);
92
74
  };
93
75
  }
94
76
  });
95
77
  }
78
+ async function raceWithAbortAndDrainCallback(fn, signal, shouldDrainAfterAbort = () => true) {
79
+ let callback;
80
+ try {
81
+ return await raceWithAbort(() => {
82
+ callback = Promise.resolve().then(fn);
83
+ return callback;
84
+ }, signal);
85
+ } catch (error) {
86
+ if (signal.aborted && callback && shouldDrainAfterAbort()) {
87
+ await callback.then(() => undefined, () => undefined);
88
+ }
89
+ throw error;
90
+ }
91
+ }
96
92
  function resolveModelFactory(connection) {
97
93
  if (!isObjectLike(connection)) {
98
94
  return undefined;
@@ -129,6 +125,7 @@ class MongooseConnection {
129
125
  sessions = new AsyncLocalStorage();
130
126
  activeRequestTransactions = new Set();
131
127
  activeSessions = new Set();
128
+ activeTransactionCallbacks = new Set();
132
129
  lifecycleState = 'ready';
133
130
  constructor(connection, dispose, connectionOptions = {
134
131
  strictTransactions: false
@@ -163,16 +160,39 @@ class MongooseConnection {
163
160
  * @returns The ambient session inside a transaction boundary, or `undefined` outside one.
164
161
  */
165
162
  currentSession() {
166
- return this.sessions.getStore();
163
+ return this.sessions.getStore()?.session;
164
+ }
165
+
166
+ /**
167
+ * Saves a Mongoose document with the active transaction session.
168
+ *
169
+ * This opt-in helper preserves the document instance and forwards caller-provided save options.
170
+ * It does not patch document instances, prototypes, or model caches.
171
+ *
172
+ * @typeParam TSaveOptions Options accepted by the document's native `save()` method.
173
+ * @typeParam TDocument Mongoose document-like value being saved.
174
+ * @param document Existing Mongoose document to save.
175
+ * @param options Native Mongoose save options to preserve while attaching the ambient session.
176
+ * @returns The same document instance after its native save operation completes.
177
+ * @throws When called outside an active transaction or with a conflicting explicit session.
178
+ */
179
+ async saveDocument(document, options) {
180
+ const session = this.currentSession();
181
+ if (!session) {
182
+ throw new Error('Mongoose document saves require an active transaction session.');
183
+ }
184
+ return document.save(resolveSessionOptions(options, session));
167
185
  }
168
186
 
169
187
  /**
170
188
  * Returns a model from the root connection, injecting the ambient transaction session into conservative operations.
171
189
  *
190
+ * @typeParam TModel Consumer-defined facade result contract for the wrapped model.
172
191
  * @param name Model name passed to the underlying Mongoose connection.
173
192
  * @param args Additional model resolver arguments forwarded unchanged.
174
193
  * @returns The real model outside transactions, or a model facade inside an active transaction boundary.
175
194
  */
195
+
176
196
  model(name, ...args) {
177
197
  const modelFactory = resolveModelFactory(this.connection);
178
198
  if (typeof modelFactory !== 'function') {
@@ -189,7 +209,7 @@ class MongooseConnection {
189
209
  for (const transaction of this.activeRequestTransactions) {
190
210
  transaction.abort(new Error('Application shutdown interrupted an open request transaction.'));
191
211
  }
192
- await Promise.allSettled([...Array.from(this.activeRequestTransactions, transaction => transaction.settled), ...Array.from(this.activeSessions, session => session.settled)]);
212
+ await Promise.allSettled([...Array.from(this.activeRequestTransactions, transaction => transaction.settled), ...Array.from(this.activeSessions, session => session.settled), ...Array.from(this.activeTransactionCallbacks, callback => callback.settled)]);
193
213
  if (this.dispose) {
194
214
  await this.dispose(this.connection);
195
215
  }
@@ -231,11 +251,18 @@ class MongooseConnection {
231
251
  if (typeof this.connection.transaction === 'function') {
232
252
  return this.runConnectionTransaction(fn);
233
253
  }
234
- const session = await this.resolveSession();
254
+ const activeCallback = this.trackActiveTransactionCallback();
255
+ let session;
256
+ try {
257
+ session = await this.resolveSession();
258
+ } catch (error) {
259
+ activeCallback.settle();
260
+ throw error;
261
+ }
235
262
  if (!session) {
236
- return fn();
263
+ return this.runDirectTransaction(fn, activeCallback);
237
264
  }
238
- return this.runManualSessionTransaction(session, fn);
265
+ return this.runManualSessionTransaction(session, fn, activeCallback);
239
266
  }
240
267
 
241
268
  /**
@@ -251,16 +278,16 @@ class MongooseConnection {
251
278
  * @returns The callback result after the request transaction finishes or the direct-execution fallback completes.
252
279
  */
253
280
  async requestTransaction(fn, signal) {
254
- const currentSession = this.sessions.getStore();
255
- if (currentSession) {
281
+ const currentScope = this.sessions.getStore();
282
+ if (currentScope) {
256
283
  this.assertRequestTransactionsAvailable();
257
284
  const abortContext = createRequestAbortContext(signal);
258
285
  const active = this.trackActiveRequestTransaction(abortContext.controller);
259
286
  try {
260
- return await raceWithAbort(fn, abortContext.signal);
287
+ return await raceWithAbortAndDrainCallback(fn, abortContext.signal);
261
288
  } finally {
262
289
  abortContext.cleanup();
263
- this.untrackActiveRequestTransaction(active);
290
+ currentScope.activeSession.retainRequestTransaction(active);
264
291
  }
265
292
  }
266
293
  this.assertRequestTransactionsAvailable();
@@ -269,15 +296,20 @@ class MongooseConnection {
269
296
  let untrackActiveInFinally = true;
270
297
  try {
271
298
  if (typeof this.connection.transaction === 'function') {
272
- return await raceWithAbort(() => this.runConnectionTransaction(() => raceWithAbort(fn, abortContext.signal)), abortContext.signal);
299
+ let delegatedCallbackStarted = false;
300
+ const delegatedTransaction = this.runConnectionTransaction(() => {
301
+ delegatedCallbackStarted = true;
302
+ return raceWithAbortAndDrainCallback(fn, abortContext.signal);
303
+ });
304
+ return await raceWithAbortAndDrainCallback(() => delegatedTransaction, abortContext.signal, () => delegatedCallbackStarted);
273
305
  }
274
306
  const resolvedSession = await this.resolveSessionForRequest(abortContext.signal, active, () => {
275
307
  untrackActiveInFinally = false;
276
308
  });
277
309
  if (!resolvedSession) {
278
- return await raceWithAbort(fn, abortContext.signal);
310
+ return await raceWithAbortAndDrainCallback(fn, abortContext.signal);
279
311
  }
280
- return await this.runManualSessionTransaction(resolvedSession, () => raceWithAbort(fn, abortContext.signal));
312
+ return await this.runManualSessionTransaction(resolvedSession, () => raceWithAbortAndDrainCallback(fn, abortContext.signal));
281
313
  } finally {
282
314
  abortContext.cleanup();
283
315
  if (untrackActiveInFinally) {
@@ -295,18 +327,29 @@ class MongooseConnection {
295
327
  throw new Error(TRANSACTION_UNAVAILABLE_ERROR);
296
328
  }
297
329
  }
298
- async runManualSessionTransaction(session, fn) {
330
+ async runManualSessionTransaction(session, fn, activeCallback) {
299
331
  const activeSession = this.trackActiveSession();
300
332
  try {
301
- return await this.sessions.run(session, () => executeSessionTransaction(session, fn));
333
+ return await this.sessions.run({
334
+ activeSession,
335
+ session
336
+ }, () => executeSessionTransaction(session, fn));
302
337
  } finally {
303
338
  try {
304
339
  await session.endSession();
305
340
  } finally {
306
341
  activeSession.settle();
342
+ activeCallback?.settle();
307
343
  }
308
344
  }
309
345
  }
346
+ async runDirectTransaction(fn, activeCallback) {
347
+ try {
348
+ return await fn();
349
+ } finally {
350
+ activeCallback.settle();
351
+ }
352
+ }
310
353
  async resolveSessionForRequest(signal, active, deferActiveSettlement) {
311
354
  const sessionPromise = this.resolveSession();
312
355
  try {
@@ -330,7 +373,10 @@ class MongooseConnection {
330
373
  if (typeof this.connection.transaction !== 'function') {
331
374
  throw new Error('Mongoose connection transaction resolver initialization failed.');
332
375
  }
333
- return await this.connection.transaction(session => this.sessions.run(session, fn));
376
+ return await this.connection.transaction(session => this.sessions.run({
377
+ activeSession,
378
+ session
379
+ }, fn));
334
380
  } finally {
335
381
  activeSession.settle();
336
382
  }
@@ -342,14 +388,37 @@ class MongooseConnection {
342
388
  settle = resolve;
343
389
  })
344
390
  };
391
+ const retainedRequestTransactions = new Set();
345
392
  this.activeSessions.add(active);
346
393
  return {
394
+ retainRequestTransaction: handle => {
395
+ retainedRequestTransactions.add(handle);
396
+ },
347
397
  settle: () => {
398
+ for (const handle of retainedRequestTransactions) {
399
+ this.untrackActiveRequestTransaction(handle);
400
+ }
401
+ retainedRequestTransactions.clear();
348
402
  this.activeSessions.delete(active);
349
403
  settle();
350
404
  }
351
405
  };
352
406
  }
407
+ trackActiveTransactionCallback() {
408
+ let settle;
409
+ const active = {
410
+ settled: new Promise(resolve => {
411
+ settle = resolve;
412
+ })
413
+ };
414
+ this.activeTransactionCallbacks.add(active);
415
+ return {
416
+ settle: () => {
417
+ this.activeTransactionCallbacks.delete(active);
418
+ settle();
419
+ }
420
+ };
421
+ }
353
422
  trackActiveRequestTransaction(controller) {
354
423
  return trackActiveRequestTransaction(this.activeRequestTransactions, controller);
355
424
  }
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`.
33
+ * @returns A module definition that exports `MongooseConnection` and its compatibility request interceptor.
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;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"}
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;AA4EvD;;;;;;;;;;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,6 +1,7 @@
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';
4
5
 
5
6
  /**
6
7
  * Async registration options accepted by `MongooseModule.forRootAsync(...)`.
@@ -11,7 +12,7 @@ import { MONGOOSE_CONNECTION, MONGOOSE_DISPOSE, MONGOOSE_OPTIONS } from './token
11
12
  */
12
13
 
13
14
  const MONGOOSE_NORMALIZED_OPTIONS = Symbol('fluo.mongoose.normalized-options');
14
- const MONGOOSE_MODULE_EXPORTS = [MongooseConnection];
15
+ const MONGOOSE_MODULE_EXPORTS = [MongooseConnection, MongooseTransactionInterceptor];
15
16
  function isObjectLike(value) {
16
17
  return typeof value === 'object' && value !== null || typeof value === 'function';
17
18
  }
@@ -42,7 +43,7 @@ function createMongooseRuntimeProviders(normalizedOptionsProvider) {
42
43
  inject: [MONGOOSE_NORMALIZED_OPTIONS],
43
44
  provide: MONGOOSE_OPTIONS,
44
45
  useFactory: options => createRuntimeOptionsProviderValue(options.strictTransactions)
45
- }, MongooseConnection];
46
+ }, MongooseConnection, MongooseTransactionInterceptor];
46
47
  }
47
48
  function createMongooseProvidersAsync(options) {
48
49
  const factory = options.useFactory;
@@ -104,7 +105,7 @@ export class MongooseModule {
104
105
  * Registers Mongoose providers from static options.
105
106
  *
106
107
  * @param options Mongoose module options with connection handle, optional dispose hook, and strict transaction mode.
107
- * @returns A module definition that exports `MongooseConnection`.
108
+ * @returns A module definition that exports `MongooseConnection` and its compatibility request interceptor.
108
109
  */
109
110
  static forRoot(options) {
110
111
  return buildMongooseModule(options);
@@ -1,3 +1,6 @@
1
+ import type { CallHandler, Interceptor, InterceptorContext } from '@fluojs/http';
2
+ import { MongooseConnection } from './connection.js';
3
+ import type { MongooseConnectionLike } from './types.js';
1
4
  type TransactionConnection = {
2
5
  transaction<T>(fn: () => Promise<T>): Promise<T>;
3
6
  };
@@ -16,5 +19,26 @@ type TransactionMethod<THost, TArgs extends unknown[], TResult> = (this: THost,
16
19
  * @returns A standard method decorator that executes the original method inside a Mongoose transaction.
17
20
  */
18
21
  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>;
22
+ /**
23
+ * Compatibility HTTP interceptor that opens a Mongoose request transaction around a routed handler.
24
+ *
25
+ * @remarks
26
+ * This deprecated 1.x bridge forwards the request `AbortSignal` to `MongooseConnection.requestTransaction(...)`.
27
+ * Prefer service-layer `@Transaction()` or an explicit request boundary for new code.
28
+ *
29
+ * @deprecated Prefer service-layer `@Transaction()` or explicit `MongooseConnection.requestTransaction(...)`.
30
+ */
31
+ export declare class MongooseTransactionInterceptor implements Interceptor {
32
+ private readonly connection;
33
+ constructor(connection: MongooseConnection<MongooseConnectionLike>);
34
+ /**
35
+ * Runs the downstream handler inside the compatibility request transaction.
36
+ *
37
+ * @param context Interceptor context containing the request cancellation signal.
38
+ * @param next Downstream handler chain.
39
+ * @returns The downstream result after the request transaction settles.
40
+ */
41
+ intercept(context: InterceptorContext, next: CallHandler): Promise<unknown>;
42
+ }
19
43
  export {};
20
44
  //# sourceMappingURL=transaction.d.ts.map
@@ -1 +1 @@
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
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,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;AAED;;;;;;;;GAQG;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,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;CAGlF"}
@@ -1,3 +1,11 @@
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';
1
9
  function isTransactionConnection(value) {
2
10
  return (typeof value === 'object' && value !== null || typeof value === 'function') && typeof value.transaction === 'function';
3
11
  }
@@ -62,4 +70,38 @@ export function Transaction(accessor) {
62
70
  return connection.transaction(() => value.apply(this, args));
63
71
  };
64
72
  };
65
- }
73
+ }
74
+
75
+ /**
76
+ * Compatibility HTTP interceptor that opens a Mongoose request transaction around a routed handler.
77
+ *
78
+ * @remarks
79
+ * This deprecated 1.x bridge forwards the request `AbortSignal` to `MongooseConnection.requestTransaction(...)`.
80
+ * Prefer service-layer `@Transaction()` or an explicit request boundary for new code.
81
+ *
82
+ * @deprecated Prefer service-layer `@Transaction()` or explicit `MongooseConnection.requestTransaction(...)`.
83
+ */
84
+ let _MongooseTransactionI;
85
+ class MongooseTransactionInterceptor {
86
+ static {
87
+ [_MongooseTransactionI, _initClass] = _applyDecs(this, [Inject(MongooseConnection)], []).c;
88
+ }
89
+ constructor(connection) {
90
+ this.connection = connection;
91
+ }
92
+
93
+ /**
94
+ * Runs the downstream handler inside the compatibility request transaction.
95
+ *
96
+ * @param context Interceptor context containing the request cancellation signal.
97
+ * @param next Downstream handler chain.
98
+ * @returns The downstream result after the request transaction settles.
99
+ */
100
+ async intercept(context, next) {
101
+ return this.connection.requestTransaction(() => next.handle(), context.requestContext.request.signal);
102
+ }
103
+ static {
104
+ _initClass();
105
+ }
106
+ }
107
+ export { _MongooseTransactionI as MongooseTransactionInterceptor };
package/dist/types.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { MaybePromise } from '@fluojs/core';
2
2
  /**
3
- * Minimal Mongoose connection seam that optionally supports session creation.
3
+ * Minimal Mongoose connection seam that optionally supports session transaction APIs.
4
4
  *
5
5
  * @remarks
6
- * Fluo only requires `startSession()` to expose transaction helpers; plain connection usage still works without it.
6
+ * Fluo can open transaction helpers through either `connection.transaction(...)` or `startSession()`;
7
+ * plain connection usage still works without either API.
7
8
  */
8
9
  export interface MongooseConnectionLike {
9
10
  startSession?(): Promise<MongooseSessionLike>;
@@ -18,6 +19,32 @@ export interface MongooseSessionLike {
18
19
  abortTransaction(): MaybePromise<void>;
19
20
  endSession(): MaybePromise<void>;
20
21
  }
22
+ /**
23
+ * Callable model facade returned by `MongooseConnection.model(...)`.
24
+ *
25
+ * @remarks
26
+ * The listed operations receive the ambient transaction session automatically when called inside a transaction boundary.
27
+ * Other model properties remain available as `unknown` because fluo does not own application schema or plugin typing.
28
+ *
29
+ * @typeParam TCreateResult Result returned by `create(...)`.
30
+ * @typeParam TFindResult Result returned by `find(...)`.
31
+ * @typeParam TFindOneResult Result returned by `findOne(...)`.
32
+ * @typeParam TAggregateResult Result returned by `aggregate(...)`.
33
+ * @typeParam TBulkWriteResult Result returned by `bulkWrite(...)`.
34
+ */
35
+ export interface MongooseModelFacade<TCreateResult = unknown, TFindResult = unknown, TFindOneResult = unknown, TAggregateResult = unknown, TBulkWriteResult = unknown> {
36
+ /** Runs a Mongoose aggregate operation with ambient session options. */
37
+ aggregate(...args: unknown[]): TAggregateResult;
38
+ /** Runs a Mongoose bulk-write operation with ambient session options. */
39
+ bulkWrite(...args: unknown[]): TBulkWriteResult;
40
+ /** Runs a Mongoose create operation with ambient session options. */
41
+ create(...args: unknown[]): TCreateResult;
42
+ /** Runs a Mongoose find operation with ambient session options. */
43
+ find(...args: unknown[]): TFindResult;
44
+ /** Runs a Mongoose find-one operation with ambient session options. */
45
+ findOne(...args: unknown[]): TFindOneResult;
46
+ readonly [key: PropertyKey]: unknown;
47
+ }
21
48
  /**
22
49
  * Module options for registering a Mongoose connection and optional shutdown disposal hook.
23
50
  *
@@ -31,7 +58,8 @@ export interface MongooseModuleOptions<TConnection extends MongooseConnectionLik
31
58
  /** Whether Mongoose providers should be visible globally. Defaults to `false`. */
32
59
  global?: boolean;
33
60
  /**
34
- * Throws when transaction helpers are used against a connection that does not implement `startSession()`.
61
+ * Throws when transaction helpers are used against a connection that implements neither `connection.transaction(...)` nor
62
+ * `startSession()`.
35
63
  *
36
64
  * @remarks
37
65
  * Leave this disabled when `transaction()` / `requestTransaction()` should fall back to direct execution.
@@ -55,7 +83,7 @@ export interface MongooseHandleProvider<TConnection extends MongooseConnectionLi
55
83
  * @param args Additional model resolver arguments forwarded unchanged.
56
84
  * @returns The root model outside transactions, or a model facade inside an active transaction boundary.
57
85
  */
58
- model(name: string, ...args: unknown[]): Record<PropertyKey, unknown>;
86
+ model<TModel extends MongooseModelFacade = MongooseModelFacade>(name: string, ...args: unknown[]): TModel;
59
87
  /**
60
88
  * Opens a Mongoose session transaction boundary around `fn`.
61
89
  *
@@ -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;;;;;;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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;;GAMG;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;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB,CAClC,aAAa,GAAG,OAAO,EACvB,WAAW,GAAG,OAAO,EACrB,cAAc,GAAG,OAAO,EACxB,gBAAgB,GAAG,OAAO,EAC1B,gBAAgB,GAAG,OAAO;IAE1B,wEAAwE;IACxE,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC;IAChD,yEAAyE;IACzE,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC;IAChD,qEAAqE;IACrE,MAAM,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC;IAC1C,mEAAmE;IACnE,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC;IACtC,uEAAuE;IACvE,OAAO,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC;IAC5C,QAAQ,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC;CACtC;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;;;;;;OAMG;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,MAAM,SAAS,mBAAmB,GAAG,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAC1G;;;;;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.1.0",
12
+ "version": "2.0.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/mongoose"
19
19
  },
20
20
  "engines": {
21
- "node": ">=20.0.0"
21
+ "node": ">=24.0.0 <27"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"
@@ -36,10 +36,10 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.0.3",
40
- "@fluojs/http": "^1.1.2",
41
- "@fluojs/di": "^1.1.0",
42
- "@fluojs/runtime": "^1.1.8"
39
+ "@fluojs/core": "^2.0.0",
40
+ "@fluojs/http": "^3.0.0",
41
+ "@fluojs/di": "^3.0.0",
42
+ "@fluojs/runtime": "^3.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "mongoose": ">=7.0.0"
@@ -50,7 +50,7 @@
50
50
  }
51
51
  },
52
52
  "devDependencies": {
53
- "vitest": "^3.2.4"
53
+ "vitest": "^4.1.11"
54
54
  },
55
55
  "scripts": {
56
56
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",