graphql-composition 0.12.2

An implementation of GraphQL federated schema composition
Documentation
mod context;
mod directive_definitions;
mod directives;
mod entity_interface;
mod enums;
mod fields;
mod input_object;
mod interface;
mod object;
mod reserved_names;
mod roots;
mod scalar;
mod validate;

pub(crate) use self::context::Context as ComposeContext;

use self::{context::Context, directives::collect_composed_directives, input_object::*};
use crate::{
    composition_ir as ir,
    diagnostics::CompositeSchemasPreMergeValidationErrorCode,
    federated_graph as federated,
    subgraphs::{self, DefinitionKind, DefinitionView, StringId},
};
use directives::create_join_type_from_definitions;
use itertools::Itertools;
use std::collections::{BTreeSet, HashSet};

pub(crate) fn compose_subgraphs(ctx: &mut Context<'_>) {
    ctx.subgraphs.iter_definition_groups(|definitions| {
        let Some(first) = definitions.first() else {
            return;
        };

        if reserved_names::validate_definition_names(definitions, ctx) {
            return;
        };

        if entity_interface::is_entity_interface(ctx.subgraphs, definitions.iter().map(|def| def.id)) {
            return entity_interface::merge_entity_interface_definitions(ctx, *first, definitions);
        }

        match first.kind {
            DefinitionKind::Object => merge_object_definitions(ctx, first, definitions),
            DefinitionKind::Union => merge_union_definitions(ctx, first, definitions),
            DefinitionKind::InputObject => merge_input_object_definitions(ctx, first, definitions),
            DefinitionKind::Interface => interface::merge_interface_definitions(ctx, first, definitions),
            DefinitionKind::Scalar => scalar::merge_scalar_definitions(*first, definitions, ctx),
            DefinitionKind::Enum => enums::merge_enum_definitions(first, definitions, ctx),
        }
    });

    roots::merge_root_fields(ctx);
    directive_definitions::compose_directive_definitions(ctx);
}

fn merge_object_definitions<'a>(ctx: &mut Context<'a>, first: &DefinitionView<'a>, definitions: &[DefinitionView<'a>]) {
    let is_shareable = definitions
        .iter()
        .any(|definition| definition.directives.shareable(ctx.subgraphs));

    if let Some(incompatible) = definitions
        .iter()
        .find(|definition| definition.kind != DefinitionKind::Object)
    {
        let first_kind = first.kind;
        let second_kind = incompatible.kind;
        let name = ctx.subgraphs[first.name].as_ref();
        let first_subgraph = ctx.subgraphs[ctx.subgraphs.at(first.subgraph_id).name].as_ref();
        let second_subgraph = ctx.subgraphs[ctx.subgraphs.at(incompatible.subgraph_id).name].as_ref();
        ctx.diagnostics.push_composite_schemas_pre_merge_validation_error(format!(
            "Cannot merge {first_kind:?} with {second_kind:?} (`{name}` in `{first_subgraph}` and `{second_subgraph}`)",
        ), CompositeSchemasPreMergeValidationErrorCode::TypeKindMismatch);
        return;
    }

    let is_entity = validate_consistent_entityness(ctx, definitions);

    let description = definitions
        .iter()
        .find_map(|def| def.description)
        .map(|desc| ctx.subgraphs[desc].as_ref());
    let mut directives = collect_composed_directives(definitions.iter().map(|def| def.directives), ctx);

    if is_entity {
        directives.extend(
            definitions
                .iter()
                .flat_map(|def| def.id.keys(ctx.subgraphs))
                .map(|key| {
                    ir::Directive::JoinType(ir::JoinTypeDirective {
                        subgraph_id: federated::SubgraphId::from(ctx.subgraphs.at(key.definition_id).subgraph_id.idx()),
                        key: Some(key.id),
                        is_interface_object: false,
                    })
                }),
        );
    } else {
        directives.extend(create_join_type_from_definitions(definitions));
    }
    let object_name = ctx.insert_string(first.name);
    ctx.insert_object(object_name, description, directives);

    if is_shareable {
        object::validate_shareable_object_fields_match(definitions, ctx);
    }

    let fields = object::compose_fields(ctx, definitions, object_name);
    for field in fields {
        ctx.insert_field(field);
    }
}

fn validate_consistent_entityness(ctx: &mut Context<'_>, definitions: &[DefinitionView<'_>]) -> bool {
    let is_entity = definitions.iter().any(|def| def.id.is_entity(ctx.subgraphs));

    if !is_entity {
        return false;
    }

    if definitions
        .iter()
        .all(|def| def.id.is_entity(ctx.subgraphs) || ctx.subgraphs.at(def.subgraph_id).federation_spec.is_apollo_v1())
    {
        return true;
    }

    let Some(definition) = definitions.first() else {
        return false;
    };
    let mut non_entity_fed_v2 = Vec::new();
    let mut entity_definitions = Vec::new();

    for definition in definitions {
        let definition = ctx.subgraphs.at(definition.id);
        let subgraph = ctx.subgraphs.at(definition.subgraph_id);
        let definition_is_entity = definition.id.is_entity(ctx.subgraphs);
        let subgraph_name = &ctx.subgraphs[subgraph.name];

        if definition_is_entity {
            entity_definitions.push(subgraph_name);
        } else if subgraph.federation_spec.is_apollo_v2() {
            non_entity_fed_v2.push(subgraph_name);
        }
    }

    ctx.diagnostics.push_fatal(format!(
        "The `{name}` object is an entity in subgraphs {entity_definitions} but not in subgraphs {non_entity_subgraphs}.",
        name = ctx.subgraphs[definition.name],
        entity_definitions = entity_definitions
            .into_iter()
            .join(", "),
        non_entity_subgraphs = non_entity_fed_v2
            .into_iter()
            .join(", "),
    ));

    false
}

fn merge_union_definitions(
    ctx: &mut Context<'_>,
    first_union: &DefinitionView<'_>,
    definitions: &[DefinitionView<'_>],
) {
    let union_name = ctx.insert_string(first_union.name);

    let description = definitions
        .iter()
        .find_map(|def| def.description)
        .map(|desc| ctx.subgraphs[desc].as_ref());
    let mut directives = collect_composed_directives(definitions.iter().map(|def| def.directives), ctx);

    for member in definitions
        .iter()
        .flat_map(|def| ctx.subgraphs.iter_union_members(def.id))
    {
        directives.push(ir::Directive::JoinUnionMember(ir::JoinUnionMemberDirective { member }));

        let member_name = ctx.insert_string(ctx.subgraphs.at(member).name);
        ctx.insert_union_member(union_name, member_name);
    }

    ctx.insert_union(union_name, directives, description);
}