Skip to main content

apollo_federation/schema/
mod.rs

1use std::hash::Hash;
2use std::hash::Hasher;
3use std::ops::Deref;
4use std::sync::Arc;
5
6use apollo_compiler::Name;
7use apollo_compiler::Schema;
8use apollo_compiler::collections::IndexSet;
9use apollo_compiler::schema::ExtendedType;
10use apollo_compiler::validation::Valid;
11use referencer::Referencers;
12
13use crate::error::FederationError;
14use crate::error::SingleFederationError;
15use crate::link::LinksMetadata;
16use crate::link::federation_spec_definition::FEDERATION_ENTITY_TYPE_NAME_IN_SPEC;
17use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph;
18use crate::schema::position::CompositeTypeDefinitionPosition;
19use crate::schema::position::DirectiveDefinitionPosition;
20use crate::schema::position::EnumTypeDefinitionPosition;
21use crate::schema::position::InputObjectTypeDefinitionPosition;
22use crate::schema::position::InterfaceTypeDefinitionPosition;
23use crate::schema::position::ObjectTypeDefinitionPosition;
24use crate::schema::position::ScalarTypeDefinitionPosition;
25use crate::schema::position::TypeDefinitionPosition;
26use crate::schema::position::UnionTypeDefinitionPosition;
27use crate::schema::subgraph_metadata::SubgraphMetadata;
28
29pub(crate) mod argument_composition_strategies;
30pub(crate) mod definitions;
31pub(crate) mod field_set;
32pub(crate) mod position;
33pub(crate) mod referencer;
34pub(crate) mod subgraph_metadata;
35
36fn compute_subgraph_metadata(
37    schema: &Valid<FederationSchema>,
38) -> Result<Option<SubgraphMetadata>, FederationError> {
39    Ok(
40        if let Ok(federation_spec_definition) = get_federation_spec_definition_from_subgraph(schema)
41        {
42            Some(SubgraphMetadata::new(schema, federation_spec_definition)?)
43        } else {
44            None
45        },
46    )
47}
48pub(crate) mod type_and_directive_specification;
49
50/// A GraphQL schema with federation data.
51#[derive(Debug)]
52pub struct FederationSchema {
53    schema: Schema,
54    referencers: Referencers,
55    links_metadata: Option<Box<LinksMetadata>>,
56    /// This is only populated for valid subgraphs, and can only be accessed if you have a
57    /// `ValidFederationSchema`.
58    subgraph_metadata: Option<Box<SubgraphMetadata>>,
59}
60
61impl FederationSchema {
62    pub(crate) fn schema(&self) -> &Schema {
63        &self.schema
64    }
65
66    /// Discard the Federation metadata and return the apollo-compiler schema.
67    pub fn into_inner(self) -> Schema {
68        self.schema
69    }
70
71    pub(crate) fn metadata(&self) -> Option<&LinksMetadata> {
72        self.links_metadata.as_deref()
73    }
74
75    pub(crate) fn referencers(&self) -> &Referencers {
76        &self.referencers
77    }
78
79    /// Returns all the types in the schema, minus builtins.
80    pub(crate) fn get_types(&self) -> impl Iterator<Item = TypeDefinitionPosition> + '_ {
81        self.schema
82            .types
83            .iter()
84            .filter(|(_, ty)| !ty.is_built_in())
85            .map(|(type_name, type_)| {
86                let type_name = type_name.clone();
87                match type_ {
88                    ExtendedType::Scalar(_) => ScalarTypeDefinitionPosition { type_name }.into(),
89                    ExtendedType::Object(_) => ObjectTypeDefinitionPosition { type_name }.into(),
90                    ExtendedType::Interface(_) => {
91                        InterfaceTypeDefinitionPosition { type_name }.into()
92                    }
93                    ExtendedType::Union(_) => UnionTypeDefinitionPosition { type_name }.into(),
94                    ExtendedType::Enum(_) => EnumTypeDefinitionPosition { type_name }.into(),
95                    ExtendedType::InputObject(_) => {
96                        InputObjectTypeDefinitionPosition { type_name }.into()
97                    }
98                }
99            })
100    }
101
102    pub(crate) fn get_directive_definitions(
103        &self,
104    ) -> impl Iterator<Item = DirectiveDefinitionPosition> + '_ {
105        self.schema
106            .directive_definitions
107            .keys()
108            .map(|name| DirectiveDefinitionPosition {
109                directive_name: name.clone(),
110            })
111    }
112
113    pub(crate) fn get_type(
114        &self,
115        type_name: Name,
116    ) -> Result<TypeDefinitionPosition, FederationError> {
117        let type_ =
118            self.schema
119                .types
120                .get(&type_name)
121                .ok_or_else(|| SingleFederationError::Internal {
122                    message: format!("Schema has no type \"{}\"", type_name),
123                })?;
124        Ok(match type_ {
125            ExtendedType::Scalar(_) => ScalarTypeDefinitionPosition { type_name }.into(),
126            ExtendedType::Object(_) => ObjectTypeDefinitionPosition { type_name }.into(),
127            ExtendedType::Interface(_) => InterfaceTypeDefinitionPosition { type_name }.into(),
128            ExtendedType::Union(_) => UnionTypeDefinitionPosition { type_name }.into(),
129            ExtendedType::Enum(_) => EnumTypeDefinitionPosition { type_name }.into(),
130            ExtendedType::InputObject(_) => InputObjectTypeDefinitionPosition { type_name }.into(),
131        })
132    }
133
134    pub(crate) fn try_get_type(&self, type_name: Name) -> Option<TypeDefinitionPosition> {
135        self.get_type(type_name).ok()
136    }
137
138    /// Return the possible runtime types for a definition.
139    ///
140    /// For a union, the possible runtime types are its members.
141    /// For an interface, the possible runtime types are its implementers.
142    ///
143    /// Note this always allocates a set for the result. Avoid calling it frequently.
144    pub(crate) fn possible_runtime_types(
145        &self,
146        composite_type_definition_position: CompositeTypeDefinitionPosition,
147    ) -> Result<IndexSet<ObjectTypeDefinitionPosition>, FederationError> {
148        Ok(match composite_type_definition_position {
149            CompositeTypeDefinitionPosition::Object(pos) => IndexSet::from_iter([pos]),
150            CompositeTypeDefinitionPosition::Interface(pos) => self
151                .referencers()
152                .get_interface_type(&pos.type_name)?
153                .object_types
154                .clone(),
155            CompositeTypeDefinitionPosition::Union(pos) => pos
156                .get(self.schema())?
157                .members
158                .iter()
159                .map(|t| ObjectTypeDefinitionPosition {
160                    type_name: t.name.clone(),
161                })
162                .collect::<IndexSet<_>>(),
163        })
164    }
165
166    /// Similar to `Self::validate` but returns `self` as part of the error should it be needed by
167    /// the caller
168    #[allow(clippy::result_large_err)] // lint is accurate but this is not in a hot path
169    pub(crate) fn validate_or_return_self(
170        mut self,
171    ) -> Result<ValidFederationSchema, (Self, FederationError)> {
172        let schema = match self.schema.validate() {
173            Ok(schema) => schema.into_inner(),
174            Err(e) => {
175                self.schema = e.partial;
176                return Err((self, e.errors.into()));
177            }
178        };
179        ValidFederationSchema::new_assume_valid(FederationSchema { schema, ..self })
180    }
181
182    pub(crate) fn assume_valid(self) -> Result<ValidFederationSchema, FederationError> {
183        ValidFederationSchema::new_assume_valid(self).map_err(|(_schema, error)| error)
184    }
185
186    pub(crate) fn get_directive_definition(
187        &self,
188        name: &Name,
189    ) -> Option<DirectiveDefinitionPosition> {
190        self.schema
191            .directive_definitions
192            .contains_key(name)
193            .then(|| DirectiveDefinitionPosition {
194                directive_name: name.clone(),
195            })
196    }
197
198    /// Note that a subgraph may have no "entities" and so no `_Entity` type.
199    pub(crate) fn entity_type(
200        &self,
201    ) -> Result<Option<UnionTypeDefinitionPosition>, FederationError> {
202        // Note that the _Entity type is special in that:
203        // 1. Spec renaming doesn't take place for it (there's no prefixing or importing needed),
204        //    in order to maintain backwards compatibility with Fed 1.
205        // 2. Its presence is optional; if absent, it means the subgraph has no resolvable keys.
206        match self.schema.types.get(&FEDERATION_ENTITY_TYPE_NAME_IN_SPEC) {
207            Some(ExtendedType::Union(_)) => Ok(Some(UnionTypeDefinitionPosition {
208                type_name: FEDERATION_ENTITY_TYPE_NAME_IN_SPEC,
209            })),
210            Some(_) => Err(FederationError::internal(format!(
211                "Unexpectedly found non-union for federation spec's `{}` type definition",
212                FEDERATION_ENTITY_TYPE_NAME_IN_SPEC
213            ))),
214            None => Ok(None),
215        }
216    }
217}
218
219/// A GraphQL schema with federation data that is known to be valid, and cheap to clone.
220#[derive(Clone)]
221pub struct ValidFederationSchema {
222    schema: Arc<Valid<FederationSchema>>,
223}
224
225impl ValidFederationSchema {
226    pub fn new(schema: Valid<Schema>) -> Result<ValidFederationSchema, FederationError> {
227        let schema = FederationSchema::new(schema.into_inner())?;
228
229        Self::new_assume_valid(schema).map_err(|(_schema, error)| error)
230    }
231
232    /// Construct a ValidFederationSchema by assuming the given FederationSchema is valid.
233    #[allow(clippy::result_large_err)] // lint is accurate but this is not in a hot path
234    fn new_assume_valid(
235        mut schema: FederationSchema,
236    ) -> Result<ValidFederationSchema, (FederationSchema, FederationError)> {
237        // Populating subgraph metadata requires a mutable FederationSchema, while computing the subgraph
238        // metadata requires a valid FederationSchema. Since valid schemas are immutable, we have
239        // to jump through some hoops here. We already assume that `schema` is valid GraphQL, so we
240        // can temporarily create a `&Valid<FederationSchema>` to compute subgraph metadata, drop
241        // that reference to populate the metadata, and finally move the finished FederationSchema into
242        // the ValidFederationSchema instance.
243        let valid_schema = Valid::assume_valid_ref(&schema);
244        let subgraph_metadata = match compute_subgraph_metadata(valid_schema) {
245            Ok(metadata) => metadata.map(Box::new),
246            Err(err) => return Err((schema, err)),
247        };
248        schema.subgraph_metadata = subgraph_metadata;
249
250        let schema = Arc::new(Valid::assume_valid(schema));
251        Ok(ValidFederationSchema { schema })
252    }
253
254    /// Access the GraphQL schema.
255    pub fn schema(&self) -> &Valid<Schema> {
256        Valid::assume_valid_ref(&self.schema.schema)
257    }
258
259    /// Returns subgraph-specific metadata.
260    ///
261    /// Returns `None` for supergraph schemas.
262    pub(crate) fn subgraph_metadata(&self) -> Option<&SubgraphMetadata> {
263        self.schema.subgraph_metadata.as_deref()
264    }
265
266    pub(crate) fn federation_type_name_in_schema(
267        &self,
268        name: Name,
269    ) -> Result<Name, FederationError> {
270        // Currently, the types used to define the federation operations, that is _Any, _Entity and _Service,
271        // are not considered part of the federation spec, and are instead hardcoded to the names above.
272        // The reason being that there is no way to maintain backward compatbility with fed2 if we were to add
273        // those to the federation spec without requiring users to add those types to their @link `import`,
274        // and that wouldn't be a good user experience (because most users don't really know what those types
275        // are/do). And so we special case it.
276        if name.starts_with('_') {
277            return Ok(name);
278        }
279
280        // TODO for composition: this otherwise needs to check for a type name in schema based
281        // on the latest federation version.
282        // This code path is not hit during planning.
283        Err(FederationError::internal(
284            "typename should have been looked in a federation feature",
285        ))
286    }
287
288    pub(crate) fn is_interface_object_type(
289        &self,
290        type_definition_position: TypeDefinitionPosition,
291    ) -> Result<bool, FederationError> {
292        let Some(subgraph_metadata) = &self.subgraph_metadata else {
293            return Ok(false);
294        };
295        let Some(interface_object_directive_definition) = subgraph_metadata
296            .federation_spec_definition()
297            .interface_object_directive_definition(self)?
298        else {
299            return Ok(false);
300        };
301        match type_definition_position {
302            TypeDefinitionPosition::Object(type_) => Ok(type_
303                .get(self.schema())?
304                .directives
305                .has(&interface_object_directive_definition.name)),
306            _ => Ok(false),
307        }
308    }
309}
310
311impl Deref for ValidFederationSchema {
312    type Target = FederationSchema;
313
314    fn deref(&self) -> &Self::Target {
315        &self.schema
316    }
317}
318
319impl Eq for ValidFederationSchema {}
320
321impl PartialEq for ValidFederationSchema {
322    fn eq(&self, other: &ValidFederationSchema) -> bool {
323        Arc::ptr_eq(&self.schema, &other.schema)
324    }
325}
326
327impl Hash for ValidFederationSchema {
328    fn hash<H: Hasher>(&self, state: &mut H) {
329        Arc::as_ptr(&self.schema).hash(state);
330    }
331}
332
333impl std::fmt::Debug for ValidFederationSchema {
334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335        write!(f, "ValidFederationSchema @ {:?}", Arc::as_ptr(&self.schema))
336    }
337}