use anyhow::anyhow;
use apollo_compiler::{
Name, Node, Schema,
ast::{
Definition, Directive, DirectiveList, Document, FieldDefinition, InputValueDefinition,
OperationType, SchemaDefinition, Type,
},
collections::IndexSet,
name,
schema::{Component, ComponentName, ComponentOrigin, ExtendedType, ObjectType, UnionType},
};
use tracing::warn;
mod definitions;
#[derive(Debug)]
pub enum FederationType {
Subgraph,
Supergraph,
None,
}
pub fn patch_ast(ast: &mut Document) -> FederationType {
let schema_extension = ast.definitions.iter().find_map(|def| match def {
Definition::SchemaExtension(node) => Some(node),
_ => None,
});
let fed_type = if ast
.definitions
.iter()
.any(|definition| definition.name().is_some_and(|name| name == "join__Graph"))
{
FederationType::Supergraph
} else if let Some(extension) = schema_extension
&& extension.directives.iter().any(|dir| dir.name == "link")
{
if !ast
.definitions
.iter()
.any(|def| matches!(def, Definition::SchemaDefinition(_)))
{
ast.definitions
.push(Definition::SchemaDefinition(Node::new(SchemaDefinition {
description: None,
directives: Default::default(),
root_operations: vec![Node::new((OperationType::Query, name!("Query")))],
})));
}
ast.definitions
.append(&mut definitions::federation_directives());
FederationType::Subgraph
} else {
FederationType::None
};
if let FederationType::Subgraph | FederationType::Supergraph = fed_type {
for def in &ast.definitions {
if let Definition::SchemaDefinition(schema_def) = def {
process_link_directives(&schema_def.directives);
}
if let Definition::SchemaExtension(schema_ext) = def {
process_link_directives(&schema_ext.directives);
}
}
}
fed_type
}
pub fn patch_schema(schema: &mut Schema, federation_type: FederationType) -> anyhow::Result<()> {
let members: IndexSet<ComponentName> = schema
.types
.iter()
.filter(|(_, ty)| ty.is_object() && is_federated_type(schema, ty))
.map(|(name, _)| ComponentName {
origin: ComponentOrigin::Definition,
name: name.clone(),
})
.collect();
let has_federated_members = !members.is_empty();
if has_federated_members {
schema.types.insert(
name!("_Entity"),
ExtendedType::Union(Node::new(UnionType {
description: None,
name: name!("_Entity"),
directives: Default::default(),
members,
})),
);
}
definitions::insert_federation_types(schema, &federation_type);
let query_type_name = if let FederationType::Subgraph = federation_type {
if schema.schema_definition.query.is_none() {
schema.schema_definition.make_mut().query = Some(name!("Query").into());
schema.types.insert(
name!("Query"),
Node::new(ObjectType {
description: None,
name: name!("Query"),
implements_interfaces: Default::default(),
directives: Default::default(),
fields: Default::default(),
})
.into(),
);
}
let query_type_name: &Name = schema.schema_definition.query.as_ref().unwrap();
if !schema.types.contains_key(query_type_name) {
schema.types.insert(
query_type_name.clone(),
Node::new(ObjectType {
description: None,
name: query_type_name.clone(),
implements_interfaces: Default::default(),
directives: Default::default(),
fields: Default::default(),
})
.into(),
);
}
query_type_name
} else {
schema
.schema_definition
.query
.as_ref()
.ok_or_else(|| anyhow!("Schema does not define a query type"))?
};
let query_root = match schema.types.get_mut(query_type_name).unwrap() {
ExtendedType::Object(obj) => obj,
_ => return Err(anyhow!("query root is not an object")),
};
if has_federated_members {
query_root.make_mut().fields.insert(
name!("_entities"),
Component::new(FieldDefinition {
description: None,
name: name!("_entities"),
arguments: vec![Node::new(InputValueDefinition {
description: None,
name: name!("representations"),
ty: Node::new(Type::NonNullList(Box::new(Type::NonNullNamed(name!(
"_Any"
))))),
default_value: None,
directives: Default::default(),
})],
ty: Type::NonNullList(Box::new(Type::Named(name!("_Entity")))),
directives: Default::default(),
}),
);
}
query_root.make_mut().fields.insert(
name!("_service"),
Component::new(FieldDefinition {
description: None,
name: name!("_service"),
arguments: vec![],
ty: Type::NonNullNamed(name!("_Service")),
directives: Default::default(),
}),
);
if let FederationType::Supergraph = federation_type {
if !schema.directive_definitions.contains_key(&name!("defer")) {
schema
.directive_definitions
.insert(name!("defer"), definitions::defer_definition());
}
if !schema.directive_definitions.contains_key(&name!("stream")) {
schema
.directive_definitions
.insert(name!("stream"), definitions::stream_definition());
}
}
Ok(())
}
fn is_federated_type(schema: &Schema, ty: &ExtendedType) -> bool {
ty.directives().iter().any(|directive| {
is_federated_directive(schema, directive)
&& schema
.schema_definition
.query
.as_ref()
.is_none_or(|query| &query.name != ty.name())
})
}
fn is_federated_directive(schema: &Schema, directive: &Component<Directive>) -> bool {
match directive.name.as_str() {
"key" | "join__type" => {
directive
.argument_by_name("resolvable", schema)
.ok()
.and_then(|arg| arg.to_bool())
.expect("the @key and @join__type directives specify 'resolvable' as a boolean argument")
}
_ => false,
}
}
fn process_link_directives(directives: &DirectiveList) {
for directive in directives {
if directive.name == "link" {
warn!("@link directive detected, but link directive resolution is not implemented.")
}
}
}