@fluojs/graphql 1.0.0-beta.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/LICENSE +21 -0
- package/README.ko.md +158 -0
- package/README.md +158 -0
- package/dist/dataloader/dataloader.d.ts +127 -0
- package/dist/dataloader/dataloader.d.ts.map +1 -0
- package/dist/dataloader/dataloader.js +151 -0
- package/dist/dataloader.d.ts +2 -0
- package/dist/dataloader.d.ts.map +1 -0
- package/dist/dataloader.js +1 -0
- package/dist/decorators.d.ts +21 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +111 -0
- package/dist/discovery.d.ts +4 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +142 -0
- package/dist/guardrails.d.ts +35 -0
- package/dist/guardrails.d.ts.map +1 -0
- package/dist/guardrails.js +162 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/internal-tokens.d.ts +7 -0
- package/dist/internal-tokens.d.ts.map +1 -0
- package/dist/internal-tokens.js +4 -0
- package/dist/metadata.d.ts +20 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +120 -0
- package/dist/module.d.ts +22 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +33 -0
- package/dist/pipeline/input-pipeline.d.ts +34 -0
- package/dist/pipeline/input-pipeline.d.ts.map +1 -0
- package/dist/pipeline/input-pipeline.js +129 -0
- package/dist/schema/schema.d.ts +20 -0
- package/dist/schema/schema.d.ts.map +1 -0
- package/dist/schema/schema.js +278 -0
- package/dist/service.d.ts +58 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +602 -0
- package/dist/transport/transport.d.ts +5 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +119 -0
- package/dist/types.d.ts +221 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +132 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fluo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @fluojs/graphql
|
|
2
|
+
|
|
3
|
+
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
|
|
5
|
+
fluo를 위한 데코레이터 기반 GraphQL 통합 패키지입니다. **GraphQL Yoga**를 기반으로 설계되었으며, 깊은 DI 통합과 퍼스트 파티 DataLoader 지원을 통해 고성능의 명세 준수 GraphQL 실행 파이프라인을 제공합니다.
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [설치](#설치)
|
|
10
|
+
- [사용 시점](#사용-시점)
|
|
11
|
+
- [빠른 시작](#빠른-시작)
|
|
12
|
+
- [핵심 기능](#핵심-기능)
|
|
13
|
+
- [운영 가드레일](#운영-가드레일)
|
|
14
|
+
- [공개 API](#공개-api)
|
|
15
|
+
- [관련 패키지](#관련-패키지)
|
|
16
|
+
- [예제 소스](#예제-소스)
|
|
17
|
+
|
|
18
|
+
## 설치
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @fluojs/graphql graphql graphql-yoga
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 사용 시점
|
|
25
|
+
|
|
26
|
+
- TypeScript 데코레이터를 사용하여 타입 안전한 GraphQL API를 구축할 때 (**Code-first**).
|
|
27
|
+
- 기존 GraphQL 스키마를 fluo 애플리케이션에 통합할 때 (**Schema-first**).
|
|
28
|
+
- GraphQL resolver 내에서 request-scoped provider를 포함한 원활한 의존성 주입이 필요할 때.
|
|
29
|
+
- Request-scoped **DataLoader** 패턴을 사용하여 효율적인 데이터 페칭을 수행할 때.
|
|
30
|
+
|
|
31
|
+
## 빠른 시작
|
|
32
|
+
|
|
33
|
+
`GraphqlModule.forRoot(...)`를 등록하고 표준 데코레이터를 사용하여 resolver를 정의합니다. 현재 `@fluojs/graphql`는 동기 모듈 엔트리포인트만 제공하며 `GraphqlModule.forRootAsync(...)` 계약은 없습니다.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Module } from '@fluojs/core';
|
|
37
|
+
import { bootstrapNodeApplication } from '@fluojs/runtime/node';
|
|
38
|
+
import { GraphqlModule, Query, Resolver, Arg } from '@fluojs/graphql';
|
|
39
|
+
|
|
40
|
+
@Resolver()
|
|
41
|
+
class HelloResolver {
|
|
42
|
+
@Query()
|
|
43
|
+
hello(@Arg('name') name: string): string {
|
|
44
|
+
return `Hello, ${name}!`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Module({
|
|
49
|
+
imports: [
|
|
50
|
+
GraphqlModule.forRoot({
|
|
51
|
+
resolvers: [HelloResolver]
|
|
52
|
+
})
|
|
53
|
+
],
|
|
54
|
+
providers: [HelloResolver]
|
|
55
|
+
})
|
|
56
|
+
class AppModule {}
|
|
57
|
+
|
|
58
|
+
const app = await bootstrapNodeApplication(AppModule);
|
|
59
|
+
await app.listen(3000);
|
|
60
|
+
// curl -X POST http://localhost:3000/graphql \
|
|
61
|
+
// -H "Content-Type: application/json" \
|
|
62
|
+
// -d '{"query": "{ hello(name: \"fluo\") }"}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 핵심 기능
|
|
66
|
+
|
|
67
|
+
### Code-first Resolvers
|
|
68
|
+
fluo는 표준 데코레이터를 사용하여 GraphQL 스키마를 정의합니다. `@Resolver`, `@Query`, `@Mutation`, `@Subscription`을 사용하여 클래스 메서드를 GraphQL 작업에 매핑합니다.
|
|
69
|
+
|
|
70
|
+
### Request-Scoped DataLoaders
|
|
71
|
+
내장된 DataLoader 통합을 통해 N+1 문제를 효율적으로 해결합니다. Loader는 각 GraphQL 작업마다 자동으로 격리됩니다.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createDataLoader, type GraphQLContext } from '@fluojs/graphql';
|
|
75
|
+
|
|
76
|
+
const userLoader = createDataLoader(async (ids: string[]) => {
|
|
77
|
+
const users = await userService.findByIds(ids);
|
|
78
|
+
return ids.map(id => users.find(u => u.id === id));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
@Resolver()
|
|
82
|
+
class UserResolver {
|
|
83
|
+
@Query()
|
|
84
|
+
async user(@Arg('id') id: string, context: GraphQLContext) {
|
|
85
|
+
return userLoader(context).load(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 프로토콜 지원
|
|
91
|
+
- **HTTP**: 표준 GET/POST 쿼리 및 뮤테이션.
|
|
92
|
+
- **SSE**: Server-Sent Events를 통한 구독(기본값).
|
|
93
|
+
- **WebSockets**: 실시간 구독을 위한 선택적 `graphql-ws` 지원.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
GraphqlModule.forRoot({
|
|
97
|
+
subscriptions: {
|
|
98
|
+
websocket: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
limits: {
|
|
101
|
+
maxConnections: 100,
|
|
102
|
+
maxPayloadBytes: 64 * 1024,
|
|
103
|
+
maxOperationsPerConnection: 25,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 운영 가드레일
|
|
111
|
+
|
|
112
|
+
- `graphiql`을 명시적으로 켜거나 `introspection: true`를 설정하지 않으면 스키마 introspection은 기본적으로 비활성화됩니다.
|
|
113
|
+
- 문서 depth, field complexity, aggregate query cost에 대한 request validation budget이 기본적으로 보수적인 값으로 활성화됩니다.
|
|
114
|
+
- WebSocket 구독 경로에는 별도의 전송 budget이 기본 적용됩니다: 동시 연결 `100`, 최대 payload 크기 `64 KiB`, 연결당 활성 operation `25`개입니다.
|
|
115
|
+
- 무제한 WebSocket 동작이 정말 필요할 때만 `subscriptions.websocket.limits = false`를 사용하고, 그 경우에도 동일한 수준의 외부 제어 수단을 마련해야 합니다.
|
|
116
|
+
- 무제한 동작이 꼭 필요할 때만 `limits: false`를 사용하고, 그 경우에는 외부 제어 수단을 함께 두어야 합니다.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
GraphqlModule.forRoot({
|
|
120
|
+
graphiql: false,
|
|
121
|
+
introspection: false,
|
|
122
|
+
limits: {
|
|
123
|
+
maxDepth: 8,
|
|
124
|
+
maxComplexity: 120,
|
|
125
|
+
maxCost: 240,
|
|
126
|
+
},
|
|
127
|
+
subscriptions: {
|
|
128
|
+
websocket: {
|
|
129
|
+
enabled: true,
|
|
130
|
+
limits: {
|
|
131
|
+
maxConnections: 100,
|
|
132
|
+
maxPayloadBytes: 64 * 1024,
|
|
133
|
+
maxOperationsPerConnection: 25,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
resolvers: [HelloResolver],
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 공개 API
|
|
142
|
+
|
|
143
|
+
- `GraphqlModule.forRoot(options)`: GraphQL 통합을 위한 메인 엔트리 포인트.
|
|
144
|
+
- `Resolver`, `Query`, `Mutation`, `Subscription`: 작업 데코레이터.
|
|
145
|
+
- `Arg`: 인자 매핑 데코레이터.
|
|
146
|
+
- `createDataLoader`, `createDataLoaderMap`: DataLoader 팩토리 헬퍼.
|
|
147
|
+
- `GraphQLContext`: GraphQL 실행 컨텍스트를 위한 타입 정의.
|
|
148
|
+
|
|
149
|
+
## 관련 패키지
|
|
150
|
+
|
|
151
|
+
- `@fluojs/core`: 핵심 DI 및 모듈 시스템.
|
|
152
|
+
- `@fluojs/http`: 기반 HTTP 추상화.
|
|
153
|
+
- `@fluojs/validation`: GraphQL 입력을 위한 통합 DTO 검증.
|
|
154
|
+
|
|
155
|
+
## 예제 소스
|
|
156
|
+
|
|
157
|
+
- `packages/graphql/src/module.test.ts`: 통합 테스트 및 사용 예제.
|
|
158
|
+
- `examples/graphql-yoga`: 전체 GraphQL 애플리케이션 예제.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @fluojs/graphql
|
|
2
|
+
|
|
3
|
+
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
|
+
|
|
5
|
+
Decorator-based GraphQL integration for fluo. Built on **GraphQL Yoga**, it provides a high-performance, specification-compliant GraphQL execution pipeline with deep DI integration and first-party DataLoader support.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [When to Use](#when-to-use)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Core Capabilities](#core-capabilities)
|
|
13
|
+
- [Operational Guardrails](#operational-guardrails)
|
|
14
|
+
- [Public API](#public-api)
|
|
15
|
+
- [Related Packages](#related-packages)
|
|
16
|
+
- [Example Sources](#example-sources)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @fluojs/graphql graphql graphql-yoga
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## When to Use
|
|
25
|
+
|
|
26
|
+
- When building type-safe GraphQL APIs using TypeScript decorators (**Code-first**).
|
|
27
|
+
- When integrating existing GraphQL schemas into a fluo application (**Schema-first**).
|
|
28
|
+
- When you need seamless dependency injection within GraphQL resolvers, including request-scoped providers.
|
|
29
|
+
- When performing efficient data fetching using request-scoped **DataLoader** patterns.
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
Register `GraphqlModule.forRoot(...)` and define a resolver using standard decorators. `@fluojs/graphql` currently exposes a synchronous module entrypoint only; there is no `GraphqlModule.forRootAsync(...)` contract.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Module } from '@fluojs/core';
|
|
37
|
+
import { bootstrapNodeApplication } from '@fluojs/runtime/node';
|
|
38
|
+
import { GraphqlModule, Query, Resolver, Arg } from '@fluojs/graphql';
|
|
39
|
+
|
|
40
|
+
@Resolver()
|
|
41
|
+
class HelloResolver {
|
|
42
|
+
@Query()
|
|
43
|
+
hello(@Arg('name') name: string): string {
|
|
44
|
+
return `Hello, ${name}!`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Module({
|
|
49
|
+
imports: [
|
|
50
|
+
GraphqlModule.forRoot({
|
|
51
|
+
resolvers: [HelloResolver]
|
|
52
|
+
})
|
|
53
|
+
],
|
|
54
|
+
providers: [HelloResolver]
|
|
55
|
+
})
|
|
56
|
+
class AppModule {}
|
|
57
|
+
|
|
58
|
+
const app = await bootstrapNodeApplication(AppModule);
|
|
59
|
+
await app.listen(3000);
|
|
60
|
+
// curl -X POST http://localhost:3000/graphql \
|
|
61
|
+
// -H "Content-Type: application/json" \
|
|
62
|
+
// -d '{"query": "{ hello(name: \"fluo\") }"}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Core Capabilities
|
|
66
|
+
|
|
67
|
+
### Code-first Resolvers
|
|
68
|
+
fluo uses standard decorators to define your GraphQL schema. Use `@Resolver`, `@Query`, `@Mutation`, and `@Subscription` to map class methods to GraphQL operations.
|
|
69
|
+
|
|
70
|
+
### Request-Scoped DataLoaders
|
|
71
|
+
Efficiently solve the N+1 problem with built-in DataLoader integration. Loaders are automatically isolated per GraphQL operation.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createDataLoader, type GraphQLContext } from '@fluojs/graphql';
|
|
75
|
+
|
|
76
|
+
const userLoader = createDataLoader(async (ids: string[]) => {
|
|
77
|
+
const users = await userService.findByIds(ids);
|
|
78
|
+
return ids.map(id => users.find(u => u.id === id));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
@Resolver()
|
|
82
|
+
class UserResolver {
|
|
83
|
+
@Query()
|
|
84
|
+
async user(@Arg('id') id: string, context: GraphQLContext) {
|
|
85
|
+
return userLoader(context).load(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Protocol Support
|
|
91
|
+
- **HTTP**: Standard GET/POST queries and mutations.
|
|
92
|
+
- **SSE**: Subscriptions over Server-Sent Events (default).
|
|
93
|
+
- **WebSockets**: Optional `graphql-ws` support for real-time subscriptions.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
GraphqlModule.forRoot({
|
|
97
|
+
subscriptions: {
|
|
98
|
+
websocket: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
limits: {
|
|
101
|
+
maxConnections: 100,
|
|
102
|
+
maxPayloadBytes: 64 * 1024,
|
|
103
|
+
maxOperationsPerConnection: 25,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Operational Guardrails
|
|
111
|
+
|
|
112
|
+
- Schema introspection is disabled by default unless you explicitly enable `graphiql` or set `introspection: true`.
|
|
113
|
+
- Request validation budgets are enabled by default with conservative limits for document depth, field complexity, and aggregate query cost.
|
|
114
|
+
- WebSocket subscriptions use separate transport budgets by default: `100` concurrent connections, `64 KiB` maximum payload size, and `25` active operations per connection.
|
|
115
|
+
- Set `subscriptions.websocket.limits = false` only when you intentionally need unbounded websocket behavior and can enforce equivalent controls elsewhere.
|
|
116
|
+
- Pass `limits: false` only when you intentionally need unbounded behavior and can compensate with external controls.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
GraphqlModule.forRoot({
|
|
120
|
+
graphiql: false,
|
|
121
|
+
introspection: false,
|
|
122
|
+
limits: {
|
|
123
|
+
maxDepth: 8,
|
|
124
|
+
maxComplexity: 120,
|
|
125
|
+
maxCost: 240,
|
|
126
|
+
},
|
|
127
|
+
subscriptions: {
|
|
128
|
+
websocket: {
|
|
129
|
+
enabled: true,
|
|
130
|
+
limits: {
|
|
131
|
+
maxConnections: 100,
|
|
132
|
+
maxPayloadBytes: 64 * 1024,
|
|
133
|
+
maxOperationsPerConnection: 25,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
resolvers: [HelloResolver],
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Public API
|
|
142
|
+
|
|
143
|
+
- `GraphqlModule.forRoot(options)`: Main entry point for GraphQL integration.
|
|
144
|
+
- `Resolver`, `Query`, `Mutation`, `Subscription`: Operation decorators.
|
|
145
|
+
- `Arg`: Argument mapping decorator.
|
|
146
|
+
- `createDataLoader`, `createDataLoaderMap`: DataLoader factory helpers.
|
|
147
|
+
- `GraphQLContext`: Type definition for the GraphQL execution context.
|
|
148
|
+
|
|
149
|
+
## Related Packages
|
|
150
|
+
|
|
151
|
+
- `@fluojs/core`: Core DI and module system.
|
|
152
|
+
- `@fluojs/http`: Underlying HTTP abstraction.
|
|
153
|
+
- `@fluojs/validation`: Integrated DTO validation for GraphQL inputs.
|
|
154
|
+
|
|
155
|
+
## Example Sources
|
|
156
|
+
|
|
157
|
+
- `packages/graphql/src/module.test.ts`: Integration tests and usage examples.
|
|
158
|
+
- `examples/graphql-yoga`: Complete GraphQL application example.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
import { type GraphQLContext } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a request-scoped loader instance from GraphQL context cache.
|
|
5
|
+
*
|
|
6
|
+
* @param context GraphQL operation context that holds the request-scoped loader cache.
|
|
7
|
+
* @param key Stable cache key for this loader instance within a single operation.
|
|
8
|
+
* @param createLoader Factory used when no loader is cached yet for `key`.
|
|
9
|
+
* @returns The cached or newly created loader instance for the current operation.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getRequestScopedDataLoader<TLoader>(context: GraphQLContext, key: string | symbol, createLoader: () => TLoader): TLoader;
|
|
12
|
+
/**
|
|
13
|
+
* Creates an accessor that resolves one request-scoped loader from context.
|
|
14
|
+
*
|
|
15
|
+
* @param key Stable cache key for this loader instance within a single operation.
|
|
16
|
+
* @param createLoader Factory used when no loader is cached yet for `key`.
|
|
17
|
+
* @returns A context accessor that always resolves the per-operation cached loader.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createRequestScopedDataLoaderFactory<TLoader>(key: string | symbol, createLoader: () => TLoader): (context: GraphQLContext) => TLoader;
|
|
20
|
+
/**
|
|
21
|
+
* Options accepted by {@link createDataLoader}. Extends the standard
|
|
22
|
+
* `DataLoader.Options` with an optional `key` used to deduplicate loader
|
|
23
|
+
* instances inside the per-operation request-scoped cache.
|
|
24
|
+
*/
|
|
25
|
+
export interface FluoDataLoaderOptions<K, V, C = K> extends DataLoader.Options<K, V, C> {
|
|
26
|
+
/**
|
|
27
|
+
* Cache key used to store/retrieve this loader in the per-operation
|
|
28
|
+
* request-scoped cache. When omitted a unique `Symbol` is generated
|
|
29
|
+
* automatically, which means every call-site gets its own loader instance
|
|
30
|
+
* per operation — usually what you want.
|
|
31
|
+
*/
|
|
32
|
+
key?: string | symbol;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A function returned by {@link createDataLoader} that, given a
|
|
36
|
+
* {@link GraphQLContext}, returns a request-scoped `DataLoader` instance.
|
|
37
|
+
*
|
|
38
|
+
* Call this inside any resolver method to obtain a DataLoader that is
|
|
39
|
+
* automatically scoped to the current GraphQL operation.
|
|
40
|
+
*/
|
|
41
|
+
export type RequestScopedDataLoaderAccessor<K, V> = (context: GraphQLContext) => DataLoader<K, V>;
|
|
42
|
+
/**
|
|
43
|
+
* Create a request-scoped `DataLoader` accessor.
|
|
44
|
+
*
|
|
45
|
+
* This is the recommended first-party entry point for DataLoader usage in
|
|
46
|
+
* `@fluojs/graphql`. It combines the `dataloader` package with Fluo's
|
|
47
|
+
* per-operation request-scoped cache so that:
|
|
48
|
+
*
|
|
49
|
+
* - Each GraphQL operation gets its own `DataLoader` instance (cache isolation).
|
|
50
|
+
* - Concurrent operations never share batched results.
|
|
51
|
+
* - The accessor is safe to call from singleton resolvers — no `@Scope('request')` required.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { createDataLoader, type GraphQLContext } from '@fluojs/graphql';
|
|
56
|
+
*
|
|
57
|
+
* const getUserById = createDataLoader<string, User | null>(async (ids) => {
|
|
58
|
+
* const users = await userRepo.findManyByIds([...ids]);
|
|
59
|
+
* const map = new Map(users.map(u => [u.id, u]));
|
|
60
|
+
* return ids.map(id => map.get(id) ?? null);
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // inside a resolver method:
|
|
64
|
+
* const user = await getUserById(context).load(userId);
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @param batchFn DataLoader batch function that maps requested keys to ordered values.
|
|
68
|
+
* @param options Optional DataLoader options plus an optional stable Fluo cache key.
|
|
69
|
+
* @returns A request-scoped accessor that resolves a `DataLoader` for the current operation.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createDataLoader<K, V, C = K>(batchFn: DataLoader.BatchLoadFn<K, V>, options?: FluoDataLoaderOptions<K, V, C>): RequestScopedDataLoaderAccessor<K, V>;
|
|
72
|
+
/**
|
|
73
|
+
* Describes a single loader entry in a {@link DataLoaderMap}.
|
|
74
|
+
*/
|
|
75
|
+
export interface DataLoaderDefinition<K, V, C = K> {
|
|
76
|
+
batch: DataLoader.BatchLoadFn<K, V>;
|
|
77
|
+
options?: DataLoader.Options<K, V, C>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A map from loader names to their definitions.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const loaders = {
|
|
85
|
+
* userById: { batch: async (ids) => ... },
|
|
86
|
+
* postsByAuthor: { batch: async (authorIds) => ..., options: { cache: false } },
|
|
87
|
+
* } satisfies DataLoaderMap;
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export type DataLoaderMap = Record<string, DataLoaderDefinition<any, any, any>>;
|
|
91
|
+
/**
|
|
92
|
+
* The resolved accessor type: each key from the definition map becomes a
|
|
93
|
+
* `DataLoader` instance keyed by the original batch function's types.
|
|
94
|
+
*/
|
|
95
|
+
export type ResolvedDataLoaders<TMap extends DataLoaderMap> = {
|
|
96
|
+
[K in keyof TMap]: TMap[K] extends DataLoaderDefinition<infer TKey, infer TValue, any> ? DataLoader<TKey, TValue> : never;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Create a set of named, request-scoped DataLoaders from a definition map.
|
|
100
|
+
*
|
|
101
|
+
* This is convenient when a resolver (or a group of resolvers) needs multiple
|
|
102
|
+
* loaders — instead of declaring each one individually, define them as a map
|
|
103
|
+
* and retrieve the whole set per operation.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const loaders = createDataLoaderMap({
|
|
108
|
+
* userById: {
|
|
109
|
+
* batch: async (ids) => {
|
|
110
|
+
* const users = await repo.findManyByIds([...ids]);
|
|
111
|
+
* const map = new Map(users.map(u => [u.id, u]));
|
|
112
|
+
* return ids.map(id => map.get(id) ?? null);
|
|
113
|
+
* },
|
|
114
|
+
* },
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* // inside resolver:
|
|
118
|
+
* const { userById } = loaders(context);
|
|
119
|
+
* const user = await userById.load('abc');
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @param definitions Loader definitions keyed by loader name.
|
|
123
|
+
* @returns A context accessor that resolves all named loaders for one GraphQL operation.
|
|
124
|
+
*/
|
|
125
|
+
export declare function createDataLoaderMap<TMap extends DataLoaderMap>(definitions: TMap): (context: GraphQLContext) => ResolvedDataLoaders<TMap>;
|
|
126
|
+
export { DataLoader };
|
|
127
|
+
//# sourceMappingURL=dataloader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataloader.d.ts","sourceRoot":"","sources":["../../src/dataloader/dataloader.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAuC,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEvF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAChD,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,GAAG,MAAM,EACpB,YAAY,EAAE,MAAM,OAAO,GAC1B,OAAO,CAaT;AAED;;;;;;GAMG;AACH,wBAAgB,oCAAoC,CAAC,OAAO,EAC1D,GAAG,EAAE,MAAM,GAAG,MAAM,EACpB,YAAY,EAAE,MAAM,OAAO,GAC1B,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAEtC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAE,SAAQ,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACrF;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,+BAA+B,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAElG;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAC1C,OAAO,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EACrC,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACvC,+BAA+B,CAAC,CAAC,EAAE,CAAC,CAAC,CAQvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC;IAC/C,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACvC;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,mBAAmB,CAAC,IAAI,SAAS,aAAa,IAAI;KAC3D,CAAC,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,oBAAoB,CAAC,MAAM,IAAI,EAAE,MAAM,MAAM,EAAE,GAAG,CAAC,GAClF,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,GACxB,KAAK;CACV,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,aAAa,EAC5D,WAAW,EAAE,IAAI,GAChB,CAAC,OAAO,EAAE,cAAc,KAAK,mBAAmB,CAAC,IAAI,CAAC,CAiBxD;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
import { GRAPHQL_REQUEST_SCOPED_LOADER_CACHE } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a request-scoped loader instance from GraphQL context cache.
|
|
6
|
+
*
|
|
7
|
+
* @param context GraphQL operation context that holds the request-scoped loader cache.
|
|
8
|
+
* @param key Stable cache key for this loader instance within a single operation.
|
|
9
|
+
* @param createLoader Factory used when no loader is cached yet for `key`.
|
|
10
|
+
* @returns The cached or newly created loader instance for the current operation.
|
|
11
|
+
*/
|
|
12
|
+
export function getRequestScopedDataLoader(context, key, createLoader) {
|
|
13
|
+
const cache = context[GRAPHQL_REQUEST_SCOPED_LOADER_CACHE] ?? new Map();
|
|
14
|
+
context[GRAPHQL_REQUEST_SCOPED_LOADER_CACHE] = cache;
|
|
15
|
+
const existing = cache.get(key);
|
|
16
|
+
if (existing !== undefined) {
|
|
17
|
+
return existing;
|
|
18
|
+
}
|
|
19
|
+
const created = createLoader();
|
|
20
|
+
cache.set(key, created);
|
|
21
|
+
return created;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates an accessor that resolves one request-scoped loader from context.
|
|
26
|
+
*
|
|
27
|
+
* @param key Stable cache key for this loader instance within a single operation.
|
|
28
|
+
* @param createLoader Factory used when no loader is cached yet for `key`.
|
|
29
|
+
* @returns A context accessor that always resolves the per-operation cached loader.
|
|
30
|
+
*/
|
|
31
|
+
export function createRequestScopedDataLoaderFactory(key, createLoader) {
|
|
32
|
+
return context => getRequestScopedDataLoader(context, key, createLoader);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options accepted by {@link createDataLoader}. Extends the standard
|
|
37
|
+
* `DataLoader.Options` with an optional `key` used to deduplicate loader
|
|
38
|
+
* instances inside the per-operation request-scoped cache.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A function returned by {@link createDataLoader} that, given a
|
|
43
|
+
* {@link GraphQLContext}, returns a request-scoped `DataLoader` instance.
|
|
44
|
+
*
|
|
45
|
+
* Call this inside any resolver method to obtain a DataLoader that is
|
|
46
|
+
* automatically scoped to the current GraphQL operation.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a request-scoped `DataLoader` accessor.
|
|
51
|
+
*
|
|
52
|
+
* This is the recommended first-party entry point for DataLoader usage in
|
|
53
|
+
* `@fluojs/graphql`. It combines the `dataloader` package with Fluo's
|
|
54
|
+
* per-operation request-scoped cache so that:
|
|
55
|
+
*
|
|
56
|
+
* - Each GraphQL operation gets its own `DataLoader` instance (cache isolation).
|
|
57
|
+
* - Concurrent operations never share batched results.
|
|
58
|
+
* - The accessor is safe to call from singleton resolvers — no `@Scope('request')` required.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { createDataLoader, type GraphQLContext } from '@fluojs/graphql';
|
|
63
|
+
*
|
|
64
|
+
* const getUserById = createDataLoader<string, User | null>(async (ids) => {
|
|
65
|
+
* const users = await userRepo.findManyByIds([...ids]);
|
|
66
|
+
* const map = new Map(users.map(u => [u.id, u]));
|
|
67
|
+
* return ids.map(id => map.get(id) ?? null);
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* // inside a resolver method:
|
|
71
|
+
* const user = await getUserById(context).load(userId);
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @param batchFn DataLoader batch function that maps requested keys to ordered values.
|
|
75
|
+
* @param options Optional DataLoader options plus an optional stable Fluo cache key.
|
|
76
|
+
* @returns A request-scoped accessor that resolves a `DataLoader` for the current operation.
|
|
77
|
+
*/
|
|
78
|
+
export function createDataLoader(batchFn, options) {
|
|
79
|
+
const {
|
|
80
|
+
key: userKey,
|
|
81
|
+
...dataloaderOptions
|
|
82
|
+
} = options ?? {};
|
|
83
|
+
const cacheKey = userKey ?? Symbol('fluo.dataloader');
|
|
84
|
+
return createRequestScopedDataLoaderFactory(cacheKey, () => new DataLoader(batchFn, dataloaderOptions));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Describes a single loader entry in a {@link DataLoaderMap}.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A map from loader names to their definitions.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const loaders = {
|
|
97
|
+
* userById: { batch: async (ids) => ... },
|
|
98
|
+
* postsByAuthor: { batch: async (authorIds) => ..., options: { cache: false } },
|
|
99
|
+
* } satisfies DataLoaderMap;
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The resolved accessor type: each key from the definition map becomes a
|
|
105
|
+
* `DataLoader` instance keyed by the original batch function's types.
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a set of named, request-scoped DataLoaders from a definition map.
|
|
110
|
+
*
|
|
111
|
+
* This is convenient when a resolver (or a group of resolvers) needs multiple
|
|
112
|
+
* loaders — instead of declaring each one individually, define them as a map
|
|
113
|
+
* and retrieve the whole set per operation.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const loaders = createDataLoaderMap({
|
|
118
|
+
* userById: {
|
|
119
|
+
* batch: async (ids) => {
|
|
120
|
+
* const users = await repo.findManyByIds([...ids]);
|
|
121
|
+
* const map = new Map(users.map(u => [u.id, u]));
|
|
122
|
+
* return ids.map(id => map.get(id) ?? null);
|
|
123
|
+
* },
|
|
124
|
+
* },
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* // inside resolver:
|
|
128
|
+
* const { userById } = loaders(context);
|
|
129
|
+
* const user = await userById.load('abc');
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @param definitions Loader definitions keyed by loader name.
|
|
133
|
+
* @returns A context accessor that resolves all named loaders for one GraphQL operation.
|
|
134
|
+
*/
|
|
135
|
+
export function createDataLoaderMap(definitions) {
|
|
136
|
+
const accessors = new Map();
|
|
137
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
138
|
+
accessors.set(name, createDataLoader(def.batch, {
|
|
139
|
+
...def.options,
|
|
140
|
+
key: Symbol(`fluo.dataloader.map.${name}`)
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
return context => {
|
|
144
|
+
const result = {};
|
|
145
|
+
for (const [name, accessor] of accessors) {
|
|
146
|
+
result[name] = accessor(context);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export { DataLoader };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataloader.d.ts","sourceRoot":"","sources":["../src/dataloader.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dataloader/dataloader.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GraphqlArgType, GraphqlRootOutputType } from './types.js';
|
|
2
|
+
type StandardClassDecoratorFn = (value: Function, context: ClassDecoratorContext) => void;
|
|
3
|
+
type StandardMethodDecoratorFn = (value: Function, context: ClassMethodDecoratorContext) => void;
|
|
4
|
+
type StandardFieldDecoratorFn = <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>) => void;
|
|
5
|
+
export interface ResolverMethodOptions {
|
|
6
|
+
fieldName?: string;
|
|
7
|
+
input?: Function;
|
|
8
|
+
topics?: string | string[];
|
|
9
|
+
argTypes?: Record<string, GraphqlArgType>;
|
|
10
|
+
outputType?: GraphqlRootOutputType;
|
|
11
|
+
}
|
|
12
|
+
type ClassDecoratorLike = StandardClassDecoratorFn;
|
|
13
|
+
type MethodDecoratorLike = StandardMethodDecoratorFn;
|
|
14
|
+
type FieldDecoratorLike = StandardFieldDecoratorFn;
|
|
15
|
+
export declare function Resolver(typeName?: string): ClassDecoratorLike;
|
|
16
|
+
export declare function Query(fieldNameOrOptions?: string | ResolverMethodOptions): MethodDecoratorLike;
|
|
17
|
+
export declare function Mutation(fieldNameOrOptions?: string | ResolverMethodOptions): MethodDecoratorLike;
|
|
18
|
+
export declare function Subscription(fieldNameOrOptions?: string | ResolverMethodOptions): MethodDecoratorLike;
|
|
19
|
+
export declare function Arg(argName?: string): FieldDecoratorLike;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=decorators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAoB,cAAc,EAAE,qBAAqB,EAA6C,MAAM,YAAY,CAAC;AAGrI,KAAK,wBAAwB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAC1F,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AACjG,KAAK,wBAAwB,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,0BAA0B,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC;AAE1H,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AACnD,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AACrD,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AAoGnD,wBAAgB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAQ9D;AAED,wBAAgB,KAAK,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,mBAAmB,CAE9F;AAED,wBAAgB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,mBAAmB,CAEjG;AAED,wBAAgB,YAAY,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,mBAAmB,CAErG;AAED,wBAAgB,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAmBxD"}
|