1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//! Apollo Federation configuration types.
use serde::{Deserialize, Serialize};
/// Circuit breaker configuration for a specific federated database/service
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PerDatabaseCircuitBreakerOverride {
/// Database or service name matching a federation entity
pub database: String,
/// Override: number of consecutive failures before opening (must be > 0)
pub failure_threshold: Option<u32>,
/// Override: seconds to wait before attempting recovery (must be > 0)
pub recovery_timeout_secs: Option<u64>,
/// Override: successes required in half-open state to close the breaker (must be > 0)
pub success_threshold: Option<u32>,
}
/// Circuit breaker configuration for Apollo Federation fan-out requests
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct FederationCircuitBreakerConfig {
/// Enable circuit breaker protection on federation fan-out
pub enabled: bool,
/// Consecutive failures before the breaker opens (default: 5, must be > 0)
pub failure_threshold: u32,
/// Seconds to wait before attempting a probe request (default: 30, must be > 0)
pub recovery_timeout_secs: u64,
/// Probe successes needed to transition from half-open to closed (default: 2, must be > 0)
pub success_threshold: u32,
/// Per-database overrides (database name must match a defined federation entity)
pub per_database: Vec<PerDatabaseCircuitBreakerOverride>,
}
impl Default for FederationCircuitBreakerConfig {
fn default() -> Self {
Self {
enabled: true,
failure_threshold: 5,
recovery_timeout_secs: 30,
success_threshold: 2,
per_database: vec![],
}
}
}
/// Federation configuration
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct FederationConfig {
/// Enable Apollo federation
#[serde(default)]
pub enabled: bool,
/// Subgraph service name (surfaced in Apollo Studio and the subgraph listing)
pub service_name: Option<String>,
/// Apollo Federation spec version string (e.g. `"v2"`).
///
/// Preferred over [`apollo_version`](Self::apollo_version); when both are set,
/// `version` wins. When neither is set, the runtime defaults to `"v2"`.
pub version: Option<String>,
/// Subgraph SDL URL (exposed at `/__subgraph_schema`)
pub schema_url: Option<String>,
/// Apollo federation major version (legacy integer form; `2` ⇒ `"v2"`).
///
/// Retained for backward compatibility; prefer the [`version`](Self::version)
/// string. Ignored when `version` is set.
pub apollo_version: Option<u32>,
/// Federated entities
pub entities: Vec<FederationEntity>,
/// Keyless `@shareable` value types — types every subgraph may define identically.
///
/// They are not entities: no `@key`, never members of the `_Entity` union. Without
/// them a value type defined in two subgraphs fails composition with
/// `INVALID_FIELD_SHARING`.
#[serde(default)]
pub shareable_types: Vec<String>,
/// Circuit breaker configuration for federation fan-out requests
pub circuit_breaker: Option<FederationCircuitBreakerConfig>,
}
impl Default for FederationConfig {
fn default() -> Self {
Self {
enabled: false,
service_name: None,
version: None,
schema_url: None,
apollo_version: Some(2),
entities: vec![],
shareable_types: vec![],
circuit_breaker: None,
}
}
}
impl FederationConfig {
/// The effective Apollo Federation spec version string.
///
/// Prefers the explicit [`version`](Self::version) string, falling back to the
/// legacy integer [`apollo_version`](Self::apollo_version) (`2` ⇒ `"v2"`).
/// `None` when neither is set, in which case the runtime defaults to `"v2"`.
#[must_use]
pub fn effective_version(&self) -> Option<String> {
self.version.clone().or_else(|| self.apollo_version.map(|v| format!("v{v}")))
}
/// Lower this TOML-authored config into the compiled
/// [`fraiseql_core::schema::FederationConfig`] the runtime consumes.
///
/// The mapping is explicit (rather than a raw serde passthrough) so the TOML and
/// compiled field names cannot silently diverge: in particular `apollo_version`
/// becomes `version`, and per-entity circuit-breaker overrides authored as
/// `per_database` become the runtime's `per_entity` list.
#[must_use]
pub fn to_compiled(&self) -> fraiseql_core::schema::FederationConfig {
use fraiseql_core::schema as core;
core::FederationConfig {
enabled: self.enabled,
version: self.effective_version(),
service_name: self.service_name.clone(),
schema_url: self.schema_url.clone(),
shareable_types: self.shareable_types.clone(),
// Lifted off the types' fields by the converter, never authored here.
inaccessible_fields: std::collections::BTreeMap::new(),
entities: self
.entities
.iter()
.map(|e| core::FederationEntity {
name: e.name.clone(),
key_fields: e.key_fields.clone(),
extends: e.extends,
external_fields: e.external_fields.clone(),
shareable_fields: e.shareable_fields.clone(),
})
.collect(),
circuit_breaker: self.circuit_breaker.as_ref().map(|cb| {
core::CircuitBreakerConfig {
enabled: cb.enabled,
failure_threshold: cb.failure_threshold,
recovery_timeout_secs: cb.recovery_timeout_secs,
success_threshold: cb.success_threshold,
per_entity: cb
.per_database
.iter()
.map(|o| core::EntityCircuitBreakerOverride {
entity: o.database.clone(),
failure_threshold: o.failure_threshold,
recovery_timeout: o.recovery_timeout_secs,
success_threshold: o.success_threshold,
})
.collect(),
}
}),
}
}
}
/// Federation entity
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct FederationEntity {
/// Entity name
pub name: String,
/// Key fields for entity resolution
pub key_fields: Vec<String>,
/// This subgraph `extend`s an entity another subgraph owns.
///
/// Renders `extend type Name @key(...)` instead of `type Name @key(...)`, which is
/// how a subgraph contributes fields to an entity it does not own — the central
/// pattern of Apollo Federation. The compiled schema and the SDL renderer have
/// always carried it; this surface could not express it (#1195).
#[serde(default)]
pub extends: bool,
/// Fields owned by another subgraph and only referenced here — typically the
/// borrowed `@key` field of an extended entity. Rendered `@external`.
#[serde(default)]
pub external_fields: Vec<String>,
/// Fields resolvable in more than one subgraph. Rendered `@shareable`.
#[serde(default)]
pub shareable_fields: Vec<String>,
}