diff --git a/router-tests/header_propagation_test.go b/router-tests/header_propagation_test.go index 5d63690cdd..1a4510159d 100644 --- a/router-tests/header_propagation_test.go +++ b/router-tests/header_propagation_test.go @@ -303,13 +303,13 @@ func TestHeaderPropagation(t *testing.T) { }) }) - t.Run("MostRestrictiveCacheControl", func(t *testing.T) { + t.Run("Cache Control Propagation", func(t *testing.T) { // Global test: All subgraphs' responses are considered and most restrictive cache directive wins - t.Run("global most restrictive cache control", func(t *testing.T) { + t.Run("enable global cache control", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", ""), - Subgraphs: cacheOptions("max-age=120", "max-age=60"), + CacheControlPolicy: config.CacheControlPolicy{Enabled: true}, + Subgraphs: cacheOptions("max-age=120", "max-age=60"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -321,11 +321,16 @@ func TestHeaderPropagation(t *testing.T) { }) // Local test: Cache control rules are applied per subgraph (employees and hobbies) - t.Run("local most restrictive cache control", func(t *testing.T) { + t.Run("only enable cache control for subgraphs", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: local(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "", ""), - Subgraphs: cacheOptions("max-age=120", "max-age=60"), + Subgraphs: cacheOptions("max-age=120", "max-age=60"), + CacheControlPolicy: config.CacheControlPolicy{ + Subgraphs: []config.SubgraphCacheControlRule{ + {Name: "employees"}, + {Name: "hobbies"}, + }, + }, }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -337,11 +342,15 @@ func TestHeaderPropagation(t *testing.T) { }) // Partial test: Only one subgraph's response is considered (e.g., employees) - t.Run("partial most restrictive cache control", func(t *testing.T) { + t.Run("only enable cache control for one subgraph", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: partial(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", ""), - Subgraphs: cacheOptions("max-age=120", "max-age=60"), + CacheControlPolicy: config.CacheControlPolicy{ + Subgraphs: []config.SubgraphCacheControlRule{ + {Name: "employees"}, + }, + }, + Subgraphs: cacheOptions("max-age=120", "max-age=60"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -353,11 +362,11 @@ func TestHeaderPropagation(t *testing.T) { }) // Test case for no-store being the most restrictive - t.Run("global no-store wins", func(t *testing.T) { + t.Run("global default value of no-store wins", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", ""), - Subgraphs: cacheOptions("no-store", "max-age=300"), + CacheControlPolicy: config.CacheControlPolicy{Enabled: true}, + Subgraphs: cacheOptions("no-store", "max-age=300"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -369,11 +378,11 @@ func TestHeaderPropagation(t *testing.T) { }) // Test case for no-cache being more restrictive than max-age - t.Run("global no-cache wins", func(t *testing.T) { + t.Run("global default value of no-cache wins", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", ""), - Subgraphs: cacheOptions("no-cache", "max-age=300"), + CacheControlPolicy: config.CacheControlPolicy{Enabled: true}, + Subgraphs: cacheOptions("no-cache", "max-age=300"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -384,11 +393,11 @@ func TestHeaderPropagation(t *testing.T) { }) }) - t.Run("global no-cache wins against no value", func(t *testing.T) { + t.Run("no-cache wins against no value", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", ""), - Subgraphs: cacheOptions("no-cache", ""), + CacheControlPolicy: config.CacheControlPolicy{Enabled: true}, + Subgraphs: cacheOptions("no-cache", ""), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -400,11 +409,11 @@ func TestHeaderPropagation(t *testing.T) { }) // Test case for max-age: shortest max-age wins - t.Run("global shortest max-age wins", func(t *testing.T) { + t.Run("shortest max-age wins", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", ""), - Subgraphs: cacheOptions("max-age=600", "max-age=300"), + CacheControlPolicy: config.CacheControlPolicy{Enabled: true}, + Subgraphs: cacheOptions("max-age=600", "max-age=300"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -416,11 +425,11 @@ func TestHeaderPropagation(t *testing.T) { }) // Test case for Expires header: earliest expiration wins - t.Run("global earliest Expires wins", func(t *testing.T) { + t.Run("earliest Expires wins", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", ""), - Subgraphs: subgraphsWithExpiresHeader, + CacheControlPolicy: config.CacheControlPolicy{Enabled: true}, + Subgraphs: subgraphsWithExpiresHeader, }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -444,8 +453,11 @@ func TestHeaderPropagation(t *testing.T) { t.Run("global default age sets for all requests", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "max-age=300"), - Subgraphs: cacheOptions("", "max-age=600"), + CacheControlPolicy: config.CacheControlPolicy{ + Enabled: true, + Value: "max-age=300", + }, + Subgraphs: cacheOptions("", "max-age=600"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -459,8 +471,11 @@ func TestHeaderPropagation(t *testing.T) { t.Run("global no-cache sets for all requests", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "no-cache"), - Subgraphs: cacheOptions("", "max-age=600"), + CacheControlPolicy: config.CacheControlPolicy{ + Enabled: true, + Value: "no-cache", + }, + Subgraphs: cacheOptions("", "max-age=600"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -474,8 +489,11 @@ func TestHeaderPropagation(t *testing.T) { t.Run("global default age sets for all requests", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "no-cache"), - Subgraphs: cacheOptions("max-age=60", "max-age=300"), + CacheControlPolicy: config.CacheControlPolicy{ + Enabled: true, + Value: "no-cache", + }, + Subgraphs: cacheOptions("max-age=60", "max-age=300"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -489,8 +507,11 @@ func TestHeaderPropagation(t *testing.T) { t.Run("allows subgraph to override default", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "max-age=300"), - Subgraphs: cacheOptions("max-age=60", "max-age=180"), + CacheControlPolicy: config.CacheControlPolicy{ + Enabled: true, + Value: "max-age=300", + }, + Subgraphs: cacheOptions("max-age=60", "max-age=180"), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -504,8 +525,13 @@ func TestHeaderPropagation(t *testing.T) { t.Run("partial default age sets for requests with information", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: local(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "", "max-age=300"), - Subgraphs: cacheOptions("", ""), + CacheControlPolicy: config.CacheControlPolicy{ + Subgraphs: []config.SubgraphCacheControlRule{ + {Name: "employees"}, + {Name: "hobbies", Value: "max-age=300"}, + }, + }, + Subgraphs: cacheOptions("", ""), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithHobby, @@ -519,8 +545,13 @@ func TestHeaderPropagation(t *testing.T) { t.Run("partial default age doesn't set for unassociated requests", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: local(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "", "max-age=300"), - Subgraphs: cacheOptions("", ""), + CacheControlPolicy: config.CacheControlPolicy{ + Subgraphs: []config.SubgraphCacheControlRule{ + {Name: "employees"}, + {Name: "hobbies", Value: "max-age=300"}, + }, + }, + Subgraphs: cacheOptions("", ""), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: queryEmployeeWithNoHobby, @@ -534,8 +565,11 @@ func TestHeaderPropagation(t *testing.T) { t.Run("no-cache is set for all mutations", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ - RouterOptions: global(config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, "", "max-age=300"), - Subgraphs: cacheOptions("", ""), + CacheControlPolicy: config.CacheControlPolicy{ + Enabled: true, + Value: "max-age=300", + }, + Subgraphs: cacheOptions("", ""), }, func(t *testing.T, xEnv *testenv.Environment) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: `mutation { updateEmployeeTag(id: 1, tag: "test") { id tag } }`, @@ -547,5 +581,67 @@ func TestHeaderPropagation(t *testing.T) { }) }) }) + + t.Run("set operation can override cache control policies", func(t *testing.T) { + t.Run("global set operation", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + CacheControlPolicy: config.CacheControlPolicy{ + Enabled: true, + Value: "max-age=300", + }, + Subgraphs: cacheOptions("max-age=180", "max-age=250"), + RouterOptions: []core.Option{core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Response: []*config.ResponseHeaderRule{ + &config.ResponseHeaderRule{ + Operation: config.HeaderRuleOperationSet, + Name: "Cache-Control", + Value: "my-fake-value", + }, + }, + }, + })}, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: queryEmployeeWithHobby, + }) + cc := res.Response.Header.Get("Cache-Control") + require.Equal(t, "my-fake-value", cc) + require.Equal(t, `{"data":{"employee":{"id":1,"hobbies":[{},{"name":"Counter Strike"},{},{},{}]}}}`, res.Body) + }) + }) + + t.Run("local subgraph set operation", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + CacheControlPolicy: config.CacheControlPolicy{ + Enabled: true, + Value: "max-age=300", + }, + Subgraphs: cacheOptions("max-age=180", "max-age=250"), + RouterOptions: []core.Option{core.WithHeaderRules(config.HeaderRules{ + Subgraphs: map[string]*config.GlobalHeaderRule{ + "employees": &config.GlobalHeaderRule{ + Response: []*config.ResponseHeaderRule{ + &config.ResponseHeaderRule{ + Operation: config.HeaderRuleOperationSet, + Name: "Cache-Control", + Value: "my-fake-value", + }, + }, + }, + }, + })}, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: queryEmployeeWithHobby, + }) + cc := res.Response.Header.Get("Cache-Control") + require.Equal(t, "my-fake-value", cc) + require.Equal(t, `{"data":{"employee":{"id":1,"hobbies":[{},{"name":"Counter Strike"},{},{},{}]}}}`, res.Body) + }) + }) + }) }) } diff --git a/router-tests/testenv/testenv.go b/router-tests/testenv/testenv.go index d20818827f..27dbb2df90 100644 --- a/router-tests/testenv/testenv.go +++ b/router-tests/testenv/testenv.go @@ -127,6 +127,7 @@ type Config struct { ShutdownDelay time.Duration NoRetryClient bool PropagationConfig config.PropagationConfig + CacheControlPolicy config.CacheControlPolicy LogObservation LogObservationConfig } @@ -579,6 +580,7 @@ func configureRouter(listenerAddr string, testConfig *Config, routerConfig *node RewritePaths: true, AllowedExtensionFields: []string{"code"}, }, + CacheControl: testConfig.CacheControlPolicy, } if testConfig.ModifyCDNConfig != nil { @@ -650,6 +652,7 @@ func configureRouter(listenerAddr string, testConfig *Config, routerConfig *node core.WithPlayground(true), core.WithEngineExecutionConfig(engineExecutionConfig), core.WithSecurityConfig(cfg.SecurityConfiguration), + core.WithCacheControlPolicy(cfg.CacheControl), core.WithCDN(cfg.CDN), core.WithListenerAddr(listenerAddr), core.WithSubgraphErrorPropagation(cfg.SubgraphErrorPropagation), diff --git a/router/cmd/instance.go b/router/cmd/instance.go index d110c27c08..868373b45f 100644 --- a/router/cmd/instance.go +++ b/router/cmd/instance.go @@ -162,6 +162,7 @@ func NewRouter(params Params, additionalOptions ...core.Option) (*core.Router, e core.WithTracing(core.TraceConfigFromTelemetry(&cfg.Telemetry)), core.WithMetrics(core.MetricConfigFromTelemetry(&cfg.Telemetry)), core.WithEngineExecutionConfig(cfg.EngineExecutionConfiguration), + core.WithCacheControlPolicy(cfg.CacheControl), core.WithSecurityConfig(cfg.SecurityConfiguration), core.WithAuthorizationConfig(&cfg.Authorization), core.WithWebSocketConfiguration(&cfg.WebSocket), diff --git a/router/core/header_rule_engine.go b/router/core/header_rule_engine.go index e8320d843f..8ab43ed030 100644 --- a/router/core/header_rule_engine.go +++ b/router/core/header_rule_engine.go @@ -108,18 +108,21 @@ type HeaderPropagation struct { hasResponseRules bool } -func NewHeaderPropagation(rules *config.HeaderRules) (*HeaderPropagation, error) { - if rules == nil { - return nil, nil - } - +func initHeaderRules(rules *config.HeaderRules) { if rules.All == nil { rules.All = &config.GlobalHeaderRule{} } if rules.Subgraphs == nil { rules.Subgraphs = make(map[string]*config.GlobalHeaderRule) } +} + +func NewHeaderPropagation(rules *config.HeaderRules) (*HeaderPropagation, error) { + if rules == nil { + return nil, nil + } + initHeaderRules(rules) hf := HeaderPropagation{ rules: rules, regex: map[string]*regexp.Regexp{}, @@ -136,6 +139,41 @@ func NewHeaderPropagation(rules *config.HeaderRules) (*HeaderPropagation, error) return &hf, nil } +func AddCacheControlPolicyToRules(rules *config.HeaderRules, cacheControl config.CacheControlPolicy) *config.HeaderRules { + if rules == nil { + rules = &config.HeaderRules{} + if !cacheControl.Enabled && cacheControl.Subgraphs == nil { + return nil + } + } + + initHeaderRules(rules) + if cacheControl.Enabled { + rules.All.Response = append(rules.All.Response, &config.ResponseHeaderRule{ + Operation: config.HeaderRuleOperationPropagate, + Algorithm: config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, + Default: cacheControl.Value, + }) + } + + for _, graph := range cacheControl.Subgraphs { + subgraphRules, ok := rules.Subgraphs[graph.Name] + if !ok { + subgraphRules = &config.GlobalHeaderRule{Response: make([]*config.ResponseHeaderRule, 0)} + } + + subgraphRules.Response = append(subgraphRules.Response, &config.ResponseHeaderRule{ + Operation: config.HeaderRuleOperationPropagate, + Algorithm: config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, + Default: graph.Value, + }) + + rules.Subgraphs[graph.Name] = subgraphRules + } + + return rules +} + func (hf *HeaderPropagation) getAllRules() ([]*config.RequestHeaderRule, []*config.ResponseHeaderRule) { rhrs := hf.rules.All.Request for _, subgraph := range hf.rules.Subgraphs { diff --git a/router/core/router.go b/router/core/router.go index 7e1dd90824..c7a288000e 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -160,6 +160,7 @@ type ( healthCheckPath string readinessCheckPath string livenessCheckPath string + cacheControlPolicy config.CacheControlPolicy routerConfigPollerConfig *RouterConfigPollerConfig cdnConfig config.CDNConfiguration persistedOperationClient persistedoperation.Client @@ -310,6 +311,7 @@ func NewRouter(opts ...Option) (*Router, error) { r.livenessCheckPath = "/health/live" } + r.headerRules = AddCacheControlPolicyToRules(r.headerRules, r.cacheControlPolicy) hr, err := NewHeaderPropagation(r.headerRules) if err != nil { return nil, err @@ -1456,6 +1458,12 @@ func WithHeaderRules(headers config.HeaderRules) Option { } } +func WithCacheControlPolicy(cfg config.CacheControlPolicy) Option { + return func(r *Router) { + r.cacheControlPolicy = cfg + } +} + func WithOverrideRoutingURL(overrideRoutingURL config.OverrideRoutingURLConfiguration) Option { return func(r *Router) { r.overrideRoutingURLConfiguration = overrideRoutingURL diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index 556ead2545..98ca51654f 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -5,12 +5,11 @@ import ( "os" "time" - "github.com/wundergraph/cosmo/router/internal/unique" - - "github.com/goccy/go-yaml" - "github.com/caarlos0/env/v11" + "github.com/goccy/go-yaml" "github.com/joho/godotenv" + + "github.com/wundergraph/cosmo/router/internal/unique" "github.com/wundergraph/cosmo/router/pkg/otel/otelconfig" ) @@ -164,6 +163,17 @@ type BackoffJitterRetry struct { Interval time.Duration `yaml:"interval" envDefault:"3s"` } +type SubgraphCacheControlRule struct { + Name string `yaml:"name"` + Value string `yaml:"value"` +} + +type CacheControlPolicy struct { + Enabled bool `yaml:"enabled" envDefault:"false" env:"CACHE_CONTROL_POLICY_ENABLED"` + Value string `yaml:"value" env:"CACHE_CONTROL_POLICY_VALUE"` + Subgraphs []SubgraphCacheControlRule `yaml:"subgraphs,omitempty"` +} + type HeaderRules struct { // All is a set of rules that apply to all requests All *GlobalHeaderRule `yaml:"all,omitempty"` @@ -593,14 +603,15 @@ type ApolloCompatibilityValueCompletion struct { type Config struct { Version string `yaml:"version,omitempty" ignored:"true"` - InstanceID string `yaml:"instance_id,omitempty" env:"INSTANCE_ID"` - Graph Graph `yaml:"graph,omitempty"` - Telemetry Telemetry `yaml:"telemetry,omitempty"` - GraphqlMetrics GraphqlMetrics `yaml:"graphql_metrics,omitempty"` - CORS CORS `yaml:"cors,omitempty"` - Cluster Cluster `yaml:"cluster,omitempty"` - Compliance ComplianceConfig `yaml:"compliance,omitempty"` - TLS TLSConfiguration `yaml:"tls,omitempty"` + InstanceID string `yaml:"instance_id,omitempty" env:"INSTANCE_ID"` + Graph Graph `yaml:"graph,omitempty"` + Telemetry Telemetry `yaml:"telemetry,omitempty"` + GraphqlMetrics GraphqlMetrics `yaml:"graphql_metrics,omitempty"` + CORS CORS `yaml:"cors,omitempty"` + Cluster Cluster `yaml:"cluster,omitempty"` + Compliance ComplianceConfig `yaml:"compliance,omitempty"` + TLS TLSConfiguration `yaml:"tls,omitempty"` + CacheControl CacheControlPolicy `yaml:"cache_control_policy"` Modules map[string]interface{} `yaml:"modules,omitempty"` Headers HeaderRules `yaml:"headers,omitempty"` diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index cae6c9122f..e6a88933ac 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -1009,7 +1009,45 @@ } } }, - "modules": { + "cache_control_policy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Determines whether cache control policy is enabled.", + "examples": [true] + }, + "value": { + "type": "string", + "description": "Global cache control value.", + "examples": ["max-age=180, public"] + }, + "subgraphs": { + "type": "array", + "description": "Subgraph-specific cache control settings.", + "required": ["name"], + "additionalProperties": false, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the subgraph.", + "examples": ["products"] + }, + "value": { + "type": "string", + "description": "Cache control value for the subgraph.", + "examples": ["max-age=60, public"] + } + } + } + } + }, + "required": ["enabled"], + "additionalProperties": false + }, + "modules": { "type": "object", "description": "The configuration for the modules. The modules are used to extend the functionality of the router. The modules are specified as a map of module names to module configurations. It needs to match with the name of the module and the configuration of the module. See https://cosmo-docs.wundergraph.com/router/custom-modules for more information.", "additionalProperties": { @@ -1740,32 +1778,12 @@ }, "algorithm": { "type": "string", - "enum": ["first_write", "last_write", "append", "most_restrictive_cache_control"], + "enum": ["first_write", "last_write", "append"], "examples": ["first_write"], - "description": "The algorith, to use when multiple headers are present. The supported operations are '\"first_write\", \"last_write\", \"append\", and \"most_restrictive_cache_control\"'. The 'first_write' retains the first value of a given header. The 'last_write' retains the last value of a given header. The 'append' appends all values of a given header. The 'most_restrictive_cache_control' specifically focuses on the 'Cache-Control'/'Expiration' headers, and applies the most restrictive value encountered from a subgraph" - } - }, - "required": ["op", "algorithm"], - "if": { - "properties": { - "algorithm": { "const": "most_restrictive_cache_control" } + "description": "The algorith, to use when multiple headers are present. The supported operations are '\"first_write\", \"last_write\", and \"append\". The 'first_write' retains the first value of a given header. The 'last_write' retains the last value of a given header. The 'append' appends all values of a given header." } }, - "then": { - "properties": { - "matching": { "not": {} }, - "named": { "not": {} }, - "rename": { "not": {} } - }, - "required": ["op", "algorithm"] - }, - "else": { - "properties": { - "matching": {}, - "named": {}, - "rename": {} - } - } + "required": ["op", "algorithm"] }, "set_header_rule": { "type": "object", diff --git a/router/pkg/config/fixtures/full.yaml b/router/pkg/config/fixtures/full.yaml index 0dc9e4839b..1a027b0672 100644 --- a/router/pkg/config/fixtures/full.yaml +++ b/router/pkg/config/fixtures/full.yaml @@ -104,6 +104,13 @@ telemetry: exclude_metrics: [] exclude_metric_labels: [] +cache_control_policy: + enabled: true + value: "max-age=180, public" + subgraphs: + - name: "products" + value: "max-age=60, public" + # Config for custom modules # See "https://cosmo-docs.wundergraph.com/router/custom-modules" for more information modules: @@ -166,8 +173,6 @@ headers: named: Subgraph-Secret default: "some-secret" response: - - op: "propagate" - algorithm: "most_restrictive_cache_control" - op: "set" name: "X-Subgraph-Key" value: "some-subgraph-secret" diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index ddf0fcf0d4..49719d7045 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -81,6 +81,11 @@ } } }, + "CacheControl": { + "Enabled": false, + "Value": "", + "Subgraphs": null + }, "Modules": null, "Headers": { "All": null, diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 1956fdcbe1..f6e4e089d1 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -99,6 +99,16 @@ } } }, + "CacheControl": { + "Enabled": true, + "Value": "max-age=180, public", + "Subgraphs": [ + { + "Name": "products", + "Value": "max-age=60, public" + } + ] + }, "Modules": { "myModule": { "value": 1 @@ -171,16 +181,6 @@ } ], "Response": [ - { - "Operation": "propagate", - "Matching": "", - "Named": "", - "Rename": "", - "Default": "", - "Algorithm": "most_restrictive_cache_control", - "Name": "", - "Value": "" - }, { "Operation": "set", "Matching": "",