@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,254 @@
1
+ import { Effect } from "effect"
2
+ import { Kind, type OperationDefinitionNode } from "graphql"
3
+ import type { GraphQLExtension, ExecutionArgs } from "./extensions"
4
+ import { ExtensionsService } from "./extensions"
5
+ import {
6
+ defaultComplexityCalculator,
7
+ type ComplexityResult,
8
+ type FieldComplexityMap,
9
+ } from "./server/complexity"
10
+
11
+ /**
12
+ * Configuration for the analyzer extension
13
+ */
14
+ export interface AnalyzerExtensionConfig {
15
+ /**
16
+ * Include the total complexity score in the response.
17
+ * @default true
18
+ */
19
+ readonly includeComplexity?: boolean
20
+
21
+ /**
22
+ * Include the maximum query depth in the response.
23
+ * @default true
24
+ */
25
+ readonly includeDepth?: boolean
26
+
27
+ /**
28
+ * Include the total field count in the response.
29
+ * @default false
30
+ */
31
+ readonly includeFieldCount?: boolean
32
+
33
+ /**
34
+ * Include the alias count in the response.
35
+ * @default false
36
+ */
37
+ readonly includeAliasCount?: boolean
38
+
39
+ /**
40
+ * The key to use in the response extensions object.
41
+ * @default "analyzer"
42
+ */
43
+ readonly key?: string
44
+
45
+ /**
46
+ * Thresholds for logging warnings when exceeded.
47
+ * When a metric exceeds its threshold, a warning is logged.
48
+ */
49
+ readonly thresholds?: {
50
+ readonly depth?: number
51
+ readonly complexity?: number
52
+ readonly fieldCount?: number
53
+ readonly aliasCount?: number
54
+ }
55
+
56
+ /**
57
+ * Default complexity cost for fields without explicit costs.
58
+ * @default 1
59
+ */
60
+ readonly defaultFieldComplexity?: number
61
+
62
+ /**
63
+ * Optional field complexity overrides.
64
+ * If not provided, uses the field complexities from the schema builder
65
+ * (passed via ExecutionArgs).
66
+ */
67
+ readonly fieldComplexities?: FieldComplexityMap
68
+ }
69
+
70
+ /**
71
+ * Output format for analyzer extension
72
+ */
73
+ export interface AnalyzerOutput {
74
+ complexity?: number
75
+ depth?: number
76
+ fieldCount?: number
77
+ aliasCount?: number
78
+ }
79
+
80
+ /**
81
+ * Create an analyzer extension that reports query complexity metrics
82
+ * in the response extensions field.
83
+ *
84
+ * Similar to async-graphql's Analyzer extension, this allows you to
85
+ * monitor the complexity of incoming queries without blocking execution.
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * // Basic usage - reports complexity and depth
90
+ * const analyzer = createAnalyzerExtension()
91
+ *
92
+ * // With all metrics and warnings
93
+ * const analyzer = createAnalyzerExtension({
94
+ * includeFieldCount: true,
95
+ * includeAliasCount: true,
96
+ * thresholds: {
97
+ * depth: 10,
98
+ * complexity: 100,
99
+ * },
100
+ * })
101
+ *
102
+ * // Add to schema builder
103
+ * const builder = GraphQLSchemaBuilder.empty.pipe(
104
+ * extension(analyzer),
105
+ * // ...queries, mutations, etc.
106
+ * )
107
+ *
108
+ * // Response will include:
109
+ * // {
110
+ * // "data": { ... },
111
+ * // "extensions": {
112
+ * // "analyzer": {
113
+ * // "complexity": 42,
114
+ * // "depth": 3
115
+ * // }
116
+ * // }
117
+ * // }
118
+ * ```
119
+ */
120
+ export const createAnalyzerExtension = (
121
+ config: AnalyzerExtensionConfig = {}
122
+ ): GraphQLExtension<ExtensionsService> => {
123
+ const {
124
+ includeComplexity = true,
125
+ includeDepth = true,
126
+ includeFieldCount = false,
127
+ includeAliasCount = false,
128
+ key = "analyzer",
129
+ thresholds,
130
+ defaultFieldComplexity = 1,
131
+ fieldComplexities: configFieldComplexities,
132
+ } = config
133
+
134
+ return {
135
+ name: "analyzer",
136
+ description: "Reports query complexity metrics in response extensions",
137
+
138
+ onExecuteStart: (args: ExecutionArgs) =>
139
+ Effect.gen(function* () {
140
+ const ext = yield* ExtensionsService
141
+
142
+ // Find the operation
143
+ const operation = findOperation(args)
144
+ if (!operation) {
145
+ return
146
+ }
147
+
148
+ // Use config field complexities if provided, otherwise use from args
149
+ const fieldComplexities = configFieldComplexities ?? args.fieldComplexities
150
+
151
+ // Calculate complexity
152
+ const calculator = defaultComplexityCalculator(defaultFieldComplexity)
153
+ const result = yield* calculator({
154
+ document: args.document,
155
+ operation,
156
+ variables: args.variableValues,
157
+ schema: args.schema,
158
+ fieldComplexities,
159
+ }).pipe(
160
+ Effect.catchAll((error) =>
161
+ Effect.logWarning("Analyzer extension: complexity calculation failed", error).pipe(
162
+ Effect.as(null)
163
+ )
164
+ )
165
+ )
166
+
167
+ if (!result) {
168
+ return
169
+ }
170
+
171
+ // Check thresholds and log warnings
172
+ yield* checkThresholds(result, thresholds)
173
+
174
+ // Build output
175
+ const output: AnalyzerOutput = {}
176
+ if (includeComplexity) {
177
+ output.complexity = result.complexity
178
+ }
179
+ if (includeDepth) {
180
+ output.depth = result.depth
181
+ }
182
+ if (includeFieldCount) {
183
+ output.fieldCount = result.fieldCount
184
+ }
185
+ if (includeAliasCount) {
186
+ output.aliasCount = result.aliasCount
187
+ }
188
+
189
+ // Add to extensions
190
+ yield* ext.set(key, output)
191
+ }),
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Find the operation to analyze from the document
197
+ */
198
+ function findOperation(args: ExecutionArgs): OperationDefinitionNode | null {
199
+ const operations = args.document.definitions.filter(
200
+ (d): d is OperationDefinitionNode => d.kind === Kind.OPERATION_DEFINITION
201
+ )
202
+
203
+ if (operations.length === 0) {
204
+ return null
205
+ }
206
+
207
+ if (args.operationName) {
208
+ return operations.find((o) => o.name?.value === args.operationName) ?? null
209
+ }
210
+
211
+ return operations[0]
212
+ }
213
+
214
+ /**
215
+ * Check thresholds and log warnings for exceeded values
216
+ */
217
+ function checkThresholds(
218
+ result: ComplexityResult,
219
+ thresholds?: AnalyzerExtensionConfig["thresholds"]
220
+ ): Effect.Effect<void> {
221
+ if (!thresholds) {
222
+ return Effect.void
223
+ }
224
+
225
+ const warnings: string[] = []
226
+
227
+ if (thresholds.depth !== undefined && result.depth > thresholds.depth) {
228
+ warnings.push(`Query depth ${result.depth} exceeds threshold ${thresholds.depth}`)
229
+ }
230
+ if (thresholds.complexity !== undefined && result.complexity > thresholds.complexity) {
231
+ warnings.push(
232
+ `Query complexity ${result.complexity} exceeds threshold ${thresholds.complexity}`
233
+ )
234
+ }
235
+ if (thresholds.fieldCount !== undefined && result.fieldCount > thresholds.fieldCount) {
236
+ warnings.push(
237
+ `Query field count ${result.fieldCount} exceeds threshold ${thresholds.fieldCount}`
238
+ )
239
+ }
240
+ if (thresholds.aliasCount !== undefined && result.aliasCount > thresholds.aliasCount) {
241
+ warnings.push(
242
+ `Query alias count ${result.aliasCount} exceeds threshold ${thresholds.aliasCount}`
243
+ )
244
+ }
245
+
246
+ if (warnings.length > 0) {
247
+ return Effect.logWarning("Analyzer extension: thresholds exceeded", {
248
+ warnings,
249
+ result,
250
+ })
251
+ }
252
+
253
+ return Effect.void
254
+ }
@@ -0,0 +1,153 @@
1
+ import { Effect, Layer } from "effect"
2
+ import {
3
+ GraphQLSchema,
4
+ GraphQLError,
5
+ parse,
6
+ validate,
7
+ execute as graphqlExecute,
8
+ type ExecutionResult,
9
+ type DocumentNode,
10
+ } from "graphql"
11
+ import type { GraphQLEffectContext } from "./types"
12
+ import {
13
+ type GraphQLExtension,
14
+ ExtensionsService,
15
+ makeExtensionsService,
16
+ runParseHooks,
17
+ runValidateHooks,
18
+ runExecuteStartHooks,
19
+ runExecuteEndHooks,
20
+ } from "../extensions"
21
+ import type { FieldComplexityMap } from "../server/complexity"
22
+
23
+ /**
24
+ * Execute a GraphQL query with a service layer
25
+ *
26
+ * This is the layer-per-request execution model. Build the schema once,
27
+ * then execute each request with its own layer (including request-scoped services).
28
+ *
29
+ * The execution follows these phases:
30
+ * 1. Parse - Convert source string to DocumentNode
31
+ * 2. Validate - Check document against schema
32
+ * 3. Execute - Run resolvers and return result
33
+ *
34
+ * Extensions can hook into each phase via onParse, onValidate, onExecuteStart, onExecuteEnd.
35
+ * Extension data is automatically merged into the response's extensions field.
36
+ */
37
+ export const execute =
38
+ <R>(
39
+ schema: GraphQLSchema,
40
+ layer: Layer.Layer<R>,
41
+ extensions: readonly GraphQLExtension<any>[] = [],
42
+ fieldComplexities: FieldComplexityMap = new Map()
43
+ ) =>
44
+ (
45
+ source: string,
46
+ variableValues?: Record<string, unknown>,
47
+ operationName?: string
48
+ ): Effect.Effect<ExecutionResult, Error> =>
49
+ Effect.gen(function* () {
50
+ // Create the ExtensionsService for this request
51
+ const extensionsService = yield* makeExtensionsService()
52
+
53
+ // Create runtime from the provided layer
54
+ const runtime = yield* Effect.runtime<R>()
55
+
56
+ // Phase 1: Parse
57
+ let document: DocumentNode
58
+ try {
59
+ document = parse(source)
60
+ } catch (parseError) {
61
+ // Parse errors are returned as GraphQL errors, not thrown
62
+ const extensionData = yield* extensionsService.get()
63
+ return {
64
+ errors: [
65
+ parseError instanceof GraphQLError ? parseError : new GraphQLError(String(parseError)),
66
+ ],
67
+ extensions: Object.keys(extensionData).length > 0 ? extensionData : undefined,
68
+ } as ExecutionResult
69
+ }
70
+
71
+ // Run onParse hooks
72
+ yield* runParseHooks(extensions, source, document).pipe(
73
+ Effect.provideService(ExtensionsService, extensionsService)
74
+ )
75
+
76
+ // Phase 2: Validate
77
+ const validationErrors = validate(schema, document)
78
+
79
+ // Run onValidate hooks
80
+ yield* runValidateHooks(extensions, document, validationErrors).pipe(
81
+ Effect.provideService(ExtensionsService, extensionsService)
82
+ )
83
+
84
+ // If validation failed, return errors without executing
85
+ if (validationErrors.length > 0) {
86
+ const extensionData = yield* extensionsService.get()
87
+ return {
88
+ errors: validationErrors,
89
+ extensions: Object.keys(extensionData).length > 0 ? extensionData : undefined,
90
+ } as ExecutionResult
91
+ }
92
+
93
+ // Phase 3: Execute
94
+ const executionArgs = {
95
+ source,
96
+ document,
97
+ variableValues,
98
+ operationName,
99
+ schema,
100
+ fieldComplexities,
101
+ }
102
+
103
+ // Run onExecuteStart hooks
104
+ yield* runExecuteStartHooks(extensions, executionArgs).pipe(
105
+ Effect.provideService(ExtensionsService, extensionsService)
106
+ )
107
+
108
+ // Execute the GraphQL query
109
+ const executeResult = yield* Effect.try({
110
+ try: () =>
111
+ graphqlExecute({
112
+ schema,
113
+ document,
114
+ variableValues,
115
+ operationName,
116
+ contextValue: { runtime } satisfies GraphQLEffectContext<R>,
117
+ }),
118
+ catch: (error) => new Error(String(error)),
119
+ })
120
+
121
+ // Await result if it's a promise (for subscriptions, it might be)
122
+ const resolvedResult: Awaited<typeof executeResult> =
123
+ executeResult && typeof executeResult === "object" && "then" in executeResult
124
+ ? yield* Effect.promise(() => executeResult)
125
+ : executeResult
126
+
127
+ // Run onExecuteEnd hooks
128
+ yield* runExecuteEndHooks(extensions, resolvedResult).pipe(
129
+ Effect.provideService(ExtensionsService, extensionsService)
130
+ )
131
+
132
+ // Merge extension data into result
133
+ const extensionData = yield* extensionsService.get()
134
+ if (Object.keys(extensionData).length > 0) {
135
+ return {
136
+ ...resolvedResult,
137
+ extensions: {
138
+ ...resolvedResult.extensions,
139
+ ...extensionData,
140
+ },
141
+ }
142
+ }
143
+
144
+ return resolvedResult
145
+ }).pipe(Effect.provide(layer)) as Effect.Effect<ExecutionResult, Error>
146
+
147
+ /**
148
+ * Execute a GraphQL query with a service layer (simple version without extensions)
149
+ *
150
+ * @deprecated Use execute() instead, which now supports extensions as an optional parameter
151
+ */
152
+ export const executeSimple = <R>(schema: GraphQLSchema, layer: Layer.Layer<R>) =>
153
+ execute(schema, layer, [])