hive-router 0.3.0

GraphQL router for Federation, part of the Hive platform
pub(crate) mod prune_inacessible;
pub(crate) mod strip_schema_internals;

use std::{
    hash::{Hash as _, Hasher},
    sync::Arc,
};

use graphql_tools::static_graphql::schema::{Definition, Document, TypeDefinition};
use prune_inacessible::PruneInaccessible;
use strip_schema_internals::StripSchemaInternals;

#[derive(Debug)]
pub struct ConsumerSchema {
    pub document: Arc<Document>,
    pub hash: u64,
}

impl From<Arc<Document>> for ConsumerSchema {
    fn from(document: Arc<Document>) -> Self {
        let hash = {
            let mut hasher = xxhash_rust::xxh3::Xxh3::new();
            document.hash(&mut hasher);
            hasher.finish()
        };
        Self { document, hash }
    }
}

impl ConsumerSchema {
    pub fn new_from_supergraph(supergraph: &Document) -> Self {
        let document: Arc<Document> = Self::create_consumer_schema(supergraph).into();
        document.into()
    }

    fn create_consumer_schema(supergraph: &Document) -> Document {
        let mut result = PruneInaccessible::prune(supergraph);
        result = StripSchemaInternals::strip_schema_internals(&result);
        // Add introspection schema to the consumer schema
        let introspection_schema = include_str!("introspection_schema.graphql");
        let mut parsed_introspection_schema =
            graphql_tools::parser::schema::parse_schema(introspection_schema).unwrap();

        let query_type_name = result
            .query_type_name()
            .map(|name| name.to_string())
            // SAFETY: Supergraph is guaranteed to have a query type, it's one of the validation rules
            .expect("Query type not found in schema");

        parsed_introspection_schema
            .definitions
            .iter_mut()
            .for_each(|def| {
                match def {
                    Definition::TypeDefinition(TypeDefinition::Object(
                        type_def_in_introspection,
                    )) => {
                        if type_def_in_introspection.name == "Query" {
                            match result.definitions.iter_mut().find(|d| {
                                if let Definition::TypeDefinition(TypeDefinition::Object(
                                    query_def,
                                )) = d
                                {
                                    query_def.name == query_type_name
                                } else {
                                    false
                                }
                            }) {
                                Some(Definition::TypeDefinition(TypeDefinition::Object(
                                    query_def,
                                ))) => {
                                    // Query type already exists, extend it
                                    query_def
                                        .fields
                                        .extend(type_def_in_introspection.fields.clone());
                                }
                                _ => {
                                    // Add the Query type from introspection schema
                                    result.definitions.push(def.clone());
                                }
                            }
                        } else {
                            // Add other types from introspection schema
                            result.definitions.push(def.clone());
                        }
                    }
                    Definition::DirectiveDefinition(directive_def_in_introspection) => {
                        // Supergraphs may already define built-in directives (e.g. `@oneOf`),
                        // keep theirs to avoid duplicate definitions
                        let already_defined = result.definitions.iter().any(|d| {
                            matches!(
                                d,
                                Definition::DirectiveDefinition(existing)
                                    if existing.name == directive_def_in_introspection.name
                            )
                        });
                        if !already_defined {
                            result.definitions.push(def.clone());
                        }
                    }
                    _ => result.definitions.push(def.clone()),
                }
            });
        result
    }
}

#[cfg(test)]
mod test {
    use graphql_tools::static_graphql::schema::Definition;

    use crate::query_planner::utils::parsing::parse_schema;

    use super::ConsumerSchema;

    #[test]
    fn does_not_duplicate_built_in_directives_defined_in_supergraph() {
        let supergraph = parse_schema(
            r#"
            schema
              @link(url: "https://specs.apollo.dev/link/v1.0")
              @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
              query: Query
            }

            directive @oneOf on INPUT_OBJECT
            directive @deprecated(
              reason: String = "No longer supported"
            ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE

            input Filter @oneOf {
              id: ID
              name: String
            }

            type Query {
              search(filter: Filter!, old: String @deprecated): String
            }
            "#,
        );

        let consumer_schema = ConsumerSchema::new_from_supergraph(&supergraph);
        let directive_count = |name: &str| {
            consumer_schema
                .document
                .definitions
                .iter()
                .filter(|d| matches!(d, Definition::DirectiveDefinition(dir) if dir.name == name))
                .count()
        };

        for name in ["oneOf", "deprecated", "skip", "include", "specifiedBy"] {
            assert_eq!(directive_count(name), 1, "@{name} should be defined once");
        }
    }
}