@effect-gql/core 0.1.0 → 1.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 (164) hide show
  1. package/README.md +100 -0
  2. package/builder/index.cjs +1446 -0
  3. package/builder/index.cjs.map +1 -0
  4. package/builder/index.d.cts +260 -0
  5. package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +50 -21
  6. package/builder/index.js +1405 -0
  7. package/builder/index.js.map +1 -0
  8. package/index.cjs +3469 -0
  9. package/index.cjs.map +1 -0
  10. package/index.d.cts +529 -0
  11. package/index.d.ts +529 -0
  12. package/index.js +3292 -0
  13. package/index.js.map +1 -0
  14. package/package.json +19 -28
  15. package/schema-builder-DKvkzU_M.d.cts +965 -0
  16. package/schema-builder-DKvkzU_M.d.ts +965 -0
  17. package/server/index.cjs +1579 -0
  18. package/server/index.cjs.map +1 -0
  19. package/server/index.d.cts +682 -0
  20. package/server/index.d.ts +682 -0
  21. package/server/index.js +1548 -0
  22. package/server/index.js.map +1 -0
  23. package/dist/analyzer-extension.d.ts +0 -105
  24. package/dist/analyzer-extension.d.ts.map +0 -1
  25. package/dist/analyzer-extension.js +0 -137
  26. package/dist/analyzer-extension.js.map +0 -1
  27. package/dist/builder/execute.d.ts +0 -26
  28. package/dist/builder/execute.d.ts.map +0 -1
  29. package/dist/builder/execute.js +0 -104
  30. package/dist/builder/execute.js.map +0 -1
  31. package/dist/builder/field-builders.d.ts +0 -30
  32. package/dist/builder/field-builders.d.ts.map +0 -1
  33. package/dist/builder/field-builders.js +0 -200
  34. package/dist/builder/field-builders.js.map +0 -1
  35. package/dist/builder/index.d.ts +0 -7
  36. package/dist/builder/index.d.ts.map +0 -1
  37. package/dist/builder/index.js +0 -31
  38. package/dist/builder/index.js.map +0 -1
  39. package/dist/builder/pipe-api.d.ts.map +0 -1
  40. package/dist/builder/pipe-api.js +0 -151
  41. package/dist/builder/pipe-api.js.map +0 -1
  42. package/dist/builder/schema-builder.d.ts +0 -301
  43. package/dist/builder/schema-builder.d.ts.map +0 -1
  44. package/dist/builder/schema-builder.js +0 -566
  45. package/dist/builder/schema-builder.js.map +0 -1
  46. package/dist/builder/type-registry.d.ts +0 -80
  47. package/dist/builder/type-registry.d.ts.map +0 -1
  48. package/dist/builder/type-registry.js +0 -505
  49. package/dist/builder/type-registry.js.map +0 -1
  50. package/dist/builder/types.d.ts +0 -283
  51. package/dist/builder/types.d.ts.map +0 -1
  52. package/dist/builder/types.js +0 -3
  53. package/dist/builder/types.js.map +0 -1
  54. package/dist/cli/generate-schema.d.ts +0 -29
  55. package/dist/cli/generate-schema.d.ts.map +0 -1
  56. package/dist/cli/generate-schema.js +0 -233
  57. package/dist/cli/generate-schema.js.map +0 -1
  58. package/dist/cli/index.d.ts +0 -19
  59. package/dist/cli/index.d.ts.map +0 -1
  60. package/dist/cli/index.js +0 -24
  61. package/dist/cli/index.js.map +0 -1
  62. package/dist/context.d.ts +0 -18
  63. package/dist/context.d.ts.map +0 -1
  64. package/dist/context.js +0 -11
  65. package/dist/context.js.map +0 -1
  66. package/dist/error.d.ts +0 -45
  67. package/dist/error.d.ts.map +0 -1
  68. package/dist/error.js +0 -29
  69. package/dist/error.js.map +0 -1
  70. package/dist/extensions.d.ts +0 -130
  71. package/dist/extensions.d.ts.map +0 -1
  72. package/dist/extensions.js +0 -78
  73. package/dist/extensions.js.map +0 -1
  74. package/dist/index.d.ts +0 -12
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js +0 -47
  77. package/dist/index.js.map +0 -1
  78. package/dist/loader.d.ts +0 -169
  79. package/dist/loader.d.ts.map +0 -1
  80. package/dist/loader.js +0 -237
  81. package/dist/loader.js.map +0 -1
  82. package/dist/resolver-context.d.ts +0 -154
  83. package/dist/resolver-context.d.ts.map +0 -1
  84. package/dist/resolver-context.js +0 -184
  85. package/dist/resolver-context.js.map +0 -1
  86. package/dist/schema-mapping.d.ts +0 -30
  87. package/dist/schema-mapping.d.ts.map +0 -1
  88. package/dist/schema-mapping.js +0 -280
  89. package/dist/schema-mapping.js.map +0 -1
  90. package/dist/server/cache-control.d.ts +0 -96
  91. package/dist/server/cache-control.d.ts.map +0 -1
  92. package/dist/server/cache-control.js +0 -308
  93. package/dist/server/cache-control.js.map +0 -1
  94. package/dist/server/complexity.d.ts +0 -165
  95. package/dist/server/complexity.d.ts.map +0 -1
  96. package/dist/server/complexity.js +0 -433
  97. package/dist/server/complexity.js.map +0 -1
  98. package/dist/server/config.d.ts +0 -66
  99. package/dist/server/config.d.ts.map +0 -1
  100. package/dist/server/config.js +0 -104
  101. package/dist/server/config.js.map +0 -1
  102. package/dist/server/graphiql.d.ts +0 -5
  103. package/dist/server/graphiql.d.ts.map +0 -1
  104. package/dist/server/graphiql.js +0 -43
  105. package/dist/server/graphiql.js.map +0 -1
  106. package/dist/server/index.d.ts +0 -18
  107. package/dist/server/index.d.ts.map +0 -1
  108. package/dist/server/index.js +0 -48
  109. package/dist/server/index.js.map +0 -1
  110. package/dist/server/router.d.ts +0 -79
  111. package/dist/server/router.d.ts.map +0 -1
  112. package/dist/server/router.js +0 -232
  113. package/dist/server/router.js.map +0 -1
  114. package/dist/server/schema-builder-extensions.d.ts +0 -42
  115. package/dist/server/schema-builder-extensions.d.ts.map +0 -1
  116. package/dist/server/schema-builder-extensions.js +0 -48
  117. package/dist/server/schema-builder-extensions.js.map +0 -1
  118. package/dist/server/sse-adapter.d.ts +0 -64
  119. package/dist/server/sse-adapter.d.ts.map +0 -1
  120. package/dist/server/sse-adapter.js +0 -227
  121. package/dist/server/sse-adapter.js.map +0 -1
  122. package/dist/server/sse-types.d.ts +0 -192
  123. package/dist/server/sse-types.d.ts.map +0 -1
  124. package/dist/server/sse-types.js +0 -63
  125. package/dist/server/sse-types.js.map +0 -1
  126. package/dist/server/ws-adapter.d.ts +0 -39
  127. package/dist/server/ws-adapter.d.ts.map +0 -1
  128. package/dist/server/ws-adapter.js +0 -247
  129. package/dist/server/ws-adapter.js.map +0 -1
  130. package/dist/server/ws-types.d.ts +0 -169
  131. package/dist/server/ws-types.d.ts.map +0 -1
  132. package/dist/server/ws-types.js +0 -11
  133. package/dist/server/ws-types.js.map +0 -1
  134. package/dist/server/ws-utils.d.ts +0 -42
  135. package/dist/server/ws-utils.d.ts.map +0 -1
  136. package/dist/server/ws-utils.js +0 -99
  137. package/dist/server/ws-utils.js.map +0 -1
  138. package/src/analyzer-extension.ts +0 -254
  139. package/src/builder/execute.ts +0 -153
  140. package/src/builder/field-builders.ts +0 -322
  141. package/src/builder/index.ts +0 -48
  142. package/src/builder/pipe-api.ts +0 -312
  143. package/src/builder/schema-builder.ts +0 -970
  144. package/src/builder/type-registry.ts +0 -670
  145. package/src/builder/types.ts +0 -305
  146. package/src/context.ts +0 -23
  147. package/src/error.ts +0 -32
  148. package/src/extensions.ts +0 -240
  149. package/src/index.ts +0 -32
  150. package/src/loader.ts +0 -363
  151. package/src/resolver-context.ts +0 -253
  152. package/src/schema-mapping.ts +0 -307
  153. package/src/server/cache-control.ts +0 -590
  154. package/src/server/complexity.ts +0 -774
  155. package/src/server/config.ts +0 -174
  156. package/src/server/graphiql.ts +0 -38
  157. package/src/server/index.ts +0 -96
  158. package/src/server/router.ts +0 -432
  159. package/src/server/schema-builder-extensions.ts +0 -51
  160. package/src/server/sse-adapter.ts +0 -327
  161. package/src/server/sse-types.ts +0 -234
  162. package/src/server/ws-adapter.ts +0 -355
  163. package/src/server/ws-types.ts +0 -192
  164. package/src/server/ws-utils.ts +0 -136
@@ -1,590 +0,0 @@
1
- import { Effect, Config } from "effect"
2
- import {
3
- DocumentNode,
4
- OperationDefinitionNode,
5
- FieldNode,
6
- FragmentDefinitionNode,
7
- SelectionSetNode,
8
- GraphQLSchema,
9
- GraphQLObjectType,
10
- GraphQLOutputType,
11
- GraphQLNonNull,
12
- GraphQLList,
13
- GraphQLScalarType,
14
- GraphQLEnumType,
15
- Kind,
16
- parse,
17
- } from "graphql"
18
- import type { CacheHint, CacheControlScope } from "../builder/types"
19
-
20
- // ============================================================================
21
- // Types
22
- // ============================================================================
23
-
24
- /**
25
- * Map of type.field -> cache hint, or type -> cache hint for type-level hints
26
- */
27
- export type CacheHintMap = Map<string, CacheHint>
28
-
29
- /**
30
- * Computed cache policy for a GraphQL response
31
- */
32
- export interface CachePolicy {
33
- /**
34
- * Maximum age in seconds the response can be cached.
35
- * This is the minimum maxAge of all resolved fields.
36
- * If 0, the response should not be cached.
37
- */
38
- readonly maxAge: number
39
-
40
- /**
41
- * Cache scope - PUBLIC means CDN-cacheable, PRIVATE means browser-only.
42
- * If any field is PRIVATE, the entire response is PRIVATE.
43
- */
44
- readonly scope: CacheControlScope
45
- }
46
-
47
- /**
48
- * Configuration for cache control
49
- */
50
- export interface CacheControlConfig {
51
- /**
52
- * Enable cache control header calculation.
53
- * @default true
54
- */
55
- readonly enabled?: boolean
56
-
57
- /**
58
- * Default maxAge for root fields (Query, Mutation).
59
- * @default 0 (no caching)
60
- */
61
- readonly defaultMaxAge?: number
62
-
63
- /**
64
- * Default scope for fields without explicit scope.
65
- * @default "PUBLIC"
66
- */
67
- readonly defaultScope?: CacheControlScope
68
-
69
- /**
70
- * Whether to set HTTP Cache-Control headers on responses.
71
- * @default true
72
- */
73
- readonly calculateHttpHeaders?: boolean
74
- }
75
-
76
- /**
77
- * Information provided to cache policy calculation
78
- */
79
- export interface CachePolicyAnalysisInfo {
80
- /** Parsed GraphQL document */
81
- readonly document: DocumentNode
82
- /** The operation being executed */
83
- readonly operation: OperationDefinitionNode
84
- /** The GraphQL schema */
85
- readonly schema: GraphQLSchema
86
- /** Cache hints from the builder (type.field -> hint or type -> hint) */
87
- readonly cacheHints: CacheHintMap
88
- /** Configuration options */
89
- readonly config: CacheControlConfig
90
- }
91
-
92
- // ============================================================================
93
- // Cache Policy Computation
94
- // ============================================================================
95
-
96
- /**
97
- * Compute the cache policy for a GraphQL response based on the fields resolved.
98
- *
99
- * The policy is computed by walking the selection set and aggregating hints:
100
- * - maxAge: Use the minimum maxAge of all resolved fields
101
- * - scope: If any field is PRIVATE, the entire response is PRIVATE
102
- *
103
- * Default behaviors (matching Apollo):
104
- * - Root fields default to maxAge: 0 (unless configured otherwise)
105
- * - Object-returning fields default to maxAge: 0
106
- * - Scalar fields inherit their parent's maxAge
107
- * - Fields with inheritMaxAge: true inherit from parent
108
- */
109
- export const computeCachePolicy = (
110
- info: CachePolicyAnalysisInfo
111
- ): Effect.Effect<CachePolicy, never, never> =>
112
- Effect.sync(() => {
113
- const fragments = new Map<string, FragmentDefinitionNode>()
114
-
115
- // Collect fragment definitions
116
- for (const definition of info.document.definitions) {
117
- if (definition.kind === Kind.FRAGMENT_DEFINITION) {
118
- fragments.set(definition.name.value, definition)
119
- }
120
- }
121
-
122
- // Get the root type for the operation
123
- const rootType = getRootType(info.schema, info.operation.operation)
124
- if (!rootType) {
125
- // No root type - return no-cache
126
- return { maxAge: 0, scope: "PUBLIC" as const }
127
- }
128
-
129
- const defaultMaxAge = info.config.defaultMaxAge ?? 0
130
- const defaultScope = info.config.defaultScope ?? "PUBLIC"
131
-
132
- // Analyze the selection set
133
- const result = analyzeSelectionSet(
134
- info.operation.selectionSet,
135
- rootType,
136
- info.schema,
137
- fragments,
138
- info.cacheHints,
139
- defaultMaxAge,
140
- defaultScope,
141
- undefined, // No parent maxAge for root
142
- new Set()
143
- )
144
-
145
- return result
146
- })
147
-
148
- /**
149
- * Compute cache policy from a query string
150
- */
151
- export const computeCachePolicyFromQuery = (
152
- query: string,
153
- operationName: string | undefined,
154
- schema: GraphQLSchema,
155
- cacheHints: CacheHintMap,
156
- config: CacheControlConfig = {}
157
- ): Effect.Effect<CachePolicy, Error, never> =>
158
- Effect.gen(function* () {
159
- // Parse the query
160
- const document = yield* Effect.try({
161
- try: () => parse(query),
162
- catch: (error) => new Error(`Failed to parse query: ${error}`),
163
- })
164
-
165
- // Find the operation
166
- const operation = yield* Effect.try({
167
- try: () => {
168
- const operations = document.definitions.filter(
169
- (d): d is OperationDefinitionNode => d.kind === Kind.OPERATION_DEFINITION
170
- )
171
-
172
- if (operations.length === 0) {
173
- throw new Error("No operation found in query")
174
- }
175
-
176
- if (operationName) {
177
- const op = operations.find((o) => o.name?.value === operationName)
178
- if (!op) {
179
- throw new Error(`Operation "${operationName}" not found`)
180
- }
181
- return op
182
- }
183
-
184
- if (operations.length > 1) {
185
- throw new Error("Multiple operations found - operationName required")
186
- }
187
-
188
- return operations[0]
189
- },
190
- catch: (error) => error as Error,
191
- })
192
-
193
- return yield* computeCachePolicy({
194
- document,
195
- operation,
196
- schema,
197
- cacheHints,
198
- config,
199
- })
200
- })
201
-
202
- /**
203
- * Convert a cache policy to an HTTP Cache-Control header value
204
- */
205
- export const toCacheControlHeader = (policy: CachePolicy): string => {
206
- if (policy.maxAge === 0) {
207
- return "no-store"
208
- }
209
-
210
- const directives: string[] = []
211
- directives.push(policy.scope === "PRIVATE" ? "private" : "public")
212
- directives.push(`max-age=${policy.maxAge}`)
213
-
214
- return directives.join(", ")
215
- }
216
-
217
- // ============================================================================
218
- // Internal Helpers
219
- // ============================================================================
220
-
221
- /**
222
- * Get the root type for an operation
223
- */
224
- function getRootType(
225
- schema: GraphQLSchema,
226
- operation: "query" | "mutation" | "subscription"
227
- ): GraphQLObjectType | null {
228
- switch (operation) {
229
- case "query":
230
- return schema.getQueryType() ?? null
231
- case "mutation":
232
- return schema.getMutationType() ?? null
233
- case "subscription":
234
- return schema.getSubscriptionType() ?? null
235
- }
236
- }
237
-
238
- /**
239
- * Get the named type from a potentially wrapped type
240
- */
241
- function getNamedType(
242
- type: GraphQLOutputType
243
- ): GraphQLObjectType | GraphQLScalarType | GraphQLEnumType | null {
244
- if (type instanceof GraphQLNonNull || type instanceof GraphQLList) {
245
- return getNamedType(type.ofType as GraphQLOutputType)
246
- }
247
- if (
248
- type instanceof GraphQLObjectType ||
249
- type instanceof GraphQLScalarType ||
250
- type instanceof GraphQLEnumType
251
- ) {
252
- return type
253
- }
254
- return null
255
- }
256
-
257
- /**
258
- * Check if a type is a scalar or enum (leaf type)
259
- */
260
- function isLeafType(type: GraphQLOutputType): boolean {
261
- const namedType = getNamedType(type)
262
- return namedType instanceof GraphQLScalarType || namedType instanceof GraphQLEnumType
263
- }
264
-
265
- /**
266
- * Context passed through the analysis functions
267
- */
268
- interface AnalysisContext {
269
- schema: GraphQLSchema
270
- fragments: Map<string, FragmentDefinitionNode>
271
- cacheHints: CacheHintMap
272
- defaultMaxAge: number
273
- defaultScope: CacheControlScope
274
- }
275
-
276
- /**
277
- * Mutable state for aggregating cache policies
278
- */
279
- interface PolicyAccumulator {
280
- minMaxAge: number | undefined
281
- hasPrivate: boolean
282
- }
283
-
284
- /**
285
- * Aggregate a field policy into the accumulator
286
- */
287
- function aggregatePolicy(acc: PolicyAccumulator, policy: CachePolicy): void {
288
- if (acc.minMaxAge === undefined) {
289
- acc.minMaxAge = policy.maxAge
290
- } else {
291
- acc.minMaxAge = Math.min(acc.minMaxAge, policy.maxAge)
292
- }
293
- if (policy.scope === "PRIVATE") {
294
- acc.hasPrivate = true
295
- }
296
- }
297
-
298
- /**
299
- * Analyze a fragment spread and return its cache policy
300
- */
301
- function analyzeFragmentSpread(
302
- fragmentName: string,
303
- ctx: AnalysisContext,
304
- parentMaxAge: number | undefined,
305
- visitedFragments: Set<string>
306
- ): CachePolicy | undefined {
307
- // Prevent infinite loops with fragment cycles
308
- if (visitedFragments.has(fragmentName)) {
309
- return undefined
310
- }
311
-
312
- const fragment = ctx.fragments.get(fragmentName)
313
- if (!fragment) {
314
- return undefined
315
- }
316
-
317
- const fragmentType = ctx.schema.getType(fragment.typeCondition.name.value)
318
- if (!(fragmentType instanceof GraphQLObjectType)) {
319
- return undefined
320
- }
321
-
322
- const newVisited = new Set(visitedFragments)
323
- newVisited.add(fragmentName)
324
-
325
- return analyzeSelectionSet(fragment.selectionSet, fragmentType, ctx, parentMaxAge, newVisited)
326
- }
327
-
328
- /**
329
- * Analyze an inline fragment and return its cache policy
330
- */
331
- function analyzeInlineFragment(
332
- selection: { typeCondition?: { name: { value: string } }; selectionSet: SelectionSetNode },
333
- parentType: GraphQLObjectType,
334
- ctx: AnalysisContext,
335
- parentMaxAge: number | undefined,
336
- visitedFragments: Set<string>
337
- ): CachePolicy {
338
- let targetType = parentType
339
-
340
- if (selection.typeCondition) {
341
- const conditionType = ctx.schema.getType(selection.typeCondition.name.value)
342
- if (conditionType instanceof GraphQLObjectType) {
343
- targetType = conditionType
344
- }
345
- }
346
-
347
- return analyzeSelectionSet(
348
- selection.selectionSet,
349
- targetType,
350
- ctx,
351
- parentMaxAge,
352
- visitedFragments
353
- )
354
- }
355
-
356
- /**
357
- * Look up the effective cache hint for a field (field-level > type-level > undefined)
358
- */
359
- function lookupEffectiveCacheHint(
360
- parentTypeName: string,
361
- fieldName: string,
362
- returnType: GraphQLOutputType,
363
- cacheHints: CacheHintMap
364
- ): CacheHint | undefined {
365
- // Priority: field-level hint > type-level hint
366
- const fieldKey = `${parentTypeName}.${fieldName}`
367
- const fieldHint = cacheHints.get(fieldKey)
368
- if (fieldHint) return fieldHint
369
-
370
- // Check type-level hint on return type
371
- const namedType = getNamedType(returnType)
372
- return namedType ? cacheHints.get(namedType.name) : undefined
373
- }
374
-
375
- /**
376
- * Compute the maxAge for a field based on hint, inheritance, and field type
377
- */
378
- function computeFieldMaxAge(
379
- hint: CacheHint | undefined,
380
- fieldType: GraphQLOutputType,
381
- parentMaxAge: number | undefined,
382
- defaultMaxAge: number
383
- ): number {
384
- if (hint) {
385
- // Use explicit hint
386
- if (hint.inheritMaxAge && parentMaxAge !== undefined) {
387
- return parentMaxAge
388
- }
389
- if (hint.maxAge !== undefined) {
390
- return hint.maxAge
391
- }
392
- // Fall through to default logic
393
- }
394
-
395
- // Scalar/enum fields inherit parent maxAge by default
396
- if (isLeafType(fieldType) && parentMaxAge !== undefined) {
397
- return parentMaxAge
398
- }
399
-
400
- // Root and object fields default to defaultMaxAge (typically 0)
401
- return defaultMaxAge
402
- }
403
-
404
- /**
405
- * Analyze a selection set and return the aggregated cache policy.
406
- * Overload with AnalysisContext for internal use.
407
- */
408
- function analyzeSelectionSet(
409
- selectionSet: SelectionSetNode,
410
- parentType: GraphQLObjectType,
411
- ctx: AnalysisContext,
412
- parentMaxAge: number | undefined,
413
- visitedFragments: Set<string>
414
- ): CachePolicy
415
- function analyzeSelectionSet(
416
- selectionSet: SelectionSetNode,
417
- parentType: GraphQLObjectType,
418
- schema: GraphQLSchema,
419
- fragments: Map<string, FragmentDefinitionNode>,
420
- cacheHints: CacheHintMap,
421
- defaultMaxAge: number,
422
- defaultScope: CacheControlScope,
423
- parentMaxAge: number | undefined,
424
- visitedFragments: Set<string>
425
- ): CachePolicy
426
- function analyzeSelectionSet(
427
- selectionSet: SelectionSetNode,
428
- parentType: GraphQLObjectType,
429
- schemaOrCtx: GraphQLSchema | AnalysisContext,
430
- fragmentsOrParentMaxAge: Map<string, FragmentDefinitionNode> | number | undefined,
431
- cacheHintsOrVisited?: CacheHintMap | Set<string>,
432
- defaultMaxAge?: number,
433
- defaultScope?: CacheControlScope,
434
- parentMaxAge?: number | undefined,
435
- visitedFragments?: Set<string>
436
- ): CachePolicy {
437
- // Normalize arguments - support both old and new signatures
438
- let ctx: AnalysisContext
439
- let actualParentMaxAge: number | undefined
440
- let actualVisitedFragments: Set<string>
441
-
442
- if (schemaOrCtx instanceof GraphQLSchema) {
443
- // Old signature
444
- ctx = {
445
- schema: schemaOrCtx,
446
- fragments: fragmentsOrParentMaxAge as Map<string, FragmentDefinitionNode>,
447
- cacheHints: cacheHintsOrVisited as CacheHintMap,
448
- defaultMaxAge: defaultMaxAge!,
449
- defaultScope: defaultScope!,
450
- }
451
- actualParentMaxAge = parentMaxAge
452
- actualVisitedFragments = visitedFragments!
453
- } else {
454
- // New signature with AnalysisContext
455
- ctx = schemaOrCtx
456
- actualParentMaxAge = fragmentsOrParentMaxAge as number | undefined
457
- actualVisitedFragments = cacheHintsOrVisited as Set<string>
458
- }
459
-
460
- const acc: PolicyAccumulator = { minMaxAge: undefined, hasPrivate: false }
461
-
462
- for (const selection of selectionSet.selections) {
463
- let fieldPolicy: CachePolicy | undefined
464
-
465
- switch (selection.kind) {
466
- case Kind.FIELD:
467
- fieldPolicy = analyzeField(
468
- selection,
469
- parentType,
470
- ctx,
471
- actualParentMaxAge,
472
- actualVisitedFragments
473
- )
474
- break
475
-
476
- case Kind.FRAGMENT_SPREAD:
477
- fieldPolicy = analyzeFragmentSpread(
478
- selection.name.value,
479
- ctx,
480
- actualParentMaxAge,
481
- actualVisitedFragments
482
- )
483
- break
484
-
485
- case Kind.INLINE_FRAGMENT:
486
- fieldPolicy = analyzeInlineFragment(
487
- selection,
488
- parentType,
489
- ctx,
490
- actualParentMaxAge,
491
- actualVisitedFragments
492
- )
493
- break
494
- }
495
-
496
- if (fieldPolicy) {
497
- aggregatePolicy(acc, fieldPolicy)
498
- }
499
- }
500
-
501
- return {
502
- maxAge: acc.minMaxAge ?? ctx.defaultMaxAge,
503
- scope: acc.hasPrivate ? "PRIVATE" : ctx.defaultScope,
504
- }
505
- }
506
-
507
- /**
508
- * Analyze a field node and return its cache policy
509
- */
510
- function analyzeField(
511
- field: FieldNode,
512
- parentType: GraphQLObjectType,
513
- ctx: AnalysisContext,
514
- parentMaxAge: number | undefined,
515
- visitedFragments: Set<string>
516
- ): CachePolicy {
517
- const fieldName = field.name.value
518
-
519
- // Introspection fields - don't affect caching
520
- if (fieldName.startsWith("__")) {
521
- return { maxAge: Infinity, scope: "PUBLIC" }
522
- }
523
-
524
- // Get the field from the schema
525
- const schemaField = parentType.getFields()[fieldName]
526
- if (!schemaField) {
527
- return { maxAge: ctx.defaultMaxAge, scope: ctx.defaultScope }
528
- }
529
-
530
- // Look up effective cache hint
531
- const effectiveHint = lookupEffectiveCacheHint(
532
- parentType.name,
533
- fieldName,
534
- schemaField.type,
535
- ctx.cacheHints
536
- )
537
-
538
- // Compute field maxAge
539
- const fieldMaxAge = computeFieldMaxAge(
540
- effectiveHint,
541
- schemaField.type,
542
- parentMaxAge,
543
- ctx.defaultMaxAge
544
- )
545
- const fieldScope: CacheControlScope = effectiveHint?.scope ?? ctx.defaultScope
546
-
547
- // If the field has a selection set, analyze it
548
- const namedType = getNamedType(schemaField.type)
549
- if (field.selectionSet && namedType instanceof GraphQLObjectType) {
550
- const nestedPolicy = analyzeSelectionSet(
551
- field.selectionSet,
552
- namedType,
553
- ctx,
554
- fieldMaxAge,
555
- visitedFragments
556
- )
557
-
558
- return {
559
- maxAge: Math.min(fieldMaxAge, nestedPolicy.maxAge),
560
- scope: fieldScope === "PRIVATE" || nestedPolicy.scope === "PRIVATE" ? "PRIVATE" : "PUBLIC",
561
- }
562
- }
563
-
564
- return { maxAge: fieldMaxAge, scope: fieldScope }
565
- }
566
-
567
- // ============================================================================
568
- // Environment Configuration
569
- // ============================================================================
570
-
571
- /**
572
- * Effect Config for loading cache control configuration from environment variables.
573
- *
574
- * Environment variables:
575
- * - GRAPHQL_CACHE_CONTROL_ENABLED: Enable cache control (default: true)
576
- * - GRAPHQL_CACHE_CONTROL_DEFAULT_MAX_AGE: Default maxAge for root fields (default: 0)
577
- * - GRAPHQL_CACHE_CONTROL_DEFAULT_SCOPE: Default scope (PUBLIC or PRIVATE, default: PUBLIC)
578
- * - GRAPHQL_CACHE_CONTROL_HTTP_HEADERS: Set HTTP headers (default: true)
579
- */
580
- export const CacheControlConfigFromEnv: Config.Config<CacheControlConfig> = Config.all({
581
- enabled: Config.boolean("GRAPHQL_CACHE_CONTROL_ENABLED").pipe(Config.withDefault(true)),
582
- defaultMaxAge: Config.number("GRAPHQL_CACHE_CONTROL_DEFAULT_MAX_AGE").pipe(Config.withDefault(0)),
583
- defaultScope: Config.string("GRAPHQL_CACHE_CONTROL_DEFAULT_SCOPE").pipe(
584
- Config.withDefault("PUBLIC"),
585
- Config.map((s) => (s === "PRIVATE" ? "PRIVATE" : "PUBLIC") as CacheControlScope)
586
- ),
587
- calculateHttpHeaders: Config.boolean("GRAPHQL_CACHE_CONTROL_HTTP_HEADERS").pipe(
588
- Config.withDefault(true)
589
- ),
590
- })