@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,305 @@
|
|
|
1
|
+
import { Effect, Runtime, Stream } from "effect"
|
|
2
|
+
import * as S from "effect/Schema"
|
|
3
|
+
import { DirectiveLocation, GraphQLResolveInfo } from "graphql"
|
|
4
|
+
import type { FieldComplexity } from "../server/complexity"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cache control scope determines whether a cached response can be shared.
|
|
8
|
+
* - PUBLIC: Response can be cached by CDNs and shared across users
|
|
9
|
+
* - PRIVATE: Response is user-specific and should only be cached in browsers
|
|
10
|
+
*/
|
|
11
|
+
export type CacheControlScope = "PUBLIC" | "PRIVATE"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cache control hint for a type or field.
|
|
15
|
+
* Used to compute the overall cache policy for a GraphQL response.
|
|
16
|
+
*/
|
|
17
|
+
export interface CacheHint {
|
|
18
|
+
/**
|
|
19
|
+
* Maximum age in seconds that this field's value can be cached.
|
|
20
|
+
* The response's maxAge will be the minimum of all field maxAges.
|
|
21
|
+
*/
|
|
22
|
+
readonly maxAge?: number
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether the cached value is user-specific (PRIVATE) or shared (PUBLIC).
|
|
26
|
+
* If any field is PRIVATE, the entire response is PRIVATE.
|
|
27
|
+
*/
|
|
28
|
+
readonly scope?: CacheControlScope
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* When true, this field inherits its maxAge from the parent field
|
|
32
|
+
* instead of using the default (which is 0 for root fields).
|
|
33
|
+
* Cannot be used together with maxAge.
|
|
34
|
+
*/
|
|
35
|
+
readonly inheritMaxAge?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for a query or mutation field
|
|
40
|
+
*/
|
|
41
|
+
export interface FieldRegistration<Args = any, A = any, E = any, R = any> {
|
|
42
|
+
type: S.Schema<A, any, any>
|
|
43
|
+
args?: S.Schema<Args, any, any>
|
|
44
|
+
description?: string
|
|
45
|
+
directives?: readonly DirectiveApplication[]
|
|
46
|
+
/**
|
|
47
|
+
* Complexity cost of this field.
|
|
48
|
+
* Can be a static number or a function that receives the resolved arguments.
|
|
49
|
+
* Used for query complexity limiting.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Static cost
|
|
53
|
+
* complexity: 5
|
|
54
|
+
*
|
|
55
|
+
* // Dynamic cost based on pagination
|
|
56
|
+
* complexity: (args) => args.limit * 2
|
|
57
|
+
*/
|
|
58
|
+
complexity?: FieldComplexity
|
|
59
|
+
/**
|
|
60
|
+
* Cache control hint for this field.
|
|
61
|
+
* Used to compute HTTP Cache-Control headers for the response.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // Cache for 1 hour
|
|
65
|
+
* cacheControl: { maxAge: 3600 }
|
|
66
|
+
*
|
|
67
|
+
* // User-specific data, don't cache in CDN
|
|
68
|
+
* cacheControl: { maxAge: 60, scope: "PRIVATE" }
|
|
69
|
+
*/
|
|
70
|
+
cacheControl?: CacheHint
|
|
71
|
+
resolve: (args: Args) => Effect.Effect<A, E, R>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Configuration for an object type
|
|
76
|
+
*/
|
|
77
|
+
export interface TypeRegistration {
|
|
78
|
+
name: string
|
|
79
|
+
schema: S.Schema<any, any, any>
|
|
80
|
+
implements?: readonly string[]
|
|
81
|
+
directives?: readonly DirectiveApplication[]
|
|
82
|
+
/**
|
|
83
|
+
* Default cache control hint for all fields returning this type.
|
|
84
|
+
* Can be overridden by field-level cacheControl.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // All User fields cacheable for 1 hour by default
|
|
88
|
+
* cacheControl: { maxAge: 3600 }
|
|
89
|
+
*/
|
|
90
|
+
cacheControl?: CacheHint
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Configuration for an interface type
|
|
95
|
+
*/
|
|
96
|
+
export interface InterfaceRegistration {
|
|
97
|
+
name: string
|
|
98
|
+
schema: S.Schema<any, any, any>
|
|
99
|
+
resolveType: (value: any) => string
|
|
100
|
+
directives?: readonly DirectiveApplication[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Configuration for an enum type
|
|
105
|
+
*/
|
|
106
|
+
export interface EnumRegistration {
|
|
107
|
+
name: string
|
|
108
|
+
values: readonly string[]
|
|
109
|
+
description?: string
|
|
110
|
+
directives?: readonly DirectiveApplication[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Configuration for a union type
|
|
115
|
+
*/
|
|
116
|
+
export interface UnionRegistration {
|
|
117
|
+
name: string
|
|
118
|
+
types: readonly string[]
|
|
119
|
+
resolveType: (value: any) => string
|
|
120
|
+
directives?: readonly DirectiveApplication[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Configuration for an input type
|
|
125
|
+
*/
|
|
126
|
+
export interface InputTypeRegistration {
|
|
127
|
+
name: string
|
|
128
|
+
schema: S.Schema<any, any, any>
|
|
129
|
+
description?: string
|
|
130
|
+
directives?: readonly DirectiveApplication[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A reference to a directive applied to a type, field, or argument
|
|
135
|
+
*/
|
|
136
|
+
export interface DirectiveApplication {
|
|
137
|
+
readonly name: string
|
|
138
|
+
readonly args?: Record<string, unknown>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Configuration for a directive definition
|
|
143
|
+
*/
|
|
144
|
+
export interface DirectiveRegistration<Args = any, R = never> {
|
|
145
|
+
name: string
|
|
146
|
+
description?: string
|
|
147
|
+
locations: readonly DirectiveLocation[]
|
|
148
|
+
args?: S.Schema<Args, any, any>
|
|
149
|
+
/**
|
|
150
|
+
* For executable directives - transforms the resolver Effect.
|
|
151
|
+
* Called with directive args, returns an Effect transformer.
|
|
152
|
+
*/
|
|
153
|
+
apply?: (args: Args) => <A, E, R2>(effect: Effect.Effect<A, E, R2>) => Effect.Effect<A, E, R | R2>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Context passed to middleware apply functions
|
|
158
|
+
* Contains the resolver's parent value, arguments, and GraphQL resolve info
|
|
159
|
+
*/
|
|
160
|
+
export interface MiddlewareContext<Parent = any, Args = any> {
|
|
161
|
+
readonly parent: Parent
|
|
162
|
+
readonly args: Args
|
|
163
|
+
readonly info: GraphQLResolveInfo
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Configuration for middleware registration
|
|
168
|
+
*
|
|
169
|
+
* Middleware wraps all resolvers (or those matching a pattern) and executes
|
|
170
|
+
* in an "onion" model - first registered middleware is the outermost layer.
|
|
171
|
+
*
|
|
172
|
+
* Unlike directives which are applied per-field explicitly, middleware is
|
|
173
|
+
* applied globally or via pattern matching.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* // Logging middleware - applies to all fields
|
|
178
|
+
* middleware({
|
|
179
|
+
* name: "logging",
|
|
180
|
+
* apply: (effect, ctx) => Effect.gen(function*() {
|
|
181
|
+
* yield* Effect.logInfo(`Resolving ${ctx.info.fieldName}`)
|
|
182
|
+
* return yield* effect
|
|
183
|
+
* })
|
|
184
|
+
* })
|
|
185
|
+
*
|
|
186
|
+
* // Admin-only middleware - pattern matched
|
|
187
|
+
* middleware({
|
|
188
|
+
* name: "adminOnly",
|
|
189
|
+
* match: (info) => info.fieldName.startsWith("admin"),
|
|
190
|
+
* apply: (effect) => Effect.gen(function*() {
|
|
191
|
+
* const auth = yield* AuthService
|
|
192
|
+
* yield* auth.requireAdmin()
|
|
193
|
+
* return yield* effect
|
|
194
|
+
* })
|
|
195
|
+
* })
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export interface MiddlewareRegistration<R = never> {
|
|
199
|
+
readonly name: string
|
|
200
|
+
readonly description?: string
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Optional predicate to filter which fields this middleware applies to.
|
|
204
|
+
* If undefined, middleware applies to all fields.
|
|
205
|
+
* Receives the GraphQL resolve info for the field being resolved.
|
|
206
|
+
*/
|
|
207
|
+
readonly match?: (info: GraphQLResolveInfo) => boolean
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Transform the resolver Effect.
|
|
211
|
+
* Receives the resolver effect and full context (parent, args, info).
|
|
212
|
+
* Returns the transformed effect.
|
|
213
|
+
*
|
|
214
|
+
* Middleware executes in "onion" order - first registered is outermost.
|
|
215
|
+
*/
|
|
216
|
+
readonly apply: <A, E, R2>(
|
|
217
|
+
effect: Effect.Effect<A, E, R2>,
|
|
218
|
+
context: MiddlewareContext
|
|
219
|
+
) => Effect.Effect<A, E, R | R2>
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Configuration for a subscription field
|
|
224
|
+
* Returns a Stream that yields values over time
|
|
225
|
+
*/
|
|
226
|
+
export interface SubscriptionFieldRegistration<Args = any, A = any, E = any, R = any> {
|
|
227
|
+
type: S.Schema<A, any, any>
|
|
228
|
+
args?: S.Schema<Args, any, any>
|
|
229
|
+
description?: string
|
|
230
|
+
directives?: readonly DirectiveApplication[]
|
|
231
|
+
/**
|
|
232
|
+
* Complexity cost of this subscription.
|
|
233
|
+
* Can be a static number or a function that receives the resolved arguments.
|
|
234
|
+
* Used for query complexity limiting.
|
|
235
|
+
*/
|
|
236
|
+
complexity?: FieldComplexity
|
|
237
|
+
/**
|
|
238
|
+
* Cache control hint for this subscription.
|
|
239
|
+
* Note: Subscriptions are real-time and typically not cached,
|
|
240
|
+
* but this can be used for initial data hints.
|
|
241
|
+
*/
|
|
242
|
+
cacheControl?: CacheHint
|
|
243
|
+
/**
|
|
244
|
+
* Subscribe function returns an Effect that produces a Stream.
|
|
245
|
+
* The Stream yields values that are passed to the resolve function.
|
|
246
|
+
*/
|
|
247
|
+
subscribe: (args: Args) => Effect.Effect<Stream.Stream<A, E, R>, E, R>
|
|
248
|
+
/**
|
|
249
|
+
* Optional resolve function to transform each yielded value.
|
|
250
|
+
* If not provided, yields values directly.
|
|
251
|
+
*/
|
|
252
|
+
resolve?: (value: A, args: Args) => Effect.Effect<A, E, R>
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Configuration for a field on an object type
|
|
257
|
+
*/
|
|
258
|
+
export interface ObjectFieldRegistration<Parent = any, Args = any, A = any, E = any, R = any> {
|
|
259
|
+
type: S.Schema<A, any, any>
|
|
260
|
+
args?: S.Schema<Args, any, any>
|
|
261
|
+
description?: string
|
|
262
|
+
directives?: readonly DirectiveApplication[]
|
|
263
|
+
/**
|
|
264
|
+
* Complexity cost of this field.
|
|
265
|
+
* Can be a static number or a function that receives the resolved arguments.
|
|
266
|
+
* Used for query complexity limiting.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* // Relation field with pagination
|
|
270
|
+
* complexity: (args) => (args.limit ?? 10) * 2
|
|
271
|
+
*/
|
|
272
|
+
complexity?: FieldComplexity
|
|
273
|
+
/**
|
|
274
|
+
* Cache control hint for this field.
|
|
275
|
+
* Used to compute HTTP Cache-Control headers for the response.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* // Expensive computation, cache for 5 minutes
|
|
279
|
+
* cacheControl: { maxAge: 300 }
|
|
280
|
+
*
|
|
281
|
+
* // Inherit cache policy from parent type
|
|
282
|
+
* cacheControl: { inheritMaxAge: true }
|
|
283
|
+
*/
|
|
284
|
+
cacheControl?: CacheHint
|
|
285
|
+
resolve: (parent: Parent, args: Args) => Effect.Effect<A, E, R>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* GraphQL context that contains the Effect runtime
|
|
290
|
+
*/
|
|
291
|
+
export interface GraphQLEffectContext<R> {
|
|
292
|
+
runtime: Runtime.Runtime<R>
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Type registries used during schema building
|
|
297
|
+
*/
|
|
298
|
+
export interface TypeRegistries {
|
|
299
|
+
types: Map<string, import("graphql").GraphQLObjectType>
|
|
300
|
+
interfaces: Map<string, import("graphql").GraphQLInterfaceType>
|
|
301
|
+
enums: Map<string, import("graphql").GraphQLEnumType>
|
|
302
|
+
unions: Map<string, import("graphql").GraphQLUnionType>
|
|
303
|
+
inputs: Map<string, import("graphql").GraphQLInputObjectType>
|
|
304
|
+
directives: Map<string, import("graphql").GraphQLDirective>
|
|
305
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Context, Layer } from "effect"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GraphQL request context containing request-specific data
|
|
5
|
+
*/
|
|
6
|
+
export interface GraphQLRequestContext {
|
|
7
|
+
readonly request: {
|
|
8
|
+
readonly headers: Record<string, string>
|
|
9
|
+
readonly query: string
|
|
10
|
+
readonly variables?: Record<string, unknown>
|
|
11
|
+
readonly operationName?: string
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GraphQLRequestContext =
|
|
16
|
+
Context.GenericTag<GraphQLRequestContext>("GraphQLRequestContext")
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a layer from request context
|
|
20
|
+
*/
|
|
21
|
+
export const makeRequestContextLayer = (
|
|
22
|
+
context: GraphQLRequestContext
|
|
23
|
+
): Layer.Layer<GraphQLRequestContext> => Layer.succeed(GraphQLRequestContext, context)
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Data } from "effect"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for GraphQL errors in Effect
|
|
5
|
+
*/
|
|
6
|
+
export class GraphQLError extends Data.TaggedError("GraphQLError")<{
|
|
7
|
+
message: string
|
|
8
|
+
extensions?: Record<string, unknown>
|
|
9
|
+
}> {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validation error for input validation failures
|
|
13
|
+
*/
|
|
14
|
+
export class ValidationError extends Data.TaggedError("ValidationError")<{
|
|
15
|
+
message: string
|
|
16
|
+
field?: string
|
|
17
|
+
}> {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Authorization error for access control failures
|
|
21
|
+
*/
|
|
22
|
+
export class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
|
|
23
|
+
message: string
|
|
24
|
+
}> {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Not found error for missing resources
|
|
28
|
+
*/
|
|
29
|
+
export class NotFoundError extends Data.TaggedError("NotFoundError")<{
|
|
30
|
+
message: string
|
|
31
|
+
resource?: string
|
|
32
|
+
}> {}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Context, Effect, Ref } from "effect"
|
|
2
|
+
import type { DocumentNode, ExecutionResult, GraphQLError, GraphQLSchema } from "graphql"
|
|
3
|
+
import type { FieldComplexityMap } from "./server/complexity"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execution arguments passed to onExecuteStart hook
|
|
7
|
+
*/
|
|
8
|
+
export interface ExecutionArgs {
|
|
9
|
+
readonly source: string
|
|
10
|
+
readonly document: DocumentNode
|
|
11
|
+
readonly variableValues?: Record<string, unknown>
|
|
12
|
+
readonly operationName?: string
|
|
13
|
+
/** The GraphQL schema being executed against */
|
|
14
|
+
readonly schema: GraphQLSchema
|
|
15
|
+
/** Field complexity definitions from the schema builder */
|
|
16
|
+
readonly fieldComplexities: FieldComplexityMap
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for a GraphQL extension
|
|
21
|
+
*
|
|
22
|
+
* Extensions provide lifecycle hooks that run at each phase of request processing,
|
|
23
|
+
* and can contribute data to the response's `extensions` field.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Tracing extension
|
|
28
|
+
* extension({
|
|
29
|
+
* name: "tracing",
|
|
30
|
+
* onExecuteStart: () => Effect.gen(function*() {
|
|
31
|
+
* const ext = yield* ExtensionsService
|
|
32
|
+
* yield* ext.set("tracing", { startTime: Date.now() })
|
|
33
|
+
* }),
|
|
34
|
+
* onExecuteEnd: () => Effect.gen(function*() {
|
|
35
|
+
* const ext = yield* ExtensionsService
|
|
36
|
+
* yield* ext.merge("tracing", { endTime: Date.now() })
|
|
37
|
+
* }),
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export interface GraphQLExtension<R = never> {
|
|
42
|
+
readonly name: string
|
|
43
|
+
readonly description?: string
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Called after the query source is parsed into a DocumentNode.
|
|
47
|
+
* Useful for query analysis, caching parsed documents, etc.
|
|
48
|
+
*/
|
|
49
|
+
readonly onParse?: (source: string, document: DocumentNode) => Effect.Effect<void, never, R>
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Called after validation completes.
|
|
53
|
+
* Receives the document and any validation errors.
|
|
54
|
+
* Useful for complexity analysis, query whitelisting, etc.
|
|
55
|
+
*/
|
|
56
|
+
readonly onValidate?: (
|
|
57
|
+
document: DocumentNode,
|
|
58
|
+
errors: readonly GraphQLError[]
|
|
59
|
+
) => Effect.Effect<void, never, R>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Called before execution begins.
|
|
63
|
+
* Receives the full execution arguments.
|
|
64
|
+
* Useful for setting up tracing, logging, etc.
|
|
65
|
+
*/
|
|
66
|
+
readonly onExecuteStart?: (args: ExecutionArgs) => Effect.Effect<void, never, R>
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Called after execution completes.
|
|
70
|
+
* Receives the execution result (including data and errors).
|
|
71
|
+
* Useful for recording metrics, finalizing traces, etc.
|
|
72
|
+
*/
|
|
73
|
+
readonly onExecuteEnd?: (result: ExecutionResult) => Effect.Effect<void, never, R>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Service for accumulating extension data during request processing.
|
|
78
|
+
*
|
|
79
|
+
* This service is automatically provided for each request and allows
|
|
80
|
+
* extensions, middleware, and resolvers to contribute to the response
|
|
81
|
+
* extensions field.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* Effect.gen(function*() {
|
|
86
|
+
* const ext = yield* ExtensionsService
|
|
87
|
+
*
|
|
88
|
+
* // Set a value (overwrites existing)
|
|
89
|
+
* yield* ext.set("complexity", { score: 42 })
|
|
90
|
+
*
|
|
91
|
+
* // Merge into existing value
|
|
92
|
+
* yield* ext.merge("tracing", { endTime: Date.now() })
|
|
93
|
+
*
|
|
94
|
+
* // Get all accumulated extensions
|
|
95
|
+
* const all = yield* ext.get()
|
|
96
|
+
* })
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export interface ExtensionsService {
|
|
100
|
+
/**
|
|
101
|
+
* Set a key-value pair in the extensions.
|
|
102
|
+
* Overwrites any existing value for this key.
|
|
103
|
+
*/
|
|
104
|
+
readonly set: (key: string, value: unknown) => Effect.Effect<void>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Deep merge an object into an existing key's value.
|
|
108
|
+
* If the key doesn't exist, sets the value.
|
|
109
|
+
* If the existing value is not an object, overwrites it.
|
|
110
|
+
*/
|
|
111
|
+
readonly merge: (key: string, value: Record<string, unknown>) => Effect.Effect<void>
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get all accumulated extensions as a record.
|
|
115
|
+
*/
|
|
116
|
+
readonly get: () => Effect.Effect<Record<string, unknown>>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Tag for the ExtensionsService
|
|
121
|
+
*/
|
|
122
|
+
export const ExtensionsService = Context.GenericTag<ExtensionsService>(
|
|
123
|
+
"@effect-gql/ExtensionsService"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Deep merge two objects
|
|
128
|
+
*/
|
|
129
|
+
function deepMerge(
|
|
130
|
+
target: Record<string, unknown>,
|
|
131
|
+
source: Record<string, unknown>
|
|
132
|
+
): Record<string, unknown> {
|
|
133
|
+
const result = { ...target }
|
|
134
|
+
for (const key of Object.keys(source)) {
|
|
135
|
+
const sourceValue = source[key]
|
|
136
|
+
const targetValue = result[key]
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
typeof sourceValue === "object" &&
|
|
140
|
+
sourceValue !== null &&
|
|
141
|
+
!Array.isArray(sourceValue) &&
|
|
142
|
+
typeof targetValue === "object" &&
|
|
143
|
+
targetValue !== null &&
|
|
144
|
+
!Array.isArray(targetValue)
|
|
145
|
+
) {
|
|
146
|
+
result[key] = deepMerge(
|
|
147
|
+
targetValue as Record<string, unknown>,
|
|
148
|
+
sourceValue as Record<string, unknown>
|
|
149
|
+
)
|
|
150
|
+
} else {
|
|
151
|
+
result[key] = sourceValue
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Create a new ExtensionsService backed by a Ref
|
|
159
|
+
*/
|
|
160
|
+
export const makeExtensionsService = (): Effect.Effect<ExtensionsService, never, never> =>
|
|
161
|
+
Effect.gen(function* () {
|
|
162
|
+
const ref = yield* Ref.make<Record<string, unknown>>({})
|
|
163
|
+
|
|
164
|
+
return ExtensionsService.of({
|
|
165
|
+
set: (key, value) => Ref.update(ref, (current) => ({ ...current, [key]: value })),
|
|
166
|
+
|
|
167
|
+
merge: (key, value) =>
|
|
168
|
+
Ref.update(ref, (current) => {
|
|
169
|
+
const existing = current[key]
|
|
170
|
+
if (typeof existing === "object" && existing !== null && !Array.isArray(existing)) {
|
|
171
|
+
return {
|
|
172
|
+
...current,
|
|
173
|
+
[key]: deepMerge(existing as Record<string, unknown>, value),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { ...current, [key]: value }
|
|
177
|
+
}),
|
|
178
|
+
|
|
179
|
+
get: () => Ref.get(ref),
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Generic helper to run extension hooks with error handling.
|
|
185
|
+
* Filters extensions that have the specified hook, runs them,
|
|
186
|
+
* and logs warnings if any hook fails.
|
|
187
|
+
*/
|
|
188
|
+
const runExtensionHooks = <R, K extends keyof GraphQLExtension<R>>(
|
|
189
|
+
extensions: readonly GraphQLExtension<R>[],
|
|
190
|
+
hookName: K,
|
|
191
|
+
getHookEffect: (ext: GraphQLExtension<R>) => Effect.Effect<void, never, R>
|
|
192
|
+
): Effect.Effect<void, never, R> =>
|
|
193
|
+
Effect.forEach(
|
|
194
|
+
extensions.filter((ext) => ext[hookName] !== undefined),
|
|
195
|
+
(ext) =>
|
|
196
|
+
getHookEffect(ext).pipe(
|
|
197
|
+
Effect.catchAllCause((cause) =>
|
|
198
|
+
Effect.logWarning(`Extension "${ext.name}" ${String(hookName)} hook failed`, cause)
|
|
199
|
+
)
|
|
200
|
+
),
|
|
201
|
+
{ discard: true }
|
|
202
|
+
) as Effect.Effect<void, never, R>
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run all onParse hooks for registered extensions
|
|
206
|
+
*/
|
|
207
|
+
export const runParseHooks = <R>(
|
|
208
|
+
extensions: readonly GraphQLExtension<R>[],
|
|
209
|
+
source: string,
|
|
210
|
+
document: DocumentNode
|
|
211
|
+
): Effect.Effect<void, never, R> =>
|
|
212
|
+
runExtensionHooks(extensions, "onParse", (ext) => ext.onParse!(source, document))
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Run all onValidate hooks for registered extensions
|
|
216
|
+
*/
|
|
217
|
+
export const runValidateHooks = <R>(
|
|
218
|
+
extensions: readonly GraphQLExtension<R>[],
|
|
219
|
+
document: DocumentNode,
|
|
220
|
+
errors: readonly GraphQLError[]
|
|
221
|
+
): Effect.Effect<void, never, R> =>
|
|
222
|
+
runExtensionHooks(extensions, "onValidate", (ext) => ext.onValidate!(document, errors))
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run all onExecuteStart hooks for registered extensions
|
|
226
|
+
*/
|
|
227
|
+
export const runExecuteStartHooks = <R>(
|
|
228
|
+
extensions: readonly GraphQLExtension<R>[],
|
|
229
|
+
args: ExecutionArgs
|
|
230
|
+
): Effect.Effect<void, never, R> =>
|
|
231
|
+
runExtensionHooks(extensions, "onExecuteStart", (ext) => ext.onExecuteStart!(args))
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Run all onExecuteEnd hooks for registered extensions
|
|
235
|
+
*/
|
|
236
|
+
export const runExecuteEndHooks = <R>(
|
|
237
|
+
extensions: readonly GraphQLExtension<R>[],
|
|
238
|
+
result: ExecutionResult
|
|
239
|
+
): Effect.Effect<void, never, R> =>
|
|
240
|
+
runExtensionHooks(extensions, "onExecuteEnd", (ext) => ext.onExecuteEnd!(result))
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export * from "./builder"
|
|
2
|
+
export * from "./schema-mapping"
|
|
3
|
+
export * from "./error"
|
|
4
|
+
export * from "./context"
|
|
5
|
+
export * from "./loader"
|
|
6
|
+
export * from "./resolver-context"
|
|
7
|
+
export * from "./server"
|
|
8
|
+
export * from "./extensions"
|
|
9
|
+
export * from "./analyzer-extension"
|
|
10
|
+
|
|
11
|
+
// Re-export commonly used graphql types to ensure single instance
|
|
12
|
+
export {
|
|
13
|
+
GraphQLSchema,
|
|
14
|
+
GraphQLObjectType,
|
|
15
|
+
GraphQLScalarType,
|
|
16
|
+
GraphQLInterfaceType,
|
|
17
|
+
GraphQLUnionType,
|
|
18
|
+
GraphQLEnumType,
|
|
19
|
+
GraphQLInputObjectType,
|
|
20
|
+
GraphQLList,
|
|
21
|
+
GraphQLNonNull,
|
|
22
|
+
GraphQLString,
|
|
23
|
+
GraphQLInt,
|
|
24
|
+
GraphQLFloat,
|
|
25
|
+
GraphQLBoolean,
|
|
26
|
+
GraphQLID,
|
|
27
|
+
printSchema,
|
|
28
|
+
lexicographicSortSchema,
|
|
29
|
+
graphql,
|
|
30
|
+
Kind,
|
|
31
|
+
} from "graphql"
|
|
32
|
+
export type { ValueNode, GraphQLFieldConfigMap } from "graphql"
|