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 #[serde(default)]
232 pub(crate) experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
233}
234
235impl PartialEq for Configuration {
236 fn eq(&self, other: &Self) -> bool {
237 self.validated_yaml == other.validated_yaml
238 }
239}
240
241impl<'de> serde::Deserialize<'de> for Configuration {
242 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
243 where
244 D: serde::Deserializer<'de>,
245 {
246 #[derive(Deserialize, Default)]
249 #[serde(default)]
250 struct AdHocConfiguration {
251 health_check: HealthCheck,
252 sandbox: Sandbox,
253 homepage: Homepage,
254 server: Server,
255 supergraph: Supergraph,
256 cors: Cors,
257 plugins: UserPlugins,
258 #[serde(flatten)]
259 apollo_plugins: ApolloPlugins,
260 tls: Tls,
261 apq: Apq,
262 persisted_queries: PersistedQueries,
263 limits: limits::Config,
264 experimental_chaos: chaos::Config,
265 batching: Batching,
266 experimental_type_conditioned_fetching: bool,
267 experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
268 }
269 let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
270
271 let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
272 .map_err(|e| serde::de::Error::custom(e.to_string()))?;
273
274 ad_hoc.apollo_plugins.plugins.insert(
277 "limits".to_string(),
278 serde_json::to_value(&ad_hoc.limits).unwrap(),
279 );
280 ad_hoc.apollo_plugins.plugins.insert(
281 "health_check".to_string(),
282 serde_json::to_value(&ad_hoc.health_check).unwrap(),
283 );
284
285 Configuration {
287 health_check: ad_hoc.health_check,
288 sandbox: ad_hoc.sandbox,
289 homepage: ad_hoc.homepage,
290 server: ad_hoc.server,
291 supergraph: ad_hoc.supergraph,
292 cors: ad_hoc.cors,
293 tls: ad_hoc.tls,
294 apq: ad_hoc.apq,
295 persisted_queries: ad_hoc.persisted_queries,
296 limits: ad_hoc.limits,
297 experimental_chaos: ad_hoc.experimental_chaos,
298 experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
299 experimental_hoist_orphan_errors: ad_hoc.experimental_hoist_orphan_errors,
300 plugins: ad_hoc.plugins,
301 apollo_plugins: ad_hoc.apollo_plugins,
302 batching: ad_hoc.batching,
303
304 notify,
306 uplink: None,
307 validated_yaml: None,
308 raw_yaml: None,
309 }
310 .validate()
311 .map_err(|e| serde::de::Error::custom(e.to_string()))
312 }
313}
314
315pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
316
317fn default_graphql_listen() -> ListenAddr {
318 SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
319}
320
321#[cfg(test)]
322#[buildstructor::buildstructor]
323impl Configuration {
324 #[builder]
325 pub(crate) fn new(
326 supergraph: Option<Supergraph>,
327 health_check: Option<HealthCheck>,
328 sandbox: Option<Sandbox>,
329 homepage: Option<Homepage>,
330 cors: Option<Cors>,
331 plugins: Map<String, Value>,
332 apollo_plugins: Map<String, Value>,
333 tls: Option<Tls>,
334 apq: Option<Apq>,
335 persisted_query: Option<PersistedQueries>,
336 operation_limits: Option<limits::Config>,
337 chaos: Option<chaos::Config>,
338 uplink: Option<UplinkConfig>,
339 experimental_type_conditioned_fetching: Option<bool>,
340 experimental_hoist_orphan_errors: Option<SubgraphConfiguration<HoistOrphanErrors>>,
341 batching: Option<Batching>,
342 server: Option<Server>,
343 ) -> Result<Self, ConfigurationError> {
344 let notify = Self::notify(&apollo_plugins)?;
345
346 let conf = Self {
347 validated_yaml: Default::default(),
348 raw_yaml: None,
349 supergraph: supergraph.unwrap_or_default(),
350 server: server.unwrap_or_default(),
351 health_check: health_check.unwrap_or_default(),
352 sandbox: sandbox.unwrap_or_default(),
353 homepage: homepage.unwrap_or_default(),
354 cors: cors.unwrap_or_default(),
355 apq: apq.unwrap_or_default(),
356 persisted_queries: persisted_query.unwrap_or_default(),
357 limits: operation_limits.unwrap_or_default(),
358 experimental_chaos: chaos.unwrap_or_default(),
359 plugins: UserPlugins {
360 plugins: Some(plugins),
361 },
362 apollo_plugins: ApolloPlugins {
363 plugins: apollo_plugins,
364 },
365 tls: tls.unwrap_or_default(),
366 uplink,
367 batching: batching.unwrap_or_default(),
368 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
369 .unwrap_or_default(),
370 experimental_hoist_orphan_errors: experimental_hoist_orphan_errors.unwrap_or_default(),
371 notify,
372 };
373
374 conf.validate()
375 }
376}
377
378impl Configuration {
379 pub(crate) fn hash(&self) -> String {
380 let mut hasher = sha2::Sha256::new();
381 let defaulted_raw = self
382 .validated_yaml
383 .as_ref()
384 .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
385 .unwrap_or_default();
386 hasher.update(defaulted_raw);
387 let hash: String = format!("{:x}", hasher.finalize());
388 hash
389 }
390
391 fn notify(
392 apollo_plugins: &Map<String, Value>,
393 ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
394 if cfg!(test) {
395 return Ok(Notify::for_tests());
396 }
397 let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
398 Some(plugin_conf) => {
399 let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
400 .map_err(|err| ConfigurationError::PluginConfiguration {
401 plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
402 error: format!("{err:?}"),
403 })?;
404 conf.queue_capacity
405 }
406 None => None,
407 };
408 Ok(Notify::builder()
409 .and_queue_size(notify_queue_cap)
410 .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
411 .heartbeat_error_message(
412 graphql::Response::builder()
413 .errors(vec![
414 graphql::Error::builder()
415 .message("the connection has been closed because it hasn't heartbeat for a while")
416 .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
417 .build()
418 ])
419 .build()
420 ).build())
421 }
422
423 pub(crate) fn rust_query_planner_config(
424 &self,
425 ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
426 use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
427 use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
428 use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
429
430 let max_evaluated_plans = self
431 .supergraph
432 .query_planning
433 .experimental_plans_limit
434 .and_then(NonZeroU32::new)
436 .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
437
438 QueryPlannerConfig {
439 subgraph_graphql_validation: false,
440 generate_query_fragments: self.supergraph.generate_query_fragments,
441 incremental_delivery: QueryPlanIncrementalDeliveryConfig {
442 enable_defer: self.supergraph.defer_support,
443 },
444 type_conditioned_fetching: self.experimental_type_conditioned_fetching,
445 debug: QueryPlannerDebugConfig {
446 max_evaluated_plans,
447 paths_limit: self.supergraph.query_planning.experimental_paths_limit,
448 },
449 }
450 }
451
452 fn apollo_plugin_enabled(&self, plugin_name: &str) -> bool {
453 self.apollo_plugins
454 .plugins
455 .get(plugin_name)
456 .and_then(|config| config.as_object().and_then(|c| c.get("enabled")))
457 .and_then(|enabled| enabled.as_bool())
458 .unwrap_or(false)
459 }
460}
461
462impl Default for Configuration {
463 fn default() -> Self {
464 Configuration::from_str("").expect("default configuration must be valid")
466 }
467}
468
469#[cfg(test)]
470#[buildstructor::buildstructor]
471impl Configuration {
472 #[builder]
473 pub(crate) fn fake_new(
474 supergraph: Option<Supergraph>,
475 health_check: Option<HealthCheck>,
476 sandbox: Option<Sandbox>,
477 homepage: Option<Homepage>,
478 cors: Option<Cors>,
479 plugins: Map<String, Value>,
480 apollo_plugins: Map<String, Value>,
481 tls: Option<Tls>,
482 notify: Option<Notify<String, graphql::Response>>,
483 apq: Option<Apq>,
484 persisted_query: Option<PersistedQueries>,
485 operation_limits: Option<limits::Config>,
486 chaos: Option<chaos::Config>,
487 uplink: Option<UplinkConfig>,
488 batching: Option<Batching>,
489 experimental_type_conditioned_fetching: Option<bool>,
490 server: Option<Server>,
491 ) -> Result<Self, ConfigurationError> {
492 let configuration = Self {
493 validated_yaml: Default::default(),
494 server: server.unwrap_or_default(),
495 supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
496 health_check: health_check.unwrap_or_else(|| HealthCheck::builder().build()),
497 sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
498 homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
499 cors: cors.unwrap_or_default(),
500 limits: operation_limits.unwrap_or_default(),
501 experimental_chaos: chaos.unwrap_or_default(),
502 plugins: UserPlugins {
503 plugins: Some(plugins),
504 },
505 apollo_plugins: ApolloPlugins {
506 plugins: apollo_plugins,
507 },
508 tls: tls.unwrap_or_default(),
509 notify: notify.unwrap_or_default(),
510 apq: apq.unwrap_or_default(),
511 persisted_queries: persisted_query.unwrap_or_default(),
512 uplink,
513 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
514 .unwrap_or_default(),
515 experimental_hoist_orphan_errors: Default::default(),
516 batching: batching.unwrap_or_default(),
517 raw_yaml: None,
518 };
519
520 configuration.validate()
521 }
522}
523
524impl Configuration {
525 pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
526 if self.sandbox.enabled && self.homepage.enabled {
528 return Err(ConfigurationError::InvalidConfiguration {
529 message: "sandbox and homepage cannot be enabled at the same time",
530 error: "disable the homepage if you want to enable sandbox".to_string(),
531 });
532 }
533 if self.sandbox.enabled && !self.supergraph.introspection {
535 return Err(ConfigurationError::InvalidConfiguration {
536 message: "sandbox requires introspection",
537 error: "sandbox needs introspection to be enabled".to_string(),
538 });
539 }
540 if !self.supergraph.path.starts_with('/') {
541 return Err(ConfigurationError::InvalidConfiguration {
542 message: "invalid 'server.graphql_path' configuration",
543 error: format!(
544 "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
545 self.supergraph.path, self.supergraph.path
546 ),
547 });
548 }
549 if self.supergraph.path.ends_with('*')
550 && !self.supergraph.path.ends_with("/*")
551 && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
552 {
553 return Err(ConfigurationError::InvalidConfiguration {
554 message: "invalid 'server.graphql_path' configuration",
555 error: format!(
556 "'{}' is invalid, you can only set a wildcard after a '/'",
557 self.supergraph.path
558 ),
559 });
560 }
561 if self.supergraph.path.contains("/*/") {
562 return Err(ConfigurationError::InvalidConfiguration {
563 message: "invalid 'server.graphql_path' configuration",
564 error: format!(
565 "'{}' 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'",
566 self.supergraph.path
567 ),
568 });
569 }
570
571 if self.persisted_queries.enabled {
573 if self.persisted_queries.safelist.enabled && self.apq.enabled {
574 return Err(ConfigurationError::InvalidConfiguration {
575 message: "apqs must be disabled to enable safelisting",
576 error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
577 });
578 } else if !self.persisted_queries.safelist.enabled
579 && self.persisted_queries.safelist.require_id
580 {
581 return Err(ConfigurationError::InvalidConfiguration {
582 message: "safelist must be enabled to require IDs",
583 error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
584 });
585 }
586 } else {
587 if self.persisted_queries.safelist.enabled {
589 return Err(ConfigurationError::InvalidConfiguration {
590 message: "persisted queries must be enabled to enable safelisting",
591 error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
592 });
593 } else if self.persisted_queries.log_unknown {
594 return Err(ConfigurationError::InvalidConfiguration {
595 message: "persisted queries must be enabled to enable logging unknown operations",
596 error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
597 });
598 }
599 }
600
601 if self.apollo_plugin_enabled("response_cache")
603 && self.apollo_plugin_enabled("preview_entity_cache")
604 {
605 return Err(ConfigurationError::InvalidConfiguration {
606 message: "entity cache and response cache features are mutually exclusive",
607 error: "either set response_cache.enabled: false or preview_entity_cache.enabled: false in your router yaml configuration".into(),
608 });
609 }
610
611 Ok(self)
612 }
613}
614
615impl FromStr for Configuration {
617 type Err = ConfigurationError;
618
619 fn from_str(s: &str) -> Result<Self, Self::Err> {
620 schema::validate_yaml_configuration(s, Expansion::default()?, schema::Mode::Upgrade)?
621 .validate()
622 }
623}
624
625fn gen_schema(
626 plugins: BTreeMap<String, Schema>,
627 hidden_plugins: Option<BTreeMap<String, Schema>>,
628) -> Schema {
629 schemars::json_schema!({
630 "type": "object",
631 "properties": plugins,
632 "additionalProperties": false,
633 "patternProperties": hidden_plugins
634 .unwrap_or_default()
635 .into_iter()
636 .map(|(k, v)| (format!("^{}$", k), v))
638 .collect::<BTreeMap<_, _>>()
639 })
640}
641
642#[derive(Clone, Debug, Default, Deserialize, Serialize)]
648#[serde(transparent)]
649pub(crate) struct ApolloPlugins {
650 pub(crate) plugins: Map<String, Value>,
651}
652
653impl JsonSchema for ApolloPlugins {
654 fn schema_name() -> std::borrow::Cow<'static, str> {
655 stringify!(Plugins).into()
656 }
657
658 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
659 let (plugin_entries, hidden_plugin_entries): (Vec<_>, Vec<_>) = crate::plugin::plugins()
663 .sorted_by_key(|factory| factory.name.clone())
664 .filter(|factory| factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
665 .partition_map(|factory| {
666 let key = factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string();
667 let schema = factory.create_schema(generator);
668 if factory.hidden_from_config_json_schema {
670 Either::Right((key, schema))
671 } else {
672 Either::Left((key, schema))
673 }
674 });
675 gen_schema(
676 plugin_entries.into_iter().collect(),
677 Some(hidden_plugin_entries.into_iter().collect()),
678 )
679 }
680}
681
682#[derive(Clone, Debug, Default, Deserialize, Serialize)]
687#[serde(transparent)]
688pub(crate) struct UserPlugins {
689 pub(crate) plugins: Option<Map<String, Value>>,
690}
691
692impl JsonSchema for UserPlugins {
693 fn schema_name() -> std::borrow::Cow<'static, str> {
694 stringify!(Plugins).into()
695 }
696
697 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
698 let plugins = crate::plugin::plugins()
702 .sorted_by_key(|factory| factory.name.clone())
703 .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
704 .map(|factory| (factory.name.to_string(), factory.create_schema(generator)))
705 .collect();
706 gen_schema(plugins, None)
707 }
708}
709
710#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
712#[serde(deny_unknown_fields)]
713#[serde(default)]
714pub(crate) struct Supergraph {
715 pub(crate) listen: ListenAddr,
718
719 #[serde(deserialize_with = "humantime_serde::deserialize")]
721 #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
722 pub(crate) connection_shutdown_timeout: Duration,
723
724 pub(crate) path: String,
727
728 pub(crate) introspection: bool,
731
732 pub(crate) redact_query_validation_errors: bool,
736
737 pub(crate) generate_query_fragments: bool,
740
741 pub(crate) defer_support: bool,
743
744 pub(crate) query_planning: QueryPlanning,
746
747 pub(crate) early_cancel: bool,
752
753 pub(crate) enable_result_coercion_errors: bool,
760
761 pub(crate) experimental_log_on_broken_pipe: bool,
764
765 pub(crate) strict_variable_validation: Mode,
769}
770
771const fn default_generate_query_fragments() -> bool {
772 true
773}
774
775fn default_defer_support() -> bool {
776 true
777}
778
779#[buildstructor::buildstructor]
780impl Supergraph {
781 #[builder]
782 pub(crate) fn new(
783 listen: Option<ListenAddr>,
784 path: Option<String>,
785 connection_shutdown_timeout: Option<Duration>,
786 introspection: Option<bool>,
787 defer_support: Option<bool>,
788 query_planning: Option<QueryPlanning>,
789 generate_query_fragments: Option<bool>,
790 early_cancel: Option<bool>,
791 experimental_log_on_broken_pipe: Option<bool>,
792 insert_result_coercion_errors: Option<bool>,
793 strict_variable_validation: Option<Mode>,
794 redact_query_validation_errors: Option<bool>,
795 ) -> Self {
796 Self {
797 listen: listen.unwrap_or_else(default_graphql_listen),
798 path: path.unwrap_or_else(default_graphql_path),
799 connection_shutdown_timeout: connection_shutdown_timeout
800 .unwrap_or_else(default_connection_shutdown_timeout),
801 introspection: introspection.unwrap_or_else(default_graphql_introspection),
802 defer_support: defer_support.unwrap_or_else(default_defer_support),
803 query_planning: query_planning.unwrap_or_default(),
804 generate_query_fragments: generate_query_fragments
805 .unwrap_or_else(default_generate_query_fragments),
806 early_cancel: early_cancel.unwrap_or_default(),
807 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
808 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
809 strict_variable_validation: strict_variable_validation
810 .unwrap_or_else(default_strict_variable_validation),
811 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
812 }
813 }
814}
815
816#[cfg(test)]
817#[buildstructor::buildstructor]
818impl Supergraph {
819 #[builder]
820 pub(crate) fn fake_new(
821 listen: Option<ListenAddr>,
822 path: Option<String>,
823 connection_shutdown_timeout: Option<Duration>,
824 introspection: Option<bool>,
825 defer_support: Option<bool>,
826 query_planning: Option<QueryPlanning>,
827 generate_query_fragments: Option<bool>,
828 early_cancel: Option<bool>,
829 experimental_log_on_broken_pipe: Option<bool>,
830 insert_result_coercion_errors: Option<bool>,
831 strict_variable_validation: Option<Mode>,
832 redact_query_validation_errors: Option<bool>,
833 ) -> Self {
834 Self {
835 listen: listen.unwrap_or_else(test_listen),
836 path: path.unwrap_or_else(default_graphql_path),
837 connection_shutdown_timeout: connection_shutdown_timeout
838 .unwrap_or_else(default_connection_shutdown_timeout),
839 introspection: introspection.unwrap_or_else(default_graphql_introspection),
840 defer_support: defer_support.unwrap_or_else(default_defer_support),
841 query_planning: query_planning.unwrap_or_default(),
842 generate_query_fragments: generate_query_fragments
843 .unwrap_or_else(default_generate_query_fragments),
844 early_cancel: early_cancel.unwrap_or_default(),
845 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
846 enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
847 strict_variable_validation: strict_variable_validation
848 .unwrap_or_else(default_strict_variable_validation),
849 redact_query_validation_errors: redact_query_validation_errors.unwrap_or_default(),
850 }
851 }
852}
853
854impl Default for Supergraph {
855 fn default() -> Self {
856 Self::builder().build()
857 }
858}
859
860impl Supergraph {
861 pub(crate) fn sanitized_path(&self) -> String {
863 let mut path = self.path.clone();
864 if self.path.ends_with("/*") {
865 path = format!("{}router_extra_path", self.path);
867 } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
868 let new_path = SUPERGRAPH_ENDPOINT_REGEX
869 .replace(&self.path, "${first_path}${sub_path}{supergraph_route}");
870 path = new_path.to_string();
871 }
872
873 path
874 }
875}
876
877#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
879#[serde(deny_unknown_fields)]
880pub(crate) struct Router {
881 #[serde(default)]
882 pub(crate) cache: Cache,
883}
884
885#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
887#[serde(deny_unknown_fields, default)]
888pub(crate) struct Apq {
889 pub(crate) enabled: bool,
891
892 pub(crate) router: Router,
893
894 pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
895}
896
897#[cfg(test)]
898#[buildstructor::buildstructor]
899impl Apq {
900 #[builder]
901 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
902 Self {
903 enabled: enabled.unwrap_or_else(default_apq),
904 ..Default::default()
905 }
906 }
907}
908
909#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
911#[serde(deny_unknown_fields, default)]
912pub(crate) struct SubgraphApq {
913 pub(crate) enabled: bool,
915}
916
917fn default_apq() -> bool {
918 true
919}
920
921impl Default for Apq {
922 fn default() -> Self {
923 Self {
924 enabled: default_apq(),
925 router: Default::default(),
926 subgraph: Default::default(),
927 }
928 }
929}
930
931#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
933#[serde(deny_unknown_fields, default)]
934pub(crate) struct QueryPlanning {
935 pub(crate) cache: QueryPlanCache,
937 pub(crate) warmed_up_queries: Option<usize>,
942
943 pub(crate) experimental_plans_limit: Option<u32>,
951
952 pub(crate) experimental_paths_limit: Option<u32>,
965
966 pub(crate) experimental_reuse_query_plans: bool,
969
970 pub(crate) experimental_cooperative_cancellation: CooperativeCancellation,
974}
975
976#[buildstructor::buildstructor]
977impl QueryPlanning {
978 #[builder]
979 #[allow(dead_code)]
980 pub(crate) fn new(
981 cache: Option<QueryPlanCache>,
982 warmed_up_queries: Option<usize>,
983 experimental_plans_limit: Option<u32>,
984 experimental_paths_limit: Option<u32>,
985 experimental_reuse_query_plans: Option<bool>,
986 experimental_cooperative_cancellation: Option<CooperativeCancellation>,
987 ) -> Self {
988 Self {
989 cache: cache.unwrap_or_default(),
990 warmed_up_queries,
991 experimental_plans_limit,
992 experimental_paths_limit,
993 experimental_reuse_query_plans: experimental_reuse_query_plans.unwrap_or_default(),
994 experimental_cooperative_cancellation: experimental_cooperative_cancellation
995 .unwrap_or_default(),
996 }
997 }
998}
999
1000#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1002#[serde(deny_unknown_fields, default)]
1003pub(crate) struct QueryPlanCache {
1004 pub(crate) in_memory: InMemoryCache,
1006 pub(crate) redis: Option<QueryPlanRedisCache>,
1008}
1009
1010#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1011#[serde(deny_unknown_fields)]
1012pub(crate) struct QueryPlanRedisCache {
1014 pub(crate) urls: Vec<url::Url>,
1016
1017 pub(crate) username: Option<String>,
1019 pub(crate) password: Option<String>,
1021
1022 #[serde(
1023 deserialize_with = "humantime_serde::deserialize",
1024 default = "default_timeout"
1025 )]
1026 #[schemars(with = "Option<String>", default)]
1027 pub(crate) timeout: Duration,
1029
1030 #[serde(
1031 deserialize_with = "humantime_serde::deserialize",
1032 default = "default_query_plan_cache_ttl"
1033 )]
1034 #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
1035 pub(crate) ttl: Duration,
1037
1038 pub(crate) namespace: Option<String>,
1040
1041 #[serde(default)]
1042 pub(crate) tls: Option<TlsClient>,
1044
1045 #[serde(default = "default_required_to_start")]
1046 pub(crate) required_to_start: bool,
1048
1049 #[serde(default = "default_reset_ttl")]
1050 pub(crate) reset_ttl: bool,
1052
1053 #[serde(default = "default_query_planner_cache_pool_size")]
1054 pub(crate) pool_size: u32,
1056}
1057
1058fn default_query_plan_cache_ttl() -> Duration {
1059 Duration::from_secs(86400 * 30)
1061}
1062
1063fn default_query_planner_cache_pool_size() -> u32 {
1064 1
1065}
1066
1067#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1069#[serde(deny_unknown_fields, default)]
1070pub(crate) struct Cache {
1071 pub(crate) in_memory: InMemoryCache,
1073 pub(crate) redis: Option<RedisCache>,
1075}
1076
1077impl From<QueryPlanCache> for Cache {
1078 fn from(value: QueryPlanCache) -> Self {
1079 Cache {
1080 in_memory: value.in_memory,
1081 redis: value.redis.map(Into::into),
1082 }
1083 }
1084}
1085
1086#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1087#[serde(deny_unknown_fields)]
1088pub(crate) struct InMemoryCache {
1090 pub(crate) limit: NonZeroUsize,
1092}
1093
1094impl Default for InMemoryCache {
1095 fn default() -> Self {
1096 Self {
1097 limit: DEFAULT_CACHE_CAPACITY,
1098 }
1099 }
1100}
1101
1102#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1103#[serde(deny_unknown_fields)]
1104pub(crate) struct RedisCache {
1106 pub(crate) urls: Vec<url::Url>,
1108
1109 pub(crate) username: Option<String>,
1111 pub(crate) password: Option<String>,
1113
1114 #[serde(
1115 deserialize_with = "humantime_serde::deserialize",
1116 default = "default_timeout"
1117 )]
1118 #[schemars(with = "Option<String>", default)]
1119 pub(crate) timeout: Duration,
1121
1122 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1123 #[schemars(with = "Option<String>", default)]
1124 pub(crate) ttl: Option<Duration>,
1126
1127 pub(crate) namespace: Option<String>,
1129
1130 #[serde(default)]
1131 pub(crate) tls: Option<TlsClient>,
1133
1134 #[serde(default = "default_required_to_start")]
1135 pub(crate) required_to_start: bool,
1137
1138 #[serde(default = "default_reset_ttl")]
1139 pub(crate) reset_ttl: bool,
1141
1142 #[serde(default = "default_pool_size")]
1143 pub(crate) pool_size: u32,
1145 #[serde(
1146 deserialize_with = "humantime_serde::deserialize",
1147 default = "default_metrics_interval"
1148 )]
1149 #[schemars(with = "Option<String>", default)]
1150 pub(crate) metrics_interval: Duration,
1152}
1153
1154fn default_timeout() -> Duration {
1155 Duration::from_millis(500)
1156}
1157
1158pub(crate) fn default_required_to_start() -> bool {
1159 false
1160}
1161
1162pub(crate) fn default_pool_size() -> u32 {
1163 1
1164}
1165
1166pub(crate) fn default_metrics_interval() -> Duration {
1167 Duration::from_secs(1)
1168}
1169
1170impl From<QueryPlanRedisCache> for RedisCache {
1171 fn from(value: QueryPlanRedisCache) -> Self {
1172 RedisCache {
1173 urls: value.urls,
1174 username: value.username,
1175 password: value.password,
1176 timeout: value.timeout,
1177 ttl: Some(value.ttl),
1178 namespace: value.namespace,
1179 tls: value.tls,
1180 required_to_start: value.required_to_start,
1181 reset_ttl: value.reset_ttl,
1182 pool_size: value.pool_size,
1183 metrics_interval: default_metrics_interval(),
1184 }
1185 }
1186}
1187
1188fn default_reset_ttl() -> bool {
1189 true
1190}
1191
1192#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1194#[serde(deny_unknown_fields)]
1195#[serde(default)]
1196pub(crate) struct Tls {
1197 pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1201 pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1203 pub(crate) connector: ConnectorConfiguration<TlsClient>,
1205}
1206
1207#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1209#[serde(deny_unknown_fields)]
1210pub(crate) struct TlsSupergraph {
1211 #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1213 #[schemars(with = "String")]
1214 pub(crate) certificate: CertificateDer<'static>,
1215 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1217 #[schemars(with = "String")]
1218 pub(crate) key: PrivateKeyDer<'static>,
1219 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1221 #[schemars(with = "String")]
1222 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1223}
1224
1225impl TlsSupergraph {
1226 pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1227 let mut certificates = vec![self.certificate.clone()];
1228 certificates.extend(self.certificate_chain.iter().cloned());
1229
1230 let mut config = ServerConfig::builder()
1231 .with_no_client_auth()
1232 .with_single_cert(certificates, self.key.clone_key())
1233 .map_err(ApolloRouterError::Rustls)?;
1234 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1235
1236 Ok(Arc::new(config))
1237 }
1238}
1239
1240fn deserialize_certificate<'de, D>(deserializer: D) -> Result<CertificateDer<'static>, D::Error>
1241where
1242 D: Deserializer<'de>,
1243{
1244 let data = String::deserialize(deserializer)?;
1245
1246 load_certs(&data)
1247 .map_err(serde::de::Error::custom)
1248 .and_then(|mut certs| {
1249 if certs.len() > 1 {
1250 Err(serde::de::Error::custom("expected exactly one certificate"))
1251 } else {
1252 certs
1253 .pop()
1254 .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1255 }
1256 })
1257}
1258
1259fn deserialize_certificate_chain<'de, D>(
1260 deserializer: D,
1261) -> Result<Vec<CertificateDer<'static>>, D::Error>
1262where
1263 D: Deserializer<'de>,
1264{
1265 let data = String::deserialize(deserializer)?;
1266
1267 load_certs(&data).map_err(serde::de::Error::custom)
1268}
1269
1270fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKeyDer<'static>, D::Error>
1271where
1272 D: Deserializer<'de>,
1273{
1274 let data = String::deserialize(deserializer)?;
1275
1276 load_key(&data).map_err(serde::de::Error::custom)
1277}
1278
1279#[derive(thiserror::Error, Debug)]
1280#[error("could not load TLS certificate: {0}")]
1281struct LoadCertError(std::io::Error);
1282
1283pub(crate) fn load_certs(data: &str) -> io::Result<Vec<CertificateDer<'static>>> {
1284 rustls_pemfile::certs(&mut BufReader::new(data.as_bytes()))
1285 .collect::<Result<Vec<_>, _>>()
1286 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, LoadCertError(error)))
1287}
1288
1289pub(crate) fn load_key(data: &str) -> io::Result<PrivateKeyDer<'static>> {
1290 let mut reader = BufReader::new(data.as_bytes());
1291 let mut key_iterator = iter::from_fn(|| rustls_pemfile::read_one(&mut reader).transpose());
1292
1293 let private_key = match key_iterator.next() {
1294 Some(Ok(rustls_pemfile::Item::Pkcs1Key(key))) => PrivateKeyDer::from(key),
1295 Some(Ok(rustls_pemfile::Item::Pkcs8Key(key))) => PrivateKeyDer::from(key),
1296 Some(Ok(rustls_pemfile::Item::Sec1Key(key))) => PrivateKeyDer::from(key),
1297 Some(Err(e)) => {
1298 return Err(io::Error::new(
1299 io::ErrorKind::InvalidInput,
1300 format!("could not parse the key: {e}"),
1301 ));
1302 }
1303 Some(_) => {
1304 return Err(io::Error::new(
1305 io::ErrorKind::InvalidInput,
1306 "expected a private key",
1307 ));
1308 }
1309 None => {
1310 return Err(io::Error::new(
1311 io::ErrorKind::InvalidInput,
1312 "could not find a private key",
1313 ));
1314 }
1315 };
1316
1317 if key_iterator.next().is_some() {
1318 return Err(io::Error::new(
1319 io::ErrorKind::InvalidInput,
1320 "expected exactly one private key",
1321 ));
1322 }
1323 Ok(private_key)
1324}
1325
1326#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq)]
1328#[serde(deny_unknown_fields)]
1329#[serde(default)]
1330pub(crate) struct TlsClient {
1331 pub(crate) certificate_authorities: Option<String>,
1333 pub(crate) client_authentication: Option<Arc<TlsClientAuth>>,
1335}
1336
1337#[buildstructor::buildstructor]
1338impl TlsClient {
1339 #[builder]
1340 pub(crate) fn new(
1341 certificate_authorities: Option<String>,
1342 client_authentication: Option<Arc<TlsClientAuth>>,
1343 ) -> Self {
1344 Self {
1345 certificate_authorities,
1346 client_authentication,
1347 }
1348 }
1349}
1350
1351impl Default for TlsClient {
1352 fn default() -> Self {
1353 Self::builder().build()
1354 }
1355}
1356
1357#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1359#[serde(deny_unknown_fields)]
1360pub(crate) struct TlsClientAuth {
1361 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1363 #[schemars(with = "String")]
1364 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1365 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1367 #[schemars(with = "String")]
1368 pub(crate) key: PrivateKeyDer<'static>,
1369}
1370
1371#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1373#[serde(deny_unknown_fields)]
1374#[serde(default)]
1375pub(crate) struct Sandbox {
1376 pub(crate) enabled: bool,
1378}
1379
1380fn default_sandbox() -> bool {
1381 false
1382}
1383
1384#[buildstructor::buildstructor]
1385impl Sandbox {
1386 #[builder]
1387 pub(crate) fn new(enabled: Option<bool>) -> Self {
1388 Self {
1389 enabled: enabled.unwrap_or_else(default_sandbox),
1390 }
1391 }
1392}
1393
1394#[cfg(test)]
1395#[buildstructor::buildstructor]
1396impl Sandbox {
1397 #[builder]
1398 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1399 Self {
1400 enabled: enabled.unwrap_or_else(default_sandbox),
1401 }
1402 }
1403}
1404
1405impl Default for Sandbox {
1406 fn default() -> Self {
1407 Self::builder().build()
1408 }
1409}
1410
1411#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1413#[serde(deny_unknown_fields)]
1414#[serde(default)]
1415pub(crate) struct Homepage {
1416 pub(crate) enabled: bool,
1418 pub(crate) graph_ref: Option<String>,
1421}
1422
1423fn default_homepage() -> bool {
1424 true
1425}
1426
1427#[buildstructor::buildstructor]
1428impl Homepage {
1429 #[builder]
1430 pub(crate) fn new(enabled: Option<bool>) -> Self {
1431 Self {
1432 enabled: enabled.unwrap_or_else(default_homepage),
1433 graph_ref: None,
1434 }
1435 }
1436}
1437
1438#[cfg(test)]
1439#[buildstructor::buildstructor]
1440impl Homepage {
1441 #[builder]
1442 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1443 Self {
1444 enabled: enabled.unwrap_or_else(default_homepage),
1445 graph_ref: None,
1446 }
1447 }
1448}
1449
1450impl Default for Homepage {
1451 fn default() -> Self {
1452 Self::builder().enabled(default_homepage()).build()
1453 }
1454}
1455
1456#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1458#[serde(untagged)]
1459pub enum ListenAddr {
1460 SocketAddr(SocketAddr),
1462 #[cfg(unix)]
1464 UnixSocket(std::path::PathBuf),
1465}
1466
1467impl ListenAddr {
1468 pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1469 #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1470 if let Self::SocketAddr(addr) = self {
1471 Some((addr.ip(), addr.port()))
1472 } else {
1473 None
1474 }
1475 }
1476}
1477
1478impl From<SocketAddr> for ListenAddr {
1479 fn from(addr: SocketAddr) -> Self {
1480 Self::SocketAddr(addr)
1481 }
1482}
1483
1484#[allow(clippy::from_over_into)]
1485impl Into<serde_json::Value> for ListenAddr {
1486 fn into(self) -> serde_json::Value {
1487 match self {
1488 Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1491 #[cfg(unix)]
1492 Self::UnixSocket(path) => serde_json::Value::String(
1493 path.as_os_str()
1494 .to_str()
1495 .expect("unsupported non-UTF-8 path")
1496 .to_string(),
1497 ),
1498 }
1499 }
1500}
1501
1502#[cfg(unix)]
1503impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1504 for ListenAddr
1505{
1506 fn from(
1507 addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1508 ) -> Self {
1509 match addr {
1510 tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1511 tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1512 addr.as_pathname()
1513 .map(ToOwned::to_owned)
1514 .unwrap_or_default(),
1515 ),
1516 }
1517 }
1518}
1519
1520impl fmt::Display for ListenAddr {
1521 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1522 match self {
1523 Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1524 #[cfg(unix)]
1525 Self::UnixSocket(path) => write!(f, "{}", path.display()),
1526 }
1527 }
1528}
1529
1530fn default_graphql_path() -> String {
1531 String::from("/")
1532}
1533
1534fn default_graphql_introspection() -> bool {
1535 false
1536}
1537
1538fn default_connection_shutdown_timeout() -> Duration {
1539 Duration::from_secs(60)
1540}
1541
1542fn default_strict_variable_validation() -> Mode {
1543 Mode::Enforce
1544}
1545
1546#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1547#[serde(deny_unknown_fields, rename_all = "snake_case")]
1548pub(crate) enum BatchingMode {
1549 #[default]
1551 BatchHttpLink,
1552}
1553
1554#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1556#[serde(deny_unknown_fields)]
1557pub(crate) struct Batching {
1558 #[serde(default)]
1560 pub(crate) enabled: bool,
1561
1562 pub(crate) mode: BatchingMode,
1564
1565 pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1567
1568 #[serde(default)]
1570 pub(crate) maximum_size: Option<usize>,
1571}
1572
1573#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1575pub(crate) struct CommonBatchingConfig {
1576 pub(crate) enabled: bool,
1578}
1579
1580impl Batching {
1581 pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1583 match &self.subgraph {
1584 Some(subgraph_batching_config) => {
1585 if subgraph_batching_config.all.enabled {
1587 subgraph_batching_config
1591 .subgraphs
1592 .get(service_name)
1593 .is_none_or(|x| x.enabled)
1594 } else {
1595 subgraph_batching_config
1598 .subgraphs
1599 .get(service_name)
1600 .is_some_and(|x| x.enabled)
1601 }
1602 }
1603 None => false,
1604 }
1605 }
1606
1607 pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1608 match self.maximum_size {
1609 Some(maximum_size) => batch.len() > maximum_size,
1610 None => false,
1611 }
1612 }
1613}
1614
1615#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1622#[serde(deny_unknown_fields)]
1623pub(crate) struct HoistOrphanErrors {
1624 #[serde(default)]
1626 pub(crate) enabled: bool,
1627}