@fluojs/drizzle 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +110 -26
- package/README.md +111 -27
- package/dist/database.d.ts +27 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +52 -0
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +17 -5
- package/dist/transaction.d.ts +15 -20
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +48 -35
- package/package.json +5 -5
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
|
-
- [
|
|
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,118 @@ export class AppModule {}
|
|
|
66
75
|
|
|
67
76
|
## 주요 패턴
|
|
68
77
|
|
|
69
|
-
###
|
|
78
|
+
### 서비스 트랜잭션 경계 (@Transaction)
|
|
79
|
+
|
|
80
|
+
`@Transaction()` 데코레이터는 서비스 레이어에서 트랜잭션 경계를 정의하는 권장 방법입니다. 이 데코레이터가 적용된 메서드 내부에서 발생하는 모든 리포지토리 호출은 동일한 Drizzle 트랜잭션을 공유합니다.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { Transaction, DrizzleDatabase, type DrizzleDatabaseFacade } from '@fluojs/drizzle';
|
|
84
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
85
|
+
import { users, profiles } from './schema';
|
|
86
|
+
|
|
87
|
+
type AppDatabase = ReturnType<typeof drizzle>;
|
|
88
|
+
|
|
89
|
+
export class UserService {
|
|
90
|
+
constructor(private readonly repo: UserRepository) {}
|
|
91
|
+
|
|
92
|
+
@Transaction()
|
|
93
|
+
async onboardUser(dto: any) {
|
|
94
|
+
const user = await this.repo.create(dto);
|
|
95
|
+
await this.repo.initProfile(user.id);
|
|
96
|
+
return user;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class UserRepository {
|
|
101
|
+
constructor(private readonly db: DrizzleDatabaseFacade<AppDatabase>) {}
|
|
102
|
+
|
|
103
|
+
async create(data: any) {
|
|
104
|
+
// facade 타입은 표준 Drizzle 메서드를 노출합니다.
|
|
105
|
+
// @Transaction() 내부에서 호출되면 자동으로 활성 트랜잭션에 참여합니다.
|
|
106
|
+
return this.db.insert(users).values(data);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async initProfile(userId: string) {
|
|
110
|
+
return this.db.insert(profiles).values({ userId });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`@Transaction()` 메서드 호출은 재진입(reentrant)이 가능합니다. 데코레이터가 적용된 메서드가 다른 데코레이터 적용 메서드를 호출하더라도 하나의 동일한 Drizzle 트랜잭션 안에서 실행됩니다.
|
|
116
|
+
|
|
117
|
+
기본적으로 `@Transaction()`은 작은 host-object heuristic으로 대상을 고릅니다. 먼저 `this.db`를 확인하고, 그다음 데코레이터가 붙은 인스턴스의 직접 property, 마지막으로 그 값들의 중첩 `.db` property 중 `transaction(...)` 메서드를 노출하는 첫 값을 사용합니다. 이 덕분에 `constructor(private readonly db: DrizzleDatabase<...>)` 같은 일반 서비스는 간결하게 유지할 수 있지만, 하나의 서비스가 Drizzle wrapper를 둘 이상 소유한다면 property 순서에 의존하지 마세요. 데코레이터가 붙은 host가 여러 transaction-capable client를 갖거나 `.db`를 노출하는 repository를 감싸는 경우에는 `@Transaction((self) => self.ordersDb)` 또는 `@Transaction((self) => self.analyticsDb, options)`처럼 명시적 accessor를 전달하세요.
|
|
118
|
+
|
|
119
|
+
### 수동 트랜잭션과 current()
|
|
120
|
+
|
|
121
|
+
`DrizzleDatabase`는 트랜잭션 범위 내에 있으면 자동으로 활성 트랜잭션 handle을, 그렇지 않으면 root handle을 반환하는 `current()` 메서드를 제공합니다. 외부 유틸리티에 handle을 전달하거나 복잡한 수동 트랜잭션 처리가 필요한 경우 escape hatch로 사용하세요.
|
|
70
122
|
|
|
71
123
|
```ts
|
|
72
124
|
import { DrizzleDatabase } from '@fluojs/drizzle';
|
|
73
|
-
import {
|
|
125
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
74
126
|
import { users } from './schema';
|
|
75
127
|
|
|
76
|
-
|
|
77
|
-
constructor(private readonly db: DrizzleDatabase) {}
|
|
128
|
+
type AppDatabase = ReturnType<typeof drizzle>;
|
|
78
129
|
|
|
79
|
-
|
|
80
|
-
|
|
130
|
+
export class AdvancedRepository {
|
|
131
|
+
constructor(private readonly db: DrizzleDatabase<AppDatabase>) {}
|
|
132
|
+
|
|
133
|
+
async customOperation() {
|
|
134
|
+
const tx = this.db.current();
|
|
135
|
+
// fluo가 자동으로 감싸지 않는 작업을 수행하거나,
|
|
136
|
+
// Drizzle handle을 직접 기대하는 외부 유틸리티에 전달할 때 tx를 사용하세요.
|
|
137
|
+
return tx.select().from(users);
|
|
81
138
|
}
|
|
82
139
|
}
|
|
83
140
|
```
|
|
84
141
|
|
|
85
|
-
|
|
142
|
+
수동 트랜잭션 블록에는 `db.transaction()`을 사용하세요:
|
|
86
143
|
|
|
87
144
|
```ts
|
|
88
145
|
await this.db.transaction(async () => {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
await
|
|
146
|
+
const current = this.db.current();
|
|
147
|
+
|
|
148
|
+
await current.insert(users).values(user);
|
|
149
|
+
await current.insert(profiles).values(profile);
|
|
92
150
|
});
|
|
93
151
|
```
|
|
94
152
|
|
|
95
153
|
중첩 호출은 활성 transaction boundary를 재사용합니다. 이미 boundary가 활성화되어 있는데 중첩 호출이 transaction option을 전달하면, 기존 transaction을 조용히 바꾸지 않고 해당 중첩 option을 거부합니다.
|
|
96
154
|
|
|
97
|
-
`database.transaction(...)`을 사용할 수 없고 `strictTransactions`가 `false
|
|
155
|
+
`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가 없어도 취소된 요청은 직접 실행 전이나 도중에 중단될 수 있습니다.
|
|
98
156
|
|
|
99
|
-
###
|
|
157
|
+
### 요청 전체 컨트롤러 경계
|
|
158
|
+
|
|
159
|
+
비즈니스 작업에는 서비스 레벨 `@Transaction()`을 우선 사용하세요. 전체 요청을 하나의 transaction으로 감싸던 NestJS controller/interceptor 패턴을 마이그레이션해야 한다면 controller, route adapter, request orchestration 경계에서 `requestTransaction(...)`을 명시적으로 호출하고 가능한 경우 request `AbortSignal`을 전달하세요.
|
|
100
160
|
|
|
101
161
|
```ts
|
|
102
|
-
import {
|
|
103
|
-
import {
|
|
162
|
+
import { Controller, Post } from '@fluojs/http';
|
|
163
|
+
import { DrizzleDatabase } from '@fluojs/drizzle';
|
|
164
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
104
165
|
|
|
105
|
-
|
|
106
|
-
|
|
166
|
+
type AppDatabase = ReturnType<typeof drizzle>;
|
|
167
|
+
|
|
168
|
+
@Controller('/checkout')
|
|
169
|
+
export class CheckoutController {
|
|
170
|
+
constructor(
|
|
171
|
+
private readonly db: DrizzleDatabase<AppDatabase>,
|
|
172
|
+
private readonly checkout: CheckoutService,
|
|
173
|
+
) {}
|
|
174
|
+
|
|
175
|
+
@Post()
|
|
176
|
+
create(input: CheckoutInput, requestSignal?: AbortSignal) {
|
|
177
|
+
return this.db.requestTransaction(
|
|
178
|
+
() => this.checkout.createOrder(input),
|
|
179
|
+
requestSignal,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
107
183
|
```
|
|
108
184
|
|
|
185
|
+
import할 수 있는 Drizzle `*TransactionInterceptor` export는 없습니다. 기존 NestJS interceptor 설계는 대부분의 transaction boundary를 서비스로 옮기고, 전체 request 작업이 서비스 메서드 하나가 아니라 같은 boundary를 공유해야 하는 드문 controller-level 호환성 사례에만 명시적 `requestTransaction(...)`을 남기세요.
|
|
186
|
+
|
|
109
187
|
### 종료와 상태 계약
|
|
110
188
|
|
|
111
|
-
|
|
189
|
+
애플리케이션 종료 중에는 `DrizzleDatabase`가 아직 활성 상태인 요청 트랜잭션을 abort하고, 열린 요청 및 수동 transaction callback이 settle되거나 rollback될 때까지 기다린 뒤 선택적 `dispose(database)` hook을 실행합니다. 이 순서는 pool이나 외부 관리 리소스를 닫기 전에 driver가 commit/rollback/cleanup 작업을 끝낼 수 있게 보장합니다.
|
|
112
190
|
기존 요청 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
191
|
종료가 시작된 뒤 새 `transaction(...)` 및 `requestTransaction(...)` 호출은 거부되므로, 종료 boundary를 지난 뒤 시작되는 늦은 트랜잭션보다 dispose가 먼저 실행되는 상황을 방지합니다.
|
|
114
192
|
요청 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 +205,7 @@ class UsersController {}
|
|
|
127
205
|
|
|
128
206
|
```ts
|
|
129
207
|
import { defineModule } from '@fluojs/runtime';
|
|
130
|
-
import {
|
|
208
|
+
import { DrizzleModule } from '@fluojs/drizzle';
|
|
131
209
|
|
|
132
210
|
const database = {
|
|
133
211
|
transaction: async <T>(callback: (tx: typeof database) => Promise<T>) => callback(database),
|
|
@@ -136,7 +214,6 @@ const database = {
|
|
|
136
214
|
class ManualDrizzleModule {}
|
|
137
215
|
|
|
138
216
|
defineModule(ManualDrizzleModule, {
|
|
139
|
-
exports: [DrizzleDatabase, DrizzleTransactionInterceptor],
|
|
140
217
|
imports: [DrizzleModule.forRoot({ database })],
|
|
141
218
|
});
|
|
142
219
|
```
|
|
@@ -145,8 +222,10 @@ defineModule(ManualDrizzleModule, {
|
|
|
145
222
|
|
|
146
223
|
- `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
|
|
147
224
|
- `DrizzleDatabase`
|
|
148
|
-
- `
|
|
225
|
+
- `DrizzleDatabaseFacade<TDatabase>`
|
|
226
|
+
- `Transaction`
|
|
149
227
|
- `DRIZZLE_DATABASE`, `DRIZZLE_DISPOSE`, `DRIZZLE_HANDLE_PROVIDER`, `DRIZZLE_OPTIONS`
|
|
228
|
+
- `DrizzleDatabase.createFacade(...)` (호환성 전용 provider wiring helper; 애플리케이션 등록은 `DrizzleModule.forRoot(...)` / `forRootAsync(...)`를 우선 사용)
|
|
150
229
|
- `createDrizzlePlatformStatusSnapshot(...)`
|
|
151
230
|
- `DrizzleDatabaseLike`
|
|
152
231
|
- `DrizzleModuleOptions`
|
|
@@ -154,17 +233,22 @@ defineModule(ManualDrizzleModule, {
|
|
|
154
233
|
|
|
155
234
|
`DRIZZLE_HANDLE_PROVIDER`는 lifecycle-aware `DrizzleDatabase` wrapper를 가리키는 alias token입니다. `@fluojs/terminus` 같은 health integration은 이 token을 통해 raw database ping으로 fallback하기 전에 `createPlatformStatusSnapshot()`을 읽습니다.
|
|
156
235
|
|
|
236
|
+
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(...)`를 우선 사용하세요.
|
|
237
|
+
|
|
238
|
+
`Transaction`은 서비스 계층 트랜잭션 경계를 위한 표준 TC39 method decorator입니다. 데코레이터가 붙은 host에서 `this.db`, 직접 property, 중첩 `.db` property 순서로 transaction-capable 대상을 resolve하고, 명시적 client 선택에는 accessor를 받을 수 있으며, 외부 경계에는 Drizzle transaction option을 전달할 수 있습니다.
|
|
239
|
+
|
|
157
240
|
### `DrizzleModule`
|
|
158
241
|
|
|
159
242
|
- `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
|
|
160
243
|
- `forRootAsync(...)`는 database/dispose/transaction 설정을 factory에서 반환하는 DI-aware Drizzle 옵션을 받습니다. provider를 전역으로 노출해야 할 때는 최상위 async 등록 옵션에 `global`을 전달하세요.
|
|
161
244
|
- `forRootAsync(...)`는 애플리케이션 container마다 옵션을 한 번 resolve합니다. 테스트나 multi-app process에서 같은 module definition을 재사용해도 memoized factory result를 공유하지 않고 각 container가 독립적인 database/dispose 결과를 받습니다.
|
|
162
245
|
- `strictTransactions: true`를 설정하면 transaction 지원이 없는 database handle에서 예외를 던집니다.
|
|
246
|
+
- sync 및 async 등록 모두에서 `database`는 실제 object/function handle이어야 하며, 누락된 handle은 모듈 등록 또는 async bootstrap 중 거부됩니다.
|
|
163
247
|
|
|
164
248
|
## 관련 패키지
|
|
165
249
|
|
|
166
250
|
- `@fluojs/runtime`: 모듈 시작과 종료 순서를 관리합니다.
|
|
167
|
-
- `@fluojs/http`:
|
|
251
|
+
- `@fluojs/http`: 명시적 `requestTransaction(...)` 경계와 함께 사용할 수 있는 요청 라이프사이클 primitive를 제공합니다.
|
|
168
252
|
- `@fluojs/prisma`, `@fluojs/mongoose`: 같은 런타임 모델 위에서 동작하는 다른 데이터 통합 패키지입니다.
|
|
169
253
|
|
|
170
254
|
## 예제 소스
|
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
|
-
- [
|
|
14
|
-
- [Manual
|
|
15
|
-
- [Request-
|
|
16
|
-
- [Shutdown and
|
|
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
|
|
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,118 @@ export class AppModule {}
|
|
|
66
75
|
|
|
67
76
|
## Common Patterns
|
|
68
77
|
|
|
69
|
-
###
|
|
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 { Transaction, DrizzleDatabase, type DrizzleDatabaseFacade } from '@fluojs/drizzle';
|
|
84
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
85
|
+
import { users, profiles } from './schema';
|
|
86
|
+
|
|
87
|
+
type AppDatabase = ReturnType<typeof drizzle>;
|
|
88
|
+
|
|
89
|
+
export class UserService {
|
|
90
|
+
constructor(private readonly repo: UserRepository) {}
|
|
91
|
+
|
|
92
|
+
@Transaction()
|
|
93
|
+
async onboardUser(dto: any) {
|
|
94
|
+
const user = await this.repo.create(dto);
|
|
95
|
+
await this.repo.initProfile(user.id);
|
|
96
|
+
return user;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class UserRepository {
|
|
101
|
+
constructor(private readonly db: DrizzleDatabaseFacade<AppDatabase>) {}
|
|
102
|
+
|
|
103
|
+
async create(data: any) {
|
|
104
|
+
// The facade type exposes standard Drizzle methods.
|
|
105
|
+
// When called inside @Transaction(), they automatically participate in the ambient transaction.
|
|
106
|
+
return this.db.insert(users).values(data);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async initProfile(userId: string) {
|
|
110
|
+
return this.db.insert(profiles).values({ userId });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Calls to `@Transaction()` methods are reentrant. If a decorated method calls another decorated method, they share the same underlying Drizzle transaction.
|
|
116
|
+
|
|
117
|
+
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. This keeps common `constructor(private readonly db: DrizzleDatabase<...>)` services 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`.
|
|
118
|
+
|
|
119
|
+
### Manual Transactions and current()
|
|
120
|
+
|
|
121
|
+
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
122
|
|
|
71
123
|
```ts
|
|
72
124
|
import { DrizzleDatabase } from '@fluojs/drizzle';
|
|
73
|
-
import {
|
|
125
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
74
126
|
import { users } from './schema';
|
|
75
127
|
|
|
76
|
-
|
|
77
|
-
constructor(private readonly db: DrizzleDatabase) {}
|
|
128
|
+
type AppDatabase = ReturnType<typeof drizzle>;
|
|
78
129
|
|
|
79
|
-
|
|
80
|
-
|
|
130
|
+
export class AdvancedRepository {
|
|
131
|
+
constructor(private readonly db: DrizzleDatabase<AppDatabase>) {}
|
|
132
|
+
|
|
133
|
+
async customOperation() {
|
|
134
|
+
const tx = this.db.current();
|
|
135
|
+
// Use tx for operations that fluo doesn't automatically wrap,
|
|
136
|
+
// or when passing to an external utility that expects a Drizzle database handle.
|
|
137
|
+
return tx.select().from(users);
|
|
81
138
|
}
|
|
82
139
|
}
|
|
83
140
|
```
|
|
84
141
|
|
|
85
|
-
|
|
142
|
+
Use `db.transaction()` for manual transaction blocks:
|
|
86
143
|
|
|
87
144
|
```ts
|
|
88
145
|
await this.db.transaction(async () => {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
await
|
|
146
|
+
const current = this.db.current();
|
|
147
|
+
|
|
148
|
+
await current.insert(users).values(user);
|
|
149
|
+
await current.insert(profiles).values(profile);
|
|
92
150
|
});
|
|
93
151
|
```
|
|
94
152
|
|
|
95
153
|
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
154
|
|
|
97
|
-
When `database.transaction(...)` is unavailable and `strictTransactions` is `false
|
|
155
|
+
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.
|
|
98
156
|
|
|
99
|
-
### Request-
|
|
157
|
+
### Request-Wide Controller Boundaries
|
|
158
|
+
|
|
159
|
+
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
160
|
|
|
101
161
|
```ts
|
|
102
|
-
import {
|
|
103
|
-
import {
|
|
162
|
+
import { Controller, Post } from '@fluojs/http';
|
|
163
|
+
import { DrizzleDatabase } from '@fluojs/drizzle';
|
|
164
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
104
165
|
|
|
105
|
-
|
|
106
|
-
|
|
166
|
+
type AppDatabase = ReturnType<typeof drizzle>;
|
|
167
|
+
|
|
168
|
+
@Controller('/checkout')
|
|
169
|
+
export class CheckoutController {
|
|
170
|
+
constructor(
|
|
171
|
+
private readonly db: DrizzleDatabase<AppDatabase>,
|
|
172
|
+
private readonly checkout: CheckoutService,
|
|
173
|
+
) {}
|
|
174
|
+
|
|
175
|
+
@Post()
|
|
176
|
+
create(input: CheckoutInput, requestSignal?: AbortSignal) {
|
|
177
|
+
return this.db.requestTransaction(
|
|
178
|
+
() => this.checkout.createOrder(input),
|
|
179
|
+
requestSignal,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
107
183
|
```
|
|
108
184
|
|
|
185
|
+
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.
|
|
186
|
+
|
|
109
187
|
### Shutdown and status contracts
|
|
110
188
|
|
|
111
|
-
|
|
189
|
+
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.
|
|
112
190
|
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
191
|
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
192
|
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 +205,7 @@ Use `DrizzleModule.forRoot(...)` / `forRootAsync(...)` to register Drizzle. When
|
|
|
127
205
|
|
|
128
206
|
```ts
|
|
129
207
|
import { defineModule } from '@fluojs/runtime';
|
|
130
|
-
import {
|
|
208
|
+
import { DrizzleModule } from '@fluojs/drizzle';
|
|
131
209
|
|
|
132
210
|
const database = {
|
|
133
211
|
transaction: async <T>(callback: (tx: typeof database) => Promise<T>) => callback(database),
|
|
@@ -136,7 +214,6 @@ const database = {
|
|
|
136
214
|
class ManualDrizzleModule {}
|
|
137
215
|
|
|
138
216
|
defineModule(ManualDrizzleModule, {
|
|
139
|
-
exports: [DrizzleDatabase, DrizzleTransactionInterceptor],
|
|
140
217
|
imports: [DrizzleModule.forRoot({ database })],
|
|
141
218
|
});
|
|
142
219
|
```
|
|
@@ -145,8 +222,10 @@ defineModule(ManualDrizzleModule, {
|
|
|
145
222
|
|
|
146
223
|
- `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
|
|
147
224
|
- `DrizzleDatabase`
|
|
148
|
-
- `
|
|
225
|
+
- `DrizzleDatabaseFacade<TDatabase>`
|
|
226
|
+
- `Transaction`
|
|
149
227
|
- `DRIZZLE_DATABASE`, `DRIZZLE_DISPOSE`, `DRIZZLE_HANDLE_PROVIDER`, `DRIZZLE_OPTIONS`
|
|
228
|
+
- `DrizzleDatabase.createFacade(...)` (compatibility-only provider wiring helper; prefer `DrizzleModule.forRoot(...)` / `forRootAsync(...)` for application registration)
|
|
150
229
|
- `createDrizzlePlatformStatusSnapshot(...)`
|
|
151
230
|
- `DrizzleDatabaseLike`
|
|
152
231
|
- `DrizzleModuleOptions`
|
|
@@ -154,17 +233,22 @@ defineModule(ManualDrizzleModule, {
|
|
|
154
233
|
|
|
155
234
|
`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
235
|
|
|
236
|
+
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(...)`.
|
|
237
|
+
|
|
238
|
+
`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, accepts an accessor for explicit client selection, and can forward Drizzle transaction options to the outer boundary.
|
|
239
|
+
|
|
157
240
|
### `DrizzleModule`
|
|
158
241
|
|
|
159
242
|
- `DrizzleModule.forRoot(options)` / `DrizzleModule.forRootAsync(options)`
|
|
160
243
|
- `forRootAsync(...)` accepts DI-aware Drizzle options whose factory returns the database/dispose/transaction settings; pass `global` on the top-level async registration when the providers should be visible globally.
|
|
161
244
|
- `forRootAsync(...)` resolves options once per application container. Reusing the same module definition across tests or multi-app processes creates isolated database/dispose results for each container instead of sharing a memoized factory result.
|
|
162
245
|
- Supports `strictTransactions: true` to throw if transaction support is missing.
|
|
246
|
+
- `database` must be a concrete object/function handle for both sync and async registration; missing handles are rejected during module registration or async bootstrap.
|
|
163
247
|
|
|
164
248
|
## Related Packages
|
|
165
249
|
|
|
166
250
|
- `@fluojs/runtime`: owns module startup and shutdown sequencing
|
|
167
|
-
- `@fluojs/http`: provides
|
|
251
|
+
- `@fluojs/http`: provides request lifecycle primitives that can be paired with explicit `requestTransaction(...)` boundaries
|
|
168
252
|
- `@fluojs/prisma` and `@fluojs/mongoose`: alternate ORM/ODM integrations with the same fluo runtime model
|
|
169
253
|
|
|
170
254
|
## Example Sources
|
package/dist/database.d.ts
CHANGED
|
@@ -20,6 +20,20 @@ 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.
|
|
30
|
+
*
|
|
31
|
+
* @param database Root Drizzle database handle registered in the module.
|
|
32
|
+
* @param dispose Optional shutdown hook used to close pools or driver resources.
|
|
33
|
+
* @param databaseOptions Runtime transaction options consumed by the Fluo wrapper.
|
|
34
|
+
* @returns A transaction-aware facade that exposes wrapper methods plus the root Drizzle handle surface.
|
|
35
|
+
*/
|
|
36
|
+
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
37
|
/**
|
|
24
38
|
* Returns the active transaction handle when present, otherwise the root Drizzle database handle.
|
|
25
39
|
*
|
|
@@ -77,5 +91,18 @@ export declare class DrizzleDatabase<TDatabase extends DrizzleDatabaseLike<TTran
|
|
|
77
91
|
private trackActiveTransactionScope;
|
|
78
92
|
private resolveTransactionRunner;
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Injection-facing Drizzle facade type that combines the Fluo wrapper methods with the registered database handle.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* `DrizzleModule` resolves `DrizzleDatabase` to a proxy that forwards unknown properties to `current()`. Use this type
|
|
99
|
+
* in repositories that call Drizzle query methods directly, and use `DrizzleDatabase<TDatabase>` when only the wrapper
|
|
100
|
+
* methods (`current()`, `transaction(...)`, `requestTransaction(...)`, and status snapshots) are needed.
|
|
101
|
+
*
|
|
102
|
+
* @typeParam TDatabase Root Drizzle database handle registered in the module.
|
|
103
|
+
* @typeParam TTransactionDatabase Transaction-scoped database handle resolved inside `database.transaction(...)` callbacks.
|
|
104
|
+
* @typeParam TTransactionOptions Options forwarded to the underlying Drizzle transaction runner.
|
|
105
|
+
*/
|
|
106
|
+
export type DrizzleDatabaseFacade<TDatabase extends DrizzleDatabaseLike<TTransactionDatabase, TTransactionOptions>, TTransactionDatabase = TDatabase, TTransactionOptions = unknown> = DrizzleDatabase<TDatabase, TTransactionDatabase, TTransactionOptions> & Omit<TDatabase, keyof DrizzleDatabase<TDatabase, TTransactionDatabase, TTransactionOptions>>;
|
|
80
107
|
export {};
|
|
81
108
|
//# sourceMappingURL=database.d.ts.map
|
package/dist/database.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AAmEF;;;;;;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;;;;;;;;;;;;OAYG;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;YAgElB,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;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
|
@@ -13,6 +13,21 @@ const TRANSACTION_NOT_SUPPORTED_ERROR = 'Transaction not supported: Drizzle data
|
|
|
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, receiver) {
|
|
19
|
+
if (prop in database) {
|
|
20
|
+
return Reflect.get(database, prop, receiver);
|
|
21
|
+
}
|
|
22
|
+
const currentDatabase = database.current();
|
|
23
|
+
const value = Reflect.get(currentDatabase, prop, currentDatabase);
|
|
24
|
+
if (typeof value === 'function') {
|
|
25
|
+
return value.bind(currentDatabase);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
16
31
|
function createRequestAbortSignalView(parentSignal, signal) {
|
|
17
32
|
if (!signal) {
|
|
18
33
|
return {
|
|
@@ -71,6 +86,25 @@ class DrizzleDatabase {
|
|
|
71
86
|
this.databaseOptions = databaseOptions;
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Creates the low-level DI facade that forwards unknown Drizzle API properties to the ambient `current()` handle.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* This compatibility helper is used by `DrizzleModule` provider wiring. Application code should prefer
|
|
94
|
+
* `DrizzleModule.forRoot(...)` or `DrizzleModule.forRootAsync(...)`, then type injected repository handles as
|
|
95
|
+
* `DrizzleDatabaseFacade<TDatabase>` when direct Drizzle methods are needed.
|
|
96
|
+
*
|
|
97
|
+
* @param database Root Drizzle database handle registered in the module.
|
|
98
|
+
* @param dispose Optional shutdown hook used to close pools or driver resources.
|
|
99
|
+
* @param databaseOptions Runtime transaction options consumed by the Fluo wrapper.
|
|
100
|
+
* @returns A transaction-aware facade that exposes wrapper methods plus the root Drizzle handle surface.
|
|
101
|
+
*/
|
|
102
|
+
static createFacade(database, dispose, databaseOptions = {
|
|
103
|
+
strictTransactions: false
|
|
104
|
+
}) {
|
|
105
|
+
return createCurrentlessDrizzleFacade(new _DrizzleDatabase(database, dispose, databaseOptions));
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
/**
|
|
75
109
|
* Returns the active transaction handle when present, otherwise the root Drizzle database handle.
|
|
76
110
|
*
|
|
@@ -146,6 +180,11 @@ class DrizzleDatabase {
|
|
|
146
180
|
async executeTransaction(fn, options, requestScoped, signal) {
|
|
147
181
|
const current = this.transactions.getStore();
|
|
148
182
|
if (current) {
|
|
183
|
+
if (requestScoped) {
|
|
184
|
+
this.assertRequestTransactionsAvailable();
|
|
185
|
+
} else {
|
|
186
|
+
this.assertTransactionsAvailable();
|
|
187
|
+
}
|
|
149
188
|
if (options !== undefined) {
|
|
150
189
|
throw new Error(NESTED_TRANSACTION_OPTIONS_NOT_SUPPORTED_ERROR);
|
|
151
190
|
}
|
|
@@ -302,4 +341,17 @@ class DrizzleDatabase {
|
|
|
302
341
|
_initClass();
|
|
303
342
|
}
|
|
304
343
|
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Injection-facing Drizzle facade type that combines the Fluo wrapper methods with the registered database handle.
|
|
347
|
+
*
|
|
348
|
+
* @remarks
|
|
349
|
+
* `DrizzleModule` resolves `DrizzleDatabase` to a proxy that forwards unknown properties to `current()`. Use this type
|
|
350
|
+
* in repositories that call Drizzle query methods directly, and use `DrizzleDatabase<TDatabase>` when only the wrapper
|
|
351
|
+
* methods (`current()`, `transaction(...)`, `requestTransaction(...)`, and status snapshots) are needed.
|
|
352
|
+
*
|
|
353
|
+
* @typeParam TDatabase Root Drizzle database handle registered in the module.
|
|
354
|
+
* @typeParam TTransactionDatabase Transaction-scoped database handle resolved inside `database.transaction(...)` callbacks.
|
|
355
|
+
* @typeParam TTransactionOptions Options forwarded to the underlying Drizzle transaction runner.
|
|
356
|
+
*/
|
|
305
357
|
export { _DrizzleDatabase as DrizzleDatabase };
|
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;AAEvD,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;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,10 +1,15 @@
|
|
|
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,
|
|
5
|
+
const DRIZZLE_MODULE_EXPORTS = [DrizzleDatabase, DRIZZLE_HANDLE_PROVIDER];
|
|
6
|
+
function isObjectLike(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null || typeof value === 'function';
|
|
8
|
+
}
|
|
7
9
|
function normalizeDrizzleModuleOptions(options) {
|
|
10
|
+
if (!isObjectLike(options.database)) {
|
|
11
|
+
throw new Error('DrizzleModule requires a database option.');
|
|
12
|
+
}
|
|
8
13
|
return {
|
|
9
14
|
...options,
|
|
10
15
|
strictTransactions: options.strictTransactions ?? false
|
|
@@ -28,17 +33,24 @@ function createDrizzleRuntimeProviders(normalizedOptionsProvider) {
|
|
|
28
33
|
inject: [DRIZZLE_NORMALIZED_OPTIONS],
|
|
29
34
|
provide: DRIZZLE_OPTIONS,
|
|
30
35
|
useFactory: options => createRuntimeOptionsProviderValue(options.strictTransactions)
|
|
31
|
-
},
|
|
36
|
+
}, {
|
|
37
|
+
inject: [DRIZZLE_DATABASE, DRIZZLE_DISPOSE, DRIZZLE_OPTIONS],
|
|
38
|
+
provide: DrizzleDatabase,
|
|
39
|
+
useFactory: (database, dispose, databaseOptions) => DrizzleDatabase.createFacade(database, dispose, databaseOptions)
|
|
40
|
+
}, {
|
|
32
41
|
provide: DRIZZLE_HANDLE_PROVIDER,
|
|
33
42
|
useExisting: DrizzleDatabase
|
|
34
|
-
}
|
|
43
|
+
}];
|
|
35
44
|
}
|
|
36
45
|
function createDrizzleProvidersAsync(options) {
|
|
37
46
|
const normalizedOptionsProvider = {
|
|
38
47
|
inject: options.inject,
|
|
39
48
|
provide: DRIZZLE_NORMALIZED_OPTIONS,
|
|
40
49
|
scope: 'singleton',
|
|
41
|
-
useFactory: async (...deps) => normalizeDrizzleModuleOptions(
|
|
50
|
+
useFactory: async (...deps) => normalizeDrizzleModuleOptions({
|
|
51
|
+
...(await options.useFactory(...deps)),
|
|
52
|
+
global: options.global
|
|
53
|
+
})
|
|
42
54
|
};
|
|
43
55
|
return createDrizzleRuntimeProviders(normalizedOptionsProvider);
|
|
44
56
|
}
|
package/dist/transaction.d.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
*
|
|
7
|
+
* Standard TC39 method decorator that runs a service method inside a Drizzle transaction boundary.
|
|
6
8
|
*
|
|
7
9
|
* @remarks
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* `@Transaction()` uses `this.db` when present and otherwise treats the decorated instance itself as the
|
|
11
|
+
* transaction-capable `DrizzleDatabase`. Pass an accessor such as `@Transaction((self) => self.analyticsDb)` to select
|
|
12
|
+
* another Drizzle wrapper explicitly. Non-function factory input is forwarded as Drizzle transaction options.
|
|
13
|
+
*
|
|
14
|
+
* @param accessorOrOptions Optional target accessor, or Drizzle transaction options.
|
|
15
|
+
* @param options Optional Drizzle transaction options when an accessor is supplied.
|
|
16
|
+
* @returns A standard 2023-11 method decorator.
|
|
10
17
|
*/
|
|
11
|
-
export declare
|
|
12
|
-
|
|
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
|
-
}
|
|
18
|
+
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>;
|
|
19
|
+
export {};
|
|
25
20
|
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"
|
|
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;;;;;;;;;;;GAWG;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,IAOZ,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EAC/C,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"}
|
package/dist/transaction.js
CHANGED
|
@@ -1,39 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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()` uses `this.db` when present and otherwise treats the decorated instance itself as the
|
|
33
|
+
* transaction-capable `DrizzleDatabase`. Pass an accessor such as `@Transaction((self) => self.analyticsDb)` to select
|
|
34
|
+
* another Drizzle wrapper explicitly. Non-function factory input is forwarded as Drizzle transaction options.
|
|
35
|
+
*
|
|
36
|
+
* @param accessorOrOptions Optional target accessor, or Drizzle transaction options.
|
|
37
|
+
* @param options Optional Drizzle transaction options when an accessor is supplied.
|
|
38
|
+
* @returns A standard 2023-11 method decorator.
|
|
39
|
+
*/
|
|
40
|
+
export function Transaction(accessorOrOptions, options) {
|
|
41
|
+
const accessor = typeof accessorOrOptions === 'function' ? accessorOrOptions : undefined;
|
|
42
|
+
const transactionOptions = accessor ? options : accessorOrOptions;
|
|
43
|
+
return function (value, context) {
|
|
44
|
+
if (context.kind !== 'method') {
|
|
45
|
+
throw new Error('@Transaction() can only decorate methods.');
|
|
46
|
+
}
|
|
47
|
+
return async function transactionMethod(...args) {
|
|
48
|
+
const drizzleDatabase = accessor ? accessor(this) : resolveDefaultTransactionTarget(this);
|
|
49
|
+
return drizzleDatabase.transaction(() => value.apply(this, args), transactionOptions);
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"transaction",
|
|
10
10
|
"als"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0
|
|
12
|
+
"version": "1.1.0",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -37,9 +37,8 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@fluojs/core": "^1.0.3",
|
|
40
|
-
"@fluojs/
|
|
41
|
-
"@fluojs/
|
|
42
|
-
"@fluojs/runtime": "^1.1.1"
|
|
40
|
+
"@fluojs/di": "^1.1.0",
|
|
41
|
+
"@fluojs/runtime": "^1.1.8"
|
|
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": "^1.1.2"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|