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