@fluojs/drizzle 1.0.2 → 1.1.1

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
@@ -2,17 +2,18 @@
2
2
 
3
3
  <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
4
 
5
- 트랜잭션 인지형 데이터베이스 래퍼와 선택적 dispose hook을 제공하는 fluo용 Drizzle ORM 통합 패키지입니다.
5
+ Node.js 전용 트랜잭션 인지형 데이터베이스 래퍼와 선택적 dispose hook을 제공하는 fluo용 Drizzle ORM 통합 패키지입니다.
6
6
 
7
7
  ## 목차
8
8
 
9
9
  - [설치](#설치)
10
+ - [런타임 지원](#런타임-지원)
10
11
  - [사용 시점](#사용-시점)
11
12
  - [빠른 시작](#빠른-시작)
12
13
  - [주요 패턴](#주요-패턴)
13
- - [repository에서 `DrizzleDatabase.current()` 사용하기](#repository에서-drizzledatabasecurrent-사용하기)
14
- - [수동 트랜잭션 경계](#수동-트랜잭션-경계)
15
- - [인터셉터 기반 요청 단위 트랜잭션](#인터셉터-기반-요청-단위-트랜잭션)
14
+ - [서비스 트랜잭션 경계 (@Transaction)](#서비스-트랜잭션-경계-transaction)
15
+ - [수동 트랜잭션과 current()](#수동-트랜잭션과-current)
16
+ - [요청 전체 컨트롤러 경계](#요청-전체-컨트롤러-경계)
16
17
  - [종료와 상태 계약](#종료와-상태-계약)
17
18
  - [수동 모듈 구성](#수동-모듈-구성)
18
19
  - [공개 API 개요](#공개-api-개요)
@@ -27,9 +28,17 @@ npm install @fluojs/drizzle drizzle-orm
27
28
  npm install pg
28
29
  ```
29
30
 
31
+ ## 런타임 지원
32
+
33
+ 루트 `@fluojs/drizzle` 패키지는 현재 Node.js 20+ 통합입니다. ambient transaction context를 유지하기 위해 Node의 `node:async_hooks` 모듈을 import하고, package manifest는 `engines.node >=20.0.0`을 선언합니다.
34
+
35
+ Drizzle ORM 자체는 Bun SQL이나 Cloudflare D1 같은 driver도 대상으로 할 수 있지만, 비 Node transaction-context adapter가 문서화되기 전까지 해당 driver runtime은 이 fluo wrapper 범위 밖입니다.
36
+
37
+ 비 Node 런타임에서는 루트 패키지를 import하지 마세요. Bun, Deno, Cloudflare Workers 또는 다른 비 Node Drizzle driver에서는 raw Drizzle driver handle을 `{ provide, useFactory }`나 `{ provide, useValue }` 같은 애플리케이션 소유 fluo provider 뒤에 등록하고, repository에는 해당 애플리케이션 토큰을 주입하세요. Canonical package chooser/surface 문서와 Bun/Cloudflare book 장에서 이런 raw-provider 패턴을 보여 줍니다.
38
+
30
39
  ## 사용 시점
31
40
 
32
- - Drizzle을 다른 fluo 모듈과 같은 DI·모듈·라이프사이클 모델 안에 넣고 싶을 때
41
+ - Node.js 20+ 애플리케이션에서 Drizzle을 다른 fluo 모듈과 같은 DI·모듈·라이프사이클 모델 안에 넣고 싶을 때
33
42
  - repository 코드가 root handle과 현재 트랜잭션 handle 사이를 `current()` 하나로 다루고 싶을 때
34
43
  - 애플리케이션 종료 시 underlying driver 정리 로직도 함께 실행해야 할 때
35
44
 
@@ -66,49 +75,127 @@ export class AppModule {}
66
75
 
67
76
  ## 주요 패턴
68
77
 
69
- ### repository에서 `DrizzleDatabase.current()` 사용하기
78
+ ### 서비스 트랜잭션 경계 (@Transaction)
79
+
80
+ `@Transaction()` 데코레이터는 서비스 레이어에서 트랜잭션 경계를 정의하는 권장 방법입니다. 이 데코레이터가 적용된 메서드 내부에서 발생하는 모든 리포지토리 호출은 동일한 Drizzle 트랜잭션을 공유합니다.
81
+
82
+ ```ts
83
+ import { Inject } from '@fluojs/core';
84
+ import { Transaction, DrizzleDatabase, type DrizzleDatabaseFacade } from '@fluojs/drizzle';
85
+ import { drizzle } from 'drizzle-orm/node-postgres';
86
+ import { users, profiles } from './schema';
87
+
88
+ type AppDatabase = ReturnType<typeof drizzle>;
89
+
90
+ @Inject(DrizzleDatabase)
91
+ export class UserRepository {
92
+ constructor(private readonly db: DrizzleDatabaseFacade<AppDatabase>) {}
93
+
94
+ async create(data: any) {
95
+ // facade 타입은 표준 Drizzle 메서드를 노출합니다.
96
+ // @Transaction() 내부에서 호출되면 자동으로 활성 트랜잭션에 참여합니다.
97
+ const [user] = await this.db.insert(users).values(data).returning();
98
+
99
+ if (!user) {
100
+ throw new Error('User insert did not return a row.');
101
+ }
102
+
103
+ return user;
104
+ }
105
+
106
+ async initProfile(userId: string) {
107
+ return this.db.insert(profiles).values({ userId });
108
+ }
109
+ }
110
+
111
+ @Inject(UserRepository)
112
+ export class UserService {
113
+ constructor(private readonly repo: UserRepository) {}
114
+
115
+ @Transaction()
116
+ async onboardUser(dto: any) {
117
+ const user = await this.repo.create(dto);
118
+ await this.repo.initProfile(user.id);
119
+ return user;
120
+ }
121
+ }
122
+ ```
123
+
124
+ `@Transaction()` 메서드 호출은 재진입(reentrant)이 가능합니다. 데코레이터가 적용된 메서드가 다른 데코레이터 적용 메서드를 호출하더라도 하나의 동일한 Drizzle 트랜잭션 안에서 실행됩니다.
125
+
126
+ 기본적으로 `@Transaction()`은 작은 host-object heuristic으로 대상을 고릅니다. 먼저 `this.db`를 확인하고, 그다음 데코레이터가 붙은 인스턴스의 직접 property, 마지막으로 그 값들의 중첩 `.db` property 중 `transaction(...)` 메서드를 노출하는 첫 값을 사용합니다. 이 후보들이 모두 맞지 않으면 데코레이터가 붙은 인스턴스 자체를 transaction 대상으로 사용합니다. 이 덕분에 `constructor(private readonly db: DrizzleDatabase<...>)` 같은 일반 서비스와 자체 facade host는 간결하게 유지할 수 있지만, 하나의 서비스가 Drizzle wrapper를 둘 이상 소유한다면 property 순서에 의존하지 마세요. 데코레이터가 붙은 host가 여러 transaction-capable client를 갖거나 `.db`를 노출하는 repository를 감싸는 경우에는 `@Transaction((self) => self.ordersDb)` 또는 `@Transaction((self) => self.analyticsDb, options)`처럼 명시적 accessor를 전달하세요.
127
+
128
+ ### 수동 트랜잭션과 current()
129
+
130
+ `DrizzleDatabase`는 트랜잭션 범위 내에 있으면 자동으로 활성 트랜잭션 handle을, 그렇지 않으면 root handle을 반환하는 `current()` 메서드를 제공합니다. 외부 유틸리티에 handle을 전달하거나 복잡한 수동 트랜잭션 처리가 필요한 경우 escape hatch로 사용하세요.
70
131
 
71
132
  ```ts
72
133
  import { DrizzleDatabase } from '@fluojs/drizzle';
73
- import { eq } from 'drizzle-orm';
134
+ import { drizzle } from 'drizzle-orm/node-postgres';
74
135
  import { users } from './schema';
75
136
 
76
- export class UserRepository {
77
- constructor(private readonly db: DrizzleDatabase) {}
137
+ type AppDatabase = ReturnType<typeof drizzle>;
138
+
139
+ export class AdvancedRepository {
140
+ constructor(private readonly db: DrizzleDatabase<AppDatabase>) {}
78
141
 
79
- async findById(id: string) {
80
- return this.db.current().select().from(users).where(eq(users.id, id));
142
+ async customOperation() {
143
+ const tx = this.db.current();
144
+ // fluo가 자동으로 감싸지 않는 작업을 수행하거나,
145
+ // Drizzle handle을 직접 기대하는 외부 유틸리티에 전달할 때 tx를 사용하세요.
146
+ return tx.select().from(users);
81
147
  }
82
148
  }
83
149
  ```
84
150
 
85
- ### 수동 트랜잭션 경계
151
+ 수동 트랜잭션 블록에는 `db.transaction()`을 사용하세요:
86
152
 
87
153
  ```ts
88
154
  await this.db.transaction(async () => {
89
- const tx = this.db.current();
90
- await tx.insert(users).values(user);
91
- await tx.insert(profiles).values(profile);
155
+ const current = this.db.current();
156
+
157
+ await current.insert(users).values(user);
158
+ await current.insert(profiles).values(profile);
92
159
  });
93
160
  ```
94
161
 
95
162
  중첩 호출은 활성 transaction boundary를 재사용합니다. 이미 boundary가 활성화되어 있는데 중첩 호출이 transaction option을 전달하면, 기존 transaction을 조용히 바꾸지 않고 해당 중첩 option을 거부합니다.
96
163
 
97
- `database.transaction(...)`을 사용할 수 없고 `strictTransactions`가 `false`이면 `transaction()`과 `requestTransaction()`은 직접 실행으로 fallback합니다. 요청 범위 호출은 경우에도 `AbortSignal`을 존중합니다.
164
+ `database.transaction(...)`을 사용할 수 없고 `strictTransactions`가 `false`(기본값)이면 `transaction()`과 `requestTransaction()`은 의도적으로 fail-open(fail-open fallback)하여 callback을 root handle에서 직접 실행합니다. 이는 local fake, read-only adapter, 점진적 migration에는 유용하지만 원자적이지 않으므로 실제 데이터베이스 transaction으로 취급하면 안 됩니다. rollback 보장이 필요한 production 경로에서는 `strictTransactions: true`를 설정하세요. 그러면 startup 및 readiness 진단에서 누락된 `database.transaction(...)` 지원을 드러내고, transaction helper는 트랜잭션 없이 조용히 실행하는 대신 예외를 던집니다. 요청 범위 fallback은 그래도 `AbortSignal`을 존중하므로, Drizzle transaction runner가 없어도 취소된 요청은 직접 실행 전이나 도중에 중단될 수 있습니다.
165
+
166
+ ### 요청 전체 컨트롤러 경계
98
167
 
99
- ### 인터셉터 기반 요청 단위 트랜잭션
168
+ 비즈니스 작업에는 서비스 레벨 `@Transaction()`을 우선 사용하세요. 전체 요청을 하나의 transaction으로 감싸던 NestJS controller/interceptor 패턴을 마이그레이션해야 한다면 controller, route adapter, request orchestration 경계에서 `requestTransaction(...)`을 명시적으로 호출하고 가능한 경우 request `AbortSignal`을 전달하세요.
100
169
 
101
170
  ```ts
102
- import { UseInterceptors } from '@fluojs/http';
103
- import { DrizzleTransactionInterceptor } from '@fluojs/drizzle';
171
+ import { Controller, Post } from '@fluojs/http';
172
+ import { DrizzleDatabase } from '@fluojs/drizzle';
173
+ import { drizzle } from 'drizzle-orm/node-postgres';
104
174
 
105
- @UseInterceptors(DrizzleTransactionInterceptor)
106
- class UsersController {}
175
+ type AppDatabase = ReturnType<typeof drizzle>;
176
+
177
+ @Controller('/checkout')
178
+ export class CheckoutController {
179
+ constructor(
180
+ private readonly db: DrizzleDatabase<AppDatabase>,
181
+ private readonly checkout: CheckoutService,
182
+ ) {}
183
+
184
+ @Post()
185
+ create(input: CheckoutInput, requestSignal?: AbortSignal) {
186
+ return this.db.requestTransaction(
187
+ () => this.checkout.createOrder(input),
188
+ requestSignal,
189
+ );
190
+ }
191
+ }
107
192
  ```
108
193
 
194
+ import할 수 있는 Drizzle `*TransactionInterceptor` export는 없습니다. 기존 NestJS interceptor 설계는 대부분의 transaction boundary를 서비스로 옮기고, 전체 request 작업이 서비스 메서드 하나가 아니라 같은 boundary를 공유해야 하는 드문 controller-level 호환성 사례에만 명시적 `requestTransaction(...)`을 남기세요. controller가 명시적 `DrizzleDatabase` 대상을 소유한다면 controller method에 `@Transaction()`을 붙이는 방식도 호환성 경로로 유지되지만, request `AbortSignal`을 직접 받을 수 있는 `requestTransaction(...)`이 더 명확한 request-wide API입니다.
195
+
109
196
  ### 종료와 상태 계약
110
197
 
111
- `DrizzleTransactionInterceptor`는 각 HTTP 요청을 `DrizzleDatabase.requestTransaction(...)`으로 실행합니다. 애플리케이션 종료 중에는 `DrizzleDatabase`가 아직 활성 상태인 요청 트랜잭션을 abort하고, 열린 요청 및 수동 transaction callback이 settle되거나 rollback될 때까지 기다린 뒤 선택적 `dispose(database)` hook을 실행합니다. 순서는 pool이나 외부 관리 리소스를 닫기 전에 driver가 commit/rollback/cleanup 작업을 끝낼 수 있게 보장합니다.
198
+ 애플리케이션 종료 중에는 `DrizzleDatabase`가 아직 활성 상태인 요청 트랜잭션을 abort하고, 열린 요청 및 수동 transaction callback이 settle되거나 rollback될 때까지 기다린 뒤 선택적 `dispose(database)` hook을 실행합니다. 여기에는 `database.transaction(...)`을 사용할 수 없고 `strictTransactions`가 `false`일 때의 fail-open 수동 `transaction(...)` callback도 포함되므로, 직접 실행 fallback도 pool이나 외부 관리 리소스를 닫기 전에 drain됩니다.
112
199
  기존 요청 boundary 안에서 열린 중첩 `requestTransaction(...)` 호출은 활성 Drizzle transaction을 재사용하면서도 ambient request abort signal을 관찰합니다. 기존 수동 transaction boundary 안에서 열린 중첩 `requestTransaction(...)` 호출도 두 번째 Drizzle transaction을 열지 않고 shutdown settlement tracking에 참여하며, 해당 settlement handle은 바깥 수동 transaction이 settle될 때까지 tracking에 남아 shutdown이 `dispose(database)`를 실행하기 전에 그 바깥 경계까지 drain하게 합니다. 단, platform status activity count는 더 짧게 유지됩니다. 중첩 request callback이 settle되는 즉시, 바깥 수동 transaction이 계속 실행 중이어도 `details.activeRequestTransactions`는 감소합니다.
113
200
  종료가 시작된 뒤 새 `transaction(...)` 및 `requestTransaction(...)` 호출은 거부되므로, 종료 boundary를 지난 뒤 시작되는 늦은 트랜잭션보다 dispose가 먼저 실행되는 상황을 방지합니다.
114
201
  요청 callback이 완료된 뒤 underlying Drizzle transaction runner가 commit 또는 rollback을 끝내기 전에 request signal이 abort되면, `requestTransaction(...)`은 먼저 해당 runner가 settle될 때까지 기다린 다음 abort reason으로 reject합니다. 이 동작은 Drizzle cleanup을 request cancellation과 직렬화하면서, 완료된 callback 결과를 반환하는 대신 늦은 request abort를 caller에게 드러냅니다.
@@ -127,7 +214,7 @@ class UsersController {}
127
214
 
128
215
  ```ts
129
216
  import { defineModule } from '@fluojs/runtime';
130
- import { DrizzleDatabase, DrizzleModule, DrizzleTransactionInterceptor } from '@fluojs/drizzle';
217
+ import { DrizzleModule } from '@fluojs/drizzle';
131
218
 
132
219
  const database = {
133
220
  transaction: async <T>(callback: (tx: typeof database) => Promise<T>) => callback(database),
@@ -136,7 +223,6 @@ const database = {
136
223
  class ManualDrizzleModule {}
137
224
 
138
225
  defineModule(ManualDrizzleModule, {
139
- exports: [DrizzleDatabase, DrizzleTransactionInterceptor],
140
226
  imports: [DrizzleModule.forRoot({ database })],
141
227
  });
142
228
  ```
@@ -145,8 +231,10 @@ defineModule(ManualDrizzleModule, {
145
231
 
146
232
  - `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
147
233
  - `DrizzleDatabase`
148
- - `DrizzleTransactionInterceptor`
234
+ - `DrizzleDatabaseFacade<TDatabase>`
235
+ - `Transaction`
149
236
  - `DRIZZLE_DATABASE`, `DRIZZLE_DISPOSE`, `DRIZZLE_HANDLE_PROVIDER`, `DRIZZLE_OPTIONS`
237
+ - `DrizzleDatabase.createFacade(...)` (호환성 전용 provider wiring helper; 애플리케이션 등록은 `DrizzleModule.forRoot(...)` / `forRootAsync(...)`를 우선 사용)
150
238
  - `createDrizzlePlatformStatusSnapshot(...)`
151
239
  - `DrizzleDatabaseLike`
152
240
  - `DrizzleModuleOptions`
@@ -154,6 +242,10 @@ defineModule(ManualDrizzleModule, {
154
242
 
155
243
  `DRIZZLE_HANDLE_PROVIDER`는 lifecycle-aware `DrizzleDatabase` wrapper를 가리키는 alias token입니다. `@fluojs/terminus` 같은 health integration은 이 token을 통해 raw database ping으로 fallback하기 전에 `createPlatformStatusSnapshot()`을 읽습니다.
156
244
 
245
+ provider가 `current()`, `transaction(...)`, `requestTransaction(...)`, `createPlatformStatusSnapshot()` 같은 wrapper 메서드만 필요로 하면 `DrizzleDatabase<TDatabase>`를 사용하세요. 리포지토리 주입에서 Drizzle query 메서드를 직접 호출해야 한다면 `DrizzleDatabaseFacade<TDatabase>`를 사용합니다. 이 facade는 활성 트랜잭션 handle이 있으면 그 handle로, 없으면 root handle로 호출을 전달합니다. `DrizzleDatabase.createFacade(...)`는 module provider wiring을 위한 low-level compatibility helper로 유지됩니다. 애플리케이션 코드는 `DrizzleModule.forRoot(...)` / `forRootAsync(...)`를 우선 사용하세요.
246
+
247
+ `Transaction`은 서비스 계층 트랜잭션 경계를 위한 표준 TC39 method decorator입니다. 데코레이터가 붙은 host에서 `this.db`, 직접 property, 중첩 `.db` property 순서로 transaction-capable 대상을 resolve한 뒤, 후보가 없으면 데코레이터가 붙은 인스턴스 자체로 fallback합니다. 명시적 client 선택에는 accessor를 받을 수 있으며, 외부 경계에는 Drizzle transaction option을 전달할 수 있습니다.
248
+
157
249
  ### `DrizzleModule`
158
250
 
159
251
  - `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
@@ -165,7 +257,7 @@ defineModule(ManualDrizzleModule, {
165
257
  ## 관련 패키지
166
258
 
167
259
  - `@fluojs/runtime`: 모듈 시작과 종료 순서를 관리합니다.
168
- - `@fluojs/http`: 요청 단위 트랜잭션에 쓰이는 인터셉터 파이프라인을 제공합니다.
260
+ - `@fluojs/http`: 명시적 `requestTransaction(...)` 경계와 함께 사용할 있는 요청 라이프사이클 primitive를 제공합니다.
169
261
  - `@fluojs/prisma`, `@fluojs/mongoose`: 같은 런타임 모델 위에서 동작하는 다른 데이터 통합 패키지입니다.
170
262
 
171
263
  ## 예제 소스
package/README.md CHANGED
@@ -2,18 +2,19 @@
2
2
 
3
3
  <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
4
 
5
- Drizzle ORM integration for fluo with a transaction-aware database wrapper and an optional dispose hook.
5
+ Node.js-only Drizzle ORM integration for fluo with a transaction-aware database wrapper and an optional dispose hook.
6
6
 
7
7
  ## Table of Contents
8
8
 
9
9
  - [Installation](#installation)
10
+ - [Runtime Support](#runtime-support)
10
11
  - [When to Use](#when-to-use)
11
12
  - [Quick Start](#quick-start)
12
13
  - [Common Patterns](#common-patterns)
13
- - [Use `DrizzleDatabase.current()` inside repositories](#use-drizzledatabasecurrent-inside-repositories)
14
- - [Manual transaction boundaries](#manual-transaction-boundaries)
15
- - [Request-scoped transactions with an interceptor](#request-scoped-transactions-with-an-interceptor)
16
- - [Shutdown and status contracts](#shutdown-and-status-contracts)
14
+ - [Service Transaction Boundary (@Transaction)](#service-transaction-boundary-transaction)
15
+ - [Manual Transactions and current()](#manual-transactions-and-current)
16
+ - [Request-Wide Controller Boundaries](#request-wide-controller-boundaries)
17
+ - [Shutdown and Status Contracts](#shutdown-and-status-contracts)
17
18
  - [Manual Module Composition](#manual-module-composition)
18
19
  - [Public API Overview](#public-api-overview)
19
20
  - [Related Packages](#related-packages)
@@ -27,9 +28,17 @@ npm install @fluojs/drizzle drizzle-orm
27
28
  npm install pg
28
29
  ```
29
30
 
31
+ ## Runtime Support
32
+
33
+ The root `@fluojs/drizzle` package is currently a Node.js 20+ integration. It imports Node's `node:async_hooks` module to maintain the ambient transaction context and the package manifest declares `engines.node >=20.0.0`.
34
+
35
+ Drizzle ORM itself can target drivers such as Bun SQL or Cloudflare D1, but those driver runtimes are outside this fluo wrapper until a non-Node transaction-context adapter is documented.
36
+
37
+ Non-Node runtimes should not import the root package. For Bun, Deno, Cloudflare Workers, or other non-Node Drizzle drivers, register the raw Drizzle driver handle behind application-owned fluo providers such as `{ provide, useFactory }` or `{ provide, useValue }`, then inject that application token into repositories. The canonical package chooser/surface docs and the Bun/Cloudflare book chapters show those raw-provider patterns.
38
+
30
39
  ## When to Use
31
40
 
32
- - when Drizzle should participate in the same module, DI, and lifecycle model as the rest of the app
41
+ - when a Node.js 20+ application needs Drizzle to participate in the same module, DI, and lifecycle model as the rest of the app
33
42
  - when repositories need a single `current()` seam that switches between the root handle and the active transaction handle
34
43
  - when application shutdown should also run an explicit cleanup hook for the underlying driver resources
35
44
 
@@ -66,49 +75,127 @@ export class AppModule {}
66
75
 
67
76
  ## Common Patterns
68
77
 
69
- ### Use `DrizzleDatabase.current()` inside repositories
78
+ ### Service Transaction Boundary (@Transaction)
79
+
80
+ 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 Drizzle transaction.
81
+
82
+ ```ts
83
+ import { Inject } from '@fluojs/core';
84
+ import { Transaction, DrizzleDatabase, type DrizzleDatabaseFacade } from '@fluojs/drizzle';
85
+ import { drizzle } from 'drizzle-orm/node-postgres';
86
+ import { users, profiles } from './schema';
87
+
88
+ type AppDatabase = ReturnType<typeof drizzle>;
89
+
90
+ @Inject(DrizzleDatabase)
91
+ export class UserRepository {
92
+ constructor(private readonly db: DrizzleDatabaseFacade<AppDatabase>) {}
93
+
94
+ async create(data: any) {
95
+ // The facade type exposes standard Drizzle methods.
96
+ // When called inside @Transaction(), they automatically participate in the ambient transaction.
97
+ const [user] = await this.db.insert(users).values(data).returning();
98
+
99
+ if (!user) {
100
+ throw new Error('User insert did not return a row.');
101
+ }
102
+
103
+ return user;
104
+ }
105
+
106
+ async initProfile(userId: string) {
107
+ return this.db.insert(profiles).values({ userId });
108
+ }
109
+ }
110
+
111
+ @Inject(UserRepository)
112
+ export class UserService {
113
+ constructor(private readonly repo: UserRepository) {}
114
+
115
+ @Transaction()
116
+ async onboardUser(dto: any) {
117
+ const user = await this.repo.create(dto);
118
+ await this.repo.initProfile(user.id);
119
+ return user;
120
+ }
121
+ }
122
+ ```
123
+
124
+ Calls to `@Transaction()` methods are reentrant. If a decorated method calls another decorated method, they share the same underlying Drizzle transaction.
125
+
126
+ By default, `@Transaction()` selects its target with a small host-object heuristic: it first checks `this.db`, then direct properties on the decorated instance, then a nested `.db` property on those values, and uses the first value that exposes a `transaction(...)` method. If none of those candidates match, the decorated instance itself becomes the transaction target. This keeps common `constructor(private readonly db: DrizzleDatabase<...>)` services and self-contained facade hosts concise, but services with more than one Drizzle wrapper should not rely on property order. Pass an explicit accessor such as `@Transaction((self) => self.ordersDb)` or `@Transaction((self) => self.analyticsDb, options)` whenever the decorated host owns multiple transaction-capable clients or wraps a repository that also exposes `.db`.
127
+
128
+ ### Manual Transactions and current()
129
+
130
+ The `DrizzleDatabase` provides a `current()` method that returns the active transaction handle if inside a transaction scope, or the root handle otherwise. Use this as an escape hatch when you need to pass the handle to external utilities or perform advanced manual transaction plumbing.
70
131
 
71
132
  ```ts
72
133
  import { DrizzleDatabase } from '@fluojs/drizzle';
73
- import { eq } from 'drizzle-orm';
134
+ import { drizzle } from 'drizzle-orm/node-postgres';
74
135
  import { users } from './schema';
75
136
 
76
- export class UserRepository {
77
- constructor(private readonly db: DrizzleDatabase) {}
137
+ type AppDatabase = ReturnType<typeof drizzle>;
138
+
139
+ export class AdvancedRepository {
140
+ constructor(private readonly db: DrizzleDatabase<AppDatabase>) {}
78
141
 
79
- async findById(id: string) {
80
- return this.db.current().select().from(users).where(eq(users.id, id));
142
+ async customOperation() {
143
+ const tx = this.db.current();
144
+ // Use tx for operations that fluo doesn't automatically wrap,
145
+ // or when passing to an external utility that expects a Drizzle database handle.
146
+ return tx.select().from(users);
81
147
  }
82
148
  }
83
149
  ```
84
150
 
85
- ### Manual transaction boundaries
151
+ Use `db.transaction()` for manual transaction blocks:
86
152
 
87
153
  ```ts
88
154
  await this.db.transaction(async () => {
89
- const tx = this.db.current();
90
- await tx.insert(users).values(user);
91
- await tx.insert(profiles).values(profile);
155
+ const current = this.db.current();
156
+
157
+ await current.insert(users).values(user);
158
+ await current.insert(profiles).values(profile);
92
159
  });
93
160
  ```
94
161
 
95
162
  Nested calls reuse the active transaction boundary. If a nested call passes transaction options while a boundary is already active, the package rejects those nested options instead of silently changing the existing transaction.
96
163
 
97
- When `database.transaction(...)` is unavailable and `strictTransactions` is `false`, `transaction()` and `requestTransaction()` fall back to direct execution; request-scoped calls still honor `AbortSignal`.
164
+ When `database.transaction(...)` is unavailable and `strictTransactions` is `false` (the default), `transaction()` and `requestTransaction()` intentionally fail open (fail-open fallback) by running the callback directly against the root handle. This is useful for local fakes, read-only adapters, or gradual migrations, but it is not atomic and should not be treated as a real database transaction. Set `strictTransactions: true` in production paths that require rollback guarantees; startup and readiness diagnostics then surface missing `database.transaction(...)` support and transaction helpers throw instead of silently running without a transaction. Request-scoped fallback still honors `AbortSignal`, so a cancelled request can stop before or during direct execution even though no Drizzle transaction runner exists.
165
+
166
+ ### Request-Wide Controller Boundaries
98
167
 
99
- ### Request-scoped transactions with an interceptor
168
+ Prefer service-level `@Transaction()` for business operations. If you are migrating a NestJS controller/interceptor pattern where an entire request must be transactional, call `requestTransaction(...)` explicitly at the controller, route adapter, or request orchestration boundary and pass the request `AbortSignal` when one is available:
100
169
 
101
170
  ```ts
102
- import { UseInterceptors } from '@fluojs/http';
103
- import { DrizzleTransactionInterceptor } from '@fluojs/drizzle';
171
+ import { Controller, Post } from '@fluojs/http';
172
+ import { DrizzleDatabase } from '@fluojs/drizzle';
173
+ import { drizzle } from 'drizzle-orm/node-postgres';
104
174
 
105
- @UseInterceptors(DrizzleTransactionInterceptor)
106
- class UsersController {}
175
+ type AppDatabase = ReturnType<typeof drizzle>;
176
+
177
+ @Controller('/checkout')
178
+ export class CheckoutController {
179
+ constructor(
180
+ private readonly db: DrizzleDatabase<AppDatabase>,
181
+ private readonly checkout: CheckoutService,
182
+ ) {}
183
+
184
+ @Post()
185
+ create(input: CheckoutInput, requestSignal?: AbortSignal) {
186
+ return this.db.requestTransaction(
187
+ () => this.checkout.createOrder(input),
188
+ requestSignal,
189
+ );
190
+ }
191
+ }
107
192
  ```
108
193
 
194
+ There is no Drizzle `*TransactionInterceptor` export to import. Existing NestJS interceptor designs should move most transaction boundaries to services and reserve explicit `requestTransaction(...)` for rare controller-level compatibility cases where all request work, not just a service method, must share the same boundary. Decorating a controller method with `@Transaction()` remains a compatibility path when the controller owns an explicit `DrizzleDatabase` target, but `requestTransaction(...)` is the clearer request-wide API because it can receive the request `AbortSignal` directly.
195
+
109
196
  ### Shutdown and status contracts
110
197
 
111
- `DrizzleTransactionInterceptor` runs each HTTP request through `DrizzleDatabase.requestTransaction(...)`. During application shutdown, `DrizzleDatabase` aborts any still-active request transaction, waits for open request and manual transaction callbacks to settle or roll back, and only then runs the optional `dispose(database)` hook. This ordering lets drivers finish commit/rollback/cleanup work before pools or externally managed resources are closed.
198
+ During application shutdown, `DrizzleDatabase` aborts any still-active request transaction, waits for open request and manual transaction callbacks to settle or roll back, and only then runs the optional `dispose(database)` hook. This includes fail-open manual `transaction(...)` callbacks when `database.transaction(...)` is unavailable and `strictTransactions` is `false`, so direct-execution fallbacks still drain before pools or externally managed resources are closed.
112
199
  Nested `requestTransaction(...)` calls opened inside an existing request boundary observe the ambient request abort signal while still reusing the active Drizzle transaction. Nested `requestTransaction(...)` calls opened inside an existing manual transaction boundary also join shutdown settlement tracking without opening a second Drizzle transaction, and their settlement handle remains tracked until the outer manual transaction settles so shutdown drains that outer boundary before `dispose(database)` runs. The platform status activity count is intentionally shorter lived: once the nested request callback settles, `details.activeRequestTransactions` is decremented even if the outer manual transaction continues running.
113
200
  New `transaction(...)` and `requestTransaction(...)` calls are rejected once shutdown begins, so disposal cannot overtake a late transaction that starts after the shutdown boundary is crossed.
114
201
  If the request signal aborts after the request callback has completed but before the underlying Drizzle transaction runner finishes committing or rolling back, `requestTransaction(...)` waits for that runner to settle first and then rejects with the abort reason. This keeps Drizzle cleanup serialized with request cancellation while making the late request abort visible to the caller instead of returning the completed callback result.
@@ -127,7 +214,7 @@ Use `DrizzleModule.forRoot(...)` / `forRootAsync(...)` to register Drizzle. When
127
214
 
128
215
  ```ts
129
216
  import { defineModule } from '@fluojs/runtime';
130
- import { DrizzleDatabase, DrizzleModule, DrizzleTransactionInterceptor } from '@fluojs/drizzle';
217
+ import { DrizzleModule } from '@fluojs/drizzle';
131
218
 
132
219
  const database = {
133
220
  transaction: async <T>(callback: (tx: typeof database) => Promise<T>) => callback(database),
@@ -136,7 +223,6 @@ const database = {
136
223
  class ManualDrizzleModule {}
137
224
 
138
225
  defineModule(ManualDrizzleModule, {
139
- exports: [DrizzleDatabase, DrizzleTransactionInterceptor],
140
226
  imports: [DrizzleModule.forRoot({ database })],
141
227
  });
142
228
  ```
@@ -145,8 +231,10 @@ defineModule(ManualDrizzleModule, {
145
231
 
146
232
  - `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
147
233
  - `DrizzleDatabase`
148
- - `DrizzleTransactionInterceptor`
234
+ - `DrizzleDatabaseFacade<TDatabase>`
235
+ - `Transaction`
149
236
  - `DRIZZLE_DATABASE`, `DRIZZLE_DISPOSE`, `DRIZZLE_HANDLE_PROVIDER`, `DRIZZLE_OPTIONS`
237
+ - `DrizzleDatabase.createFacade(...)` (compatibility-only provider wiring helper; prefer `DrizzleModule.forRoot(...)` / `forRootAsync(...)` for application registration)
150
238
  - `createDrizzlePlatformStatusSnapshot(...)`
151
239
  - `DrizzleDatabaseLike`
152
240
  - `DrizzleModuleOptions`
@@ -154,6 +242,10 @@ defineModule(ManualDrizzleModule, {
154
242
 
155
243
  `DRIZZLE_HANDLE_PROVIDER` is an alias token for the lifecycle-aware `DrizzleDatabase` wrapper. Health integrations such as `@fluojs/terminus` use this token to read `createPlatformStatusSnapshot()` before falling back to raw database pings.
156
244
 
245
+ Use `DrizzleDatabase<TDatabase>` when a provider only needs wrapper methods such as `current()`, `transaction(...)`, `requestTransaction(...)`, or `createPlatformStatusSnapshot()`. Use `DrizzleDatabaseFacade<TDatabase>` for repository injections that call Drizzle query methods directly; the facade forwards those calls to the active transaction handle when one exists and to the root handle otherwise. `DrizzleDatabase.createFacade(...)` is retained as a low-level compatibility helper for module-provider wiring; application code should prefer `DrizzleModule.forRoot(...)` / `forRootAsync(...)`.
246
+
247
+ `Transaction` is a standard TC39 method decorator for service-layer transaction boundaries. It resolves a transaction-capable target from the decorated host by checking `this.db`, then direct properties, then nested `.db` properties, then falling back to the decorated instance itself; it also accepts an accessor for explicit client selection and can forward Drizzle transaction options to the outer boundary.
248
+
157
249
  ### `DrizzleModule`
158
250
 
159
251
  - `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
@@ -165,7 +257,7 @@ defineModule(ManualDrizzleModule, {
165
257
  ## Related Packages
166
258
 
167
259
  - `@fluojs/runtime`: owns module startup and shutdown sequencing
168
- - `@fluojs/http`: provides the interceptor pipeline used for request transactions
260
+ - `@fluojs/http`: provides request lifecycle primitives that can be paired with explicit `requestTransaction(...)` boundaries
169
261
  - `@fluojs/prisma` and `@fluojs/mongoose`: alternate ORM/ODM integrations with the same fluo runtime model
170
262
 
171
263
  ## Example Sources
@@ -20,6 +20,21 @@ export declare class DrizzleDatabase<TDatabase extends DrizzleDatabaseLike<TTran
20
20
  private activeRequestTransactionStatusCount;
21
21
  private lifecycleState;
22
22
  constructor(database: TDatabase, dispose?: ((database: TDatabase) => Promise<void> | void) | undefined, databaseOptions?: DrizzleRuntimeOptions);
23
+ /**
24
+ * Creates the low-level DI facade that forwards unknown Drizzle API properties to the ambient `current()` handle.
25
+ *
26
+ * @remarks
27
+ * This compatibility helper is used by `DrizzleModule` provider wiring. Application code should prefer
28
+ * `DrizzleModule.forRoot(...)` or `DrizzleModule.forRootAsync(...)`, then type injected repository handles as
29
+ * `DrizzleDatabaseFacade<TDatabase>` when direct Drizzle methods are needed. Wrapper and lifecycle methods remain
30
+ * bound to the lifecycle owner while unknown Drizzle query properties forward to the ambient `current()` handle.
31
+ *
32
+ * @param database Root Drizzle database handle registered in the module.
33
+ * @param dispose Optional shutdown hook used to close pools or driver resources.
34
+ * @param databaseOptions Runtime transaction options consumed by the Fluo wrapper.
35
+ * @returns A transaction-aware facade that exposes wrapper methods plus the root Drizzle handle surface.
36
+ */
37
+ static createFacade<TDatabase extends DrizzleDatabaseLike<TTransactionDatabase, TTransactionOptions>, TTransactionDatabase = TDatabase, TTransactionOptions = unknown>(database: TDatabase, dispose?: (database: TDatabase) => Promise<void> | void, databaseOptions?: DrizzleRuntimeOptions): DrizzleDatabaseFacade<TDatabase, TTransactionDatabase, TTransactionOptions>;
23
38
  /**
24
39
  * Returns the active transaction handle when present, otherwise the root Drizzle database handle.
25
40
  *
@@ -65,6 +80,7 @@ export declare class DrizzleDatabase<TDatabase extends DrizzleDatabaseLike<TTran
65
80
  */
66
81
  requestTransaction<T>(fn: () => Promise<T>, signal?: AbortSignal, options?: TTransactionOptions): Promise<T>;
67
82
  private executeTransaction;
83
+ private executeManualRootTransaction;
68
84
  private executeRequestTransaction;
69
85
  private executeNestedRequestTransaction;
70
86
  private executeRequestFallback;
@@ -75,7 +91,21 @@ export declare class DrizzleDatabase<TDatabase extends DrizzleDatabaseLike<TTran
75
91
  private untrackActiveRequestTransaction;
76
92
  private markRequestTransactionInactiveForStatus;
77
93
  private trackActiveTransactionScope;
94
+ private trackAvailableTransactionScope;
78
95
  private resolveTransactionRunner;
79
96
  }
97
+ /**
98
+ * Injection-facing Drizzle facade type that combines the Fluo wrapper methods with the registered database handle.
99
+ *
100
+ * @remarks
101
+ * `DrizzleModule` resolves `DrizzleDatabase` to a proxy that forwards unknown properties to `current()`. Use this type
102
+ * in repositories that call Drizzle query methods directly, and use `DrizzleDatabase<TDatabase>` when only the wrapper
103
+ * methods (`current()`, `transaction(...)`, `requestTransaction(...)`, and status snapshots) are needed.
104
+ *
105
+ * @typeParam TDatabase Root Drizzle database handle registered in the module.
106
+ * @typeParam TTransactionDatabase Transaction-scoped database handle resolved inside `database.transaction(...)` callbacks.
107
+ * @typeParam TTransactionOptions Options forwarded to the underlying Drizzle transaction runner.
108
+ */
109
+ export type DrizzleDatabaseFacade<TDatabase extends DrizzleDatabaseLike<TTransactionDatabase, TTransactionOptions>, TTransactionDatabase = TDatabase, TTransactionOptions = unknown> = DrizzleDatabase<TDatabase, TTransactionDatabase, TTransactionOptions> & Omit<TDatabase, keyof DrizzleDatabase<TDatabase, TTransactionDatabase, TTransactionOptions>>;
80
110
  export {};
81
111
  //# sourceMappingURL=database.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAK7D,OAAO,KAAK,EACV,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAgCpB,KAAK,qBAAqB,GAAG;IAC3B,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AA8CF;;;;;;GAMG;AACH,qBACa,eAAe,CAC1B,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,CAC7B,YAAW,qBAAqB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,qBAAqB;IAS3G,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe;IATlC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqE;IAClG,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAuC;IACjF,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqC;IAC7E,OAAO,CAAC,mCAAmC,CAAK;IAChD,OAAO,CAAC,cAAc,CAAkD;gBAGrD,QAAQ,EAAE,SAAS,EACnB,OAAO,CAAC,GAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,aAAA,EACvD,eAAe,GAAE,qBAAqD;IAGzF;;;;;;;;;OASG;IACH,OAAO,IAAI,SAAS,GAAG,oBAAoB;IAI3C,qGAAqG;IAC/F,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5C,yFAAyF;IACzF,4BAA4B;IAS5B;;;;;;;;;;;;;OAaG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrF;;;;;;;;;;;;OAYG;IACG,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;YAIpG,kBAAkB;YA0DlB,yBAAyB;YA8BzB,+BAA+B;YA6C/B,sBAAsB;IAkBpC,OAAO,CAAC,kCAAkC;IAM1C,OAAO,CAAC,2BAA2B;IAMnC,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,6BAA6B;IAOrC,OAAO,CAAC,+BAA+B;IAKvC,OAAO,CAAC,uCAAuC;IAO/C,OAAO,CAAC,2BAA2B;IAkBnC,OAAO,CAAC,wBAAwB;CAWjC"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAU7D,OAAO,KAAK,EACV,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAgCpB,KAAK,qBAAqB,GAAG;IAC3B,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAyEF;;;;;;GAMG;AACH,qBACa,eAAe,CAC1B,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,CAC7B,YAAW,qBAAqB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,qBAAqB;IAS3G,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe;IATlC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqE;IAClG,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAuC;IACjF,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqC;IAC7E,OAAO,CAAC,mCAAmC,CAAK;IAChD,OAAO,CAAC,cAAc,CAAkD;gBAGrD,QAAQ,EAAE,SAAS,EACnB,OAAO,CAAC,GAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,aAAA,EACvD,eAAe,GAAE,qBAAqD;IAGzF;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,YAAY,CACjB,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,EAE7B,QAAQ,EAAE,SAAS,EACnB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EACvD,eAAe,GAAE,qBAAqD,GACrE,qBAAqB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC;IAM9E;;;;;;;;;OASG;IACH,OAAO,IAAI,SAAS,GAAG,oBAAoB;IAI3C,qGAAqG;IAC/F,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5C,yFAAyF;IACzF,4BAA4B;IAS5B;;;;;;;;;;;;;OAaG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrF;;;;;;;;;;;;OAYG;IACG,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;YAIpG,kBAAkB;YAsClB,4BAA4B;YA+B5B,yBAAyB;YA8BzB,+BAA+B;YA6C/B,sBAAsB;IAkBpC,OAAO,CAAC,kCAAkC;IAM1C,OAAO,CAAC,2BAA2B;IAMnC,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,6BAA6B;IAOrC,OAAO,CAAC,+BAA+B;IAKvC,OAAO,CAAC,uCAAuC;IAO/C,OAAO,CAAC,2BAA2B;IAkBnC,OAAO,CAAC,8BAA8B;IAMtC,OAAO,CAAC,wBAAwB;CAWjC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,qBAAqB,CAC/B,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,IAC3B,eAAe,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,GACvE,IAAI,CAAC,SAAS,EAAE,MAAM,eAAe,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,CAAC,CAAC"}
package/dist/database.js CHANGED
@@ -5,14 +5,33 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
5
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
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
7
  import { AsyncLocalStorage } from 'node:async_hooks';
8
- import { createAbortError, createRequestAbortContext, raceWithAbort, trackActiveRequestTransaction, untrackActiveRequestTransaction } from '@fluojs/runtime';
9
8
  import { Inject } from '@fluojs/core';
10
- import { DRIZZLE_DATABASE, DRIZZLE_DISPOSE, DRIZZLE_OPTIONS } from './tokens.js';
9
+ import { createAbortError, createRequestAbortContext, raceWithAbort, trackActiveRequestTransaction, untrackActiveRequestTransaction } from '@fluojs/runtime';
11
10
  import { createDrizzlePlatformStatusSnapshot } from './status.js';
11
+ import { DRIZZLE_DATABASE, DRIZZLE_DISPOSE, DRIZZLE_OPTIONS } from './tokens.js';
12
12
  const TRANSACTION_NOT_SUPPORTED_ERROR = 'Transaction not supported: Drizzle database does not implement transaction.';
13
13
  const NESTED_TRANSACTION_OPTIONS_NOT_SUPPORTED_ERROR = 'Nested Drizzle transaction options are not supported because the active transaction context is reused.';
14
14
  const TRANSACTION_UNAVAILABLE_ERROR = 'Drizzle transactions are not available during application shutdown.';
15
15
  const REQUEST_TRANSACTION_UNAVAILABLE_ERROR = 'Drizzle request transactions are not available during shutdown.';
16
+ function createCurrentlessDrizzleFacade(target) {
17
+ return new Proxy(target, {
18
+ get(database, prop) {
19
+ if (prop in database) {
20
+ const value = Reflect.get(database, prop, database);
21
+ if (typeof value === 'function') {
22
+ return value.bind(database);
23
+ }
24
+ return value;
25
+ }
26
+ const currentDatabase = database.current();
27
+ const value = Reflect.get(currentDatabase, prop, currentDatabase);
28
+ if (typeof value === 'function') {
29
+ return value.bind(currentDatabase);
30
+ }
31
+ return value;
32
+ }
33
+ });
34
+ }
16
35
  function createRequestAbortSignalView(parentSignal, signal) {
17
36
  if (!signal) {
18
37
  return {
@@ -71,6 +90,26 @@ class DrizzleDatabase {
71
90
  this.databaseOptions = databaseOptions;
72
91
  }
73
92
 
93
+ /**
94
+ * Creates the low-level DI facade that forwards unknown Drizzle API properties to the ambient `current()` handle.
95
+ *
96
+ * @remarks
97
+ * This compatibility helper is used by `DrizzleModule` provider wiring. Application code should prefer
98
+ * `DrizzleModule.forRoot(...)` or `DrizzleModule.forRootAsync(...)`, then type injected repository handles as
99
+ * `DrizzleDatabaseFacade<TDatabase>` when direct Drizzle methods are needed. Wrapper and lifecycle methods remain
100
+ * bound to the lifecycle owner while unknown Drizzle query properties forward to the ambient `current()` handle.
101
+ *
102
+ * @param database Root Drizzle database handle registered in the module.
103
+ * @param dispose Optional shutdown hook used to close pools or driver resources.
104
+ * @param databaseOptions Runtime transaction options consumed by the Fluo wrapper.
105
+ * @returns A transaction-aware facade that exposes wrapper methods plus the root Drizzle handle surface.
106
+ */
107
+ static createFacade(database, dispose, databaseOptions = {
108
+ strictTransactions: false
109
+ }) {
110
+ return createCurrentlessDrizzleFacade(new _DrizzleDatabase(database, dispose, databaseOptions));
111
+ }
112
+
74
113
  /**
75
114
  * Returns the active transaction handle when present, otherwise the root Drizzle database handle.
76
115
  *
@@ -146,6 +185,11 @@ class DrizzleDatabase {
146
185
  async executeTransaction(fn, options, requestScoped, signal) {
147
186
  const current = this.transactions.getStore();
148
187
  if (current) {
188
+ if (requestScoped) {
189
+ this.assertRequestTransactionsAvailable();
190
+ } else {
191
+ this.assertTransactionsAvailable();
192
+ }
149
193
  if (options !== undefined) {
150
194
  throw new Error(NESTED_TRANSACTION_OPTIONS_NOT_SUPPORTED_ERROR);
151
195
  }
@@ -155,31 +199,32 @@ class DrizzleDatabase {
155
199
  return fn();
156
200
  }
157
201
  if (!requestScoped) {
158
- this.assertTransactionsAvailable();
202
+ return this.executeManualRootTransaction(fn, options);
159
203
  }
160
204
  const transactionRunner = this.resolveTransactionRunner();
161
205
  if (!transactionRunner) {
162
- if (requestScoped) {
163
- return this.executeRequestFallback(fn, signal);
164
- }
165
- return fn();
206
+ return this.executeRequestFallback(fn, signal);
166
207
  }
167
- if (!requestScoped) {
168
- const deferredRequestTransactionSettlements = new Set();
169
- const activeTransactionScope = this.trackActiveTransactionScope();
170
- try {
171
- return await transactionRunner(transactionDatabase => this.transactions.run({
172
- database: transactionDatabase,
173
- deferredRequestTransactionSettlements
174
- }, fn), options);
175
- } finally {
176
- for (const handle of deferredRequestTransactionSettlements) {
177
- this.untrackActiveRequestTransaction(handle);
178
- }
179
- activeTransactionScope.settle();
208
+ return this.executeRequestTransaction(transactionRunner, fn, options, signal);
209
+ }
210
+ async executeManualRootTransaction(fn, options) {
211
+ const deferredRequestTransactionSettlements = new Set();
212
+ const activeTransactionScope = this.trackAvailableTransactionScope();
213
+ try {
214
+ const transactionRunner = this.resolveTransactionRunner();
215
+ if (!transactionRunner) {
216
+ return await fn();
217
+ }
218
+ return await transactionRunner(transactionDatabase => this.transactions.run({
219
+ database: transactionDatabase,
220
+ deferredRequestTransactionSettlements
221
+ }, fn), options);
222
+ } finally {
223
+ for (const handle of deferredRequestTransactionSettlements) {
224
+ this.untrackActiveRequestTransaction(handle);
180
225
  }
226
+ activeTransactionScope.settle();
181
227
  }
182
- return this.executeRequestTransaction(transactionRunner, fn, options, signal);
183
228
  }
184
229
  async executeRequestTransaction(transactionRunner, fn, options, signal) {
185
230
  this.assertRequestTransactionsAvailable();
@@ -289,6 +334,10 @@ class DrizzleDatabase {
289
334
  }
290
335
  };
291
336
  }
337
+ trackAvailableTransactionScope() {
338
+ this.assertTransactionsAvailable();
339
+ return this.trackActiveTransactionScope();
340
+ }
292
341
  resolveTransactionRunner() {
293
342
  if (typeof this.database.transaction !== 'function') {
294
343
  if (this.databaseOptions.strictTransactions) {
@@ -302,4 +351,17 @@ class DrizzleDatabase {
302
351
  _initClass();
303
352
  }
304
353
  }
354
+
355
+ /**
356
+ * Injection-facing Drizzle facade type that combines the Fluo wrapper methods with the registered database handle.
357
+ *
358
+ * @remarks
359
+ * `DrizzleModule` resolves `DrizzleDatabase` to a proxy that forwards unknown properties to `current()`. Use this type
360
+ * in repositories that call Drizzle query methods directly, and use `DrizzleDatabase<TDatabase>` when only the wrapper
361
+ * methods (`current()`, `transaction(...)`, `requestTransaction(...)`, and status snapshots) are needed.
362
+ *
363
+ * @typeParam TDatabase Root Drizzle database handle registered in the module.
364
+ * @typeParam TTransactionDatabase Transaction-scoped database handle resolved inside `database.transaction(...)` callbacks.
365
+ * @typeParam TTransactionOptions Options forwarded to the underlying Drizzle transaction runner.
366
+ */
305
367
  export { _DrizzleDatabase as DrizzleDatabase };
@@ -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;AAEvD,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAc5E,KAAK,yBAAyB,CAC5B,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,EACpB,mBAAmB,IACjB,kBAAkB,CAAC,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAChH,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,QAAQ,CAAC,CAAC;AAwH7F;;GAEG;AACH,qBAAa,aAAa;IACxB,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CACZ,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,EAC7B,OAAO,EAAE,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,GAAG,UAAU;IAIlG,uEAAuE;IACvE,MAAM,CAAC,YAAY,CACjB,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,EAC7B,OAAO,EAAE,yBAAyB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,GAAG,UAAU;CAGxG"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAc5E,KAAK,yBAAyB,CAC5B,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,EACpB,mBAAmB,IACjB,kBAAkB,CAAC,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAChH,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,QAAQ,CAAC,CAAC;AAgI7F;;GAEG;AACH,qBAAa,aAAa;IACxB,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CACZ,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,EAC7B,OAAO,EAAE,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,GAAG,UAAU;IAIlG,uEAAuE;IACvE,MAAM,CAAC,YAAY,CACjB,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAChF,oBAAoB,GAAG,SAAS,EAChC,mBAAmB,GAAG,OAAO,EAC7B,OAAO,EAAE,yBAAyB,CAAC,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,CAAC,GAAG,UAAU;CAGxG"}
package/dist/module.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { defineModule } from '@fluojs/runtime';
2
2
  import { DrizzleDatabase } from './database.js';
3
3
  import { DRIZZLE_DATABASE, DRIZZLE_DISPOSE, DRIZZLE_HANDLE_PROVIDER, DRIZZLE_OPTIONS } from './tokens.js';
4
- import { DrizzleTransactionInterceptor } from './transaction.js';
5
4
  const DRIZZLE_NORMALIZED_OPTIONS = Symbol('fluo.drizzle.normalized-options');
6
- const DRIZZLE_MODULE_EXPORTS = [DrizzleDatabase, DrizzleTransactionInterceptor, DRIZZLE_HANDLE_PROVIDER];
5
+ const DRIZZLE_MODULE_EXPORTS = [DrizzleDatabase, DRIZZLE_HANDLE_PROVIDER];
7
6
  function isObjectLike(value) {
8
7
  return typeof value === 'object' && value !== null || typeof value === 'function';
9
8
  }
@@ -34,10 +33,14 @@ function createDrizzleRuntimeProviders(normalizedOptionsProvider) {
34
33
  inject: [DRIZZLE_NORMALIZED_OPTIONS],
35
34
  provide: DRIZZLE_OPTIONS,
36
35
  useFactory: options => createRuntimeOptionsProviderValue(options.strictTransactions)
37
- }, DrizzleDatabase, {
36
+ }, {
37
+ inject: [DRIZZLE_DATABASE, DRIZZLE_DISPOSE, DRIZZLE_OPTIONS],
38
+ provide: DrizzleDatabase,
39
+ useFactory: (database, dispose, databaseOptions) => DrizzleDatabase.createFacade(database, dispose, databaseOptions)
40
+ }, {
38
41
  provide: DRIZZLE_HANDLE_PROVIDER,
39
42
  useExisting: DrizzleDatabase
40
- }, DrizzleTransactionInterceptor];
43
+ }];
41
44
  }
42
45
  function createDrizzleProvidersAsync(options) {
43
46
  const normalizedOptionsProvider = {
@@ -1,25 +1,21 @@
1
- import type { Interceptor, InterceptorContext } from '@fluojs/http';
2
- import { DrizzleDatabase } from './database.js';
3
- import type { DrizzleDatabaseLike } from './types.js';
1
+ type TransactionCapableDrizzle<TTransactionOptions = unknown> = {
2
+ transaction<T>(fn: () => Promise<T>, options?: TTransactionOptions): Promise<T>;
3
+ };
4
+ type TransactionAccessor<THost, TTransactionOptions> = (self: THost) => TransactionCapableDrizzle<TTransactionOptions>;
5
+ type TransactionMethod<THost, TArgs extends unknown[], TResult> = (this: THost, ...args: TArgs) => Promise<TResult>;
4
6
  /**
5
- * HTTP interceptor that wraps each request in a Drizzle request transaction boundary.
7
+ * Standard TC39 method decorator that runs a service method inside a Drizzle transaction boundary.
6
8
  *
7
9
  * @remarks
8
- * Pair this with repository/service code that reads `DrizzleDatabase.current()` so downstream calls share the same
9
- * request-scoped transaction handle.
10
+ * `@Transaction()` selects the first transaction-capable target by checking `this.db`, then direct host properties,
11
+ * then nested `.db` properties on those values. If none of those candidates exposes `transaction(...)`, the decorated
12
+ * instance itself is used as the transaction target. Pass an accessor such as `@Transaction((self) => self.analyticsDb)`
13
+ * to select another Drizzle wrapper explicitly. Non-function factory input is forwarded as Drizzle transaction options.
14
+ *
15
+ * @param accessorOrOptions Optional target accessor, or Drizzle transaction options.
16
+ * @param options Optional Drizzle transaction options when an accessor is supplied.
17
+ * @returns A standard 2023-11 method decorator.
10
18
  */
11
- export declare class DrizzleTransactionInterceptor implements Interceptor {
12
- private readonly database;
13
- constructor(database: DrizzleDatabase<DrizzleDatabaseLike<unknown, unknown>, unknown, unknown>);
14
- /**
15
- * Runs the downstream handler inside a Drizzle request transaction boundary.
16
- *
17
- * @param context Interceptor context that supplies the request abort signal.
18
- * @param next Downstream handler chain.
19
- * @returns The downstream handler result after the request transaction settles.
20
- */
21
- intercept(context: InterceptorContext, next: {
22
- handle(): Promise<unknown>;
23
- }): Promise<unknown>;
24
- }
19
+ export declare function Transaction<THost, TTransactionOptions = unknown>(accessorOrOptions?: TransactionAccessor<THost, TTransactionOptions> | TTransactionOptions, options?: TTransactionOptions): <TArgs extends unknown[], TResult>(value: TransactionMethod<THost, TArgs, TResult>, context: ClassMethodDecoratorContext<THost, TransactionMethod<THost, TArgs, TResult>>) => TransactionMethod<THost, TArgs, TResult>;
20
+ export {};
25
21
  //# sourceMappingURL=transaction.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;GAMG;AACH,qBACa,6BAA8B,YAAW,WAAW;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,eAAe,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;IAE/G;;;;;;OAMG;IACG,SAAS,CAAC,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAGrG"}
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,KAAK,yBAAyB,CAAC,mBAAmB,GAAG,OAAO,IAAI;IAC9D,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjF,CAAC;AAEF,KAAK,mBAAmB,CAAC,KAAK,EAAE,mBAAmB,IAAI,CACrD,IAAI,EAAE,KAAK,KACR,yBAAyB,CAAC,mBAAmB,CAAC,CAAC;AAEpD,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;AAwCtB;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,EAC9D,iBAAiB,CAAC,EAAE,mBAAmB,CAAC,KAAK,EAAE,mBAAmB,CAAC,GAAG,mBAAmB,EACzF,OAAO,CAAC,EAAE,mBAAmB,IAOrB,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EACtC,OAAO,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAC/C,SAAS,2BAA2B,CAAC,KAAK,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,KACpF,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAc5C"}
@@ -1,39 +1,53 @@
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 { DrizzleDatabase } from './database.js';
9
- let _DrizzleTransactionIn;
10
- /**
11
- * HTTP interceptor that wraps each request in a Drizzle request transaction boundary.
12
- *
13
- * @remarks
14
- * Pair this with repository/service code that reads `DrizzleDatabase.current()` so downstream calls share the same
15
- * request-scoped transaction handle.
16
- */
17
- class DrizzleTransactionInterceptor {
18
- static {
19
- [_DrizzleTransactionIn, _initClass] = _applyDecs(this, [Inject(DrizzleDatabase)], []).c;
20
- }
21
- constructor(database) {
22
- this.database = database;
1
+ function isTransactionCapableDrizzle(value) {
2
+ return typeof value?.transaction === 'function';
3
+ }
4
+ function findNestedTransactionTarget(value) {
5
+ if (!value || typeof value !== 'object' && typeof value !== 'function') {
6
+ return undefined;
23
7
  }
24
-
25
- /**
26
- * Runs the downstream handler inside a Drizzle request transaction boundary.
27
- *
28
- * @param context Interceptor context that supplies the request abort signal.
29
- * @param next Downstream handler chain.
30
- * @returns The downstream handler result after the request transaction settles.
31
- */
32
- async intercept(context, next) {
33
- return this.database.requestTransaction(async () => next.handle(), context.requestContext.request.signal);
8
+ const directDatabase = value.db;
9
+ if (isTransactionCapableDrizzle(directDatabase)) {
10
+ return directDatabase;
34
11
  }
35
- static {
36
- _initClass();
12
+ for (const propertyValue of Object.values(value)) {
13
+ if (isTransactionCapableDrizzle(propertyValue)) {
14
+ return propertyValue;
15
+ }
16
+ const nestedDatabase = propertyValue?.db;
17
+ if (isTransactionCapableDrizzle(nestedDatabase)) {
18
+ return nestedDatabase;
19
+ }
37
20
  }
21
+ return undefined;
38
22
  }
39
- export { _DrizzleTransactionIn as DrizzleTransactionInterceptor };
23
+ function resolveDefaultTransactionTarget(self) {
24
+ const implicitTarget = findNestedTransactionTarget(self) ?? self;
25
+ return implicitTarget;
26
+ }
27
+
28
+ /**
29
+ * Standard TC39 method decorator that runs a service method inside a Drizzle transaction boundary.
30
+ *
31
+ * @remarks
32
+ * `@Transaction()` selects the first transaction-capable target by checking `this.db`, then direct host properties,
33
+ * then nested `.db` properties on those values. If none of those candidates exposes `transaction(...)`, the decorated
34
+ * instance itself is used as the transaction target. Pass an accessor such as `@Transaction((self) => self.analyticsDb)`
35
+ * to select another Drizzle wrapper explicitly. Non-function factory input is forwarded as Drizzle transaction options.
36
+ *
37
+ * @param accessorOrOptions Optional target accessor, or Drizzle transaction options.
38
+ * @param options Optional Drizzle transaction options when an accessor is supplied.
39
+ * @returns A standard 2023-11 method decorator.
40
+ */
41
+ export function Transaction(accessorOrOptions, options) {
42
+ const accessor = typeof accessorOrOptions === 'function' ? accessorOrOptions : undefined;
43
+ const transactionOptions = accessor ? options : accessorOrOptions;
44
+ return (value, context) => {
45
+ if (context.kind !== 'method') {
46
+ throw new Error('@Transaction() can only decorate methods.');
47
+ }
48
+ return async function transactionMethod(...args) {
49
+ const drizzleDatabase = accessor ? accessor(this) : resolveDefaultTransactionTarget(this);
50
+ return drizzleDatabase.transaction(() => value.apply(this, args), transactionOptions);
51
+ };
52
+ };
53
+ }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MaybePromise } from '@fluojs/core';
2
+ import type { PersistencePlatformStatusSnapshot } from '@fluojs/runtime';
2
3
  type DrizzleTransactionCallback<TTransactionDatabase, TResult> = (database: TTransactionDatabase) => Promise<TResult>;
3
4
  type DrizzleTransactionRunner<TTransactionDatabase, TTransactionOptions> = <T>(callback: DrizzleTransactionCallback<TTransactionDatabase, T>, options?: TTransactionOptions) => Promise<T>;
4
5
  /**
@@ -40,6 +41,8 @@ export interface DrizzleModuleOptions<TDatabase extends DrizzleDatabaseLike<TTra
40
41
  * @typeParam TTransactionOptions Options forwarded to `database.transaction(...)`.
41
42
  */
42
43
  export interface DrizzleHandleProvider<TDatabase extends DrizzleDatabaseLike<TTransactionDatabase, TTransactionOptions>, TTransactionDatabase = TDatabase, TTransactionOptions = unknown> {
44
+ /** Produces the platform diagnostics snapshot for health and readiness integrations. */
45
+ createPlatformStatusSnapshot(): PersistencePlatformStatusSnapshot;
43
46
  /** Returns the ambient transaction database when present, or the root Drizzle handle otherwise. */
44
47
  current(): TDatabase | TTransactionDatabase;
45
48
  /**
@@ -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,KAAK,0BAA0B,CAAC,oBAAoB,EAAE,OAAO,IAAI,CAAC,QAAQ,EAAE,oBAAoB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtH,KAAK,wBAAwB,CAAC,oBAAoB,EAAE,mBAAmB,IAAI,CAAC,CAAC,EAC3E,QAAQ,EAAE,0BAA0B,CAAC,oBAAoB,EAAE,CAAC,CAAC,EAC7D,OAAO,CAAC,EAAE,mBAAmB,KAC1B,OAAO,CAAC,CAAC,CAAC,CAAC;AAEhB;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB,CAAC,oBAAoB,GAAG,OAAO,EAAE,mBAAmB,GAAG,OAAO;IAChG,WAAW,CAAC,EAAE,wBAAwB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,CAAC;CACnF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,SAAS,EAAE,mBAAmB,GAAG,OAAO;IACrL,8EAA8E;IAC9E,QAAQ,EAAE,SAAS,CAAC;IACpB,kGAAkG;IAClG,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACtD,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB,CAAC,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,SAAS,EAAE,mBAAmB,GAAG,OAAO;IACtL,mGAAmG;IACnG,OAAO,IAAI,SAAS,GAAG,oBAAoB,CAAC;IAC5C;;;;;;;OAOG;IACH,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7G;;;;;;OAMG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,iBAAiB,CAAC;AAEzE,KAAK,0BAA0B,CAAC,oBAAoB,EAAE,OAAO,IAAI,CAAC,QAAQ,EAAE,oBAAoB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtH,KAAK,wBAAwB,CAAC,oBAAoB,EAAE,mBAAmB,IAAI,CAAC,CAAC,EAC3E,QAAQ,EAAE,0BAA0B,CAAC,oBAAoB,EAAE,CAAC,CAAC,EAC7D,OAAO,CAAC,EAAE,mBAAmB,KAC1B,OAAO,CAAC,CAAC,CAAC,CAAC;AAEhB;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB,CAAC,oBAAoB,GAAG,OAAO,EAAE,mBAAmB,GAAG,OAAO;IAChG,WAAW,CAAC,EAAE,wBAAwB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,CAAC;CACnF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,SAAS,EAAE,mBAAmB,GAAG,OAAO;IACrL,8EAA8E;IAC9E,QAAQ,EAAE,SAAS,CAAC;IACpB,kGAAkG;IAClG,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACtD,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB,CAAC,SAAS,SAAS,mBAAmB,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,SAAS,EAAE,mBAAmB,GAAG,OAAO;IACtL,wFAAwF;IACxF,4BAA4B,IAAI,iCAAiC,CAAC;IAClE,mGAAmG;IACnG,OAAO,IAAI,SAAS,GAAG,oBAAoB,CAAC;IAC5C;;;;;;;OAOG;IACH,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7G;;;;;;OAMG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjF"}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "transaction",
10
10
  "als"
11
11
  ],
12
- "version": "1.0.2",
12
+ "version": "1.1.1",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -36,10 +36,9 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.0.3",
40
- "@fluojs/http": "^1.1.0",
41
- "@fluojs/di": "^1.0.3",
42
- "@fluojs/runtime": "^1.1.2"
39
+ "@fluojs/core": "^1.1.0",
40
+ "@fluojs/di": "^2.0.0",
41
+ "@fluojs/runtime": "^2.0.1"
43
42
  },
44
43
  "peerDependencies": {
45
44
  "drizzle-orm": ">=0.30.0"
@@ -50,7 +49,8 @@
50
49
  }
51
50
  },
52
51
  "devDependencies": {
53
- "vitest": "^3.2.4"
52
+ "vitest": "^3.2.4",
53
+ "@fluojs/http": "^2.0.1"
54
54
  },
55
55
  "scripts": {
56
56
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",