@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 CHANGED
@@ -22,24 +22,50 @@ fluo를 위한 데코레이터 기반 GraphQL 통합 패키지입니다. **Graph
22
22
  pnpm add @fluojs/graphql graphql graphql-yoga
23
23
  ```
24
24
 
25
- `@fluojs/graphql`은 Node.js `>=20.16.0`을 지원하며, 유효 하한을 `engines.node`로 선언합니다. 필수 dependency graph는 `@fluojs/runtime`을 통해 `@fluojs/config`에 도달하며, `@fluojs/config`도 Node.js `>=20.16.0`을 요구합니다. 다른 필수 first-party dependency가 선언한 낮은 하한은 이 범위와 호환됩니다. 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도 필요합니다.
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
- - Request-scoped **DataLoader** 패턴을 사용하여 효율적인 데이터 페칭을 수행할 때.
34
+ - GraphQL operation 범위 **DataLoader** 패턴을 사용하여 효율적인 데이터 페칭을 수행할 때.
33
35
 
34
36
  ## 빠른 시작
35
37
 
36
- `GraphqlModule.forRoot(...)`를 등록하고 표준 데코레이터를 사용하여 resolver를 정의합니다. 현재 `@fluojs/graphql`는 동기 모듈 엔트리포인트만 제공하며 `GraphqlModule.forRootAsync(...)` 계약은 없습니다.
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/runtime/node';
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
- TC39 표준 데코레이터는 parameter decorator를 지원하지 않습니다. fluo의 standard-decorator 계약을 지키기 위해 `@Parent()`와 `@Context()`는 zero-based parameter index바인딩하는 method decorator입니다. 기본값은 parent/source object를 parameter `0`에, `GraphQLContext`를 parameter `1`에 매핑합니다. Method 순서가 다르면 index를 명시적으로 전달하세요.
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
- @Parent()
146
- @Context()
147
- author(book: { authorId: string }, context: GraphQLContext) {
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 중 실패합니다. Field argument DTO binding과 schema-first field-resolver attachment는 runtime 계약 범위 밖입니다. `nullable` option은 예약되어 있습니다. 기존 field nullability는 유지되며, `type`으로 추가한 field는 GraphQL의 nullable 기본값을 사용합니다.
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
- ### Request-Scoped DataLoaders
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
- - Resolver 메서드는 `GraphQLContext`를 받으며, 내장 필드에는 fluo `request`, middleware 또는 guard가 설정한 인증된 HTTP `principal`, WebSocket subscription의 `connectionParams`와 `socket`, 그리고 `GraphqlModule.forRoot({ context })`가 반환한 사용자 정의 필드가 포함됩니다.
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
- - Request-scoped DataLoader helper는 같은 `GraphQLContext` operation 경계를 사용하므로 loader cache는 하나의 GraphQL operation 안에서만 공유됩니다.
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 `>=20.16.0` runtime에서 HTTP query/mutation과 기본 SSE subscription 경로는 fluo의 Web-standard HTTP 추상화를 통해 실행됩니다. 이 내부 transport seam은 Bun, Deno, Cloudflare Workers 지원 보장이 아닙니다. 선택적 WebSocket transport는 server-backed Node HTTP/S adapter 표면도 필요하므로 지원 범위가 더 좁습니다.
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
- - GraphQL 스키마 해석 이후 bootstrap이 실패하면 임시 `graphql/jsutils/instanceOf` 패치를 원복한 원래 오류를 다시 던지므로, 실패한 시작 시도가 이후 애플리케이션 시작의 process-wide GraphQL 동작을 오염시키지 않습니다.
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/context parameter-index binding.
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
- 지원되는 module option에는 `schema`, `context`, `plugins`, `graphiql`, `introspection`, `limits`, `subscriptions.websocket.enabled`, `subscriptions.websocket.limits`, `subscriptions.websocket.connectionInitWaitTimeoutMs`, `subscriptions.websocket.keepAliveMs`가 포함됩니다.
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 resolverdiscovery, schema attachment, parent/context binding, invalid placement를 실행 가능한 형태로 검증하는 테스트.
351
+ - `packages/graphql/src/field-resolver-input.test.ts`: Object field DTO inputHTTP, 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` supports Node.js `>=20.16.0` and declares that effective floor through `engines.node`. Its mandatory dependency graph reaches `@fluojs/config` through `@fluojs/runtime`; `@fluojs/config` also requires Node.js `>=20.16.0`, while the lower floors declared by other mandatory first-party dependencies remain compatible. 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.
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 request-scoped **DataLoader** patterns.
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. `@fluojs/graphql` currently exposes a synchronous module entrypoint only; there is no `GraphqlModule.forRootAsync(...)` contract.
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/runtime/node';
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
- TC39 standard decorators do not support parameter decorators. To preserve fluo's standard-decorator contract, `@Parent()` and `@Context()` are method decorators that bind zero-based parameter indexes. Their defaults map the parent/source object to parameter `0` and `GraphQLContext` to parameter `1`; pass an explicit index when your method uses a different order.
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
- @Parent()
146
- @Context()
147
- author(book: { authorId: string }, context: GraphQLContext) {
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. Field argument DTO binding and schema-first field-resolver attachment remain outside this first runtime contract. The `nullable` option is reserved; existing field nullability is preserved, while fields added with `type` use GraphQL's nullable default.
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
- ### Request-Scoped DataLoaders
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
- - Resolver methods receive a `GraphQLContext` whose built-in fields expose the underlying fluo `request`, the authenticated HTTP `principal` when middleware or guards set one, websocket `connectionParams` and `socket` for websocket subscriptions, and any custom fields returned from `GraphqlModule.forRoot({ context })`.
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
- - Request-scoped DataLoader helpers use the same `GraphQLContext` operation boundary, so loader caches are shared only within one GraphQL operation.
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 `>=20.16.0` runtime, 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.
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
- - Bootstrap failures after GraphQL schema resolution restore the package's temporary `graphql/jsutils/instanceOf` patch before rethrowing, so failed startups do not leak process-wide GraphQL behavior into later app attempts.
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/context parameter-index bindings.
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 module options include `schema`, `context`, `plugins`, `graphiql`, `introspection`, `limits`, `subscriptions.websocket.enabled`, `subscriptions.websocket.limits`, `subscriptions.websocket.connectionInitWaitTimeoutMs`, and `subscriptions.websocket.keepAliveMs`.
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 discovery, schema attachment, parent/context binding, and invalid-placement coverage for object field resolvers.
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,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"}
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
- const existing = cache.get(key);
16
- if (existing !== undefined) {
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);
@@ -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
  *
@@ -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;AA8KnD;;;;;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;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAmBxD"}
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"}
@@ -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(`@${kind === 'parent' ? 'Parent' : 'Context'}() parameter index must be a non-negative integer.`);
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(`@${name}() cannot be used on private methods.`);
133
+ throw new Error(`@${decoratorName}() cannot be used on private methods.`);
128
134
  }
129
135
  if (context.static) {
130
- throw new Error(`@${name}() cannot be used on static methods.`);
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
  *
@@ -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,CAwDtB"}
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
@@ -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