@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.
- package/README.md +100 -0
- package/builder/index.cjs +1446 -0
- package/builder/index.cjs.map +1 -0
- package/builder/index.d.cts +260 -0
- package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +50 -21
- package/builder/index.js +1405 -0
- package/builder/index.js.map +1 -0
- package/index.cjs +3469 -0
- package/index.cjs.map +1 -0
- package/index.d.cts +529 -0
- package/index.d.ts +529 -0
- package/index.js +3292 -0
- package/index.js.map +1 -0
- package/package.json +19 -28
- package/schema-builder-DKvkzU_M.d.cts +965 -0
- package/schema-builder-DKvkzU_M.d.ts +965 -0
- package/server/index.cjs +1579 -0
- package/server/index.cjs.map +1 -0
- package/server/index.d.cts +682 -0
- package/server/index.d.ts +682 -0
- package/server/index.js +1548 -0
- package/server/index.js.map +1 -0
- package/dist/analyzer-extension.d.ts +0 -105
- package/dist/analyzer-extension.d.ts.map +0 -1
- package/dist/analyzer-extension.js +0 -137
- package/dist/analyzer-extension.js.map +0 -1
- package/dist/builder/execute.d.ts +0 -26
- package/dist/builder/execute.d.ts.map +0 -1
- package/dist/builder/execute.js +0 -104
- package/dist/builder/execute.js.map +0 -1
- package/dist/builder/field-builders.d.ts +0 -30
- package/dist/builder/field-builders.d.ts.map +0 -1
- package/dist/builder/field-builders.js +0 -200
- package/dist/builder/field-builders.js.map +0 -1
- package/dist/builder/index.d.ts +0 -7
- package/dist/builder/index.d.ts.map +0 -1
- package/dist/builder/index.js +0 -31
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/pipe-api.d.ts.map +0 -1
- package/dist/builder/pipe-api.js +0 -151
- package/dist/builder/pipe-api.js.map +0 -1
- package/dist/builder/schema-builder.d.ts +0 -301
- package/dist/builder/schema-builder.d.ts.map +0 -1
- package/dist/builder/schema-builder.js +0 -566
- package/dist/builder/schema-builder.js.map +0 -1
- package/dist/builder/type-registry.d.ts +0 -80
- package/dist/builder/type-registry.d.ts.map +0 -1
- package/dist/builder/type-registry.js +0 -505
- package/dist/builder/type-registry.js.map +0 -1
- package/dist/builder/types.d.ts +0 -283
- package/dist/builder/types.d.ts.map +0 -1
- package/dist/builder/types.js +0 -3
- package/dist/builder/types.js.map +0 -1
- package/dist/cli/generate-schema.d.ts +0 -29
- package/dist/cli/generate-schema.d.ts.map +0 -1
- package/dist/cli/generate-schema.js +0 -233
- package/dist/cli/generate-schema.js.map +0 -1
- package/dist/cli/index.d.ts +0 -19
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -24
- package/dist/cli/index.js.map +0 -1
- package/dist/context.d.ts +0 -18
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -11
- package/dist/context.js.map +0 -1
- package/dist/error.d.ts +0 -45
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -29
- package/dist/error.js.map +0 -1
- package/dist/extensions.d.ts +0 -130
- package/dist/extensions.d.ts.map +0 -1
- package/dist/extensions.js +0 -78
- package/dist/extensions.js.map +0 -1
- package/dist/index.d.ts +0 -12
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -47
- package/dist/index.js.map +0 -1
- package/dist/loader.d.ts +0 -169
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -237
- package/dist/loader.js.map +0 -1
- package/dist/resolver-context.d.ts +0 -154
- package/dist/resolver-context.d.ts.map +0 -1
- package/dist/resolver-context.js +0 -184
- package/dist/resolver-context.js.map +0 -1
- package/dist/schema-mapping.d.ts +0 -30
- package/dist/schema-mapping.d.ts.map +0 -1
- package/dist/schema-mapping.js +0 -280
- package/dist/schema-mapping.js.map +0 -1
- package/dist/server/cache-control.d.ts +0 -96
- package/dist/server/cache-control.d.ts.map +0 -1
- package/dist/server/cache-control.js +0 -308
- package/dist/server/cache-control.js.map +0 -1
- package/dist/server/complexity.d.ts +0 -165
- package/dist/server/complexity.d.ts.map +0 -1
- package/dist/server/complexity.js +0 -433
- package/dist/server/complexity.js.map +0 -1
- package/dist/server/config.d.ts +0 -66
- package/dist/server/config.d.ts.map +0 -1
- package/dist/server/config.js +0 -104
- package/dist/server/config.js.map +0 -1
- package/dist/server/graphiql.d.ts +0 -5
- package/dist/server/graphiql.d.ts.map +0 -1
- package/dist/server/graphiql.js +0 -43
- package/dist/server/graphiql.js.map +0 -1
- package/dist/server/index.d.ts +0 -18
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -48
- package/dist/server/index.js.map +0 -1
- package/dist/server/router.d.ts +0 -79
- package/dist/server/router.d.ts.map +0 -1
- package/dist/server/router.js +0 -232
- package/dist/server/router.js.map +0 -1
- package/dist/server/schema-builder-extensions.d.ts +0 -42
- package/dist/server/schema-builder-extensions.d.ts.map +0 -1
- package/dist/server/schema-builder-extensions.js +0 -48
- package/dist/server/schema-builder-extensions.js.map +0 -1
- package/dist/server/sse-adapter.d.ts +0 -64
- package/dist/server/sse-adapter.d.ts.map +0 -1
- package/dist/server/sse-adapter.js +0 -227
- package/dist/server/sse-adapter.js.map +0 -1
- package/dist/server/sse-types.d.ts +0 -192
- package/dist/server/sse-types.d.ts.map +0 -1
- package/dist/server/sse-types.js +0 -63
- package/dist/server/sse-types.js.map +0 -1
- package/dist/server/ws-adapter.d.ts +0 -39
- package/dist/server/ws-adapter.d.ts.map +0 -1
- package/dist/server/ws-adapter.js +0 -247
- package/dist/server/ws-adapter.js.map +0 -1
- package/dist/server/ws-types.d.ts +0 -169
- package/dist/server/ws-types.d.ts.map +0 -1
- package/dist/server/ws-types.js +0 -11
- package/dist/server/ws-types.js.map +0 -1
- package/dist/server/ws-utils.d.ts +0 -42
- package/dist/server/ws-utils.d.ts.map +0 -1
- package/dist/server/ws-utils.js +0 -99
- package/dist/server/ws-utils.js.map +0 -1
- package/src/analyzer-extension.ts +0 -254
- package/src/builder/execute.ts +0 -153
- package/src/builder/field-builders.ts +0 -322
- package/src/builder/index.ts +0 -48
- package/src/builder/pipe-api.ts +0 -312
- package/src/builder/schema-builder.ts +0 -970
- package/src/builder/type-registry.ts +0 -670
- package/src/builder/types.ts +0 -305
- package/src/context.ts +0 -23
- package/src/error.ts +0 -32
- package/src/extensions.ts +0 -240
- package/src/index.ts +0 -32
- package/src/loader.ts +0 -363
- package/src/resolver-context.ts +0 -253
- package/src/schema-mapping.ts +0 -307
- package/src/server/cache-control.ts +0 -590
- package/src/server/complexity.ts +0 -774
- package/src/server/config.ts +0 -174
- package/src/server/graphiql.ts +0 -38
- package/src/server/index.ts +0 -96
- package/src/server/router.ts +0 -432
- package/src/server/schema-builder-extensions.ts +0 -51
- package/src/server/sse-adapter.ts +0 -327
- package/src/server/sse-types.ts +0 -234
- package/src/server/ws-adapter.ts +0 -355
- package/src/server/ws-types.ts +0 -192
- 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
|
-
})
|