apollo-federation 2.16.0

Apollo Federation
Documentation
use std::collections::BTreeSet;

use apollo_compiler::Name;
use apollo_compiler::collections::IndexMap;
use apollo_compiler::collections::IndexSet;
use tracing::trace;

use crate::error::CompositionError;
use crate::error::FederationError;
use crate::merger::merge::Merger;
use crate::schema::position::HasAppliedDirectives;
use crate::schema::position::InterfaceTypeDefinitionPosition;
use crate::schema::position::ObjectOrInterfaceTypeDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
use crate::schema::position::TypeDefinitionPosition;
use crate::utils::human_readable::human_readable_types;

impl Merger {
    // Returns whether the interface has a key (even a non-resolvable one) in any subgraph.
    fn validate_interface_keys(
        &mut self,
        dest: &InterfaceTypeDefinitionPosition,
    ) -> Result<bool, FederationError> {
        trace!("Validating interface keys");
        // Remark: it might be ok to filter @inaccessible types in `supergraphImplementations`, but this requires
        // some more thinking (and I'm not even sure it makes a practical difference given the rules for validity
        // of @inaccessible) and it will be backward compatible to filter them later, while the reverse wouldn't
        // technically be, so we stay on the safe side.
        let supergraph_implementations = self.merged.possible_runtime_types(dest.clone().into())?;

        // Validate that if a source defines a (resolvable) @key on an interface, then that subgraph defines
        // all the implementations of that interface in the supergraph.
        let mut has_key = false;
        for subgraph in self.subgraphs.iter() {
            if !subgraph.schema().is_interface(&dest.type_name) {
                continue;
            }

            let Some(key_directive_name) = subgraph.key_directive_name() else {
                continue;
            };
            let interface_pos: TypeDefinitionPosition = dest.clone().into();
            let keys = interface_pos.get_applied_directives(subgraph.schema(), &key_directive_name);
            has_key = has_key || !keys.is_empty();
            let federation_spec_definition = subgraph.metadata().federation_spec_definition();
            let Some(resolvable_key) = keys.iter().find(|key| {
                federation_spec_definition
                    .key_directive_arguments(key)
                    .map(|args| args.resolvable)
                    .unwrap_or(true) // @key(resolvable:) defaults to true in its definition
            }) else {
                continue;
            };
            let implementations_in_subgraph = subgraph
                .schema()
                .possible_runtime_types(dest.clone().into())?;
            let missing_implementations: IndexSet<_> = supergraph_implementations
                .difference(&implementations_in_subgraph)
                .collect();
            let subgraph_name = &subgraph.name;
            if !missing_implementations.is_empty() {
                self.error_reporter.add_error(CompositionError::InterfaceKeyMissingImplementationType {
                            message: format!("[{subgraph_name}] Interface type \"{dest}\" has a resolvable key ({}) in subgraph \"{subgraph_name}\" but that subgraph is missing some of the supergraph implementation types of \"{dest}\". Subgraph \"{subgraph_name}\" should define {} (and have {} implement \"{dest}\").",
                                resolvable_key.serialize(),
                                human_readable_types(missing_implementations.iter().map(|impl_type| &impl_type.type_name)),
                                if missing_implementations.len() > 1 { "them" } else { "it" },
                            )
                        });
            }
        }
        Ok(has_key)
    }

    fn validate_interface_objects(
        &mut self,
        dest: &InterfaceTypeDefinitionPosition,
    ) -> Result<(), FederationError> {
        trace!("Validating interface objects");
        let supergraph_implementations = self.merged.possible_runtime_types(dest.clone().into())?;
        let mut is_interface_object = false;

        // Validates that if a source defines the interface as an @interfaceObject, then it doesn't define any
        // of the implementations. We can discuss if there is ways to lift that limitation later, but an
        // @interfaceObject already "provides" fields for all the underlying impelmentations, so also defining
        // one those implementation would require additional care for shareability and more. This also feel
        // like this can get easily be done by mistake and gets rather confusing, so it's worth some additional
        // consideration before allowing.
        for subgraph in self.subgraphs.iter() {
            // If `dest` has an interface object in this subgraph, it will be an object type (not
            // an interface). So we have to check for the object equivalent of `dest` instead of
            // checking `dest` directly.
            let ty_as_obj = ObjectTypeDefinitionPosition {
                type_name: dest.type_name.clone(),
            };
            if !subgraph.is_interface_object_type(&ty_as_obj.into()) {
                continue;
            }
            is_interface_object = true;

            let subgraph_name = &subgraph.name;
            let defined_implementations: IndexSet<_> = supergraph_implementations
                .iter()
                .filter(|implementation| implementation.get(subgraph.schema().schema()).is_ok())
                .collect::<IndexSet<_>>();
            if !defined_implementations.is_empty() {
                self.error_reporter.add_error(CompositionError::InterfaceObjectUsageError {
                    message: format!("[{subgraph_name}] Interface type \"{dest}\" is defined as an @interfaceObject in subgraph \"{subgraph_name}\" so that subgraph should not define any of the implementation types of \"{dest}\", but it defines {}",
                        human_readable_types(defined_implementations.iter().map(|impl_type| &impl_type.type_name)),
                    )
                });
            }
        }

        // @interfaceObject cannot be implemented by other interfaces
        if is_interface_object {
            for interface_implements_intf_object in self
                .merged
                .all_implementation_types(dest)?
                .iter()
                .filter(|implementation| {
                    matches!(
                        implementation,
                        ObjectOrInterfaceTypeDefinitionPosition::Interface(_)
                    )
                })
            {
                self.error_reporter.add_error(CompositionError::InterfaceObjectUsageError {
                    message: format!(
                        "Interfaces implementing @interfaceObject are not supported: @interfaceObject \"{dest}\" is implemented by an interface \"{interface_implements_intf_object}\".",
                    ),
                });
            }
        }
        Ok(())
    }

    /// Validates that `@interfaceObject` types within the same subgraph are pairwise
    /// disjoint in their implementing types. Query planning assumes that when jumping
    /// to a type in a subgraph via `@key`, there's only one version of that type.
    /// Multiple `@interfaceObject` types sharing an implementation type would violate
    /// this assumption.
    pub(crate) fn validate_interface_object_disjointness(&mut self) -> Result<(), FederationError> {
        for subgraph in self.subgraphs.iter() {
            if subgraph.metadata().interface_object_types().is_empty() {
                continue;
            }

            // collect all @interfaceObject types in this subgraph and their implementations
            let mut impls_to_intf_objects: IndexMap<
                ObjectTypeDefinitionPosition,
                IndexSet<InterfaceTypeDefinitionPosition>,
            > = IndexMap::default();
            for intf_object in subgraph.metadata().interface_object_types() {
                let itf_pos = InterfaceTypeDefinitionPosition {
                    type_name: intf_object.clone(),
                };
                if let Ok(runtime_types) =
                    self.merged.possible_runtime_types(itf_pos.clone().into())
                {
                    runtime_types.iter().for_each(|t| {
                        impls_to_intf_objects
                            .entry(t.clone())
                            .or_default()
                            .insert(itf_pos.clone());
                    });
                }
            }

            impls_to_intf_objects.iter()
                .filter(|(_, intf_objects)| intf_objects.len() > 1)
                .fold(IndexMap::<BTreeSet<Name>, IndexSet<Name>>::default(), |mut acc, (impl_, intf_objects)| {
                    let key: BTreeSet<Name> = BTreeSet::from_iter(intf_objects.iter().map(|i| i.type_name.clone()));
                    acc.entry(key)
                        .or_default()
                        .insert(impl_.type_name.clone());
                    acc
                })
                .iter()
                .for_each(|(intf_objects, impls)|
                  self.error_reporter.add_error(
                      CompositionError::InterfaceObjectUsageError {
                          message: format!(
                              "[{}] @interfaceObject {} in subgraph \"{}\" share implementation {}. Each @interfaceObject type in a subgraph must have a disjoint set of implementations.",
                              subgraph.name,
                              human_readable_types(intf_objects.iter()),
                              subgraph.name,
                              human_readable_types(impls.iter()),
                          ),
                      },
                  )
                );
        }
        Ok(())
    }

    pub(crate) fn merge_interface(
        &mut self,
        itf: InterfaceTypeDefinitionPosition,
    ) -> Result<(), FederationError> {
        let has_key = self.validate_interface_keys(&itf)?;
        self.validate_interface_objects(&itf)?;

        let added = self.add_fields_shallow(itf.clone())?;

        for (dest_field, subgraph_fields) in added {
            if !has_key {
                let subgraph_types = self
                    .subgraphs
                    .iter()
                    .enumerate()
                    .map(|(idx, subgraph)| {
                        let maybe_ty: Option<ObjectOrInterfaceTypeDefinitionPosition> = subgraph
                            .schema()
                            .get_type(&itf.type_name)
                            .ok()
                            .and_then(|ty| ty.try_into().ok());
                        (idx, maybe_ty)
                    })
                    .collect();
                self.hint_on_inconsistent_value_type_field(
                    &subgraph_types,
                    &ObjectOrInterfaceTypeDefinitionPosition::Interface(itf.clone()),
                    &dest_field,
                )?;
            }
            let merge_context = self.validate_override(&subgraph_fields, &dest_field)?;
            trace!("Merging interface field {}", dest_field.field_name());
            self.merge_field(&subgraph_fields, &dest_field, &merge_context)?;
        }
        Ok(())
    }
}