@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.
Files changed (145) hide show
  1. package/LICENSE +7 -0
  2. package/dist/analyzer-extension.d.ts +105 -0
  3. package/dist/analyzer-extension.d.ts.map +1 -0
  4. package/dist/analyzer-extension.js +137 -0
  5. package/dist/analyzer-extension.js.map +1 -0
  6. package/dist/builder/execute.d.ts +26 -0
  7. package/dist/builder/execute.d.ts.map +1 -0
  8. package/dist/builder/execute.js +104 -0
  9. package/dist/builder/execute.js.map +1 -0
  10. package/dist/builder/field-builders.d.ts +30 -0
  11. package/dist/builder/field-builders.d.ts.map +1 -0
  12. package/dist/builder/field-builders.js +200 -0
  13. package/dist/builder/field-builders.js.map +1 -0
  14. package/dist/builder/index.d.ts +7 -0
  15. package/dist/builder/index.d.ts.map +1 -0
  16. package/dist/builder/index.js +31 -0
  17. package/dist/builder/index.js.map +1 -0
  18. package/dist/builder/pipe-api.d.ts +231 -0
  19. package/dist/builder/pipe-api.d.ts.map +1 -0
  20. package/dist/builder/pipe-api.js +151 -0
  21. package/dist/builder/pipe-api.js.map +1 -0
  22. package/dist/builder/schema-builder.d.ts +301 -0
  23. package/dist/builder/schema-builder.d.ts.map +1 -0
  24. package/dist/builder/schema-builder.js +566 -0
  25. package/dist/builder/schema-builder.js.map +1 -0
  26. package/dist/builder/type-registry.d.ts +80 -0
  27. package/dist/builder/type-registry.d.ts.map +1 -0
  28. package/dist/builder/type-registry.js +505 -0
  29. package/dist/builder/type-registry.js.map +1 -0
  30. package/dist/builder/types.d.ts +283 -0
  31. package/dist/builder/types.d.ts.map +1 -0
  32. package/dist/builder/types.js +3 -0
  33. package/dist/builder/types.js.map +1 -0
  34. package/dist/cli/generate-schema.d.ts +29 -0
  35. package/dist/cli/generate-schema.d.ts.map +1 -0
  36. package/dist/cli/generate-schema.js +233 -0
  37. package/dist/cli/generate-schema.js.map +1 -0
  38. package/dist/cli/index.d.ts +19 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +24 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/context.d.ts +18 -0
  43. package/dist/context.d.ts.map +1 -0
  44. package/dist/context.js +11 -0
  45. package/dist/context.js.map +1 -0
  46. package/dist/error.d.ts +45 -0
  47. package/dist/error.d.ts.map +1 -0
  48. package/dist/error.js +29 -0
  49. package/dist/error.js.map +1 -0
  50. package/dist/extensions.d.ts +130 -0
  51. package/dist/extensions.d.ts.map +1 -0
  52. package/dist/extensions.js +78 -0
  53. package/dist/extensions.js.map +1 -0
  54. package/dist/index.d.ts +12 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +47 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/loader.d.ts +169 -0
  59. package/dist/loader.d.ts.map +1 -0
  60. package/dist/loader.js +237 -0
  61. package/dist/loader.js.map +1 -0
  62. package/dist/resolver-context.d.ts +154 -0
  63. package/dist/resolver-context.d.ts.map +1 -0
  64. package/dist/resolver-context.js +184 -0
  65. package/dist/resolver-context.js.map +1 -0
  66. package/dist/schema-mapping.d.ts +30 -0
  67. package/dist/schema-mapping.d.ts.map +1 -0
  68. package/dist/schema-mapping.js +280 -0
  69. package/dist/schema-mapping.js.map +1 -0
  70. package/dist/server/cache-control.d.ts +96 -0
  71. package/dist/server/cache-control.d.ts.map +1 -0
  72. package/dist/server/cache-control.js +308 -0
  73. package/dist/server/cache-control.js.map +1 -0
  74. package/dist/server/complexity.d.ts +165 -0
  75. package/dist/server/complexity.d.ts.map +1 -0
  76. package/dist/server/complexity.js +433 -0
  77. package/dist/server/complexity.js.map +1 -0
  78. package/dist/server/config.d.ts +66 -0
  79. package/dist/server/config.d.ts.map +1 -0
  80. package/dist/server/config.js +104 -0
  81. package/dist/server/config.js.map +1 -0
  82. package/dist/server/graphiql.d.ts +5 -0
  83. package/dist/server/graphiql.d.ts.map +1 -0
  84. package/dist/server/graphiql.js +43 -0
  85. package/dist/server/graphiql.js.map +1 -0
  86. package/dist/server/index.d.ts +18 -0
  87. package/dist/server/index.d.ts.map +1 -0
  88. package/dist/server/index.js +48 -0
  89. package/dist/server/index.js.map +1 -0
  90. package/dist/server/router.d.ts +79 -0
  91. package/dist/server/router.d.ts.map +1 -0
  92. package/dist/server/router.js +232 -0
  93. package/dist/server/router.js.map +1 -0
  94. package/dist/server/schema-builder-extensions.d.ts +42 -0
  95. package/dist/server/schema-builder-extensions.d.ts.map +1 -0
  96. package/dist/server/schema-builder-extensions.js +48 -0
  97. package/dist/server/schema-builder-extensions.js.map +1 -0
  98. package/dist/server/sse-adapter.d.ts +64 -0
  99. package/dist/server/sse-adapter.d.ts.map +1 -0
  100. package/dist/server/sse-adapter.js +227 -0
  101. package/dist/server/sse-adapter.js.map +1 -0
  102. package/dist/server/sse-types.d.ts +192 -0
  103. package/dist/server/sse-types.d.ts.map +1 -0
  104. package/dist/server/sse-types.js +63 -0
  105. package/dist/server/sse-types.js.map +1 -0
  106. package/dist/server/ws-adapter.d.ts +39 -0
  107. package/dist/server/ws-adapter.d.ts.map +1 -0
  108. package/dist/server/ws-adapter.js +247 -0
  109. package/dist/server/ws-adapter.js.map +1 -0
  110. package/dist/server/ws-types.d.ts +169 -0
  111. package/dist/server/ws-types.d.ts.map +1 -0
  112. package/dist/server/ws-types.js +11 -0
  113. package/dist/server/ws-types.js.map +1 -0
  114. package/dist/server/ws-utils.d.ts +42 -0
  115. package/dist/server/ws-utils.d.ts.map +1 -0
  116. package/dist/server/ws-utils.js +99 -0
  117. package/dist/server/ws-utils.js.map +1 -0
  118. package/package.json +61 -0
  119. package/src/analyzer-extension.ts +254 -0
  120. package/src/builder/execute.ts +153 -0
  121. package/src/builder/field-builders.ts +322 -0
  122. package/src/builder/index.ts +48 -0
  123. package/src/builder/pipe-api.ts +312 -0
  124. package/src/builder/schema-builder.ts +970 -0
  125. package/src/builder/type-registry.ts +670 -0
  126. package/src/builder/types.ts +305 -0
  127. package/src/context.ts +23 -0
  128. package/src/error.ts +32 -0
  129. package/src/extensions.ts +240 -0
  130. package/src/index.ts +32 -0
  131. package/src/loader.ts +363 -0
  132. package/src/resolver-context.ts +253 -0
  133. package/src/schema-mapping.ts +307 -0
  134. package/src/server/cache-control.ts +590 -0
  135. package/src/server/complexity.ts +774 -0
  136. package/src/server/config.ts +174 -0
  137. package/src/server/graphiql.ts +38 -0
  138. package/src/server/index.ts +96 -0
  139. package/src/server/router.ts +432 -0
  140. package/src/server/schema-builder-extensions.ts +51 -0
  141. package/src/server/sse-adapter.ts +327 -0
  142. package/src/server/sse-types.ts +234 -0
  143. package/src/server/ws-adapter.ts +355 -0
  144. package/src/server/ws-types.ts +192 -0
  145. 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"