@fluojs/graphql 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +69 -21
- package/README.md +74 -21
- package/dist/dataloader/dataloader.d.ts.map +1 -1
- package/dist/dataloader/dataloader.js +2 -3
- package/dist/decorators.d.ts +14 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +25 -4
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/instance-of-patch.d.ts +21 -0
- package/dist/instance-of-patch.d.ts.map +1 -0
- package/dist/instance-of-patch.js +126 -0
- package/dist/module.d.ts +14 -10
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +69 -14
- package/dist/node/graphql-websocket-transport.d.ts +17 -0
- package/dist/node/graphql-websocket-transport.d.ts.map +1 -1
- package/dist/node/graphql-websocket-transport.js +85 -22
- package/dist/pipeline/input-pipeline.js +1 -1
- package/dist/schema/object-field-resolvers.d.ts +5 -4
- package/dist/schema/object-field-resolvers.d.ts.map +1 -1
- package/dist/schema/object-field-resolvers.js +12 -4
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +3 -3
- package/dist/service.d.ts +9 -6
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +166 -170
- package/dist/transport/transport.d.ts.map +1 -1
- package/dist/transport/transport.js +1 -1
- package/dist/types.d.ts +25 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/package.json +11 -10
package/README.ko.md
CHANGED
|
@@ -22,24 +22,50 @@ fluo를 위한 데코레이터 기반 GraphQL 통합 패키지입니다. **Graph
|
|
|
22
22
|
pnpm add @fluojs/graphql graphql graphql-yoga
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`@fluojs/graphql`은
|
|
25
|
+
`@fluojs/graphql`은 선택적 GraphQL-over-WebSocket subscription을 위해 `ws@^8.21.0`을 포함합니다. 업그레이드할 때 application lockfile을 갱신해 패치된 package-owned WebSocket runtime이 설치되도록 하세요. 애플리케이션이 `ws`를 직접 import하지 않는 한 별도로 추가할 필요는 없습니다.
|
|
26
|
+
|
|
27
|
+
`@fluojs/graphql`은 Node.js `>=24.0.0 <27`을 지원하며, 이 정확한 범위를 `engines.node`로 선언합니다. 이 범위는 패키지 자체의 검증된 Node 지원 계약입니다. Portable `@fluojs/runtime`과 `@fluojs/config`에는 package-wide `engines.node`가 없으며, config의 Node 전용 env-file 및 watch 기능은 실행 시점에 보호됩니다. HTTP query/mutation과 기본 SSE subscription 경로는 내부적으로 Web-standard request/response primitive를 사용하지만, 이 구현 세부 사항이 Bun, Deno, Cloudflare Workers에 대한 package 지원을 의미하지는 않습니다. 전체 dependency metadata와 native runtime suite가 완전한 GraphQL 계약을 입증하기 전까지 해당 runtime은 지원하지 않습니다. 선택적 WebSocket subscription에는 server-backed Node HTTP/S upgrade 표면을 노출하는 adapter도 필요합니다.
|
|
26
28
|
|
|
27
29
|
## 사용 시점
|
|
28
30
|
|
|
29
31
|
- TypeScript 데코레이터를 사용하여 타입 안전한 GraphQL API를 구축할 때 (**Code-first**).
|
|
30
32
|
- 기존의 executable `GraphQLSchema` 객체를 fluo 애플리케이션에 통합할 때.
|
|
31
33
|
- GraphQL resolver 내에서 request-scoped provider를 포함한 원활한 의존성 주입이 필요할 때.
|
|
32
|
-
-
|
|
34
|
+
- GraphQL operation 범위 **DataLoader** 패턴을 사용하여 효율적인 데이터 페칭을 수행할 때.
|
|
33
35
|
|
|
34
36
|
## 빠른 시작
|
|
35
37
|
|
|
36
|
-
`GraphqlModule.forRoot(...)`를 등록하고 표준 데코레이터를 사용하여 resolver를 정의합니다.
|
|
38
|
+
`GraphqlModule.forRoot(...)`를 등록하고 표준 데코레이터를 사용하여 resolver를 정의합니다. GraphQL 등록 전에 명시적인 application graph 의존성에서 module option을 해석해야 한다면 `GraphqlModule.forRootAsync({ inject, useFactory })`를 사용하세요.
|
|
39
|
+
|
|
40
|
+
`forRootAsync(...)`는 GraphQL lifecycle이 시작되기 전에 application context마다 factory를 한 번 해석합니다. 명시적인 `inject` token과 `useFactory`만 지원하며 NestJS 스타일의 `imports`, `useClass`, `useExisting`, 암시적 discovery는 거부합니다.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
class GraphqlSettings {
|
|
44
|
+
graphiql = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Module({
|
|
48
|
+
imports: [
|
|
49
|
+
GraphqlModule.forRootAsync({
|
|
50
|
+
inject: [GraphqlSettings],
|
|
51
|
+
useFactory: async (settings) => ({
|
|
52
|
+
graphiql: settings.graphiql,
|
|
53
|
+
resolvers: [HelloResolver],
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
providers: [GraphqlSettings, HelloResolver],
|
|
58
|
+
})
|
|
59
|
+
export class AppModule {}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Async registration 전용 example application은 추가하지 않습니다. 이 Quick Start와 [Chapter 18](../../book/intermediate/ch18-graphql.ko.md)이 유지 관리되는 example surface입니다.
|
|
37
63
|
|
|
38
64
|
Code-first resolver discovery 대신 schema-first 통합을 원하면 executable `GraphQLSchema`를 `schema`로 전달할 수도 있습니다.
|
|
39
65
|
|
|
40
66
|
```typescript
|
|
41
67
|
import { Module } from '@fluojs/core';
|
|
42
|
-
import { bootstrapNodeApplication } from '@fluojs/
|
|
68
|
+
import { bootstrapNodeApplication } from '@fluojs/platform-nodejs';
|
|
43
69
|
import { GraphqlModule, Query, Resolver, Arg } from '@fluojs/graphql';
|
|
44
70
|
|
|
45
71
|
class HelloInput {
|
|
@@ -72,6 +98,8 @@ await app.listen(3000);
|
|
|
72
98
|
// -d '{"query": "{ hello(name: \"fluo\") }"}'
|
|
73
99
|
```
|
|
74
100
|
|
|
101
|
+
NestJS에서 이전하나요? Resolver authorization, schema nullability, scope, subscription을 옮기기 전에 [NestJS → fluo Migration Map](../../docs/getting-started/migrate-from-nestjs.ko.md#graphql-마이그레이션-경계)을 읽으세요.
|
|
102
|
+
|
|
75
103
|
## 핵심 기능
|
|
76
104
|
|
|
77
105
|
### Code-first Resolvers
|
|
@@ -109,11 +137,13 @@ class UserResolver {
|
|
|
109
137
|
|
|
110
138
|
`@FieldResolver(...)`는 provider method를 `@Resolver('TypeName')`이 소유하는 named object type의 field에 연결합니다. 대상 object type은 code-first root operation output에서 도달 가능해야 합니다. 해당 field가 `GraphQLObjectType`에 이미 존재하거나, schema builder가 field를 추가할 수 있도록 field resolver가 `type`을 선언해야 합니다.
|
|
111
139
|
|
|
112
|
-
|
|
140
|
+
`@FieldResolver({ input })`은 root resolver DTO argument pipeline을 그대로 재사용합니다. `@Arg(...)` field가 GraphQL argument를 정의하고, 값은 DTO로 materialize되며, validation error는 계속 GraphQL `BAD_USER_INPUT` error로 반환됩니다. list argument type은 root operation과 동일하게 `argTypes`로 지정하세요.
|
|
141
|
+
|
|
142
|
+
TC39 표준 데코레이터는 parameter decorator를 지원하지 않습니다. fluo의 standard-decorator 계약을 지키기 위해 `@Args()`, `@Parent()`, `@Context()`는 zero-based parameter index를 바인딩하는 method decorator입니다. `@Args()`와 `@Parent()`의 기본값은 모두 `0`이므로 둘을 함께 사용할 때는 서로 다른 명시적 index를 지정해야 합니다. `@Context()`의 기본값은 `1`입니다. 같은 index를 두 번 바인딩하면 decorator evaluation 중 실패합니다. `@FieldResolver({ input })`에는 `@Args()`가 필요하고, `@Args()`에는 `input`이 필요합니다.
|
|
113
143
|
|
|
114
144
|
```typescript
|
|
115
145
|
import { GraphQLObjectType, GraphQLString } from 'graphql';
|
|
116
|
-
import { Context, FieldResolver, Parent, Query, Resolver, type GraphQLContext } from '@fluojs/graphql';
|
|
146
|
+
import { Arg, Args, Context, FieldResolver, Parent, Query, Resolver, type GraphQLContext } from '@fluojs/graphql';
|
|
117
147
|
|
|
118
148
|
const AuthorType = new GraphQLObjectType({
|
|
119
149
|
name: 'Author',
|
|
@@ -131,6 +161,11 @@ const BookType = new GraphQLObjectType({
|
|
|
131
161
|
},
|
|
132
162
|
});
|
|
133
163
|
|
|
164
|
+
class AuthorInput {
|
|
165
|
+
@Arg('locale')
|
|
166
|
+
locale = 'en';
|
|
167
|
+
}
|
|
168
|
+
|
|
134
169
|
@Resolver()
|
|
135
170
|
class BookQueryResolver {
|
|
136
171
|
@Query({ outputType: BookType })
|
|
@@ -141,18 +176,19 @@ class BookQueryResolver {
|
|
|
141
176
|
|
|
142
177
|
@Resolver('Book')
|
|
143
178
|
class BookFieldResolver {
|
|
144
|
-
@FieldResolver({ fieldName: 'author', type: AuthorType })
|
|
145
|
-
@
|
|
146
|
-
@
|
|
147
|
-
|
|
179
|
+
@FieldResolver({ fieldName: 'author', input: AuthorInput, type: AuthorType })
|
|
180
|
+
@Args(0)
|
|
181
|
+
@Parent(1)
|
|
182
|
+
@Context(2)
|
|
183
|
+
author(input: AuthorInput, book: { authorId: string }, context: GraphQLContext) {
|
|
148
184
|
return authorLoader(context).load(book.authorId);
|
|
149
185
|
}
|
|
150
186
|
}
|
|
151
187
|
```
|
|
152
188
|
|
|
153
|
-
두 resolver class를 module provider 또는 controller로 등록하고, `GraphqlModule.forRoot({ resolvers })`를 allowlist로 사용할 때는 둘 다 포함하세요. 중복 `TypeName.fieldName` 등록, code-first root output에서 도달할 수 없는 field target, root operation method에 배치한 `@Parent()` / `@Context()` binding은 bootstrap 중 실패합니다.
|
|
189
|
+
두 resolver class를 module provider 또는 controller로 등록하고, `GraphqlModule.forRoot({ resolvers })`를 allowlist로 사용할 때는 둘 다 포함하세요. Field resolver DTO input은 root resolver와 같은 HTTP 및 subscription operation container scope를 따릅니다. 중복 `TypeName.fieldName` 등록, code-first root output에서 도달할 수 없는 field target, root operation method에 배치한 `@Args()` / `@Parent()` / `@Context()` binding은 bootstrap 중 실패합니다. Schema-first field-resolver attachment는 이 runtime 계약 범위 밖입니다. 명시적 `type`으로 추가하는 field에는 `nullable: false`를 전달해 non-null GraphQL output을 노출할 수 있으며, `nullable: true`와 option 생략은 GraphQL의 nullable 기본값을 유지합니다. 기존 field configuration은 object type이 이미 소유하므로 `nullable`이 그 declared nullability를 바꾸지 않습니다.
|
|
154
190
|
|
|
155
|
-
###
|
|
191
|
+
### GraphQL Operation 범위 DataLoaders
|
|
156
192
|
내장된 DataLoader 통합을 통해 N+1 문제를 효율적으로 해결합니다. Loader는 각 GraphQL 작업마다 자동으로 격리됩니다.
|
|
157
193
|
|
|
158
194
|
```typescript
|
|
@@ -167,7 +203,7 @@ const UserType = new GraphQLObjectType({
|
|
|
167
203
|
},
|
|
168
204
|
});
|
|
169
205
|
|
|
170
|
-
const userLoader = createDataLoader(async (ids: string[]) => {
|
|
206
|
+
const userLoader = createDataLoader(async (ids: readonly string[]) => {
|
|
171
207
|
const users = await userService.findByIds(ids);
|
|
172
208
|
return ids.map(id => users.find(u => u.id === id));
|
|
173
209
|
});
|
|
@@ -187,14 +223,21 @@ class UserResolver {
|
|
|
187
223
|
```
|
|
188
224
|
|
|
189
225
|
## Resolver Lifecycle 계약
|
|
226
|
+
<!-- fluo:graphql-nestjs-migration: principal=before-graphql; connection-params=untrusted-record; endpoint=fixed-/graphql; nest-path-option=unsupported; root-signature=input-context; decorator-targets=public-instance; private-static-targets=rejected; output-nullability=explicit; arg-nullability=nullable; resolver-scope=request; operation-disposal=completion-or-disconnect; async-iterable-cleanup=application-owned; field-resolver=code-first; schema-first-field-resolver=unsupported; nest-dynamic-module=unsupported; parameter-decorators=unsupported -->
|
|
190
227
|
|
|
191
228
|
- Singleton resolver가 기본값이며, 각 operation에서 애플리케이션 컨테이너를 통해 resolve됩니다.
|
|
192
229
|
- Request-scoped provider를 주입하는 resolver는 resolver 자체에도 `@Scope('request')`를 지정해야 합니다. 이렇게 해야 DI lifetime 규칙이 명시적으로 유지되고 singleton-to-request dependency mismatch를 피할 수 있습니다.
|
|
193
230
|
- `@fluojs/graphql`은 HTTP GraphQL 요청 또는 WebSocket subscription operation마다 operation-scoped DI 컨테이너를 하나 만들고, 해당 operation 안의 resolver 호출들이 이를 공유하며, operation 완료 또는 WebSocket operation 종료 시 dispose합니다.
|
|
194
|
-
-
|
|
231
|
+
- GraphQL이 request를 소비하기 전에 등록된 bootstrap/application middleware만 `requestContext.principal`을 설정할 수 있습니다. `GraphqlModule` 뒤에 등록된 HTTP route guard는 실행되지 않습니다. 각 operation의 resolver에서 `context.principal`로 authorization을 수행하세요.
|
|
232
|
+
- WebSocket `connectionParams`는 client가 제공하는 신뢰할 수 없는 `Record<string, unknown>`입니다. Application-owned subscription setup에서 이를 parse 및 authorize한 뒤 application stream을 만드세요.
|
|
233
|
+
- HTTP endpoint는 `/graphql`로 고정되며 NestJS `GraphQLModule.forRoot({ path })` 설정에 대응하는 fluo option은 없습니다.
|
|
234
|
+
- Resolver decorator에는 public instance target이 필요합니다. Root 및 field decorator는 private/static method를 거부하고, `@Arg()`는 private/static input field를 거부합니다.
|
|
235
|
+
- 새 output field는 `nullable: false`일 때만 non-null입니다. Option을 생략하거나 `nullable: true`면 nullable이며, `@Arg(...)`는 nullable scalar 또는 list argument를 만들고 DTO validation도 이를 SDL의 non-null로 바꾸지 않습니다.
|
|
236
|
+
- Resolver 메서드는 `GraphQLContext`를 받으며, 내장 필드에는 fluo `request`, 앞서 설정된 인증된 HTTP `principal`, WebSocket subscription의 `connectionParams`와 `socket`, 그리고 `GraphqlModule.forRoot({ context })`가 반환한 사용자 정의 필드가 포함됩니다.
|
|
195
237
|
- Object field resolver는 root resolver와 같은 provider scope 및 operation container를 사용합니다. `@Parent()`와 `@Context()`는 positional method argument만 제어합니다.
|
|
196
|
-
-
|
|
238
|
+
- GraphQL operation 범위 DataLoader helper는 같은 `GraphQLContext` operation 경계를 사용하므로 loader cache는 하나의 GraphQL operation 안에서만 공유됩니다.
|
|
197
239
|
- 애플리케이션 shutdown은 WebSocket transport를 등록 해제하고, 살아 있는 WebSocket client를 닫으며, 아직 활성 상태인 WebSocket operation container를 정상 operation 완료 때와 같은 request-scoped provider teardown 경로로 dispose합니다.
|
|
240
|
+
- HTTP operation-container, WebSocket operation-container 또는 WebSocket transport teardown이 실패하면 소유자를 이후 `Application.close()` 재시도까지 보존합니다. Shutdown은 남은 모든 cleanup 실패를 함께 보고하며, 이미 성공한 cleanup은 반복하지 않습니다.
|
|
198
241
|
|
|
199
242
|
```typescript
|
|
200
243
|
import { Inject, Scope } from '@fluojs/core';
|
|
@@ -224,7 +267,7 @@ class RequestResolver {
|
|
|
224
267
|
- **SSE**: Server-Sent Events를 통한 구독(기본값).
|
|
225
268
|
- **WebSockets**: 활성 adapter가 upgrade listener를 지원하는 Node HTTP/S 서버를 노출할 때(예: Node HTTP adapter) 사용할 수 있는 선택적 `graphql-ws` 실시간 구독 지원.
|
|
226
269
|
|
|
227
|
-
지원되는 Node.js `>=
|
|
270
|
+
지원되는 Node.js `>=24.0.0 <27` runtime 범위에서 HTTP query/mutation과 기본 SSE subscription 경로는 fluo의 Web-standard HTTP 추상화를 통해 실행됩니다. 이 내부 transport seam은 Bun, Deno, Cloudflare Workers 지원 보장이 아닙니다. 선택적 WebSocket transport는 server-backed Node HTTP/S adapter 표면도 필요하므로 지원 범위가 더 좁습니다.
|
|
228
271
|
|
|
229
272
|
```typescript
|
|
230
273
|
GraphqlModule.forRoot({
|
|
@@ -250,7 +293,9 @@ GraphqlModule.forRoot({
|
|
|
250
293
|
- `graphiql` 기본값은 `false`입니다. `introspection`은 명시하지 않으면 `graphiql` 설정을 따르므로, production 앱은 기본적으로 비공개 상태를 유지하고 로컬 GraphiQL 세션만 opt in할 수 있습니다.
|
|
251
294
|
- `limits`에는 request validation budget을 전달하거나 `false`를 전달할 수 있습니다. `false`는 fluo 밖에서 동등한 제어를 적용할 때만 사용하세요.
|
|
252
295
|
- Streaming GraphQL 응답은 downstream response stream이 닫히거나 오류를 내면 upstream fetch body를 cancel하므로 SSE subscription 리소스를 즉시 해제합니다.
|
|
253
|
-
-
|
|
296
|
+
- Downstream streaming 실패와 upstream cancellation cleanup 실패가 동시에 발생하면 downstream 실패가 계속 관찰 가능하며 cancellation cleanup은 best-effort로 처리됩니다.
|
|
297
|
+
- GraphQL 스키마 해석 이후 bootstrap이 실패하면 원래 오류를 다시 던지기 전에 실패한 service의 cross-realm GraphQL object allowlist만 제거합니다. Bootstrap은 읽기 전용 ESM namespace 대신 변경 가능한 `graphql/jsutils/instanceOf` module owner를 patch하며, 각 owner는 외부 교체와 재-patch 이후에도 모든 활성 애플리케이션 allowlist를 유지합니다.
|
|
298
|
+
- Shutdown은 해당 module object의 마지막 활성 GraphQL 애플리케이션이 release한 뒤 package가 소유한 patch만 원복합니다. 다른 GraphQL module instance와 다른 integration이 교체한 `instanceOf` 구현은 그대로 둡니다.
|
|
254
299
|
- WebSocket 구독 경로에는 별도의 전송 budget이 기본 적용됩니다: 동시 연결 `100`, 최대 payload 크기 `64 KiB`, 연결당 활성 operation `25`개입니다.
|
|
255
300
|
- `subscriptions.websocket.enabled` 기본값은 `false`입니다. 활성화하려면 upgrade를 지원하는 Node HTTP/S adapter가 필요합니다. `connectionInitWaitTimeoutMs`는 연결 초기화를 위해 `graphql-ws`로 전달되고, `keepAliveMs`는 설정 시 WebSocket keepalive ping 주기를 제어합니다.
|
|
256
301
|
- 무제한 WebSocket 동작이 정말 필요할 때만 `subscriptions.websocket.limits = false`를 사용하고, 그 경우에도 동일한 수준의 외부 제어 수단을 마련해야 합니다.
|
|
@@ -282,14 +327,16 @@ GraphqlModule.forRoot({
|
|
|
282
327
|
## 공개 API
|
|
283
328
|
|
|
284
329
|
- `GraphqlModule.forRoot(options)`: GraphQL 통합을 위한 메인 엔트리 포인트.
|
|
330
|
+
- `GraphqlModule.forRootAsync(options)`: Endpoint wiring 전에 명시적인 application-graph 의존성으로 GraphQL option을 비동기 해석합니다.
|
|
331
|
+
- `GraphqlAsyncModuleOptions<TDependencies>`: 주입된 의존성 tuple에 맞춰 순서대로 `useFactory` parameter를 typing하는 공개 비동기 등록 계약입니다.
|
|
285
332
|
- `Resolver`, `Query`, `Mutation`, `Subscription`: Resolver 및 root operation 데코레이터.
|
|
286
|
-
- `FieldResolver`, `Parent`, `Context`: Code-first object field resolution과 명시적 parent
|
|
333
|
+
- `FieldResolver`, `Args`, `Parent`, `Context`: Code-first object field resolution과 명시적 DTO input, parent, context parameter-index binding.
|
|
287
334
|
- `Arg`: Input DTO 필드를 GraphQL 인자로 매핑하는 데코레이터.
|
|
288
335
|
- `createDataLoader`, `createDataLoaderMap`, `getRequestScopedDataLoader`, `createRequestScopedDataLoaderFactory`, `DataLoader`: DataLoader factory helper와 type.
|
|
289
336
|
- `listOf`, `isGraphqlListTypeRef`: list output type reference helper.
|
|
290
|
-
- `GraphQLContext` 및 export되는 option/metadata type: GraphQL 실행과 module 설정을 위한 타입 정의.
|
|
337
|
+
- `GraphQLContext` 및 export되는 option/metadata type: `subscriptions.websocket.limits`에 사용하는 `GraphqlWebSocketLimitsOptions`를 포함한 GraphQL 실행과 module 설정을 위한 타입 정의.
|
|
291
338
|
|
|
292
|
-
|
|
339
|
+
동기 `GraphqlModule.forRoot(...)` option에는 `schema`, `context`, `plugins`, `graphiql`, `introspection`, `limits`, `subscriptions.websocket.enabled`, `subscriptions.websocket.limits`, `subscriptions.websocket.connectionInitWaitTimeoutMs`, `subscriptions.websocket.keepAliveMs`가 포함됩니다. `GraphqlModule.forRootAsync({ inject, useFactory })`는 별도의 비동기 등록 API이며, 명시적인 `inject` token과 `useFactory`만 받습니다.
|
|
293
340
|
|
|
294
341
|
## 관련 패키지
|
|
295
342
|
|
|
@@ -299,7 +346,8 @@ GraphqlModule.forRoot({
|
|
|
299
346
|
|
|
300
347
|
## 예제 소스
|
|
301
348
|
|
|
349
|
+
- `../../examples/graphql/README.ko.md`: module registration, resolver discovery, operation 범위 DataLoader 사용, SSE subscription을 다루는 공식 실행 가능 애플리케이션입니다.
|
|
302
350
|
- `packages/graphql/src/module.test.ts`: 모듈 등록, resolver 실행, request-scoped container, subscription, guardrail 기본값을 다루는 통합 테스트 및 사용 예제.
|
|
303
|
-
- `packages/graphql/src/field-resolver.test.ts`: Object field
|
|
351
|
+
- `packages/graphql/src/field-resolver-input.test.ts`: Object field DTO input의 HTTP, request scope, validation, scalar/list argument, subscription, binding collision을 실행 가능한 형태로 검증하는 테스트.
|
|
304
352
|
- `packages/graphql/src/runtime-support.test.ts`: Package의 Node.js engine 하한이 필수 first-party dependency graph에서 가장 높은 하한 이상인지 검증하는 회귀 테스트.
|
|
305
353
|
- `packages/graphql/field-resolver-rfc.md`: Object field resolver의 구현된 계약과 후속 범위.
|
package/README.md
CHANGED
|
@@ -22,24 +22,55 @@ Decorator-based GraphQL integration for fluo. Built on **GraphQL Yoga**, it prov
|
|
|
22
22
|
pnpm add @fluojs/graphql graphql graphql-yoga
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`@fluojs/graphql`
|
|
25
|
+
`@fluojs/graphql` includes `ws@^8.21.0` for optional GraphQL-over-WebSocket subscriptions. Refresh the application lockfile when upgrading so the patched package-owned WebSocket runtime is installed; applications do not need to add `ws` directly unless they import it themselves.
|
|
26
|
+
|
|
27
|
+
`@fluojs/graphql` supports Node.js `>=24.0.0 <27` and declares that exact range through `engines.node`. That range is the package's own verified Node support contract; portable `@fluojs/runtime` and `@fluojs/config` have no package-wide `engines.node`, and config guards its Node-only env-file and watch features at execution. HTTP queries/mutations and the default SSE subscription path use Web-standard request/response primitives internally, but that implementation detail does not establish package support for Bun, Deno, or Cloudflare Workers. Those runtimes remain unsupported until the complete dependency metadata and native runtime suites prove the full GraphQL contract. Optional WebSocket subscriptions additionally require an adapter that exposes a server-backed Node HTTP/S upgrade surface.
|
|
26
28
|
|
|
27
29
|
## When to Use
|
|
28
30
|
|
|
29
31
|
- When building type-safe GraphQL APIs using TypeScript decorators (**Code-first**).
|
|
30
32
|
- When integrating an existing executable `GraphQLSchema` object into a fluo application.
|
|
31
33
|
- When you need seamless dependency injection within GraphQL resolvers, including request-scoped providers.
|
|
32
|
-
- When performing efficient data fetching using
|
|
34
|
+
- When performing efficient data fetching using GraphQL-operation-scoped **DataLoader** patterns.
|
|
33
35
|
|
|
34
36
|
## Quick Start
|
|
35
37
|
|
|
36
|
-
Register `GraphqlModule.forRoot(...)` and define a resolver using standard decorators.
|
|
38
|
+
Register `GraphqlModule.forRoot(...)` and define a resolver using standard decorators. Use
|
|
39
|
+
`GraphqlModule.forRootAsync({ inject, useFactory })` when module options must be resolved from
|
|
40
|
+
explicit application-graph dependencies before GraphQL registration.
|
|
41
|
+
|
|
42
|
+
`forRootAsync(...)` resolves its factory once per application context before the GraphQL lifecycle
|
|
43
|
+
starts. It supports only explicit `inject` tokens and `useFactory`; NestJS-style `imports`,
|
|
44
|
+
`useClass`, `useExisting`, and implicit discovery are rejected.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
class GraphqlSettings {
|
|
48
|
+
graphiql = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Module({
|
|
52
|
+
imports: [
|
|
53
|
+
GraphqlModule.forRootAsync({
|
|
54
|
+
inject: [GraphqlSettings],
|
|
55
|
+
useFactory: async (settings) => ({
|
|
56
|
+
graphiql: settings.graphiql,
|
|
57
|
+
resolvers: [HelloResolver],
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
providers: [GraphqlSettings, HelloResolver],
|
|
62
|
+
})
|
|
63
|
+
export class AppModule {}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
No separate example application is added for async registration: this Quick Start and
|
|
67
|
+
[Chapter 18](../../book/intermediate/ch18-graphql.md) are the maintained example surfaces.
|
|
37
68
|
|
|
38
69
|
You can also pass an executable `GraphQLSchema` via `schema` when you want schema-first integration instead of code-first resolver discovery.
|
|
39
70
|
|
|
40
71
|
```typescript
|
|
41
72
|
import { Module } from '@fluojs/core';
|
|
42
|
-
import { bootstrapNodeApplication } from '@fluojs/
|
|
73
|
+
import { bootstrapNodeApplication } from '@fluojs/platform-nodejs';
|
|
43
74
|
import { GraphqlModule, Query, Resolver, Arg } from '@fluojs/graphql';
|
|
44
75
|
|
|
45
76
|
class HelloInput {
|
|
@@ -72,6 +103,8 @@ await app.listen(3000);
|
|
|
72
103
|
// -d '{"query": "{ hello(name: \"fluo\") }"}'
|
|
73
104
|
```
|
|
74
105
|
|
|
106
|
+
Migrating from NestJS? Read the [NestJS → fluo Migration Map](../../docs/getting-started/migrate-from-nestjs.md#graphql-migration-boundaries) before porting resolver authorization, schema nullability, scopes, or subscriptions.
|
|
107
|
+
|
|
75
108
|
## Core Capabilities
|
|
76
109
|
|
|
77
110
|
### Code-first Resolvers
|
|
@@ -109,11 +142,13 @@ class UserResolver {
|
|
|
109
142
|
|
|
110
143
|
`@FieldResolver(...)` attaches a provider method to a field on the named object type owned by `@Resolver('TypeName')`. The target object type must be reachable from a code-first root operation output. The field must already exist on that `GraphQLObjectType`, or the field resolver must declare `type` so the schema builder can add it.
|
|
111
144
|
|
|
112
|
-
|
|
145
|
+
`@FieldResolver({ input })` reuses the root resolver DTO argument pipeline: `@Arg(...)` fields define GraphQL arguments, values are materialized into the DTO, and validation errors remain GraphQL `BAD_USER_INPUT` errors. Use `argTypes` for list argument types exactly as with root operations.
|
|
146
|
+
|
|
147
|
+
TC39 standard decorators do not support parameter decorators. To preserve fluo's standard-decorator contract, `@Args()`, `@Parent()`, and `@Context()` are method decorators that bind zero-based parameter indexes. `@Args()` defaults to `0`, as does `@Parent()`; when a field resolver receives both, assign distinct explicit indexes. `@Context()` defaults to `1`. Binding the same index twice fails during decorator evaluation; `@FieldResolver({ input })` requires `@Args()`, and `@Args()` requires `input`.
|
|
113
148
|
|
|
114
149
|
```typescript
|
|
115
150
|
import { GraphQLObjectType, GraphQLString } from 'graphql';
|
|
116
|
-
import { Context, FieldResolver, Parent, Query, Resolver, type GraphQLContext } from '@fluojs/graphql';
|
|
151
|
+
import { Arg, Args, Context, FieldResolver, Parent, Query, Resolver, type GraphQLContext } from '@fluojs/graphql';
|
|
117
152
|
|
|
118
153
|
const AuthorType = new GraphQLObjectType({
|
|
119
154
|
name: 'Author',
|
|
@@ -131,6 +166,11 @@ const BookType = new GraphQLObjectType({
|
|
|
131
166
|
},
|
|
132
167
|
});
|
|
133
168
|
|
|
169
|
+
class AuthorInput {
|
|
170
|
+
@Arg('locale')
|
|
171
|
+
locale = 'en';
|
|
172
|
+
}
|
|
173
|
+
|
|
134
174
|
@Resolver()
|
|
135
175
|
class BookQueryResolver {
|
|
136
176
|
@Query({ outputType: BookType })
|
|
@@ -141,18 +181,19 @@ class BookQueryResolver {
|
|
|
141
181
|
|
|
142
182
|
@Resolver('Book')
|
|
143
183
|
class BookFieldResolver {
|
|
144
|
-
@FieldResolver({ fieldName: 'author', type: AuthorType })
|
|
145
|
-
@
|
|
146
|
-
@
|
|
147
|
-
|
|
184
|
+
@FieldResolver({ fieldName: 'author', input: AuthorInput, type: AuthorType })
|
|
185
|
+
@Args(0)
|
|
186
|
+
@Parent(1)
|
|
187
|
+
@Context(2)
|
|
188
|
+
author(input: AuthorInput, book: { authorId: string }, context: GraphQLContext) {
|
|
148
189
|
return authorLoader(context).load(book.authorId);
|
|
149
190
|
}
|
|
150
191
|
}
|
|
151
192
|
```
|
|
152
193
|
|
|
153
|
-
Register both resolver classes as module providers or controllers and include both when `GraphqlModule.forRoot({ resolvers })` is used as an allowlist. Duplicate `TypeName.fieldName` registrations, field targets that are not reachable from a code-first root output, and `@Parent()` / `@Context()` bindings placed on root operation methods fail during bootstrap.
|
|
194
|
+
Register both resolver classes as module providers or controllers and include both when `GraphqlModule.forRoot({ resolvers })` is used as an allowlist. Field resolver DTO inputs follow the same HTTP and subscription operation container scope as root resolvers. Duplicate `TypeName.fieldName` registrations, field targets that are not reachable from a code-first root output, and `@Args()` / `@Parent()` / `@Context()` bindings placed on root operation methods fail during bootstrap. Schema-first field-resolver attachment remains outside this runtime contract. For a field added with an explicit `type`, pass `nullable: false` to expose a non-null GraphQL output; `nullable: true` and an omitted option preserve GraphQL's nullable default. Existing field configurations retain their declared nullability because `nullable` does not change fields the object type already owns.
|
|
154
195
|
|
|
155
|
-
###
|
|
196
|
+
### GraphQL-Operation-Scoped DataLoaders
|
|
156
197
|
Efficiently solve the N+1 problem with built-in DataLoader integration. Loaders are automatically isolated per GraphQL operation.
|
|
157
198
|
|
|
158
199
|
```typescript
|
|
@@ -167,7 +208,7 @@ const UserType = new GraphQLObjectType({
|
|
|
167
208
|
},
|
|
168
209
|
});
|
|
169
210
|
|
|
170
|
-
const userLoader = createDataLoader(async (ids: string[]) => {
|
|
211
|
+
const userLoader = createDataLoader(async (ids: readonly string[]) => {
|
|
171
212
|
const users = await userService.findByIds(ids);
|
|
172
213
|
return ids.map(id => users.find(u => u.id === id));
|
|
173
214
|
});
|
|
@@ -187,14 +228,21 @@ class UserResolver {
|
|
|
187
228
|
```
|
|
188
229
|
|
|
189
230
|
## Resolver Lifecycle Contracts
|
|
231
|
+
<!-- fluo:graphql-nestjs-migration: principal=before-graphql; connection-params=untrusted-record; endpoint=fixed-/graphql; nest-path-option=unsupported; root-signature=input-context; decorator-targets=public-instance; private-static-targets=rejected; output-nullability=explicit; arg-nullability=nullable; resolver-scope=request; operation-disposal=completion-or-disconnect; async-iterable-cleanup=application-owned; field-resolver=code-first; schema-first-field-resolver=unsupported; nest-dynamic-module=unsupported; parameter-decorators=unsupported -->
|
|
190
232
|
|
|
191
233
|
- Singleton resolvers are the default and are resolved from the application container for every operation.
|
|
192
234
|
- Resolvers that inject request-scoped providers must also be marked with `@Scope('request')`; this keeps DI lifetime rules explicit and avoids singleton-to-request dependency mismatches.
|
|
193
235
|
- `@fluojs/graphql` creates one operation-scoped DI container for each HTTP GraphQL request or websocket subscription operation, shares it across resolver calls in that operation, and disposes it when the operation completes or the websocket operation disconnects.
|
|
194
|
-
-
|
|
236
|
+
- Only bootstrap/application middleware registered before GraphQL consumes a request can establish `requestContext.principal`; HTTP route guards registered after `GraphqlModule` do not run. Authorize each operation in its resolver using `context.principal`.
|
|
237
|
+
- WebSocket `connectionParams` is an untrusted client-provided `Record<string, unknown>`; parse and authorize it in application-owned subscription setup before creating an application stream.
|
|
238
|
+
- The HTTP endpoint is fixed at `/graphql`; a NestJS `GraphQLModule.forRoot({ path })` setting has no fluo option.
|
|
239
|
+
- Resolver decorators require public instance targets: root and field decorators reject private or static methods, and `@Arg()` rejects private or static input fields.
|
|
240
|
+
- New output fields are non-null only with `nullable: false`; omitted or `nullable: true` fields remain nullable. `@Arg(...)` produces nullable scalar or list arguments, and DTO validation does not make them non-null in the SDL.
|
|
241
|
+
- Resolver methods receive a `GraphQLContext` whose built-in fields expose the underlying fluo `request`, that pre-established authenticated HTTP `principal`, websocket `connectionParams` and `socket` for websocket subscriptions, and any custom fields returned from `GraphqlModule.forRoot({ context })`.
|
|
195
242
|
- Object field resolvers use the same provider scope and operation container as root resolvers; `@Parent()` and `@Context()` only control positional method arguments.
|
|
196
|
-
-
|
|
243
|
+
- GraphQL-operation-scoped DataLoader helpers use the same `GraphQLContext` operation boundary, so loader caches are shared only within one GraphQL operation.
|
|
197
244
|
- Application shutdown unregisters the websocket transport, closes live websocket clients, and disposes any still-active websocket operation containers through the same request-scoped provider teardown path used when an operation completes normally.
|
|
245
|
+
- Failed HTTP operation-container, websocket operation-container, or websocket transport teardown retains its owner for a later `Application.close()` retry. Shutdown reports every remaining cleanup failure together and never repeats cleanup that already succeeded.
|
|
198
246
|
|
|
199
247
|
```typescript
|
|
200
248
|
import { Inject, Scope } from '@fluojs/core';
|
|
@@ -224,7 +272,7 @@ class RequestResolver {
|
|
|
224
272
|
- **SSE**: Subscriptions over Server-Sent Events (default).
|
|
225
273
|
- **WebSockets**: Optional `graphql-ws` support for real-time subscriptions when the active adapter exposes a Node HTTP/S server with upgrade listeners (for example, the Node HTTP adapter).
|
|
226
274
|
|
|
227
|
-
On the supported Node.js `>=
|
|
275
|
+
On the supported Node.js `>=24.0.0 <27` runtime range, HTTP queries/mutations and the default SSE subscription path run through fluo's Web-standard HTTP abstraction. This internal transport seam is not a Bun, Deno, or Cloudflare Workers support guarantee. The optional websocket transport is narrower still because it requires a server-backed Node HTTP/S adapter surface.
|
|
228
276
|
|
|
229
277
|
```typescript
|
|
230
278
|
GraphqlModule.forRoot({
|
|
@@ -250,7 +298,9 @@ GraphqlModule.forRoot({
|
|
|
250
298
|
- `graphiql` defaults to `false`. `introspection` follows `graphiql` unless set explicitly, so production apps stay private by default while local GraphiQL sessions can opt in.
|
|
251
299
|
- `limits` accepts request validation budgets or `false`; use `false` only when equivalent controls exist outside fluo.
|
|
252
300
|
- Streaming GraphQL responses cancel the upstream fetch body when the downstream response stream closes or errors, so SSE subscription resources are released promptly.
|
|
253
|
-
-
|
|
301
|
+
- If downstream streaming fails while upstream cancellation cleanup also fails, the downstream failure remains observable and cancellation cleanup is best-effort.
|
|
302
|
+
- Bootstrap failures after GraphQL schema resolution remove only the failed service's cross-realm GraphQL object allowlist before rethrowing. Bootstrap patches the mutable `graphql/jsutils/instanceOf` module owner rather than its read-only ESM namespace, and each owner retains every active application allowlist across external replacement and re-patch.
|
|
303
|
+
- Shutdown restores only the package-owned patch for that module object after its final active GraphQL application releases. It leaves other GraphQL module instances and an `instanceOf` implementation replaced by another integration untouched.
|
|
254
304
|
- WebSocket subscriptions use separate transport budgets by default: `100` concurrent connections, `64 KiB` maximum payload size, and `25` active operations per connection.
|
|
255
305
|
- `subscriptions.websocket.enabled` defaults to `false`; enabling it requires a Node HTTP/S adapter with upgrade support. `connectionInitWaitTimeoutMs` is forwarded to `graphql-ws` for connection initialization, and `keepAliveMs` controls websocket keepalive pings when configured.
|
|
256
306
|
- Set `subscriptions.websocket.limits = false` only when you intentionally need unbounded websocket behavior and can enforce equivalent controls elsewhere.
|
|
@@ -282,14 +332,16 @@ GraphqlModule.forRoot({
|
|
|
282
332
|
## Public API
|
|
283
333
|
|
|
284
334
|
- `GraphqlModule.forRoot(options)`: Main entry point for GraphQL integration.
|
|
335
|
+
- `GraphqlModule.forRootAsync(options)`: Asynchronously resolves GraphQL options from explicit application-graph dependencies before endpoint wiring.
|
|
336
|
+
- `GraphqlAsyncModuleOptions<TDependencies>`: Public async registration contract whose injected dependency tuple types the `useFactory` parameters in order.
|
|
285
337
|
- `Resolver`, `Query`, `Mutation`, `Subscription`: Resolver and root operation decorators.
|
|
286
|
-
- `FieldResolver`, `Parent`, `Context`: Code-first object field resolution and explicit parent
|
|
338
|
+
- `FieldResolver`, `Args`, `Parent`, `Context`: Code-first object field resolution and explicit DTO input, parent, and context parameter-index bindings.
|
|
287
339
|
- `Arg`: Input DTO field-to-GraphQL-argument mapping decorator.
|
|
288
340
|
- `createDataLoader`, `createDataLoaderMap`, `getRequestScopedDataLoader`, `createRequestScopedDataLoaderFactory`, `DataLoader`: DataLoader factory helpers and types.
|
|
289
341
|
- `listOf`, `isGraphqlListTypeRef`: Helpers for list output type references.
|
|
290
|
-
- `GraphQLContext` and exported option/metadata types: Type definitions for GraphQL execution and module configuration.
|
|
342
|
+
- `GraphQLContext` and exported option/metadata types: Type definitions for GraphQL execution and module configuration, including `GraphqlWebSocketLimitsOptions` for `subscriptions.websocket.limits`.
|
|
291
343
|
|
|
292
|
-
Supported
|
|
344
|
+
Supported synchronous `GraphqlModule.forRoot(...)` options include `schema`, `context`, `plugins`, `graphiql`, `introspection`, `limits`, `subscriptions.websocket.enabled`, `subscriptions.websocket.limits`, `subscriptions.websocket.connectionInitWaitTimeoutMs`, and `subscriptions.websocket.keepAliveMs`. `GraphqlModule.forRootAsync({ inject, useFactory })` is the separate asynchronous registration API; it accepts only explicit `inject` tokens and `useFactory`.
|
|
293
345
|
|
|
294
346
|
## Related Packages
|
|
295
347
|
|
|
@@ -299,7 +351,8 @@ Supported module options include `schema`, `context`, `plugins`, `graphiql`, `in
|
|
|
299
351
|
|
|
300
352
|
## Example Sources
|
|
301
353
|
|
|
354
|
+
- `../../examples/graphql/README.md`: Official runnable application for module registration, resolver discovery, operation-scoped DataLoader use, and an SSE subscription.
|
|
302
355
|
- `packages/graphql/src/module.test.ts`: Integration tests and usage examples for module registration, resolver execution, request-scoped containers, subscriptions, and guardrail defaults.
|
|
303
|
-
- `packages/graphql/src/field-resolver.test.ts`: Executable
|
|
356
|
+
- `packages/graphql/src/field-resolver-input.test.ts`: Executable HTTP, request-scope, validation, scalar/list argument, subscription, and binding-collision coverage for object field DTO inputs.
|
|
304
357
|
- `packages/graphql/src/runtime-support.test.ts`: Regression coverage that keeps the package's Node.js engine floor at or above the highest floor in its mandatory first-party dependency graph.
|
|
305
358
|
- `packages/graphql/field-resolver-rfc.md`: Implemented contract and follow-up boundaries for object field resolvers.
|
|
@@ -1 +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,
|
|
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,CAYT;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"}
|
|
@@ -12,9 +12,8 @@ import { GRAPHQL_REQUEST_SCOPED_LOADER_CACHE } from '../types.js';
|
|
|
12
12
|
export function getRequestScopedDataLoader(context, key, createLoader) {
|
|
13
13
|
const cache = context[GRAPHQL_REQUEST_SCOPED_LOADER_CACHE] ?? new Map();
|
|
14
14
|
context[GRAPHQL_REQUEST_SCOPED_LOADER_CACHE] = cache;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return existing;
|
|
15
|
+
if (cache.has(key)) {
|
|
16
|
+
return cache.get(key);
|
|
18
17
|
}
|
|
19
18
|
const created = createLoader();
|
|
20
19
|
cache.set(key, created);
|
package/dist/decorators.d.ts
CHANGED
|
@@ -15,7 +15,9 @@ export interface ResolverMethodOptions {
|
|
|
15
15
|
* Describes an object field resolver's field name and optional output type override.
|
|
16
16
|
*/
|
|
17
17
|
export interface FieldResolverOptions {
|
|
18
|
+
argTypes?: Record<string, GraphqlArgType>;
|
|
18
19
|
fieldName?: string;
|
|
20
|
+
input?: Function;
|
|
19
21
|
type?: GraphqlRootOutputType;
|
|
20
22
|
nullable?: boolean;
|
|
21
23
|
}
|
|
@@ -81,6 +83,18 @@ export declare function Parent(parameterIndex?: number): MethodDecoratorLike;
|
|
|
81
83
|
* @returns A TC39 standard method decorator.
|
|
82
84
|
*/
|
|
83
85
|
export declare function Context(parameterIndex?: number): MethodDecoratorLike;
|
|
86
|
+
/**
|
|
87
|
+
* Binds a field resolver method parameter to the DTO materialized from GraphQL field arguments.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* TC39 standard decorators do not support parameter decorators, so this
|
|
91
|
+
* standard method decorator records the parameter index explicitly. The default
|
|
92
|
+
* index is `0`. Pair it with `@FieldResolver({ input: InputDto })`.
|
|
93
|
+
*
|
|
94
|
+
* @param parameterIndex Zero-based method parameter index to receive the validated input DTO.
|
|
95
|
+
* @returns A TC39 standard method decorator.
|
|
96
|
+
*/
|
|
97
|
+
export declare function Args(parameterIndex?: number): MethodDecoratorLike;
|
|
84
98
|
/**
|
|
85
99
|
* Arg.
|
|
86
100
|
*
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAIV,cAAc,EACd,qBAAqB,EAGtB,MAAM,YAAY,CAAC;AAGpB,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;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AACnD,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AACrD,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAIV,cAAc,EACd,qBAAqB,EAGtB,MAAM,YAAY,CAAC;AAGpB,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;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AACnD,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AACrD,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AAoLnD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAQ9D;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,mBAAmB,CAE9F;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,mBAAmB,CAEjG;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,qBAAqB,GAAG,mBAAmB,CAErG;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,oBAAoB,GAAG,mBAAmB,CAerG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CAAC,cAAc,SAAI,GAAG,mBAAmB,CAE9D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,cAAc,SAAI,GAAG,mBAAmB,CAE/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,cAAc,SAAI,GAAG,mBAAmB,CAE5D;AAED;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAmBxD"}
|
package/dist/decorators.js
CHANGED
|
@@ -111,23 +111,29 @@ function normalizeFieldResolverMetadata(fieldNameOrOptions) {
|
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
return {
|
|
114
|
+
argTypes: fieldNameOrOptions?.argTypes,
|
|
114
115
|
fieldName: fieldNameOrOptions?.fieldName?.trim() || undefined,
|
|
116
|
+
inputClass: fieldNameOrOptions?.input,
|
|
115
117
|
nullable: fieldNameOrOptions?.nullable,
|
|
116
118
|
outputType: fieldNameOrOptions?.type,
|
|
117
119
|
type: 'field'
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
function createFieldResolverParameterDecorator(kind, parameterIndex) {
|
|
123
|
+
const decoratorName = {
|
|
124
|
+
context: 'Context',
|
|
125
|
+
input: 'Args',
|
|
126
|
+
parent: 'Parent'
|
|
127
|
+
}[kind];
|
|
121
128
|
if (!Number.isSafeInteger(parameterIndex) || parameterIndex < 0) {
|
|
122
|
-
throw new Error(`@${
|
|
129
|
+
throw new Error(`@${decoratorName}() parameter index must be a non-negative integer.`);
|
|
123
130
|
}
|
|
124
131
|
const decorator = (_value, context) => {
|
|
125
|
-
const name = kind === 'parent' ? 'Parent' : 'Context';
|
|
126
132
|
if (context.private) {
|
|
127
|
-
throw new Error(`@${
|
|
133
|
+
throw new Error(`@${decoratorName}() cannot be used on private methods.`);
|
|
128
134
|
}
|
|
129
135
|
if (context.static) {
|
|
130
|
-
throw new Error(`@${
|
|
136
|
+
throw new Error(`@${decoratorName}() cannot be used on static methods.`);
|
|
131
137
|
}
|
|
132
138
|
defineStandardFieldResolverParameterMetadata(context.metadata, context.name, parameterIndex, kind);
|
|
133
139
|
};
|
|
@@ -229,6 +235,21 @@ export function Context(parameterIndex = 1) {
|
|
|
229
235
|
return createFieldResolverParameterDecorator('context', parameterIndex);
|
|
230
236
|
}
|
|
231
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Binds a field resolver method parameter to the DTO materialized from GraphQL field arguments.
|
|
240
|
+
*
|
|
241
|
+
* @remarks
|
|
242
|
+
* TC39 standard decorators do not support parameter decorators, so this
|
|
243
|
+
* standard method decorator records the parameter index explicitly. The default
|
|
244
|
+
* index is `0`. Pair it with `@FieldResolver({ input: InputDto })`.
|
|
245
|
+
*
|
|
246
|
+
* @param parameterIndex Zero-based method parameter index to receive the validated input DTO.
|
|
247
|
+
* @returns A TC39 standard method decorator.
|
|
248
|
+
*/
|
|
249
|
+
export function Args(parameterIndex = 0) {
|
|
250
|
+
return createFieldResolverParameterDecorator('input', parameterIndex);
|
|
251
|
+
}
|
|
252
|
+
|
|
232
253
|
/**
|
|
233
254
|
* Arg.
|
|
234
255
|
*
|
package/dist/discovery.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAQtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkI3E;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,oBAAoB,GAC5B,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAQtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkI3E;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,oBAAoB,GAC5B,kBAAkB,EAAE,CAoEtB"}
|
package/dist/discovery.js
CHANGED
|
@@ -129,7 +129,13 @@ export function discoverResolverDescriptors(compiledModules, options) {
|
|
|
129
129
|
const argFields = inputClass !== undefined ? getArgFieldMetadataEntries(inputClass.prototype).map(argField => argField.metadata) : [];
|
|
130
130
|
const parameterBindings = getFieldResolverParameterMetadataEntries(candidate.targetType.prototype, entry.propertyKey);
|
|
131
131
|
if (entry.metadata.type !== 'field' && parameterBindings.length > 0) {
|
|
132
|
-
throw new Error(`@Parent() and @Context() can only bind parameters on @FieldResolver() methods. ` + `Invalid placement: ${candidate.targetType.name}.${methodKeyToName(entry.propertyKey)}.`);
|
|
132
|
+
throw new Error(`@Parent() and @Context() can only bind parameters on @FieldResolver() methods; @Args() follows the same rule. ` + `Invalid placement: ${candidate.targetType.name}.${methodKeyToName(entry.propertyKey)}.`);
|
|
133
|
+
}
|
|
134
|
+
if (entry.metadata.type === 'field' && inputClass !== undefined && !parameterBindings.some(binding => binding.kind === 'input')) {
|
|
135
|
+
throw new Error(`@FieldResolver({ input }) requires @Args() on ${candidate.targetType.name}.${methodKeyToName(entry.propertyKey)}.`);
|
|
136
|
+
}
|
|
137
|
+
if (entry.metadata.type === 'field' && inputClass === undefined && parameterBindings.some(binding => binding.kind === 'input')) {
|
|
138
|
+
throw new Error(`@Args() requires @FieldResolver({ input }) on ${candidate.targetType.name}.${methodKeyToName(entry.propertyKey)}.`);
|
|
133
139
|
}
|
|
134
140
|
return {
|
|
135
141
|
argFields,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './dataloader.js';
|
|
2
2
|
export * from './decorators.js';
|
|
3
3
|
export { GraphqlModule } from './module.js';
|
|
4
|
-
export type { ArgFieldMetadata, FieldResolverParameterBindingMetadata, FieldResolverParameterKind, GraphQLContext, GraphqlArgType, GraphqlListTypeRef, GraphqlModuleOptions, GraphqlRequestContext, GraphqlRequestLimitsOptions, GraphqlRootOutputNamedType, GraphqlRootOutputType, GraphqlScalarTypeName, GraphqlSubscriptionsOptions, GraphqlWebSocketSubscriptionsOptions, ResolverHandlerMetadata, ResolverHandlerType, ResolverMetadata, } from './types.js';
|
|
4
|
+
export type { ArgFieldMetadata, FieldResolverParameterBindingMetadata, FieldResolverParameterKind, GraphQLContext, GraphqlArgType, GraphqlAsyncModuleOptions, GraphqlListTypeRef, GraphqlModuleOptions, GraphqlRequestContext, GraphqlRequestLimitsOptions, GraphqlRootOutputNamedType, GraphqlRootOutputType, GraphqlScalarTypeName, GraphqlSubscriptionsOptions, GraphqlWebSocketLimitsOptions, GraphqlWebSocketSubscriptionsOptions, ResolverHandlerMetadata, ResolverHandlerType, ResolverMetadata, } from './types.js';
|
|
5
5
|
export { isGraphqlListTypeRef, listOf, } from './types.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EACV,gBAAgB,EAChB,qCAAqC,EACrC,0BAA0B,EAC1B,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACrB,2BAA2B,EAC3B,oCAAoC,EACpC,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,MAAM,GACP,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EACV,gBAAgB,EAChB,qCAAqC,EACrC,0BAA0B,EAC1B,cAAc,EACd,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACrB,2BAA2B,EAC3B,6BAA6B,EAC7B,oCAAoC,EACpC,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,MAAM,GACP,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Represents a GraphQL constructor inspected by the private `instanceOf` helper. */
|
|
2
|
+
export type GraphqlConstructor = Function & {
|
|
3
|
+
readonly prototype: {
|
|
4
|
+
readonly [Symbol.toStringTag]?: string;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
/** Represents the private GraphQL `instanceOf` helper signature. */
|
|
8
|
+
export type GraphqlInstanceOf = (value: unknown, constructor: GraphqlConstructor) => boolean;
|
|
9
|
+
/** Represents the mutable private GraphQL module object that owns the helper. */
|
|
10
|
+
export type GraphqlInstanceOfModule = {
|
|
11
|
+
instanceOf: GraphqlInstanceOf;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Installs and releases a cross-realm GraphQL `instanceOf` patch for one module object.
|
|
15
|
+
*
|
|
16
|
+
* @param instanceOfModule The GraphQL module object that owns the `instanceOf` helper.
|
|
17
|
+
* @param allowedObjects The active application's cross-realm GraphQL object allowlist.
|
|
18
|
+
* @returns A one-time release callback for the application's allowlist.
|
|
19
|
+
*/
|
|
20
|
+
export declare function installGraphqlInstanceOfPatch(instanceOfModule: GraphqlInstanceOfModule, allowedObjects: WeakSet<object>): () => void;
|
|
21
|
+
//# sourceMappingURL=instance-of-patch.d.ts.map
|