1use std::collections::BTreeMap;
3use std::fmt;
4use std::hash::Hash;
5use std::io;
6use std::io::BufReader;
7use std::iter;
8use std::net::IpAddr;
9use std::net::SocketAddr;
10use std::num::NonZeroU32;
11use std::num::NonZeroUsize;
12use std::str::FromStr;
13use std::sync::Arc;
14use std::time::Duration;
15
16use connector::ConnectorConfiguration;
17use derivative::Derivative;
18use displaydoc::Display;
19use itertools::Either;
20use itertools::Itertools;
21use once_cell::sync::Lazy;
22pub(crate) use persisted_queries::PersistedQueries;
23pub(crate) use persisted_queries::PersistedQueriesPrewarmQueryPlanCache;
24#[cfg(test)]
25pub(crate) use persisted_queries::PersistedQueriesSafelist;
26use regex::Regex;
27use rustls::ServerConfig;
28use rustls::pki_types::CertificateDer;
29use rustls::pki_types::PrivateKeyDer;
30use schemars::JsonSchema;
31use schemars::Schema;
32use schemars::SchemaGenerator;
33use serde::Deserialize;
34use serde::Deserializer;
35use serde::Serialize;
36use serde_json::Map;
37use serde_json::Value;
38use sha2::Digest;
39use thiserror::Error;
40
41use self::cors::Cors;
42use self::expansion::Expansion;
43pub(crate) use self::experimental::Discussed;
44pub(crate) use self::schema::generate_config_schema;
45pub(crate) use self::schema::generate_upgrade;
46pub(crate) use self::schema::validate_yaml_configuration;
47use self::server::Server;
48use self::subgraph::SubgraphConfiguration;
49use crate::ApolloRouterError;
50use crate::cache::DEFAULT_CACHE_CAPACITY;
51use crate::configuration::cooperative_cancellation::CooperativeCancellation;
52use crate::configuration::mode::Mode;
53use crate::graphql;
54use crate::plugin::plugins;
55use crate::plugins::chaos;
56use crate::plugins::chaos::Config;
57use crate::plugins::healthcheck::Config as HealthCheck;
58#[cfg(test)]
59use crate::plugins::healthcheck::test_listen;
60use crate::plugins::limits;
61use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN;
62use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN_NAME;
63use crate::plugins::subscription::SubscriptionConfig;
64use crate::plugins::subscription::notification::Notify;
65use crate::uplink::UplinkConfig;
66
67pub(crate) mod connector;
68pub(crate) mod cooperative_cancellation;
69pub(crate) mod cors;
70pub(crate) mod expansion;
71mod experimental;
72pub(crate) mod metrics;
73pub(crate) mod mode;
74mod persisted_queries;
75pub(crate) mod schema;
76pub(crate) mod server;
77pub(crate) mod shared;
78pub(crate) mod subgraph;
79#[cfg(test)]
80mod tests;
81mod upgrade;
82mod yaml;
83
84static HEARTBEAT_TIMEOUT_DURATION_SECONDS: u64 = 15;
86
87static SUPERGRAPH_ENDPOINT_REGEX: Lazy<Regex> = Lazy::new(|| {
88 Regex::new(r"(?P<first_path>.*/)(?P<sub_path>.+)\*$")
89 .expect("this regex to check the path is valid")
90});
91
92#[derive(Debug, Error, Display)]
94#[non_exhaustive]
95pub enum ConfigurationError {
96 CannotExpandVariable { key: String, cause: String },
98 UnknownExpansionMode {
100 key: String,
101 supported_modes: String,
102 },
103 PluginUnknown(String),
105 PluginConfiguration { plugin: String, error: String },
107 InvalidConfiguration {
109 message: &'static str,
110 error: String,
111 },
112 DeserializeConfigError(serde_json::Error),
114
115 InvalidExpansionModeConfig,
117
118 MigrationFailure { error: String },
120
121 CertificateAuthorities { error: String },
123}
124
125impl From<proteus::Error> for ConfigurationError {
126 fn from(error: proteus::Error) -> Self {
127 Self::MigrationFailure {
128 error: error.to_string(),
129 }
130 }
131}
132
133impl From<proteus::parser::Error> for ConfigurationError {
134 fn from(error: proteus::parser::Error) -> Self {
135 Self::MigrationFailure {
136 error: error.to_string(),
137 }
138 }
139}
140
141#[derive(Clone, Derivative, Serialize, JsonSchema)]
146#[derivative(Debug)]
147pub struct Configuration {
149 #[serde(skip)]
151 pub(crate) validated_yaml: Option<Value>,
152
153 #[serde(skip)]
155 pub(crate) raw_yaml: Option<Arc<str>>,
156
157 #[serde(default)]
159 pub(crate) health_check: HealthCheck,
160
161 #[serde(default)]
163 pub(crate) sandbox: Sandbox,
164
165 #[serde(default)]
167 pub(crate) homepage: Homepage,
168
169 #[serde(default)]
171 pub(crate) server: Server,
172
173 #[serde(default)]
175 pub(crate) supergraph: Supergraph,
176
177 #[serde(default)]
179 pub(crate) cors: Cors,
180
181 #[serde(default)]
182 pub(crate) tls: Tls,
183
184 #[serde(default)]
186 pub(crate) apq: Apq,
187
188 #[serde(default)]
190 pub persisted_queries: PersistedQueries,
191
192 #[serde(default)]
194 pub(crate) limits: limits::Config,
195
196 #[serde(default)]
199 pub(crate) experimental_chaos: Config,
200
201 #[serde(default)]
203 pub(crate) plugins: UserPlugins,
204
205 #[serde(default)]
207 #[serde(flatten)]
208 pub(crate) apollo_plugins: ApolloPlugins,
209
210 #[serde(skip)]
212 pub uplink: Option<UplinkConfig>,
213
214 #[serde(default, skip_serializing, skip_deserializing)]
217 pub(crate) notify: Notify<String, graphql::Response>,
218
219 #[serde(default)]
221 pub(crate) batching: Batching,
222
223 #[serde(default)]
225 pub(crate) experimental_type_conditioned_fetching: bool,
226}
227
228impl PartialEq for Configuration {
229 fn eq(&self, other: &Self) -> bool {
230 self.validated_yaml == other.validated_yaml
231 }
232}
233
234impl<'de> serde::Deserialize<'de> for Configuration {
235 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236 where
237 D: serde::Deserializer<'de>,
238 {
239 #[derive(Deserialize, Default)]
242 #[serde(default)]
243 struct AdHocConfiguration {
244 health_check: HealthCheck,
245 sandbox: Sandbox,
246 homepage: Homepage,
247 server: Server,
248 supergraph: Supergraph,
249 cors: Cors,
250 plugins: UserPlugins,
251 #[serde(flatten)]
252 apollo_plugins: ApolloPlugins,
253 tls: Tls,
254 apq: Apq,
255 persisted_queries: PersistedQueries,
256 limits: limits::Config,
257 experimental_chaos: chaos::Config,
258 batching: Batching,
259 experimental_type_conditioned_fetching: bool,
260 }
261 let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
262
263 let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
264 .map_err(|e| serde::de::Error::custom(e.to_string()))?;
265
266 ad_hoc.apollo_plugins.plugins.insert(
269 "limits".to_string(),
270 serde_json::to_value(&ad_hoc.limits).unwrap(),
271 );
272 ad_hoc.apollo_plugins.plugins.insert(
273 "health_check".to_string(),
274 serde_json::to_value(&ad_hoc.health_check).unwrap(),
275 );
276
277 Configuration {
279 health_check: ad_hoc.health_check,
280 sandbox: ad_hoc.sandbox,
281 homepage: ad_hoc.homepage,
282 server: ad_hoc.server,
283 supergraph: ad_hoc.supergraph,
284 cors: ad_hoc.cors,
285 tls: ad_hoc.tls,
286 apq: ad_hoc.apq,
287 persisted_queries: ad_hoc.persisted_queries,
288 limits: ad_hoc.limits,
289 experimental_chaos: ad_hoc.experimental_chaos,
290 experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
291 plugins: ad_hoc.plugins,
292 apollo_plugins: ad_hoc.apollo_plugins,
293 batching: ad_hoc.batching,
294
295 notify,
297 uplink: None,
298 validated_yaml: None,
299 raw_yaml: None,
300 }
301 .validate()
302 .map_err(|e| serde::de::Error::custom(e.to_string()))
303 }
304}
305
306pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
307
308fn default_graphql_listen() -> ListenAddr {
309 SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
310}
311
312#[cfg(test)]
313#[buildstructor::buildstructor]
314impl Configuration {
315 #[builder]
316 pub(crate) fn new(
317 supergraph: Option<Supergraph>,
318 health_check: Option<HealthCheck>,
319 sandbox: Option<Sandbox>,
320 homepage: Option<Homepage>,
321 cors: Option<Cors>,
322 plugins: Map<String, Value>,
323 apollo_plugins: Map<String, Value>,
324 tls: Option<Tls>,
325 apq: Option<Apq>,
326 persisted_query: Option<PersistedQueries>,
327 operation_limits: Option<limits::Config>,
328 chaos: Option<chaos::Config>,
329 uplink: Option<UplinkConfig>,
330 experimental_type_conditioned_fetching: Option<bool>,
331 batching: Option<Batching>,
332 server: Option<Server>,
333 ) -> Result<Self, ConfigurationError> {
334 let notify = Self::notify(&apollo_plugins)?;
335
336 let conf = Self {
337 validated_yaml: Default::default(),
338 raw_yaml: None,
339 supergraph: supergraph.unwrap_or_default(),
340 server: server.unwrap_or_default(),
341 health_check: health_check.unwrap_or_default(),
342 sandbox: sandbox.unwrap_or_default(),
343 homepage: homepage.unwrap_or_default(),
344 cors: cors.unwrap_or_default(),
345 apq: apq.unwrap_or_default(),
346 persisted_queries: persisted_query.unwrap_or_default(),
347 limits: operation_limits.unwrap_or_default(),
348 experimental_chaos: chaos.unwrap_or_default(),
349 plugins: UserPlugins {
350 plugins: Some(plugins),
351 },
352 apollo_plugins: ApolloPlugins {
353 plugins: apollo_plugins,
354 },
355 tls: tls.unwrap_or_default(),
356 uplink,
357 batching: batching.unwrap_or_default(),
358 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
359 .unwrap_or_default(),
360 notify,
361 };
362
363 conf.validate()
364 }
365}
366
367impl Configuration {
368 pub(crate) fn hash(&self) -> String {
369 let mut hasher = sha2::Sha256::new();
370 let defaulted_raw = self
371 .validated_yaml
372 .as_ref()
373 .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
374 .unwrap_or_default();
375 hasher.update(defaulted_raw);
376 let hash: String = format!("{:x}", hasher.finalize());
377 hash
378 }
379
380 fn notify(
381 apollo_plugins: &Map<String, Value>,
382 ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
383 if cfg!(test) {
384 return Ok(Notify::for_tests());
385 }
386 let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
387 Some(plugin_conf) => {
388 let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
389 .map_err(|err| ConfigurationError::PluginConfiguration {
390 plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
391 error: format!("{err:?}"),
392 })?;
393 conf.queue_capacity
394 }
395 None => None,
396 };
397 Ok(Notify::builder()
398 .and_queue_size(notify_queue_cap)
399 .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
400 .heartbeat_error_message(
401 graphql::Response::builder()
402 .errors(vec![
403 graphql::Error::builder()
404 .message("the connection has been closed because it hasn't heartbeat for a while")
405 .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
406 .build()
407 ])
408 .build()
409 ).build())
410 }
411
412 pub(crate) fn rust_query_planner_config(
413 &self,
414 ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
415 use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
416 use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
417 use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
418
419 let max_evaluated_plans = self
420 .supergraph
421 .query_planning
422 .experimental_plans_limit
423 .and_then(NonZeroU32::new)
425 .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
426
427 QueryPlannerConfig {
428 subgraph_graphql_validation: false,
429 generate_query_fragments: self.supergraph.generate_query_fragments,
430 incremental_delivery: QueryPlanIncrementalDeliveryConfig {
431 enable_defer: self.supergraph.defer_support,
432 },
433 type_conditioned_fetching: self.experimental_type_conditioned_fetching,
434 debug: QueryPlannerDebugConfig {
435 max_evaluated_plans,
436 paths_limit: self.supergraph.query_planning.experimental_paths_limit,
437 },
438 }
439 }
440
441 fn apollo_plugin_enabled(&self, plugin_name: &str) -> bool {
442 self.apollo_plugins
443 .plugins
444 .get(plugin_name)
445 .and_then(|config| config.as_object().and_then(|c| c.get("enabled")))
446 .and_then(|enabled| enabled.as_bool())
447 .unwrap_or(false)
448 }
449}
450
451impl Default for Configuration {
452 fn default() -> Self {
453 Configuration::from_str("").expect("default configuration must be valid")
455 }
456}
457
458#[cfg(test)]
459#[buildstructor::buildstructor]
460impl Configuration {
461 #[builder]
462 pub(crate) fn fake_new(
463 supergraph: Option<Supergraph>,
464 health_check: Option<HealthCheck>,
465 sandbox: Option<Sandbox>,
466 homepage: Option<Homepage>,
467 cors: Option<Cors>,
468 plugins: Map<String, Value>,
469 apollo_plugins: Map<String, Value>,
470 tls: Option<Tls>,
471 notify: Option<Notify<String, graphql::Response>>,
472 apq: Option<Apq>,
473 persisted_query: Option<PersistedQueries>,
474 operation_limits: Option<limits::Config>,
475 chaos: Option<chaos::Config>,
476 uplink: Option<UplinkConfig>,
477 batching: Option<Batching>,
478 experimental_type_conditioned_fetching: Option<bool>,
479 server: Option<Server>,
480 ) -> Result<Self, ConfigurationError> {
481 let configuration = Self {
482 validated_yaml: Default::default(),
483 server: server.unwrap_or_default(),
484 supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
485 health_check: health_check.unwrap_or_else(|| HealthCheck::builder().build()),
486 sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
487 homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
488 cors: cors.unwrap_or_default(),
489 limits: operation_limits.unwrap_or_default(),
490 experimental_chaos: chaos.unwrap_or_default(),
491 plugins: UserPlugins {
492 plugins: Some(plugins),
493 },
494 apollo_plugins: ApolloPlugins {
495 plugins: apollo_plugins,
496 },
497 tls: tls.unwrap_or_default(),
498 notify: notify.unwrap_or_default(),
499 apq: apq.unwrap_or_default(),
500 persisted_queries: persisted_query.unwrap_or_default(),
501 uplink,
502 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
503 .unwrap_or_default(),
504 batching: batching.unwrap_or_default(),
505 raw_yaml: None,
506 };
507
508 configuration.validate()
509 }
510}
511
512impl Configuration {
513 pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
514 if self.sandbox.enabled && self.homepage.enabled {
516 return Err(ConfigurationError::InvalidConfiguration {
517 message: "sandbox and homepage cannot be enabled at the same time",
518 error: "disable the homepage if you want to enable sandbox".to_string(),
519 });
520 }
521 if self.sandbox.enabled && !self.supergraph.introspection {
523 return Err(ConfigurationError::InvalidConfiguration {
524 message: "sandbox requires introspection",
525 error: "sandbox needs introspection to be enabled".to_string(),
526 });
527 }
528 if !self.supergraph.path.starts_with('/') {
529 return Err(ConfigurationError::InvalidConfiguration {
530 message: "invalid 'server.graphql_path' configuration",
531 error: format!(
532 "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
533 self.supergraph.path, self.supergraph.path
534 ),
535 });
536 }
537 if self.supergraph.path.ends_with('*')
538 && !self.supergraph.path.ends_with("/*")
539 && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
540 {
541 return Err(ConfigurationError::InvalidConfiguration {
542 message: "invalid 'server.graphql_path' configuration",
543 error: format!(
544 "'{}' is invalid, you can only set a wildcard after a '/'",
545 self.supergraph.path
546 ),
547 });
548 }
549 if self.supergraph.path.contains("/*/") {
550 return Err(ConfigurationError::InvalidConfiguration {
551 message: "invalid 'server.graphql_path' configuration",
552 error: format!(
553 "'{}' is invalid, if you need to set a path like '/*/graphql' then specify it as a path parameter with a name, for example '/:my_project_key/graphql'",
554 self.supergraph.path
555 ),
556 });
557 }
558
559 if self.persisted_queries.enabled {
561 if self.persisted_queries.safelist.enabled && self.apq.enabled {
562 return Err(ConfigurationError::InvalidConfiguration {
563 message: "apqs must be disabled to enable safelisting",
564 error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
565 });
566 } else if !self.persisted_queries.safelist.enabled
567 && self.persisted_queries.safelist.require_id
568 {
569 return Err(ConfigurationError::InvalidConfiguration {
570 message: "safelist must be enabled to require IDs",
571 error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
572 });
573 }
574 } else {
575 if self.persisted_queries.safelist.enabled {
577 return Err(ConfigurationError::InvalidConfiguration {
578 message: "persisted queries must be enabled to enable safelisting",
579 error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
580 });
581 } else if self.persisted_queries.log_unknown {
582 return Err(ConfigurationError::InvalidConfiguration {
583 message: "persisted queries must be enabled to enable logging unknown operations",
584 error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
585 });
586 }
587 }
588
589 if self.apollo_plugin_enabled("response_cache")
591 && self.apollo_plugin_enabled("preview_entity_cache")
592 {
593 return Err(ConfigurationError::InvalidConfiguration {
594 message: "entity cache and response cache features are mutually exclusive",
595 error: "either set response_cache.enabled: false or preview_entity_cache.enabled: false in your router yaml configuration".into(),
596 });
597 }
598
599 Ok(self)
600 }
601}
602
603impl FromStr for Configuration {
605 type Err = ConfigurationError;
606
607 fn from_str(s: &str) -> Result<Self, Self::Err> {
608 schema::validate_yaml_configuration(s, Expansion::default()?, schema::Mode::Upgrade)?
609 .validate()
610 }
611}
612
613fn gen_schema(
614 plugins: BTreeMap<String, Schema>,
615 hidden_plugins: Option<BTreeMap<String, Schema>>,
616) -> Schema {
617 schemars::json_schema!({
618 "type": "object",
619 "properties": plugins,
620 "additionalProperties": false,
621 "patternProperties": hidden_plugins
622 .unwrap_or_default()
623 .into_iter()
624 .map(|(k, v)| (format!("^{}$", k), v))
626 .collect::<BTreeMap<_, _>>()
627 })
628}
629
630#[derive(Clone, Debug, Default, Deserialize, Serialize)]
636#[serde(transparent)]
637pub(crate) struct ApolloPlugins {
638 pub(crate) plugins: Map<String, Value>,
639}
640
641impl JsonSchema for ApolloPlugins {
642 fn schema_name() -> std::borrow::Cow<'static, str> {
643 stringify!(Plugins).into()
644 }
645
646 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
647 let (plugin_entries, hidden_plugin_entries): (Vec<_>, Vec<_>) = crate::plugin::plugins()
651 .sorted_by_key(|factory| factory.name.clone())
652 .filter(|factory| factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
653 .partition_map(|factory| {
654 let key = factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string();
655 let schema = factory.create_schema(generator);
656 if factory.hidden_from_config_json_schema {
658 Either::Right((key, schema))
659 } else {
660 Either::Left((key, schema))
661 }
662 });
663 gen_schema(
664 plugin_entries.into_iter().collect(),
665 Some(hidden_plugin_entries.into_iter().collect()),
666 )
667 }
668}
669
670#[derive(Clone, Debug, Default, Deserialize, Serialize)]
675#[serde(transparent)]
676pub(crate) struct UserPlugins {
677 pub(crate) plugins: Option<Map<String, Value>>,
678}
679
680impl JsonSchema for UserPlugins {
681 fn schema_name() -> std::borrow::Cow<'static, str> {
682 stringify!(Plugins).into()
683 }
684
685 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
686 let plugins = crate::plugin::plugins()
690 .sorted_by_key(|factory| factory.name.clone())
691 .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
692 .map(|factory| (factory.name.to_string(), factory.create_schema(generator)))
693 .collect();
694 gen_schema(plugins, None)
695 }
696}
697
698#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
700#[serde(deny_unknown_fields)]
701#[serde(default)]
702pub(crate) struct Supergraph {
703 pub(crate) listen: ListenAddr,
706
707 #[serde(deserialize_with = "humantime_serde::deserialize")]
709 #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
710 pub(crate) connection_shutdown_timeout: Duration,
711
712 pub(crate) path: String,
715
716 pub(crate) introspection: bool,
719
720 pub(crate) redact_query_validation_errors: bool,
724
725 pub(crate) generate_query_fragments: bool,
728
729 pub(crate) defer_support: bool,
731
732 pub(crate) query_planning: QueryPlanning,
734
735 pub(crate) early_cancel: bool,
740
741 pub(crate) enable_result_coercion_errors: bool,
748
749 pub(crate) experimental_log_on_broken_pipe: bool,
752
753 pub(crate) strict_variable_validation: Mode,
757}
758
759const fn default_generate_query_fragments() -> bool {
760 true
761}
762
763fn default_defer_support() -> bool {
764 true
765}
766
767#[buildstructor::buildstructor]
768impl Supergraph {
769 #[builder]
770 pub(crate) fn new(
771 listen: Option<ListenAddr>,
772 path: Option<String>,
773 connection_shutdown_timeout: Option<Duration>,
774 introspection: Option<bool>,
775 defer_support: Option<bool>,
776 query_planning: Option<QueryPlanning>,
777 generate_query_fragments: Option<bool>,
778 early_cancel: Option<bool>,
779 experimental_log_on_broken_pipe: Option<bool>,
780 insert_result_coercion_errors: Option<bool>,
781 strict_variable_validation: Option<Mode>,
782 redact_query_validation_errors: Option<bool>,
783 ) -> Self {
784 Self {
785 listen: listen.unwrap_or_else(default_graphql_listen),
786 path: path.unwrap_or_else(default_graphql_path),
787 connection_shutdown_timeout: connection_shutdown_timeout
788 .unwrap_or_else(default_connection_shutdown_timeout),
789 introspection: introspection.unwrap_or_else(default_graphql_introspection),
790 defer_support: defer_support.unwrap_or_else(default_defer_support),
791 query_planning: query_planning.unwrap_or_default(),
792 generate_query_fragments: generate_query_fragments
793 .unwrap_or_else(default_generate_query_fragments),
794 early_cancel: early_cancel.unwrap_or_default(),
795 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
796 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
797 strict_variable_validation: strict_variable_validation
798 .unwrap_or_else(default_strict_variable_validation),
799 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
800 }
801 }
802}
803
804#[cfg(test)]
805#[buildstructor::buildstructor]
806impl Supergraph {
807 #[builder]
808 pub(crate) fn fake_new(
809 listen: Option<ListenAddr>,
810 path: Option<String>,
811 connection_shutdown_timeout: Option<Duration>,
812 introspection: Option<bool>,
813 defer_support: Option<bool>,
814 query_planning: Option<QueryPlanning>,
815 generate_query_fragments: Option<bool>,
816 early_cancel: Option<bool>,
817 experimental_log_on_broken_pipe: Option<bool>,
818 insert_result_coercion_errors: Option<bool>,
819 strict_variable_validation: Option<Mode>,
820 redact_query_validation_errors: Option<bool>,
821 ) -> Self {
822 Self {
823 listen: listen.unwrap_or_else(test_listen),
824 path: path.unwrap_or_else(default_graphql_path),
825 connection_shutdown_timeout: connection_shutdown_timeout
826 .unwrap_or_else(default_connection_shutdown_timeout),
827 introspection: introspection.unwrap_or_else(default_graphql_introspection),
828 defer_support: defer_support.unwrap_or_else(default_defer_support),
829 query_planning: query_planning.unwrap_or_default(),
830 generate_query_fragments: generate_query_fragments
831 .unwrap_or_else(default_generate_query_fragments),
832 early_cancel: early_cancel.unwrap_or_default(),
833 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
834 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
835 strict_variable_validation: strict_variable_validation
836 .unwrap_or_else(default_strict_variable_validation),
837 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
838 }
839 }
840}
841
842impl Default for Supergraph {
843 fn default() -> Self {
844 Self::builder().build()
845 }
846}
847
848impl Supergraph {
849 pub(crate) fn sanitized_path(&self) -> String {
851 let mut path = self.path.clone();
852 if self.path.ends_with("/*") {
853 path = format!("{}router_extra_path", self.path);
855 } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
856 let new_path = SUPERGRAPH_ENDPOINT_REGEX
857 .replace(&self.path, "${first_path}${sub_path}{supergraph_route}");
858 path = new_path.to_string();
859 }
860
861 path
862 }
863}
864
865#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
867#[serde(deny_unknown_fields)]
868pub(crate) struct Router {
869 #[serde(default)]
870 pub(crate) cache: Cache,
871}
872
873#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
875#[serde(deny_unknown_fields, default)]
876pub(crate) struct Apq {
877 pub(crate) enabled: bool,
879
880 pub(crate) router: Router,
881
882 pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
883}
884
885#[cfg(test)]
886#[buildstructor::buildstructor]
887impl Apq {
888 #[builder]
889 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
890 Self {
891 enabled: enabled.unwrap_or_else(default_apq),
892 ..Default::default()
893 }
894 }
895}
896
897#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
899#[serde(deny_unknown_fields, default)]
900pub(crate) struct SubgraphApq {
901 pub(crate) enabled: bool,
903}
904
905fn default_apq() -> bool {
906 true
907}
908
909impl Default for Apq {
910 fn default() -> Self {
911 Self {
912 enabled: default_apq(),
913 router: Default::default(),
914 subgraph: Default::default(),
915 }
916 }
917}
918
919#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
921#[serde(deny_unknown_fields, default)]
922pub(crate) struct QueryPlanning {
923 pub(crate) cache: QueryPlanCache,
925 pub(crate) warmed_up_queries: Option<usize>,
930
931 pub(crate) experimental_plans_limit: Option<u32>,
939
940 pub(crate) experimental_paths_limit: Option<u32>,
953
954 pub(crate) experimental_reuse_query_plans: bool,
957
958 pub(crate) experimental_cooperative_cancellation: CooperativeCancellation,
962}
963
964#[buildstructor::buildstructor]
965impl QueryPlanning {
966 #[builder]
967 #[allow(dead_code)]
968 pub(crate) fn new(
969 cache: Option<QueryPlanCache>,
970 warmed_up_queries: Option<usize>,
971 experimental_plans_limit: Option<u32>,
972 experimental_paths_limit: Option<u32>,
973 experimental_reuse_query_plans: Option<bool>,
974 experimental_cooperative_cancellation: Option<CooperativeCancellation>,
975 ) -> Self {
976 Self {
977 cache: cache.unwrap_or_default(),
978 warmed_up_queries,
979 experimental_plans_limit,
980 experimental_paths_limit,
981 experimental_reuse_query_plans: experimental_reuse_query_plans.unwrap_or_default(),
982 experimental_cooperative_cancellation: experimental_cooperative_cancellation
983 .unwrap_or_default(),
984 }
985 }
986}
987
988#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
990#[serde(deny_unknown_fields, default)]
991pub(crate) struct QueryPlanCache {
992 pub(crate) in_memory: InMemoryCache,
994 pub(crate) redis: Option<QueryPlanRedisCache>,
996}
997
998#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
999#[serde(deny_unknown_fields)]
1000pub(crate) struct QueryPlanRedisCache {
1002 pub(crate) urls: Vec<url::Url>,
1004
1005 pub(crate) username: Option<String>,
1007 pub(crate) password: Option<String>,
1009
1010 #[serde(
1011 deserialize_with = "humantime_serde::deserialize",
1012 default = "default_timeout"
1013 )]
1014 #[schemars(with = "Option<String>", default)]
1015 pub(crate) timeout: Duration,
1017
1018 #[serde(
1019 deserialize_with = "humantime_serde::deserialize",
1020 default = "default_query_plan_cache_ttl"
1021 )]
1022 #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
1023 pub(crate) ttl: Duration,
1025
1026 pub(crate) namespace: Option<String>,
1028
1029 #[serde(default)]
1030 pub(crate) tls: Option<TlsClient>,
1032
1033 #[serde(default = "default_required_to_start")]
1034 pub(crate) required_to_start: bool,
1036
1037 #[serde(default = "default_reset_ttl")]
1038 pub(crate) reset_ttl: bool,
1040
1041 #[serde(default = "default_query_planner_cache_pool_size")]
1042 pub(crate) pool_size: u32,
1044}
1045
1046fn default_query_plan_cache_ttl() -> Duration {
1047 Duration::from_secs(86400 * 30)
1049}
1050
1051fn default_query_planner_cache_pool_size() -> u32 {
1052 1
1053}
1054
1055#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1057#[serde(deny_unknown_fields, default)]
1058pub(crate) struct Cache {
1059 pub(crate) in_memory: InMemoryCache,
1061 pub(crate) redis: Option<RedisCache>,
1063}
1064
1065impl From<QueryPlanCache> for Cache {
1066 fn from(value: QueryPlanCache) -> Self {
1067 Cache {
1068 in_memory: value.in_memory,
1069 redis: value.redis.map(Into::into),
1070 }
1071 }
1072}
1073
1074#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1075#[serde(deny_unknown_fields)]
1076pub(crate) struct InMemoryCache {
1078 pub(crate) limit: NonZeroUsize,
1080}
1081
1082impl Default for InMemoryCache {
1083 fn default() -> Self {
1084 Self {
1085 limit: DEFAULT_CACHE_CAPACITY,
1086 }
1087 }
1088}
1089
1090#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1091#[serde(deny_unknown_fields)]
1092pub(crate) struct RedisCache {
1094 pub(crate) urls: Vec<url::Url>,
1096
1097 pub(crate) username: Option<String>,
1099 pub(crate) password: Option<String>,
1101
1102 #[serde(
1103 deserialize_with = "humantime_serde::deserialize",
1104 default = "default_timeout"
1105 )]
1106 #[schemars(with = "Option<String>", default)]
1107 pub(crate) timeout: Duration,
1109
1110 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1111 #[schemars(with = "Option<String>", default)]
1112 pub(crate) ttl: Option<Duration>,
1114
1115 pub(crate) namespace: Option<String>,
1117
1118 #[serde(default)]
1119 pub(crate) tls: Option<TlsClient>,
1121
1122 #[serde(default = "default_required_to_start")]
1123 pub(crate) required_to_start: bool,
1125
1126 #[serde(default = "default_reset_ttl")]
1127 pub(crate) reset_ttl: bool,
1129
1130 #[serde(default = "default_pool_size")]
1131 pub(crate) pool_size: u32,
1133 #[serde(
1134 deserialize_with = "humantime_serde::deserialize",
1135 default = "default_metrics_interval"
1136 )]
1137 #[schemars(with = "Option<String>", default)]
1138 pub(crate) metrics_interval: Duration,
1140}
1141
1142fn default_timeout() -> Duration {
1143 Duration::from_millis(500)
1144}
1145
1146pub(crate) fn default_required_to_start() -> bool {
1147 false
1148}
1149
1150pub(crate) fn default_pool_size() -> u32 {
1151 1
1152}
1153
1154pub(crate) fn default_metrics_interval() -> Duration {
1155 Duration::from_secs(1)
1156}
1157
1158impl From<QueryPlanRedisCache> for RedisCache {
1159 fn from(value: QueryPlanRedisCache) -> Self {
1160 RedisCache {
1161 urls: value.urls,
1162 username: value.username,
1163 password: value.password,
1164 timeout: value.timeout,
1165 ttl: Some(value.ttl),
1166 namespace: value.namespace,
1167 tls: value.tls,
1168 required_to_start: value.required_to_start,
1169 reset_ttl: value.reset_ttl,
1170 pool_size: value.pool_size,
1171 metrics_interval: default_metrics_interval(),
1172 }
1173 }
1174}
1175
1176fn default_reset_ttl() -> bool {
1177 true
1178}
1179
1180#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1182#[serde(deny_unknown_fields)]
1183#[serde(default)]
1184pub(crate) struct Tls {
1185 pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1189 pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1191 pub(crate) connector: ConnectorConfiguration<TlsClient>,
1193}
1194
1195#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1197#[serde(deny_unknown_fields)]
1198pub(crate) struct TlsSupergraph {
1199 #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1201 #[schemars(with = "String")]
1202 pub(crate) certificate: CertificateDer<'static>,
1203 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1205 #[schemars(with = "String")]
1206 pub(crate) key: PrivateKeyDer<'static>,
1207 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1209 #[schemars(with = "String")]
1210 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1211}
1212
1213impl TlsSupergraph {
1214 pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1215 let mut certificates = vec![self.certificate.clone()];
1216 certificates.extend(self.certificate_chain.iter().cloned());
1217
1218 let mut config = ServerConfig::builder()
1219 .with_no_client_auth()
1220 .with_single_cert(certificates, self.key.clone_key())
1221 .map_err(ApolloRouterError::Rustls)?;
1222 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1223
1224 Ok(Arc::new(config))
1225 }
1226}
1227
1228fn deserialize_certificate<'de, D>(deserializer: D) -> Result<CertificateDer<'static>, D::Error>
1229where
1230 D: Deserializer<'de>,
1231{
1232 let data = String::deserialize(deserializer)?;
1233
1234 load_certs(&data)
1235 .map_err(serde::de::Error::custom)
1236 .and_then(|mut certs| {
1237 if certs.len() > 1 {
1238 Err(serde::de::Error::custom("expected exactly one certificate"))
1239 } else {
1240 certs
1241 .pop()
1242 .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1243 }
1244 })
1245}
1246
1247fn deserialize_certificate_chain<'de, D>(
1248 deserializer: D,
1249) -> Result<Vec<CertificateDer<'static>>, D::Error>
1250where
1251 D: Deserializer<'de>,
1252{
1253 let data = String::deserialize(deserializer)?;
1254
1255 load_certs(&data).map_err(serde::de::Error::custom)
1256}
1257
1258fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKeyDer<'static>, D::Error>
1259where
1260 D: Deserializer<'de>,
1261{
1262 let data = String::deserialize(deserializer)?;
1263
1264 load_key(&data).map_err(serde::de::Error::custom)
1265}
1266
1267#[derive(thiserror::Error, Debug)]
1268#[error("could not load TLS certificate: {0}")]
1269struct LoadCertError(std::io::Error);
1270
1271pub(crate) fn load_certs(data: &str) -> io::Result<Vec<CertificateDer<'static>>> {
1272 rustls_pemfile::certs(&mut BufReader::new(data.as_bytes()))
1273 .collect::<Result<Vec<_>, _>>()
1274 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, LoadCertError(error)))
1275}
1276
1277pub(crate) fn load_key(data: &str) -> io::Result<PrivateKeyDer<'static>> {
1278 let mut reader = BufReader::new(data.as_bytes());
1279 let mut key_iterator = iter::from_fn(|| rustls_pemfile::read_one(&mut reader).transpose());
1280
1281 let private_key = match key_iterator.next() {
1282 Some(Ok(rustls_pemfile::Item::Pkcs1Key(key))) => PrivateKeyDer::from(key),
1283 Some(Ok(rustls_pemfile::Item::Pkcs8Key(key))) => PrivateKeyDer::from(key),
1284 Some(Ok(rustls_pemfile::Item::Sec1Key(key))) => PrivateKeyDer::from(key),
1285 Some(Err(e)) => {
1286 return Err(io::Error::new(
1287 io::ErrorKind::InvalidInput,
1288 format!("could not parse the key: {e}"),
1289 ));
1290 }
1291 Some(_) => {
1292 return Err(io::Error::new(
1293 io::ErrorKind::InvalidInput,
1294 "expected a private key",
1295 ));
1296 }
1297 None => {
1298 return Err(io::Error::new(
1299 io::ErrorKind::InvalidInput,
1300 "could not find a private key",
1301 ));
1302 }
1303 };
1304
1305 if key_iterator.next().is_some() {
1306 return Err(io::Error::new(
1307 io::ErrorKind::InvalidInput,
1308 "expected exactly one private key",
1309 ));
1310 }
1311 Ok(private_key)
1312}
1313
1314#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq)]
1316#[serde(deny_unknown_fields)]
1317#[serde(default)]
1318pub(crate) struct TlsClient {
1319 pub(crate) certificate_authorities: Option<String>,
1321 pub(crate) client_authentication: Option<Arc<TlsClientAuth>>,
1323}
1324
1325#[buildstructor::buildstructor]
1326impl TlsClient {
1327 #[builder]
1328 pub(crate) fn new(
1329 certificate_authorities: Option<String>,
1330 client_authentication: Option<Arc<TlsClientAuth>>,
1331 ) -> Self {
1332 Self {
1333 certificate_authorities,
1334 client_authentication,
1335 }
1336 }
1337}
1338
1339impl Default for TlsClient {
1340 fn default() -> Self {
1341 Self::builder().build()
1342 }
1343}
1344
1345#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1347#[serde(deny_unknown_fields)]
1348pub(crate) struct TlsClientAuth {
1349 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1351 #[schemars(with = "String")]
1352 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1353 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1355 #[schemars(with = "String")]
1356 pub(crate) key: PrivateKeyDer<'static>,
1357}
1358
1359#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1361#[serde(deny_unknown_fields)]
1362#[serde(default)]
1363pub(crate) struct Sandbox {
1364 pub(crate) enabled: bool,
1366}
1367
1368fn default_sandbox() -> bool {
1369 false
1370}
1371
1372#[buildstructor::buildstructor]
1373impl Sandbox {
1374 #[builder]
1375 pub(crate) fn new(enabled: Option<bool>) -> Self {
1376 Self {
1377 enabled: enabled.unwrap_or_else(default_sandbox),
1378 }
1379 }
1380}
1381
1382#[cfg(test)]
1383#[buildstructor::buildstructor]
1384impl Sandbox {
1385 #[builder]
1386 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1387 Self {
1388 enabled: enabled.unwrap_or_else(default_sandbox),
1389 }
1390 }
1391}
1392
1393impl Default for Sandbox {
1394 fn default() -> Self {
1395 Self::builder().build()
1396 }
1397}
1398
1399#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1401#[serde(deny_unknown_fields)]
1402#[serde(default)]
1403pub(crate) struct Homepage {
1404 pub(crate) enabled: bool,
1406 pub(crate) graph_ref: Option<String>,
1409}
1410
1411fn default_homepage() -> bool {
1412 true
1413}
1414
1415#[buildstructor::buildstructor]
1416impl Homepage {
1417 #[builder]
1418 pub(crate) fn new(enabled: Option<bool>) -> Self {
1419 Self {
1420 enabled: enabled.unwrap_or_else(default_homepage),
1421 graph_ref: None,
1422 }
1423 }
1424}
1425
1426#[cfg(test)]
1427#[buildstructor::buildstructor]
1428impl Homepage {
1429 #[builder]
1430 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1431 Self {
1432 enabled: enabled.unwrap_or_else(default_homepage),
1433 graph_ref: None,
1434 }
1435 }
1436}
1437
1438impl Default for Homepage {
1439 fn default() -> Self {
1440 Self::builder().enabled(default_homepage()).build()
1441 }
1442}
1443
1444#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1446#[serde(untagged)]
1447pub enum ListenAddr {
1448 SocketAddr(SocketAddr),
1450 #[cfg(unix)]
1452 UnixSocket(std::path::PathBuf),
1453}
1454
1455impl ListenAddr {
1456 pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1457 #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1458 if let Self::SocketAddr(addr) = self {
1459 Some((addr.ip(), addr.port()))
1460 } else {
1461 None
1462 }
1463 }
1464}
1465
1466impl From<SocketAddr> for ListenAddr {
1467 fn from(addr: SocketAddr) -> Self {
1468 Self::SocketAddr(addr)
1469 }
1470}
1471
1472#[allow(clippy::from_over_into)]
1473impl Into<serde_json::Value> for ListenAddr {
1474 fn into(self) -> serde_json::Value {
1475 match self {
1476 Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1479 #[cfg(unix)]
1480 Self::UnixSocket(path) => serde_json::Value::String(
1481 path.as_os_str()
1482 .to_str()
1483 .expect("unsupported non-UTF-8 path")
1484 .to_string(),
1485 ),
1486 }
1487 }
1488}
1489
1490#[cfg(unix)]
1491impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1492 for ListenAddr
1493{
1494 fn from(
1495 addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1496 ) -> Self {
1497 match addr {
1498 tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1499 tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1500 addr.as_pathname()
1501 .map(ToOwned::to_owned)
1502 .unwrap_or_default(),
1503 ),
1504 }
1505 }
1506}
1507
1508impl fmt::Display for ListenAddr {
1509 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1510 match self {
1511 Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1512 #[cfg(unix)]
1513 Self::UnixSocket(path) => write!(f, "{}", path.display()),
1514 }
1515 }
1516}
1517
1518fn default_graphql_path() -> String {
1519 String::from("/")
1520}
1521
1522fn default_graphql_introspection() -> bool {
1523 false
1524}
1525
1526fn default_connection_shutdown_timeout() -> Duration {
1527 Duration::from_secs(60)
1528}
1529
1530fn default_strict_variable_validation() -> Mode {
1531 Mode::Enforce
1532}
1533
1534#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1535#[serde(deny_unknown_fields, rename_all = "snake_case")]
1536pub(crate) enum BatchingMode {
1537 #[default]
1539 BatchHttpLink,
1540}
1541
1542#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1544#[serde(deny_unknown_fields)]
1545pub(crate) struct Batching {
1546 #[serde(default)]
1548 pub(crate) enabled: bool,
1549
1550 pub(crate) mode: BatchingMode,
1552
1553 pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1555
1556 #[serde(default)]
1558 pub(crate) maximum_size: Option<usize>,
1559}
1560
1561#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1563pub(crate) struct CommonBatchingConfig {
1564 pub(crate) enabled: bool,
1566}
1567
1568impl Batching {
1569 pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1571 match &self.subgraph {
1572 Some(subgraph_batching_config) => {
1573 if subgraph_batching_config.all.enabled {
1575 subgraph_batching_config
1579 .subgraphs
1580 .get(service_name)
1581 .is_none_or(|x| x.enabled)
1582 } else {
1583 subgraph_batching_config
1586 .subgraphs
1587 .get(service_name)
1588 .is_some_and(|x| x.enabled)
1589 }
1590 }
1591 None => false,
1592 }
1593 }
1594
1595 pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1596 match self.maximum_size {
1597 Some(maximum_size) => batch.len() > maximum_size,
1598 None => false,
1599 }
1600 }
1601}