@fluojs/graphql 1.0.0-beta.1

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