-
Notifications
You must be signed in to change notification settings - Fork 158
Expand file tree
/
Copy pathfederation_metadata.go
More file actions
425 lines (364 loc) · 18.5 KB
/
federation_metadata.go
File metadata and controls
425 lines (364 loc) · 18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
package plan
import (
"encoding/json"
"slices"
"time"
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
)
type FederationMetaData struct {
Keys FederationFieldConfigurations
Requires FederationFieldConfigurations
Provides FederationFieldConfigurations
EntityInterfaces []EntityInterfaceConfiguration
InterfaceObjects []EntityInterfaceConfiguration
EntityCaching EntityCacheConfigurations
RootFieldCaching RootFieldCacheConfigurations
MutationFieldCaching MutationFieldCacheConfigurations
SubscriptionEntityPopulation SubscriptionEntityPopulationConfigurations
MutationCacheInvalidation MutationCacheInvalidationConfigurations
entityTypeNames map[string]struct{}
}
type FederationInfo interface {
HasKeyRequirement(typeName, requiresFields string) bool
RequiredFieldsByKey(typeName string) []FederationFieldConfiguration
RequiredFieldsByRequires(typeName, fieldName string) (cfg FederationFieldConfiguration, exists bool)
HasEntity(typeName string) bool
HasInterfaceObject(typeName string) bool
HasEntityInterface(typeName string) bool
EntityInterfaceNames() []string
EntityCacheConfig(typeName string) *EntityCacheConfiguration
RootFieldCacheConfig(typeName, fieldName string) *RootFieldCacheConfiguration
MutationCacheInvalidationConfig(fieldName string) *MutationCacheInvalidationConfiguration
MutationFieldCacheConfig(fieldName string) *MutationFieldCacheConfiguration
}
func (d *FederationMetaData) HasKeyRequirement(typeName, requiresFields string) bool {
return d.Keys.HasSelectionSet(typeName, "", requiresFields)
}
func (d *FederationMetaData) RequiredFieldsByKey(typeName string) []FederationFieldConfiguration {
return d.Keys.FilterByTypeAndResolvability(typeName, true)
}
func (d *FederationMetaData) HasEntity(typeName string) bool {
_, ok := d.entityTypeNames[typeName]
return ok
}
func (d *FederationMetaData) RequiredFieldsByRequires(typeName, fieldName string) (cfg FederationFieldConfiguration, exists bool) {
return d.Requires.FirstByTypeAndField(typeName, fieldName)
}
func (d *FederationMetaData) HasInterfaceObject(typeName string) bool {
return slices.ContainsFunc(d.InterfaceObjects, func(interfaceObjCfg EntityInterfaceConfiguration) bool {
return slices.Contains(interfaceObjCfg.ConcreteTypeNames, typeName) || interfaceObjCfg.InterfaceTypeName == typeName
})
}
func (d *FederationMetaData) HasEntityInterface(typeName string) bool {
return slices.ContainsFunc(d.EntityInterfaces, func(interfaceObjCfg EntityInterfaceConfiguration) bool {
return slices.Contains(interfaceObjCfg.ConcreteTypeNames, typeName) || interfaceObjCfg.InterfaceTypeName == typeName
})
}
func (d *FederationMetaData) EntityInterfaceNames() (out []string) {
if len(d.EntityInterfaces) == 0 {
return nil
}
for i := range d.EntityInterfaces {
out = append(out, d.EntityInterfaces[i].InterfaceTypeName)
}
return out
}
type EntityInterfaceConfiguration struct {
InterfaceTypeName string
ConcreteTypeNames []string
}
// EntityCacheConfiguration defines L2 caching behavior for a specific entity type.
// This configuration is subgraph-local: each subgraph configures caching for entities it provides.
// Caching is opt-in: entities without configuration will not be cached in L2.
type EntityCacheConfiguration struct {
// TypeName is the GraphQL type name of the entity to cache (e.g., "User", "Product").
// This must match the __typename returned by the subgraph for _entities queries.
TypeName string `json:"type_name"`
// CacheName identifies which LoaderCache instance to use for storing this entity.
// Multiple entity types can share a cache by using the same CacheName.
// The cache name must be registered in the Loader's caches map at runtime.
CacheName string `json:"cache_name"`
// TTL (Time To Live) specifies how long cached entities remain valid.
// After TTL expires, the next request will fetch fresh data from the subgraph.
// A zero TTL means entries never expire (not recommended for production).
TTL time.Duration `json:"ttl"`
// IncludeSubgraphHeaderPrefix controls whether forwarded headers affect cache keys.
// When true, cache keys include a hash of the headers sent to the subgraph,
// ensuring different header configurations (e.g., different auth tokens) use
// separate cache entries. Set to true when subgraph responses vary by headers.
IncludeSubgraphHeaderPrefix bool `json:"include_subgraph_header_prefix"`
// EnablePartialCacheLoad enables fetching only cache-missed entities from the subgraph.
// Default behavior (false): If ANY entity in a batch is missing from cache, ALL entities
// are fetched from the subgraph. This keeps the cache fresh but may overfetch.
// When enabled (true): Only missing entities are fetched; cached entities are served
// directly from cache. This reduces subgraph load but cached entities may become stale
// within their TTL window. Use when cache freshness is acceptable within TTL bounds.
EnablePartialCacheLoad bool `json:"enable_partial_cache_load"`
// HashAnalyticsKeys controls whether entity keys are hashed (true) or stored raw (false)
// in cache analytics EntityFieldHash entries. When true, KeyHash is populated instead of KeyRaw.
HashAnalyticsKeys bool `json:"hash_analytics_keys"`
// ShadowMode enables shadow caching for this entity type.
// When true, L2 cache reads and writes still occur, but cached data is never served.
// Instead, fresh data is always fetched from the subgraph and compared against the cached value
// to detect staleness. L1 cache works normally (not affected by shadow mode).
ShadowMode bool `json:"shadow_mode"`
// NegativeCacheTTL is the TTL for caching null entity results (entity not found).
// When > 0, null responses (entity returned null without errors from _entities) are cached
// as negative sentinels to avoid repeated subgraph lookups for non-existent entities.
// When 0 (default), null entities are not cached and will be re-fetched on every request.
NegativeCacheTTL time.Duration `json:"negative_cache_ttl,omitzero"`
}
// EntityCacheConfigurations is a collection of entity cache configurations.
type EntityCacheConfigurations []EntityCacheConfiguration
// FindByTypeName returns the cache configuration for the given entity type.
// Returns nil if no configuration exists (caching disabled for this entity).
func (c EntityCacheConfigurations) FindByTypeName(typeName string) *EntityCacheConfiguration {
for i := range c {
if c[i].TypeName == typeName {
return &c[i]
}
}
return nil
}
// RootFieldCacheConfiguration defines L2 caching behavior for a specific root field.
// This configuration is subgraph-local: each subgraph configures caching for root fields it provides.
type RootFieldCacheConfiguration struct {
// TypeName is the type containing the field (e.g., "Query", "Mutation")
TypeName string `json:"type_name"`
// FieldName is the name of the root field to cache (e.g., "topProducts", "me")
FieldName string `json:"field_name"`
// CacheName is the name of the cache to use (maps to LoaderCache instances)
CacheName string `json:"cache_name"`
// TTL is the time-to-live for cached responses
TTL time.Duration `json:"ttl"`
// IncludeSubgraphHeaderPrefix indicates if forwarded headers affect cache key.
// When true, different header values result in different cache keys.
IncludeSubgraphHeaderPrefix bool `json:"include_subgraph_header_prefix"`
// EntityKeyMappings configures derived entity cache keys for this root field.
// When set, the L2 cache key uses entity key format instead of root field format,
// enabling cache sharing between root field queries and entity fetches.
EntityKeyMappings []EntityKeyMapping `json:"entity_key_mappings,omitempty"`
// ShadowMode enables shadow caching for this root field.
// When true, L2 cache reads and writes still occur, but cached data is never served.
// Instead, fresh data is always fetched from the subgraph and compared against the cached value.
// Note: shadow mode behavior is currently implemented for entity fetches only.
ShadowMode bool `json:"shadow_mode"`
}
// EntityKeyMapping defines how a root field's arguments map to entity @key fields.
// When configured, the root field's L2 cache key uses the entity key format
// (e.g., {"__typename":"User","key":{"id":"123"}}) instead of the root field format.
// This enables cache sharing between root field queries and entity fetches.
type EntityKeyMapping struct {
// EntityTypeName is the entity type returned by the root field (e.g., "User")
EntityTypeName string `json:"entity_type_name"`
// FieldMappings maps entity @key fields to root field arguments
FieldMappings []FieldMapping `json:"field_mappings"`
}
// FieldMapping maps an entity @key field to a root field argument path.
type FieldMapping struct {
// EntityKeyField is the @key field name on the entity (e.g., "id")
EntityKeyField string `json:"entity_key_field"`
// ArgumentPath is the path into ctx.Variables to extract the argument value.
// Uses the same []string format as ContextVariable.Path.
// Object keys: ["id"], ["input", "userId"]
// Array index: ["ids", "0"] (decimal string)
// Subject to ctx.RemapVariables when len==1
ArgumentPath []string `json:"argument_path"`
}
// RootFieldCacheConfigurations is a collection of root field cache configurations.
type RootFieldCacheConfigurations []RootFieldCacheConfiguration
// FindByTypeAndField returns the cache configuration for the given type and field.
// Returns nil if no configuration exists (caching disabled for this root field).
func (c RootFieldCacheConfigurations) FindByTypeAndField(typeName, fieldName string) *RootFieldCacheConfiguration {
for i := range c {
if c[i].TypeName == typeName && c[i].FieldName == fieldName {
return &c[i]
}
}
return nil
}
// MutationFieldCacheConfiguration controls cache behavior for entity fetches
// triggered by a specific mutation root field. The subgraph that owns the mutation
// field decides whether entity data fetched during that mutation populates L2.
type MutationFieldCacheConfiguration struct {
// FieldName is the mutation root field name (e.g., "addReview", "deleteUser").
FieldName string `json:"field_name"`
// EnableEntityL2CachePopulation allows entity fetches triggered by this
// mutation to write to the L2 cache. Mutations always skip L2 reads
// (existing behavior). By default, mutations do NOT populate L2.
// Set to true to opt in to L2 cache population for this mutation field.
EnableEntityL2CachePopulation bool `json:"enable_entity_l2_cache_population"`
}
// MutationFieldCacheConfigurations is a collection of mutation field cache configurations.
type MutationFieldCacheConfigurations []MutationFieldCacheConfiguration
// FindByFieldName returns the mutation field cache config for the given field name.
// Returns nil if no configuration exists.
func (c MutationFieldCacheConfigurations) FindByFieldName(fieldName string) *MutationFieldCacheConfiguration {
for i := range c {
if c[i].FieldName == fieldName {
return &c[i]
}
}
return nil
}
// SubscriptionEntityPopulationConfiguration defines how a subscription should
// manage L2 cache entries for root entities received via subscription events.
//
// Two modes are supported:
// - Populate: When the subscription selects entity fields beyond @key, write those
// fields to L2 on each event. This allows subsequent queries to hit the L2 cache.
// - Invalidate: When the subscription only provides @key fields (and
// EnableInvalidationOnKeyOnly is true), DELETE the L2 cache entry on each event.
// This ensures stale data is evicted when the entity changes.
type SubscriptionEntityPopulationConfiguration struct {
// TypeName is the entity type managed by this subscription (e.g., "Product").
TypeName string `json:"type_name"`
// CacheName identifies which LoaderCache instance to use.
CacheName string `json:"cache_name"`
// TTL is the time-to-live for populated cache entries.
TTL time.Duration `json:"ttl"`
// IncludeSubgraphHeaderPrefix controls whether forwarded headers affect cache keys.
IncludeSubgraphHeaderPrefix bool `json:"include_subgraph_header_prefix"`
// EnableInvalidationOnKeyOnly: when true and the subscription only provides
// @key fields (no additional entity fields), DELETE the L2 cache entry on
// each subscription event instead of populating it.
EnableInvalidationOnKeyOnly bool `json:"enable_invalidation_on_key_only"`
}
// SubscriptionEntityPopulationConfigurations is a collection of subscription entity population configurations.
type SubscriptionEntityPopulationConfigurations []SubscriptionEntityPopulationConfiguration
// FindByTypeName returns the subscription entity population config for the given entity type.
// Returns nil if no configuration exists.
func (c SubscriptionEntityPopulationConfigurations) FindByTypeName(typeName string) *SubscriptionEntityPopulationConfiguration {
for i := range c {
if c[i].TypeName == typeName {
return &c[i]
}
}
return nil
}
// MutationCacheInvalidationConfiguration defines which mutation fields should
// invalidate (delete) L2 cache entries for the entity they return.
type MutationCacheInvalidationConfiguration struct {
// FieldName is the mutation field name (e.g., "updateUser", "deleteUser").
FieldName string `json:"field_name"`
// EntityTypeName is the return entity type (e.g., "User").
// If empty, it is inferred from the mutation return type at plan time.
EntityTypeName string `json:"entity_type_name,omitempty"`
}
// MutationCacheInvalidationConfigurations is a collection of mutation cache invalidation configurations.
type MutationCacheInvalidationConfigurations []MutationCacheInvalidationConfiguration
// FindByFieldName returns the invalidation config for the given mutation field.
// Returns nil if no configuration exists (no invalidation for this field).
func (c MutationCacheInvalidationConfigurations) FindByFieldName(fieldName string) *MutationCacheInvalidationConfiguration {
for i := range c {
if c[i].FieldName == fieldName {
return &c[i]
}
}
return nil
}
// EntityCacheConfig returns the cache configuration for the given entity type.
// Returns nil if no configuration exists (caching should be disabled for this entity).
func (d *FederationMetaData) EntityCacheConfig(typeName string) *EntityCacheConfiguration {
return d.EntityCaching.FindByTypeName(typeName)
}
// RootFieldCacheConfig returns the cache configuration for the given root field.
// Returns nil if no configuration exists (caching should be disabled for this root field).
func (d *FederationMetaData) RootFieldCacheConfig(typeName, fieldName string) *RootFieldCacheConfiguration {
return d.RootFieldCaching.FindByTypeAndField(typeName, fieldName)
}
// MutationCacheInvalidationConfig returns the invalidation config for the given mutation field.
// Returns nil if no configuration exists (no invalidation for this field).
func (d *FederationMetaData) MutationCacheInvalidationConfig(fieldName string) *MutationCacheInvalidationConfiguration {
return d.MutationCacheInvalidation.FindByFieldName(fieldName)
}
// MutationFieldCacheConfig returns the cache configuration for the given mutation field.
// Returns nil if no configuration exists.
func (d *FederationMetaData) MutationFieldCacheConfig(fieldName string) *MutationFieldCacheConfiguration {
return d.MutationFieldCaching.FindByFieldName(fieldName)
}
type FederationFieldConfiguration struct {
TypeName string `json:"type_name"` // TypeName is the name of the Entity the Fragment is for
FieldName string `json:"field_name,omitempty"` // FieldName is empty for key requirements, otherwise, it is the name of the field that has requires or provides directive
SelectionSet string `json:"selection_set"` // SelectionSet is the selection set that is required for the given field (keys, requires, provides)
DisableEntityResolver bool `json:"-"` // applicable only for the keys. If true it means that the given entity could not be resolved by this key.
Conditions []KeyCondition `json:"conditions,omitempty"` // conditions stores coordinates under which we could use implicit key, while on other paths this key is not available
parsedSelectionSet *ast.Document
RemappedPaths map[string]string
}
type KeyCondition struct {
Coordinates []FieldCoordinate `json:"coordinates"`
FieldPath []string `json:"field_path"`
}
type FieldCoordinate struct {
TypeName string `json:"type_name"`
FieldName string `json:"field_name"`
}
// parseSelectionSet parses the selection set and stores the parsed AST in parsedSelectionSet.
// should have pointer receiver to preserve the value
func (f *FederationFieldConfiguration) parseSelectionSet() error {
if f.parsedSelectionSet != nil {
return nil
}
doc, report := RequiredFieldsFragment(f.TypeName, f.SelectionSet, false)
if report.HasErrors() {
return report
}
f.parsedSelectionSet = doc
return nil
}
// String - implements fmt.Stringer
// NOTE: do not change to pointer receiver, it won't work for not pointer values
func (f FederationFieldConfiguration) String() string {
b, _ := json.Marshal(f)
return string(b)
}
type FederationFieldConfigurations []FederationFieldConfiguration
func (f *FederationFieldConfigurations) FilterByTypeAndResolvability(typeName string, skipUnresovable bool) (out []FederationFieldConfiguration) {
for i := range *f {
if (*f)[i].TypeName != typeName || (*f)[i].FieldName != "" {
continue
}
if skipUnresovable && (*f)[i].DisableEntityResolver {
continue
}
out = append(out, (*f)[i])
}
return out
}
func (f *FederationFieldConfigurations) UniqueTypes() (out []string) {
seen := map[string]struct{}{}
for i := range *f {
seen[(*f)[i].TypeName] = struct{}{}
}
for k := range seen {
out = append(out, k)
}
return out
}
func (f *FederationFieldConfigurations) FirstByTypeAndField(typeName, fieldName string) (cfg FederationFieldConfiguration, exists bool) {
for i := range *f {
if (*f)[i].TypeName == typeName && (*f)[i].FieldName == fieldName {
return (*f)[i], true
}
}
return FederationFieldConfiguration{}, false
}
func (f *FederationFieldConfigurations) HasSelectionSet(typeName, fieldName, selectionSet string) bool {
for i := range *f {
if typeName == (*f)[i].TypeName &&
fieldName == (*f)[i].FieldName &&
selectionSet == (*f)[i].SelectionSet {
return true
}
}
return false
}
func (f *FederationFieldConfigurations) AppendIfNotPresent(config FederationFieldConfiguration) (added bool) {
ok := f.HasSelectionSet(config.TypeName, config.FieldName, config.SelectionSet)
if ok {
return false
}
*f = append(*f, config)
return true
}