@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 +92 -18
- package/README.md +91 -18
- package/dist/connection.d.ts +22 -3
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +113 -44
- package/dist/module.d.ts +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +4 -3
- package/dist/transaction.d.ts +24 -0
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +43 -1
- package/dist/types.d.ts +32 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
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
|
|
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 {
|
|
80
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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)` — 트랜잭션 밖에서는
|
|
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
|
|
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 {
|
|
78
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/dist/connection.d.ts
CHANGED
|
@@ -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[]):
|
|
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;
|
package/dist/connection.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/connection.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
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
|
|
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
|
|
255
|
-
if (
|
|
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
|
|
287
|
+
return await raceWithAbortAndDrainCallback(fn, abortContext.signal);
|
|
261
288
|
} finally {
|
|
262
289
|
abortContext.cleanup();
|
|
263
|
-
|
|
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
|
-
|
|
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
|
|
310
|
+
return await raceWithAbortAndDrainCallback(fn, abortContext.signal);
|
|
279
311
|
}
|
|
280
|
-
return await this.runManualSessionTransaction(resolvedSession, () =>
|
|
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(
|
|
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(
|
|
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
|
/**
|
package/dist/module.d.ts.map
CHANGED
|
@@ -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;
|
|
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);
|
package/dist/transaction.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/transaction.js
CHANGED
|
@@ -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
|
|
3
|
+
* Minimal Mongoose connection seam that optionally supports session transaction APIs.
|
|
4
4
|
*
|
|
5
5
|
* @remarks
|
|
6
|
-
* Fluo
|
|
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
|
|
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[]):
|
|
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
|
*
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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
|
|
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": "
|
|
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": ">=
|
|
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": "^
|
|
40
|
-
"@fluojs/http": "^
|
|
41
|
-
"@fluojs/di": "^
|
|
42
|
-
"@fluojs/runtime": "^
|
|
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": "^
|
|
53
|
+
"vitest": "^4.1.11"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|