@effect-gql/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/dist/analyzer-extension.d.ts +105 -0
- package/dist/analyzer-extension.d.ts.map +1 -0
- package/dist/analyzer-extension.js +137 -0
- package/dist/analyzer-extension.js.map +1 -0
- package/dist/builder/execute.d.ts +26 -0
- package/dist/builder/execute.d.ts.map +1 -0
- package/dist/builder/execute.js +104 -0
- package/dist/builder/execute.js.map +1 -0
- package/dist/builder/field-builders.d.ts +30 -0
- package/dist/builder/field-builders.d.ts.map +1 -0
- package/dist/builder/field-builders.js +200 -0
- package/dist/builder/field-builders.js.map +1 -0
- package/dist/builder/index.d.ts +7 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +31 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/builder/pipe-api.d.ts +231 -0
- package/dist/builder/pipe-api.d.ts.map +1 -0
- package/dist/builder/pipe-api.js +151 -0
- package/dist/builder/pipe-api.js.map +1 -0
- package/dist/builder/schema-builder.d.ts +301 -0
- package/dist/builder/schema-builder.d.ts.map +1 -0
- package/dist/builder/schema-builder.js +566 -0
- package/dist/builder/schema-builder.js.map +1 -0
- package/dist/builder/type-registry.d.ts +80 -0
- package/dist/builder/type-registry.d.ts.map +1 -0
- package/dist/builder/type-registry.js +505 -0
- package/dist/builder/type-registry.js.map +1 -0
- package/dist/builder/types.d.ts +283 -0
- package/dist/builder/types.d.ts.map +1 -0
- package/dist/builder/types.js +3 -0
- package/dist/builder/types.js.map +1 -0
- package/dist/cli/generate-schema.d.ts +29 -0
- package/dist/cli/generate-schema.d.ts.map +1 -0
- package/dist/cli/generate-schema.js +233 -0
- package/dist/cli/generate-schema.js.map +1 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +24 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +11 -0
- package/dist/context.js.map +1 -0
- package/dist/error.d.ts +45 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +29 -0
- package/dist/error.js.map +1 -0
- package/dist/extensions.d.ts +130 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +78 -0
- package/dist/extensions.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +169 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +237 -0
- package/dist/loader.js.map +1 -0
- package/dist/resolver-context.d.ts +154 -0
- package/dist/resolver-context.d.ts.map +1 -0
- package/dist/resolver-context.js +184 -0
- package/dist/resolver-context.js.map +1 -0
- package/dist/schema-mapping.d.ts +30 -0
- package/dist/schema-mapping.d.ts.map +1 -0
- package/dist/schema-mapping.js +280 -0
- package/dist/schema-mapping.js.map +1 -0
- package/dist/server/cache-control.d.ts +96 -0
- package/dist/server/cache-control.d.ts.map +1 -0
- package/dist/server/cache-control.js +308 -0
- package/dist/server/cache-control.js.map +1 -0
- package/dist/server/complexity.d.ts +165 -0
- package/dist/server/complexity.d.ts.map +1 -0
- package/dist/server/complexity.js +433 -0
- package/dist/server/complexity.js.map +1 -0
- package/dist/server/config.d.ts +66 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +104 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/graphiql.d.ts +5 -0
- package/dist/server/graphiql.d.ts.map +1 -0
- package/dist/server/graphiql.js +43 -0
- package/dist/server/graphiql.js.map +1 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +48 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/router.d.ts +79 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +232 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema-builder-extensions.d.ts +42 -0
- package/dist/server/schema-builder-extensions.d.ts.map +1 -0
- package/dist/server/schema-builder-extensions.js +48 -0
- package/dist/server/schema-builder-extensions.js.map +1 -0
- package/dist/server/sse-adapter.d.ts +64 -0
- package/dist/server/sse-adapter.d.ts.map +1 -0
- package/dist/server/sse-adapter.js +227 -0
- package/dist/server/sse-adapter.js.map +1 -0
- package/dist/server/sse-types.d.ts +192 -0
- package/dist/server/sse-types.d.ts.map +1 -0
- package/dist/server/sse-types.js +63 -0
- package/dist/server/sse-types.js.map +1 -0
- package/dist/server/ws-adapter.d.ts +39 -0
- package/dist/server/ws-adapter.d.ts.map +1 -0
- package/dist/server/ws-adapter.js +247 -0
- package/dist/server/ws-adapter.js.map +1 -0
- package/dist/server/ws-types.d.ts +169 -0
- package/dist/server/ws-types.d.ts.map +1 -0
- package/dist/server/ws-types.js +11 -0
- package/dist/server/ws-types.js.map +1 -0
- package/dist/server/ws-utils.d.ts +42 -0
- package/dist/server/ws-utils.d.ts.map +1 -0
- package/dist/server/ws-utils.js +99 -0
- package/dist/server/ws-utils.js.map +1 -0
- package/package.json +61 -0
- package/src/analyzer-extension.ts +254 -0
- package/src/builder/execute.ts +153 -0
- package/src/builder/field-builders.ts +322 -0
- package/src/builder/index.ts +48 -0
- package/src/builder/pipe-api.ts +312 -0
- package/src/builder/schema-builder.ts +970 -0
- package/src/builder/type-registry.ts +670 -0
- package/src/builder/types.ts +305 -0
- package/src/context.ts +23 -0
- package/src/error.ts +32 -0
- package/src/extensions.ts +240 -0
- package/src/index.ts +32 -0
- package/src/loader.ts +363 -0
- package/src/resolver-context.ts +253 -0
- package/src/schema-mapping.ts +307 -0
- package/src/server/cache-control.ts +590 -0
- package/src/server/complexity.ts +774 -0
- package/src/server/config.ts +174 -0
- package/src/server/graphiql.ts +38 -0
- package/src/server/index.ts +96 -0
- package/src/server/router.ts +432 -0
- package/src/server/schema-builder-extensions.ts +51 -0
- package/src/server/sse-adapter.ts +327 -0
- package/src/server/sse-types.ts +234 -0
- package/src/server/ws-adapter.ts +355 -0
- package/src/server/ws-types.ts +192 -0
- package/src/server/ws-utils.ts +136 -0
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
import { Effect, Pipeable } from "effect"
|
|
2
|
+
import * as S from "effect/Schema"
|
|
3
|
+
import {
|
|
4
|
+
GraphQLSchema,
|
|
5
|
+
GraphQLObjectType,
|
|
6
|
+
GraphQLInterfaceType,
|
|
7
|
+
GraphQLEnumType,
|
|
8
|
+
GraphQLUnionType,
|
|
9
|
+
GraphQLInputObjectType,
|
|
10
|
+
GraphQLFieldConfigMap,
|
|
11
|
+
GraphQLDirective,
|
|
12
|
+
DirectiveLocation,
|
|
13
|
+
} from "graphql"
|
|
14
|
+
import type {
|
|
15
|
+
FieldRegistration,
|
|
16
|
+
TypeRegistration,
|
|
17
|
+
InterfaceRegistration,
|
|
18
|
+
EnumRegistration,
|
|
19
|
+
UnionRegistration,
|
|
20
|
+
InputTypeRegistration,
|
|
21
|
+
DirectiveRegistration,
|
|
22
|
+
DirectiveApplication,
|
|
23
|
+
SubscriptionFieldRegistration,
|
|
24
|
+
ObjectFieldRegistration,
|
|
25
|
+
MiddlewareRegistration,
|
|
26
|
+
MiddlewareContext,
|
|
27
|
+
CacheHint,
|
|
28
|
+
} from "./types"
|
|
29
|
+
import type { GraphQLExtension, ExecutionArgs } from "../extensions"
|
|
30
|
+
import type {
|
|
31
|
+
GraphQLResolveInfo,
|
|
32
|
+
DocumentNode,
|
|
33
|
+
ExecutionResult,
|
|
34
|
+
GraphQLError as GQLError,
|
|
35
|
+
} from "graphql"
|
|
36
|
+
import type { FieldComplexity, FieldComplexityMap } from "../server/complexity"
|
|
37
|
+
import type { CacheHintMap } from "../server/cache-control"
|
|
38
|
+
import {
|
|
39
|
+
getSchemaName,
|
|
40
|
+
schemaToFields,
|
|
41
|
+
schemaToInputFields,
|
|
42
|
+
toGraphQLArgsWithRegistry,
|
|
43
|
+
buildReverseLookups,
|
|
44
|
+
buildInputTypeLookupCache,
|
|
45
|
+
type TypeConversionContext,
|
|
46
|
+
} from "./type-registry"
|
|
47
|
+
import {
|
|
48
|
+
buildField,
|
|
49
|
+
buildObjectField,
|
|
50
|
+
buildSubscriptionField,
|
|
51
|
+
type FieldBuilderContext,
|
|
52
|
+
} from "./field-builders"
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Internal state for the builder
|
|
56
|
+
*/
|
|
57
|
+
interface BuilderState {
|
|
58
|
+
types: Map<string, TypeRegistration>
|
|
59
|
+
interfaces: Map<string, InterfaceRegistration>
|
|
60
|
+
enums: Map<string, EnumRegistration>
|
|
61
|
+
unions: Map<string, UnionRegistration>
|
|
62
|
+
inputs: Map<string, InputTypeRegistration>
|
|
63
|
+
directives: Map<string, DirectiveRegistration>
|
|
64
|
+
middlewares: readonly MiddlewareRegistration[] // Array to preserve registration order
|
|
65
|
+
extensions: readonly GraphQLExtension<any>[] // Array to preserve registration order
|
|
66
|
+
queries: Map<string, FieldRegistration>
|
|
67
|
+
mutations: Map<string, FieldRegistration>
|
|
68
|
+
subscriptions: Map<string, SubscriptionFieldRegistration>
|
|
69
|
+
objectFields: Map<string, Map<string, ObjectFieldRegistration>>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new state with one map updated
|
|
74
|
+
*/
|
|
75
|
+
function updateState<K extends keyof BuilderState>(
|
|
76
|
+
state: BuilderState,
|
|
77
|
+
key: K,
|
|
78
|
+
value: BuilderState[K]
|
|
79
|
+
): BuilderState {
|
|
80
|
+
return { ...state, [key]: value }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* GraphQL Schema Builder with type-safe service requirements (Layer-per-Request Pattern)
|
|
85
|
+
*
|
|
86
|
+
* The type parameter R accumulates all service requirements from resolvers.
|
|
87
|
+
* Unlike the runtime-in-context approach, this pattern builds the schema without
|
|
88
|
+
* executing any Effects. At request time, you provide a Layer with all required services.
|
|
89
|
+
*/
|
|
90
|
+
export class GraphQLSchemaBuilder<R = never> implements Pipeable.Pipeable {
|
|
91
|
+
private constructor(private readonly state: BuilderState) {}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Pipeable interface implementation - enables fluent .pipe() syntax
|
|
95
|
+
*/
|
|
96
|
+
pipe<A>(this: A): A
|
|
97
|
+
pipe<A, B>(this: A, ab: (a: A) => B): B
|
|
98
|
+
pipe<A, B, C>(this: A, ab: (a: A) => B, bc: (b: B) => C): C
|
|
99
|
+
pipe<A, B, C, D>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D
|
|
100
|
+
pipe<A, B, C, D, E>(
|
|
101
|
+
this: A,
|
|
102
|
+
ab: (a: A) => B,
|
|
103
|
+
bc: (b: B) => C,
|
|
104
|
+
cd: (c: C) => D,
|
|
105
|
+
de: (d: D) => E
|
|
106
|
+
): E
|
|
107
|
+
pipe<A, B, C, D, E, F>(
|
|
108
|
+
this: A,
|
|
109
|
+
ab: (a: A) => B,
|
|
110
|
+
bc: (b: B) => C,
|
|
111
|
+
cd: (c: C) => D,
|
|
112
|
+
de: (d: D) => E,
|
|
113
|
+
ef: (e: E) => F
|
|
114
|
+
): F
|
|
115
|
+
pipe<A, B, C, D, E, F, G>(
|
|
116
|
+
this: A,
|
|
117
|
+
ab: (a: A) => B,
|
|
118
|
+
bc: (b: B) => C,
|
|
119
|
+
cd: (c: C) => D,
|
|
120
|
+
de: (d: D) => E,
|
|
121
|
+
ef: (e: E) => F,
|
|
122
|
+
fg: (f: F) => G
|
|
123
|
+
): G
|
|
124
|
+
pipe<A, B, C, D, E, F, G, H>(
|
|
125
|
+
this: A,
|
|
126
|
+
ab: (a: A) => B,
|
|
127
|
+
bc: (b: B) => C,
|
|
128
|
+
cd: (c: C) => D,
|
|
129
|
+
de: (d: D) => E,
|
|
130
|
+
ef: (e: E) => F,
|
|
131
|
+
fg: (f: F) => G,
|
|
132
|
+
gh: (g: G) => H
|
|
133
|
+
): H
|
|
134
|
+
pipe<A, B, C, D, E, F, G, H, I>(
|
|
135
|
+
this: A,
|
|
136
|
+
ab: (a: A) => B,
|
|
137
|
+
bc: (b: B) => C,
|
|
138
|
+
cd: (c: C) => D,
|
|
139
|
+
de: (d: D) => E,
|
|
140
|
+
ef: (e: E) => F,
|
|
141
|
+
fg: (f: F) => G,
|
|
142
|
+
gh: (g: G) => H,
|
|
143
|
+
hi: (h: H) => I
|
|
144
|
+
): I
|
|
145
|
+
pipe() {
|
|
146
|
+
return Pipeable.pipeArguments(this, arguments)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create an empty schema builder
|
|
151
|
+
*/
|
|
152
|
+
static readonly empty = new GraphQLSchemaBuilder<never>({
|
|
153
|
+
types: new Map(),
|
|
154
|
+
interfaces: new Map(),
|
|
155
|
+
enums: new Map(),
|
|
156
|
+
unions: new Map(),
|
|
157
|
+
inputs: new Map(),
|
|
158
|
+
directives: new Map(),
|
|
159
|
+
middlewares: [],
|
|
160
|
+
extensions: [],
|
|
161
|
+
queries: new Map(),
|
|
162
|
+
mutations: new Map(),
|
|
163
|
+
subscriptions: new Map(),
|
|
164
|
+
objectFields: new Map(),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a new builder with updated state
|
|
169
|
+
*/
|
|
170
|
+
private with(newState: BuilderState): GraphQLSchemaBuilder<any> {
|
|
171
|
+
return new GraphQLSchemaBuilder(newState)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Registration Methods
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Add a query field
|
|
180
|
+
*/
|
|
181
|
+
query<A, E, R2, Args = void>(
|
|
182
|
+
name: string,
|
|
183
|
+
config: {
|
|
184
|
+
type: S.Schema<A, any, any>
|
|
185
|
+
args?: S.Schema<Args, any, any>
|
|
186
|
+
description?: string
|
|
187
|
+
directives?: readonly DirectiveApplication[]
|
|
188
|
+
/**
|
|
189
|
+
* Complexity cost of this field for query complexity limiting.
|
|
190
|
+
* Can be a static number or a function that receives the resolved arguments.
|
|
191
|
+
*/
|
|
192
|
+
complexity?: FieldComplexity
|
|
193
|
+
/**
|
|
194
|
+
* Cache control hint for this field.
|
|
195
|
+
* Used to compute HTTP Cache-Control headers for the response.
|
|
196
|
+
*/
|
|
197
|
+
cacheControl?: CacheHint
|
|
198
|
+
resolve: (args: Args) => Effect.Effect<A, E, R2>
|
|
199
|
+
}
|
|
200
|
+
): GraphQLSchemaBuilder<R | R2> {
|
|
201
|
+
const newQueries = new Map(this.state.queries)
|
|
202
|
+
newQueries.set(name, config)
|
|
203
|
+
return this.with(updateState(this.state, "queries", newQueries))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Add a mutation field
|
|
208
|
+
*/
|
|
209
|
+
mutation<A, E, R2, Args = void>(
|
|
210
|
+
name: string,
|
|
211
|
+
config: {
|
|
212
|
+
type: S.Schema<A, any, any>
|
|
213
|
+
args?: S.Schema<Args, any, any>
|
|
214
|
+
description?: string
|
|
215
|
+
directives?: readonly DirectiveApplication[]
|
|
216
|
+
/**
|
|
217
|
+
* Complexity cost of this field for query complexity limiting.
|
|
218
|
+
* Can be a static number or a function that receives the resolved arguments.
|
|
219
|
+
*/
|
|
220
|
+
complexity?: FieldComplexity
|
|
221
|
+
resolve: (args: Args) => Effect.Effect<A, E, R2>
|
|
222
|
+
}
|
|
223
|
+
): GraphQLSchemaBuilder<R | R2> {
|
|
224
|
+
const newMutations = new Map(this.state.mutations)
|
|
225
|
+
newMutations.set(name, config)
|
|
226
|
+
return this.with(updateState(this.state, "mutations", newMutations))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Add a subscription field
|
|
231
|
+
*
|
|
232
|
+
* Subscriptions return a Stream that yields values over time.
|
|
233
|
+
* The subscribe function returns an Effect that produces a Stream.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* builder.subscription("userCreated", {
|
|
238
|
+
* type: User,
|
|
239
|
+
* subscribe: Effect.gen(function*() {
|
|
240
|
+
* const pubsub = yield* PubSubService
|
|
241
|
+
* return pubsub.subscribe("USER_CREATED")
|
|
242
|
+
* }),
|
|
243
|
+
* })
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
subscription<A, E, R2, Args = void>(
|
|
247
|
+
name: string,
|
|
248
|
+
config: {
|
|
249
|
+
type: S.Schema<A, any, any>
|
|
250
|
+
args?: S.Schema<Args, any, any>
|
|
251
|
+
description?: string
|
|
252
|
+
directives?: readonly DirectiveApplication[]
|
|
253
|
+
/**
|
|
254
|
+
* Complexity cost of this subscription for query complexity limiting.
|
|
255
|
+
* Can be a static number or a function that receives the resolved arguments.
|
|
256
|
+
*/
|
|
257
|
+
complexity?: FieldComplexity
|
|
258
|
+
/**
|
|
259
|
+
* Cache control hint for this subscription.
|
|
260
|
+
* Note: Subscriptions are typically not cached, but this can be used for initial response hints.
|
|
261
|
+
*/
|
|
262
|
+
cacheControl?: CacheHint
|
|
263
|
+
subscribe: (args: Args) => Effect.Effect<import("effect").Stream.Stream<A, E, R2>, E, R2>
|
|
264
|
+
resolve?: (value: A, args: Args) => Effect.Effect<A, E, R2>
|
|
265
|
+
}
|
|
266
|
+
): GraphQLSchemaBuilder<R | R2> {
|
|
267
|
+
const newSubscriptions = new Map(this.state.subscriptions)
|
|
268
|
+
newSubscriptions.set(name, config)
|
|
269
|
+
return this.with(updateState(this.state, "subscriptions", newSubscriptions))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Register an object type from a schema
|
|
274
|
+
*/
|
|
275
|
+
objectType<A, R2 = never>(config: {
|
|
276
|
+
name?: string
|
|
277
|
+
schema: S.Schema<A, any, any>
|
|
278
|
+
implements?: readonly string[]
|
|
279
|
+
directives?: readonly DirectiveApplication[]
|
|
280
|
+
/**
|
|
281
|
+
* Default cache control hint for all fields returning this type.
|
|
282
|
+
* Can be overridden by field-level cacheControl.
|
|
283
|
+
*/
|
|
284
|
+
cacheControl?: CacheHint
|
|
285
|
+
fields?: Record<
|
|
286
|
+
string,
|
|
287
|
+
{
|
|
288
|
+
type: S.Schema<any, any, any>
|
|
289
|
+
args?: S.Schema<any, any, any>
|
|
290
|
+
description?: string
|
|
291
|
+
directives?: readonly DirectiveApplication[]
|
|
292
|
+
/**
|
|
293
|
+
* Complexity cost of this field for query complexity limiting.
|
|
294
|
+
*/
|
|
295
|
+
complexity?: FieldComplexity
|
|
296
|
+
/**
|
|
297
|
+
* Cache control hint for this field.
|
|
298
|
+
*/
|
|
299
|
+
cacheControl?: CacheHint
|
|
300
|
+
resolve: (parent: A, args: any) => Effect.Effect<any, any, any>
|
|
301
|
+
}
|
|
302
|
+
>
|
|
303
|
+
}): GraphQLSchemaBuilder<R | R2> {
|
|
304
|
+
const { schema, implements: implementsInterfaces, directives, cacheControl, fields } = config
|
|
305
|
+
const name = config.name ?? getSchemaName(schema)
|
|
306
|
+
if (!name) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
"objectType requires a name. Either provide one explicitly or use a TaggedStruct/TaggedClass/Schema.Class"
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const newTypes = new Map(this.state.types)
|
|
313
|
+
newTypes.set(name, { name, schema, implements: implementsInterfaces, directives, cacheControl })
|
|
314
|
+
|
|
315
|
+
let newObjectFields = this.state.objectFields
|
|
316
|
+
if (fields) {
|
|
317
|
+
newObjectFields = new Map(this.state.objectFields)
|
|
318
|
+
const typeFields = new Map<string, ObjectFieldRegistration>()
|
|
319
|
+
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
320
|
+
typeFields.set(fieldName, fieldConfig as ObjectFieldRegistration)
|
|
321
|
+
}
|
|
322
|
+
newObjectFields.set(name, typeFields)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return this.with({
|
|
326
|
+
...this.state,
|
|
327
|
+
types: newTypes,
|
|
328
|
+
objectFields: newObjectFields,
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Register an interface type from a schema
|
|
334
|
+
*/
|
|
335
|
+
interfaceType(config: {
|
|
336
|
+
name?: string
|
|
337
|
+
schema: S.Schema<any, any, any>
|
|
338
|
+
resolveType?: (value: any) => string
|
|
339
|
+
directives?: readonly DirectiveApplication[]
|
|
340
|
+
}): GraphQLSchemaBuilder<R> {
|
|
341
|
+
const { schema, resolveType, directives } = config
|
|
342
|
+
const name = config.name ?? getSchemaName(schema)
|
|
343
|
+
if (!name) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
"interfaceType requires a name. Either provide one explicitly or use a TaggedStruct/TaggedClass/Schema.Class"
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const newInterfaces = new Map(this.state.interfaces)
|
|
350
|
+
newInterfaces.set(name, {
|
|
351
|
+
name,
|
|
352
|
+
schema,
|
|
353
|
+
resolveType: resolveType ?? ((value: any) => value._tag),
|
|
354
|
+
directives,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
return this.with(updateState(this.state, "interfaces", newInterfaces))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Register an enum type
|
|
362
|
+
*/
|
|
363
|
+
enumType(config: {
|
|
364
|
+
name: string
|
|
365
|
+
values: readonly string[]
|
|
366
|
+
description?: string
|
|
367
|
+
directives?: readonly DirectiveApplication[]
|
|
368
|
+
}): GraphQLSchemaBuilder<R> {
|
|
369
|
+
const { name, values, description, directives } = config
|
|
370
|
+
const newEnums = new Map(this.state.enums)
|
|
371
|
+
newEnums.set(name, { name, values, description, directives })
|
|
372
|
+
return this.with(updateState(this.state, "enums", newEnums))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Register a union type
|
|
377
|
+
*/
|
|
378
|
+
unionType(config: {
|
|
379
|
+
name: string
|
|
380
|
+
types: readonly string[]
|
|
381
|
+
resolveType?: (value: any) => string
|
|
382
|
+
directives?: readonly DirectiveApplication[]
|
|
383
|
+
}): GraphQLSchemaBuilder<R> {
|
|
384
|
+
const { name, types, resolveType, directives } = config
|
|
385
|
+
const newUnions = new Map(this.state.unions)
|
|
386
|
+
newUnions.set(name, {
|
|
387
|
+
name,
|
|
388
|
+
types,
|
|
389
|
+
resolveType: resolveType ?? ((value: any) => value._tag),
|
|
390
|
+
directives,
|
|
391
|
+
})
|
|
392
|
+
return this.with(updateState(this.state, "unions", newUnions))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Register an input type
|
|
397
|
+
*/
|
|
398
|
+
inputType(config: {
|
|
399
|
+
name?: string
|
|
400
|
+
schema: S.Schema<any, any, any>
|
|
401
|
+
description?: string
|
|
402
|
+
directives?: readonly DirectiveApplication[]
|
|
403
|
+
}): GraphQLSchemaBuilder<R> {
|
|
404
|
+
const { schema, description, directives } = config
|
|
405
|
+
const name = config.name ?? getSchemaName(schema)
|
|
406
|
+
if (!name) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
"inputType requires a name. Either provide one explicitly or use a TaggedStruct/TaggedClass/Schema.Class"
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const newInputs = new Map(this.state.inputs)
|
|
413
|
+
newInputs.set(name, { name, schema, description, directives })
|
|
414
|
+
return this.with(updateState(this.state, "inputs", newInputs))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Register a directive
|
|
419
|
+
*/
|
|
420
|
+
directive<Args = void, R2 = never>(config: {
|
|
421
|
+
name: string
|
|
422
|
+
description?: string
|
|
423
|
+
locations: readonly DirectiveLocation[]
|
|
424
|
+
args?: S.Schema<Args, any, any>
|
|
425
|
+
apply?: (
|
|
426
|
+
args: Args
|
|
427
|
+
) => <A, E, R3>(effect: Effect.Effect<A, E, R3>) => Effect.Effect<A, E, R2 | R3>
|
|
428
|
+
}): GraphQLSchemaBuilder<R | R2> {
|
|
429
|
+
const newDirectives = new Map(this.state.directives)
|
|
430
|
+
newDirectives.set(config.name, config as DirectiveRegistration)
|
|
431
|
+
return this.with(updateState(this.state, "directives", newDirectives))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Register a middleware
|
|
436
|
+
*
|
|
437
|
+
* Middleware wraps all resolvers (or those matching a pattern) and executes
|
|
438
|
+
* in an "onion" model - first registered middleware is the outermost layer.
|
|
439
|
+
*
|
|
440
|
+
* @param config.name - Middleware name (for debugging/logging)
|
|
441
|
+
* @param config.description - Optional description
|
|
442
|
+
* @param config.match - Optional predicate to filter which fields this applies to
|
|
443
|
+
* @param config.apply - Function that transforms the resolver Effect
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```typescript
|
|
447
|
+
* builder.middleware({
|
|
448
|
+
* name: "logging",
|
|
449
|
+
* apply: (effect, ctx) => Effect.gen(function*() {
|
|
450
|
+
* yield* Effect.logInfo(`Resolving ${ctx.info.fieldName}`)
|
|
451
|
+
* const start = Date.now()
|
|
452
|
+
* const result = yield* effect
|
|
453
|
+
* yield* Effect.logInfo(`Resolved in ${Date.now() - start}ms`)
|
|
454
|
+
* return result
|
|
455
|
+
* })
|
|
456
|
+
* })
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
middleware<R2 = never>(config: {
|
|
460
|
+
name: string
|
|
461
|
+
description?: string
|
|
462
|
+
match?: (info: GraphQLResolveInfo) => boolean
|
|
463
|
+
apply: <A, E, R3>(
|
|
464
|
+
effect: Effect.Effect<A, E, R3>,
|
|
465
|
+
context: MiddlewareContext
|
|
466
|
+
) => Effect.Effect<A, E, R2 | R3>
|
|
467
|
+
}): GraphQLSchemaBuilder<R | R2> {
|
|
468
|
+
const newMiddlewares = [...this.state.middlewares, config as MiddlewareRegistration]
|
|
469
|
+
return this.with({ ...this.state, middlewares: newMiddlewares })
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Register an extension
|
|
474
|
+
*
|
|
475
|
+
* Extensions provide lifecycle hooks that run at each phase of request processing
|
|
476
|
+
* (parse, validate, execute) and can contribute data to the response's extensions field.
|
|
477
|
+
*
|
|
478
|
+
* @param config.name - Extension name (for debugging/logging)
|
|
479
|
+
* @param config.description - Optional description
|
|
480
|
+
* @param config.onParse - Called after query parsing
|
|
481
|
+
* @param config.onValidate - Called after validation
|
|
482
|
+
* @param config.onExecuteStart - Called before execution begins
|
|
483
|
+
* @param config.onExecuteEnd - Called after execution completes
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```typescript
|
|
487
|
+
* builder.extension({
|
|
488
|
+
* name: "tracing",
|
|
489
|
+
* onExecuteStart: () => Effect.gen(function*() {
|
|
490
|
+
* const ext = yield* ExtensionsService
|
|
491
|
+
* yield* ext.set("tracing", { startTime: Date.now() })
|
|
492
|
+
* }),
|
|
493
|
+
* onExecuteEnd: () => Effect.gen(function*() {
|
|
494
|
+
* const ext = yield* ExtensionsService
|
|
495
|
+
* yield* ext.merge("tracing", { endTime: Date.now() })
|
|
496
|
+
* }),
|
|
497
|
+
* })
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
extension<R2 = never>(config: {
|
|
501
|
+
name: string
|
|
502
|
+
description?: string
|
|
503
|
+
onParse?: (source: string, document: DocumentNode) => Effect.Effect<void, never, R2>
|
|
504
|
+
onValidate?: (
|
|
505
|
+
document: DocumentNode,
|
|
506
|
+
errors: readonly GQLError[]
|
|
507
|
+
) => Effect.Effect<void, never, R2>
|
|
508
|
+
onExecuteStart?: (args: ExecutionArgs) => Effect.Effect<void, never, R2>
|
|
509
|
+
onExecuteEnd?: (result: ExecutionResult) => Effect.Effect<void, never, R2>
|
|
510
|
+
}): GraphQLSchemaBuilder<R | R2> {
|
|
511
|
+
const newExtensions = [...this.state.extensions, config as GraphQLExtension<R2>]
|
|
512
|
+
return this.with({ ...this.state, extensions: newExtensions })
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get the registered extensions for use by the execution layer
|
|
517
|
+
*/
|
|
518
|
+
getExtensions(): readonly GraphQLExtension<any>[] {
|
|
519
|
+
return this.state.extensions
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Add a computed/relational field to an object type
|
|
524
|
+
*/
|
|
525
|
+
field<Parent, A, E, R2, Args = void>(
|
|
526
|
+
typeName: string,
|
|
527
|
+
fieldName: string,
|
|
528
|
+
config: {
|
|
529
|
+
type: S.Schema<A, any, any>
|
|
530
|
+
args?: S.Schema<Args, any, any>
|
|
531
|
+
description?: string
|
|
532
|
+
directives?: readonly DirectiveApplication[]
|
|
533
|
+
/**
|
|
534
|
+
* Complexity cost of this field for query complexity limiting.
|
|
535
|
+
* Can be a static number or a function that receives the resolved arguments.
|
|
536
|
+
*/
|
|
537
|
+
complexity?: FieldComplexity
|
|
538
|
+
/**
|
|
539
|
+
* Cache control hint for this field.
|
|
540
|
+
* Used to compute HTTP Cache-Control headers for the response.
|
|
541
|
+
*/
|
|
542
|
+
cacheControl?: CacheHint
|
|
543
|
+
resolve: (parent: Parent, args: Args) => Effect.Effect<A, E, R2>
|
|
544
|
+
}
|
|
545
|
+
): GraphQLSchemaBuilder<R | R2> {
|
|
546
|
+
const newObjectFields = new Map(this.state.objectFields)
|
|
547
|
+
const typeFields = newObjectFields.get(typeName) || new Map()
|
|
548
|
+
typeFields.set(fieldName, config)
|
|
549
|
+
newObjectFields.set(typeName, typeFields)
|
|
550
|
+
return this.with(updateState(this.state, "objectFields", newObjectFields))
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ============================================================================
|
|
554
|
+
// Schema Building
|
|
555
|
+
// ============================================================================
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Get the field complexity map for use in complexity validation.
|
|
559
|
+
* Maps "TypeName.fieldName" to the complexity value or function.
|
|
560
|
+
*/
|
|
561
|
+
getFieldComplexities(): FieldComplexityMap {
|
|
562
|
+
const complexities: FieldComplexityMap = new Map()
|
|
563
|
+
|
|
564
|
+
// Query fields
|
|
565
|
+
for (const [name, config] of this.state.queries) {
|
|
566
|
+
if (config.complexity !== undefined) {
|
|
567
|
+
complexities.set(`Query.${name}`, config.complexity)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Mutation fields
|
|
572
|
+
for (const [name, config] of this.state.mutations) {
|
|
573
|
+
if (config.complexity !== undefined) {
|
|
574
|
+
complexities.set(`Mutation.${name}`, config.complexity)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Subscription fields
|
|
579
|
+
for (const [name, config] of this.state.subscriptions) {
|
|
580
|
+
if (config.complexity !== undefined) {
|
|
581
|
+
complexities.set(`Subscription.${name}`, config.complexity)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Object type fields
|
|
586
|
+
for (const [typeName, fields] of this.state.objectFields) {
|
|
587
|
+
for (const [fieldName, config] of fields) {
|
|
588
|
+
if (config.complexity !== undefined) {
|
|
589
|
+
complexities.set(`${typeName}.${fieldName}`, config.complexity)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return complexities
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Get the cache hint map for use in cache control calculation.
|
|
599
|
+
* Maps "TypeName.fieldName" to the cache hint for field-level hints,
|
|
600
|
+
* or "TypeName" to the cache hint for type-level hints.
|
|
601
|
+
*/
|
|
602
|
+
getCacheHints(): CacheHintMap {
|
|
603
|
+
const hints: CacheHintMap = new Map()
|
|
604
|
+
|
|
605
|
+
// Type-level hints
|
|
606
|
+
for (const [typeName, typeReg] of this.state.types) {
|
|
607
|
+
if (typeReg.cacheControl !== undefined) {
|
|
608
|
+
hints.set(typeName, typeReg.cacheControl)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Query fields
|
|
613
|
+
for (const [name, config] of this.state.queries) {
|
|
614
|
+
if (config.cacheControl !== undefined) {
|
|
615
|
+
hints.set(`Query.${name}`, config.cacheControl)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Subscription fields
|
|
620
|
+
for (const [name, config] of this.state.subscriptions) {
|
|
621
|
+
if (config.cacheControl !== undefined) {
|
|
622
|
+
hints.set(`Subscription.${name}`, config.cacheControl)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Object type fields
|
|
627
|
+
for (const [typeName, fields] of this.state.objectFields) {
|
|
628
|
+
for (const [fieldName, config] of fields) {
|
|
629
|
+
if (config.cacheControl !== undefined) {
|
|
630
|
+
hints.set(`${typeName}.${fieldName}`, config.cacheControl)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return hints
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Build the GraphQL schema (no services required)
|
|
640
|
+
*/
|
|
641
|
+
buildSchema(): GraphQLSchema {
|
|
642
|
+
// Build all registries
|
|
643
|
+
const directiveRegistry = this.buildDirectiveRegistry()
|
|
644
|
+
const enumRegistry = this.buildEnumRegistry()
|
|
645
|
+
const inputRegistry = this.buildInputRegistry(enumRegistry)
|
|
646
|
+
const interfaceRegistry = this.buildInterfaceRegistry(enumRegistry)
|
|
647
|
+
const { typeRegistry, unionRegistry } = this.buildTypeAndUnionRegistries(
|
|
648
|
+
enumRegistry,
|
|
649
|
+
interfaceRegistry
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
// Build field builder context
|
|
653
|
+
const fieldCtx = this.createFieldBuilderContext(
|
|
654
|
+
typeRegistry,
|
|
655
|
+
interfaceRegistry,
|
|
656
|
+
enumRegistry,
|
|
657
|
+
unionRegistry,
|
|
658
|
+
inputRegistry
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
// Build root type fields
|
|
662
|
+
const queryFields = this.buildQueryFields(fieldCtx)
|
|
663
|
+
const mutationFields = this.buildMutationFields(fieldCtx)
|
|
664
|
+
const subscriptionFields = this.buildSubscriptionFields(fieldCtx)
|
|
665
|
+
|
|
666
|
+
// Assemble schema
|
|
667
|
+
return this.assembleSchema({
|
|
668
|
+
directiveRegistry,
|
|
669
|
+
enumRegistry,
|
|
670
|
+
inputRegistry,
|
|
671
|
+
interfaceRegistry,
|
|
672
|
+
typeRegistry,
|
|
673
|
+
unionRegistry,
|
|
674
|
+
queryFields,
|
|
675
|
+
mutationFields,
|
|
676
|
+
subscriptionFields,
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private buildDirectiveRegistry(): Map<string, GraphQLDirective> {
|
|
681
|
+
const registry = new Map<string, GraphQLDirective>()
|
|
682
|
+
|
|
683
|
+
// Build cache once for O(1) lookups across all directives
|
|
684
|
+
const cache = buildInputTypeLookupCache(this.state.inputs, this.state.enums)
|
|
685
|
+
|
|
686
|
+
for (const [name, reg] of this.state.directives) {
|
|
687
|
+
const graphqlDirective = new GraphQLDirective({
|
|
688
|
+
name,
|
|
689
|
+
description: reg.description,
|
|
690
|
+
locations: [...reg.locations],
|
|
691
|
+
args: reg.args
|
|
692
|
+
? toGraphQLArgsWithRegistry(
|
|
693
|
+
reg.args,
|
|
694
|
+
new Map(),
|
|
695
|
+
new Map(),
|
|
696
|
+
this.state.inputs,
|
|
697
|
+
this.state.enums,
|
|
698
|
+
cache
|
|
699
|
+
)
|
|
700
|
+
: undefined,
|
|
701
|
+
})
|
|
702
|
+
registry.set(name, graphqlDirective)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return registry
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private buildEnumRegistry(): Map<string, GraphQLEnumType> {
|
|
709
|
+
const registry = new Map<string, GraphQLEnumType>()
|
|
710
|
+
|
|
711
|
+
for (const [name, reg] of this.state.enums) {
|
|
712
|
+
const enumValues: Record<string, { value: string }> = {}
|
|
713
|
+
for (const value of reg.values) {
|
|
714
|
+
enumValues[value] = { value }
|
|
715
|
+
}
|
|
716
|
+
registry.set(
|
|
717
|
+
name,
|
|
718
|
+
new GraphQLEnumType({
|
|
719
|
+
name,
|
|
720
|
+
values: enumValues,
|
|
721
|
+
description: reg.description,
|
|
722
|
+
extensions: reg.directives ? { directives: reg.directives } : undefined,
|
|
723
|
+
})
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return registry
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private buildInputRegistry(
|
|
731
|
+
enumRegistry: Map<string, GraphQLEnumType>
|
|
732
|
+
): Map<string, GraphQLInputObjectType> {
|
|
733
|
+
const registry = new Map<string, GraphQLInputObjectType>()
|
|
734
|
+
|
|
735
|
+
// Build cache once for O(1) lookups across all input types
|
|
736
|
+
const cache = buildInputTypeLookupCache(this.state.inputs, this.state.enums)
|
|
737
|
+
|
|
738
|
+
for (const [name, reg] of this.state.inputs) {
|
|
739
|
+
const inputType = new GraphQLInputObjectType({
|
|
740
|
+
name,
|
|
741
|
+
description: reg.description,
|
|
742
|
+
fields: () =>
|
|
743
|
+
schemaToInputFields(
|
|
744
|
+
reg.schema,
|
|
745
|
+
enumRegistry,
|
|
746
|
+
registry,
|
|
747
|
+
this.state.inputs,
|
|
748
|
+
this.state.enums,
|
|
749
|
+
cache
|
|
750
|
+
),
|
|
751
|
+
extensions: reg.directives ? { directives: reg.directives } : undefined,
|
|
752
|
+
})
|
|
753
|
+
registry.set(name, inputType)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return registry
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private buildInterfaceRegistry(
|
|
760
|
+
enumRegistry: Map<string, GraphQLEnumType>
|
|
761
|
+
): Map<string, GraphQLInterfaceType> {
|
|
762
|
+
const registry = new Map<string, GraphQLInterfaceType>()
|
|
763
|
+
// We need type and union registries for interface fields, but they're built later
|
|
764
|
+
// Use empty maps for now - interfaces shouldn't reference object types directly
|
|
765
|
+
const typeRegistry = new Map<string, GraphQLObjectType>()
|
|
766
|
+
const unionRegistry = new Map<string, GraphQLUnionType>()
|
|
767
|
+
|
|
768
|
+
// Create shared TypeConversionContext once for all interface field builders
|
|
769
|
+
const sharedCtx: TypeConversionContext = {
|
|
770
|
+
types: this.state.types,
|
|
771
|
+
interfaces: this.state.interfaces,
|
|
772
|
+
enums: this.state.enums,
|
|
773
|
+
unions: this.state.unions,
|
|
774
|
+
inputs: this.state.inputs,
|
|
775
|
+
typeRegistry,
|
|
776
|
+
interfaceRegistry: registry,
|
|
777
|
+
enumRegistry,
|
|
778
|
+
unionRegistry,
|
|
779
|
+
inputRegistry: new Map(),
|
|
780
|
+
}
|
|
781
|
+
// Pre-build reverse lookup maps once
|
|
782
|
+
buildReverseLookups(sharedCtx)
|
|
783
|
+
|
|
784
|
+
for (const [name, reg] of this.state.interfaces) {
|
|
785
|
+
const interfaceType = new GraphQLInterfaceType({
|
|
786
|
+
name,
|
|
787
|
+
fields: () => schemaToFields(reg.schema, sharedCtx),
|
|
788
|
+
resolveType: reg.resolveType,
|
|
789
|
+
extensions: reg.directives ? { directives: reg.directives } : undefined,
|
|
790
|
+
})
|
|
791
|
+
registry.set(name, interfaceType)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return registry
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private buildTypeAndUnionRegistries(
|
|
798
|
+
enumRegistry: Map<string, GraphQLEnumType>,
|
|
799
|
+
interfaceRegistry: Map<string, GraphQLInterfaceType>
|
|
800
|
+
): {
|
|
801
|
+
typeRegistry: Map<string, GraphQLObjectType>
|
|
802
|
+
unionRegistry: Map<string, GraphQLUnionType>
|
|
803
|
+
} {
|
|
804
|
+
const typeRegistry = new Map<string, GraphQLObjectType>()
|
|
805
|
+
const unionRegistry = new Map<string, GraphQLUnionType>()
|
|
806
|
+
|
|
807
|
+
// Create shared TypeConversionContext once and reuse for all lazy field builders
|
|
808
|
+
const sharedCtx: TypeConversionContext = {
|
|
809
|
+
types: this.state.types,
|
|
810
|
+
interfaces: this.state.interfaces,
|
|
811
|
+
enums: this.state.enums,
|
|
812
|
+
unions: this.state.unions,
|
|
813
|
+
inputs: this.state.inputs,
|
|
814
|
+
typeRegistry,
|
|
815
|
+
interfaceRegistry,
|
|
816
|
+
enumRegistry,
|
|
817
|
+
unionRegistry,
|
|
818
|
+
inputRegistry: new Map(),
|
|
819
|
+
}
|
|
820
|
+
// Pre-build reverse lookup maps once
|
|
821
|
+
buildReverseLookups(sharedCtx)
|
|
822
|
+
|
|
823
|
+
// Create shared FieldBuilderContext for additional fields
|
|
824
|
+
const sharedFieldCtx = this.createFieldBuilderContext(
|
|
825
|
+
typeRegistry,
|
|
826
|
+
interfaceRegistry,
|
|
827
|
+
enumRegistry,
|
|
828
|
+
unionRegistry,
|
|
829
|
+
new Map()
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
// Build object types with lazy field builders (allows circular references)
|
|
833
|
+
for (const [typeName, typeReg] of this.state.types) {
|
|
834
|
+
const implementedInterfaces =
|
|
835
|
+
typeReg.implements?.map((name) => interfaceRegistry.get(name)!).filter(Boolean) ?? []
|
|
836
|
+
|
|
837
|
+
const graphqlType = new GraphQLObjectType({
|
|
838
|
+
name: typeName,
|
|
839
|
+
fields: () => {
|
|
840
|
+
const baseFields = schemaToFields(typeReg.schema, sharedCtx)
|
|
841
|
+
const additionalFields = this.state.objectFields.get(typeName)
|
|
842
|
+
|
|
843
|
+
if (additionalFields) {
|
|
844
|
+
for (const [fieldName, fieldConfig] of additionalFields) {
|
|
845
|
+
baseFields[fieldName] = buildObjectField(fieldConfig, sharedFieldCtx)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return baseFields
|
|
850
|
+
},
|
|
851
|
+
interfaces: implementedInterfaces.length > 0 ? implementedInterfaces : undefined,
|
|
852
|
+
extensions: typeReg.directives ? { directives: typeReg.directives } : undefined,
|
|
853
|
+
})
|
|
854
|
+
typeRegistry.set(typeName, graphqlType)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Build union types (reference object types)
|
|
858
|
+
for (const [name, reg] of this.state.unions) {
|
|
859
|
+
const unionType = new GraphQLUnionType({
|
|
860
|
+
name,
|
|
861
|
+
types: () => reg.types.map((typeName) => typeRegistry.get(typeName)!).filter(Boolean),
|
|
862
|
+
resolveType: reg.resolveType,
|
|
863
|
+
extensions: reg.directives ? { directives: reg.directives } : undefined,
|
|
864
|
+
})
|
|
865
|
+
unionRegistry.set(name, unionType)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return { typeRegistry, unionRegistry }
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private createFieldBuilderContext(
|
|
872
|
+
typeRegistry: Map<string, GraphQLObjectType>,
|
|
873
|
+
interfaceRegistry: Map<string, GraphQLInterfaceType>,
|
|
874
|
+
enumRegistry: Map<string, GraphQLEnumType>,
|
|
875
|
+
unionRegistry: Map<string, GraphQLUnionType>,
|
|
876
|
+
inputRegistry: Map<string, GraphQLInputObjectType>
|
|
877
|
+
): FieldBuilderContext {
|
|
878
|
+
// Build cache once for O(1) input type lookups across all fields
|
|
879
|
+
const inputTypeLookupCache = buildInputTypeLookupCache(this.state.inputs, this.state.enums)
|
|
880
|
+
|
|
881
|
+
return {
|
|
882
|
+
types: this.state.types,
|
|
883
|
+
interfaces: this.state.interfaces,
|
|
884
|
+
enums: this.state.enums,
|
|
885
|
+
unions: this.state.unions,
|
|
886
|
+
inputs: this.state.inputs,
|
|
887
|
+
typeRegistry,
|
|
888
|
+
interfaceRegistry,
|
|
889
|
+
enumRegistry,
|
|
890
|
+
unionRegistry,
|
|
891
|
+
inputRegistry,
|
|
892
|
+
directiveRegistrations: this.state.directives,
|
|
893
|
+
middlewares: this.state.middlewares,
|
|
894
|
+
inputTypeLookupCache,
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private buildQueryFields(ctx: FieldBuilderContext): GraphQLFieldConfigMap<any, any> {
|
|
899
|
+
const fields: GraphQLFieldConfigMap<any, any> = {}
|
|
900
|
+
for (const [name, config] of this.state.queries) {
|
|
901
|
+
fields[name] = buildField(config, ctx)
|
|
902
|
+
}
|
|
903
|
+
return fields
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private buildMutationFields(ctx: FieldBuilderContext): GraphQLFieldConfigMap<any, any> {
|
|
907
|
+
const fields: GraphQLFieldConfigMap<any, any> = {}
|
|
908
|
+
for (const [name, config] of this.state.mutations) {
|
|
909
|
+
fields[name] = buildField(config, ctx)
|
|
910
|
+
}
|
|
911
|
+
return fields
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private buildSubscriptionFields(ctx: FieldBuilderContext): GraphQLFieldConfigMap<any, any> {
|
|
915
|
+
const fields: GraphQLFieldConfigMap<any, any> = {}
|
|
916
|
+
for (const [name, config] of this.state.subscriptions) {
|
|
917
|
+
fields[name] = buildSubscriptionField(config, ctx)
|
|
918
|
+
}
|
|
919
|
+
return fields
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private assembleSchema(registries: {
|
|
923
|
+
directiveRegistry: Map<string, GraphQLDirective>
|
|
924
|
+
enumRegistry: Map<string, GraphQLEnumType>
|
|
925
|
+
inputRegistry: Map<string, GraphQLInputObjectType>
|
|
926
|
+
interfaceRegistry: Map<string, GraphQLInterfaceType>
|
|
927
|
+
typeRegistry: Map<string, GraphQLObjectType>
|
|
928
|
+
unionRegistry: Map<string, GraphQLUnionType>
|
|
929
|
+
queryFields: GraphQLFieldConfigMap<any, any>
|
|
930
|
+
mutationFields: GraphQLFieldConfigMap<any, any>
|
|
931
|
+
subscriptionFields: GraphQLFieldConfigMap<any, any>
|
|
932
|
+
}): GraphQLSchema {
|
|
933
|
+
const schemaConfig: any = {
|
|
934
|
+
types: [
|
|
935
|
+
...Array.from(registries.enumRegistry.values()),
|
|
936
|
+
...Array.from(registries.inputRegistry.values()),
|
|
937
|
+
...Array.from(registries.interfaceRegistry.values()),
|
|
938
|
+
...Array.from(registries.typeRegistry.values()),
|
|
939
|
+
...Array.from(registries.unionRegistry.values()),
|
|
940
|
+
],
|
|
941
|
+
directives:
|
|
942
|
+
registries.directiveRegistry.size > 0
|
|
943
|
+
? [...Array.from(registries.directiveRegistry.values())]
|
|
944
|
+
: undefined,
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (Object.keys(registries.queryFields).length > 0) {
|
|
948
|
+
schemaConfig.query = new GraphQLObjectType({
|
|
949
|
+
name: "Query",
|
|
950
|
+
fields: registries.queryFields,
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (Object.keys(registries.mutationFields).length > 0) {
|
|
955
|
+
schemaConfig.mutation = new GraphQLObjectType({
|
|
956
|
+
name: "Mutation",
|
|
957
|
+
fields: registries.mutationFields,
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (Object.keys(registries.subscriptionFields).length > 0) {
|
|
962
|
+
schemaConfig.subscription = new GraphQLObjectType({
|
|
963
|
+
name: "Subscription",
|
|
964
|
+
fields: registries.subscriptionFields,
|
|
965
|
+
})
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return new GraphQLSchema(schemaConfig)
|
|
969
|
+
}
|
|
970
|
+
}
|