use std::collections::HashMap;
use std::collections::HashSet;
use apollo_compiler::Name;
use apollo_compiler::Node;
use apollo_compiler::ast::Argument;
use apollo_compiler::ast::DirectiveList;
use apollo_compiler::ast::InputValueDefinition;
use apollo_compiler::ast::Type;
use apollo_compiler::ast::Value;
use apollo_compiler::name;
use apollo_compiler::schema::Component;
use apollo_compiler::schema::Directive;
use apollo_compiler::schema::ExtendedType;
use apollo_compiler::schema::FieldDefinition;
use indexmap::IndexMap;
use indexmap::IndexSet;
use tracing::instrument;
use tracing::trace;
use crate::bail;
use crate::error::CompositionError;
use crate::error::FederationError;
use crate::error::HasLocations;
use crate::error::SubgraphLocation;
use crate::link::federation_spec_definition::FEDERATION_CONTEXT_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_EXTERNAL_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_FIELD_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_FIELDS_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_FROM_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_GRAPH_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_NAME_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_SELECTION_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_TYPE_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_USED_OVERRIDEN_ARGUMENT_NAME;
use crate::link::join_spec_definition::JOIN_OVERRIDE_LABEL_ARGUMENT_NAME;
use crate::merger::merge::Merger;
use crate::merger::merge::Sources;
use crate::merger::merge::map_sources;
use crate::merger::merge_argument::HasArguments;
use crate::schema::blueprint::FEDERATION_OPERATION_FIELDS;
use crate::schema::position::DirectiveTargetPosition;
use crate::schema::position::FieldDefinitionPosition;
use crate::schema::position::HasAppliedDirectives;
use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
use crate::schema::position::ObjectFieldDefinitionPosition;
use crate::schema::position::ObjectOrInterfaceFieldDefinitionPosition;
use crate::schema::position::ObjectOrInterfaceTypeDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
use crate::schema::position::TypeDefinitionPosition;
use crate::schema::validators::from_context::parse_context;
use crate::utils::human_readable::human_readable_subgraph_names;
use crate::utils::human_readable::human_readable_types;
#[derive(Debug, Clone)]
struct SubgraphWithIndex {
subgraph: String,
idx: usize,
}
#[derive(Debug, Clone)]
struct SubgraphField {
subgraph: SubgraphWithIndex,
field: FieldDefinitionPosition,
}
trait HasSubgraph {
fn subgraph(&self) -> &str;
}
impl HasSubgraph for SubgraphWithIndex {
fn subgraph(&self) -> &str {
&self.subgraph
}
}
impl HasSubgraph for SubgraphField {
fn subgraph(&self) -> &str {
&self.subgraph.subgraph
}
}
impl SubgraphField {
fn locations(&self, merger: &Merger) -> Vec<SubgraphLocation> {
let Some(subgraph) = merger.subgraphs.get(self.subgraph.idx) else {
return Vec::new();
};
self.field.locations(subgraph)
}
}
impl Merger {
pub(crate) fn add_fields_shallow<T>(
&mut self,
ty: T,
) -> Result<
IndexMap<
ObjectOrInterfaceFieldDefinitionPosition,
Sources<ObjectOrInterfaceFieldDefinitionPosition>,
>,
FederationError,
>
where
T: Into<ObjectOrInterfaceTypeDefinitionPosition>,
{
let obj_or_itf: ObjectOrInterfaceTypeDefinitionPosition = ty.into();
trace!("Adding fields shallow for type {}", obj_or_itf);
let mut added: IndexMap<
ObjectOrInterfaceFieldDefinitionPosition,
Sources<ObjectOrInterfaceFieldDefinitionPosition>,
> = Default::default();
let mut fields_to_add: IndexMap<usize, IndexSet<ObjectOrInterfaceFieldDefinitionPosition>> =
Default::default();
let mut field_types: HashMap<ObjectOrInterfaceFieldDefinitionPosition, Type> =
Default::default();
let mut extra_sources: Sources<ObjectOrInterfaceFieldDefinitionPosition> =
Default::default();
trace!("Gathering fields to add for type {}", obj_or_itf);
for (idx, subgraph) in self.subgraphs.iter().enumerate() {
for interface in obj_or_itf.implemented_interfaces(&self.merged)? {
if subgraph
.schema()
.get_type(interface.name.clone())
.as_ref()
.is_ok_and(|ty| subgraph.is_interface_object_type(ty))
{
extra_sources.insert(idx, None);
}
}
if let Some(type_position) = subgraph
.schema()
.try_get_type(obj_or_itf.type_name().clone())
{
let object_or_interface_in_subgraph: ObjectOrInterfaceTypeDefinitionPosition =
type_position.try_into()?;
for field in object_or_interface_in_subgraph.fields(subgraph.schema().schema())? {
let field_node = field.get(subgraph.schema().schema())?;
field_types.insert(field.clone(), field_node.ty.clone());
fields_to_add.entry(idx).or_default().insert(field);
}
extra_sources.insert(idx, None);
}
}
trace!("Adding fields to supergraph schema for type {}", obj_or_itf);
for (idx, field_set) in fields_to_add {
for subgraph_field in field_set {
let is_merged_field = !self.subgraphs[idx]
.schema()
.is_root_type(subgraph_field.type_name())
|| !FEDERATION_OPERATION_FIELDS.contains(subgraph_field.field_name());
if !is_merged_field {
continue;
}
let supergraph_field = obj_or_itf.field(subgraph_field.field_name().clone());
if !added.contains_key(&supergraph_field)
&& let Some(ty) = field_types.get(&subgraph_field)
{
supergraph_field.insert(
&mut self.merged,
Component::new(FieldDefinition {
description: None,
name: supergraph_field.field_name().clone(),
arguments: vec![],
ty: ty.clone(),
directives: Default::default(),
}),
)?;
}
added
.entry(supergraph_field.clone())
.or_insert_with(|| extra_sources.clone())
.insert(idx, Some(subgraph_field));
}
}
Ok(added)
}
#[instrument(skip(self, sources, merge_context))]
pub(crate) fn merge_field(
&mut self,
sources: &Sources<ObjectOrInterfaceFieldDefinitionPosition>,
dest: &ObjectOrInterfaceFieldDefinitionPosition,
merge_context: &FieldMergeContext,
) -> Result<(), FederationError> {
let every_source_is_external = sources.iter().all(|(i, source)| {
let Some(metadata) = self.subgraphs.get(*i).map(|s| s.metadata()) else {
return false;
};
match source {
None => self
.fields_in_source_if_abstracted_by_interface_object(dest, *i)
.unwrap_or_default()
.into_iter()
.all(|f| metadata.external_metadata().is_external(&f.into())),
Some(obj_or_itf) => metadata
.external_metadata()
.is_external(&obj_or_itf.clone().into()),
}
});
if every_source_is_external {
let defining_subgraphs: Vec<String> = sources
.iter()
.filter_map(|(i, source)| {
match source {
Some(_source_field) => {
Some(self.names[*i].clone())
}
None => {
let itf_object_fields = self
.fields_in_source_if_abstracted_by_interface_object(dest, *i)
.unwrap_or_default();
if itf_object_fields.is_empty() {
return None;
}
Some(format!(
"{} (through @interfaceObject {})",
self.names[*i],
human_readable_types(
itf_object_fields.iter().map(|f| f.type_name.clone())
)
))
}
}
})
.collect();
let error = CompositionError::ExternalMissingOnBase {
message: format!(
"Field \"{}\" is marked @external on all the subgraphs in which it is listed ({}).",
dest,
human_readable_subgraph_names(defining_subgraphs.iter())
),
};
self.error_reporter.add_error(error);
return Ok(());
}
let without_external = self.validate_and_filter_external(sources);
self.merge_description(&without_external, dest)?;
self.record_applied_directives_to_merge(&without_external, dest)?;
let arg_names = self.add_arguments_shallow(&without_external, dest)?;
for arg_name in arg_names {
let subgraph_args = map_sources(&without_external, |field| {
field
.as_ref()
.map(|f| f.argument_position(arg_name.clone()))
});
let dest_arg = dest.argument_position(arg_name);
self.merge_argument(&subgraph_args, &dest_arg)?;
}
let sources_for_type_merge =
if Self::some_sources(&without_external, |source, _| source.is_some()) {
&without_external
} else {
sources
};
let all_types_equal = self.merge_type_reference(sources_for_type_merge, dest, false)?;
let field_sources: Sources<FieldDefinitionPosition> = sources
.iter()
.map(|(idx, source)| {
let field_pos = source.clone().map(|pos| pos.into());
(*idx, field_pos)
})
.collect();
if self.has_external(&field_sources) {
self.validate_external_fields(&field_sources, &dest.clone().into(), all_types_equal)?;
}
self.add_join_field(sources, dest, all_types_equal, merge_context)?;
self.add_join_directive_directives(sources, dest)?;
Ok(())
}
pub(in crate::merger) fn fields_in_source_if_abstracted_by_interface_object(
&self,
dest_field: &ObjectOrInterfaceFieldDefinitionPosition,
source_idx: usize,
) -> Result<Vec<ObjectFieldDefinitionPosition>, FederationError> {
let mut interface_object_fields = Vec::new();
let parent_name_in_supergraph = dest_field.type_name();
let subgraph = &self.subgraphs[source_idx];
if matches!(
dest_field,
ObjectOrInterfaceFieldDefinitionPosition::Interface(_)
) || subgraph
.schema()
.try_get_type(parent_name_in_supergraph.clone())
.is_some()
{
return Ok(interface_object_fields);
}
for itf in dest_field.parent().implemented_interfaces(&self.merged)? {
let itf_as_obj = ObjectTypeDefinitionPosition {
type_name: itf.name.clone(),
};
if subgraph
.is_interface_object_type(&TypeDefinitionPosition::Object(itf_as_obj.clone()))
{
let field = itf_as_obj.field(dest_field.field_name().clone());
if field.try_get(subgraph.schema().schema()).is_some() {
interface_object_fields.push(field);
}
}
}
Ok(interface_object_fields)
}
fn validate_and_filter_external(
&mut self,
sources: &Sources<ObjectOrInterfaceFieldDefinitionPosition>,
) -> Sources<ObjectOrInterfaceFieldDefinitionPosition> {
let mut filtered: Sources<ObjectOrInterfaceFieldDefinitionPosition> = sources.clone();
for (idx, source) in sources {
let Some(source) = source else {
continue;
};
let subgraph = &self.subgraphs[*idx];
if subgraph
.metadata()
.is_field_external(&source.clone().into())
{
filtered.insert(*idx, None);
self.validate_external_field_directives(*idx, source);
}
}
filtered
}
fn validate_external_field_directives(
&mut self,
source_idx: usize,
field_pos: &ObjectOrInterfaceFieldDefinitionPosition,
) {
let Ok(field_def) = field_pos.get(self.subgraphs[source_idx].validated_schema().schema())
else {
return;
};
for directive in &field_def.directives {
if self.is_merged_directive(&self.names[source_idx], directive) {
let error = CompositionError::MergedDirectiveApplicationOnExternal {
message: format!(
"[{}] Cannot apply merged directive {} to external field \"{field_pos}\"",
self.names[source_idx], directive,
),
};
self.error_reporter.add_error(error);
}
}
}
pub(in crate::merger) fn is_field_external(
&self,
source_idx: usize,
field: &FieldDefinitionPosition,
) -> bool {
self.subgraphs[source_idx]
.metadata()
.is_field_external(field)
}
fn has_external(&self, sources: &Sources<FieldDefinitionPosition>) -> bool {
Self::some_sources(sources, |source, idx| {
source
.as_ref()
.is_some_and(|pos| self.is_field_external(idx, pos))
})
}
fn validate_external_fields(
&mut self,
sources: &Sources<FieldDefinitionPosition>,
dest: &FieldDefinitionPosition,
all_types_equal: bool,
) -> Result<(), FederationError> {
let dest_field = dest.get(self.merged.schema())?;
let dest_field_ty = dest_field.ty.clone();
let dest_args = dest_field.arguments.to_vec();
let mut has_invalid_types = false;
let mut invalid_args_presence = HashSet::new();
let mut invalid_args_types = HashSet::new();
let mut invalid_args_defaults = HashSet::new();
for (source_idx, source) in sources.iter() {
let Some(source_field_pos) = source else {
continue;
};
if !self.is_field_external(*source_idx, source_field_pos) {
continue;
}
let source_field =
source_field_pos.get(self.subgraphs[*source_idx].validated_schema().schema())?;
let source_args = source_field.arguments.to_vec();
let is_subtype = if all_types_equal {
false
} else {
self.is_strict_subtype(&dest_field_ty, &source_field.ty)
.unwrap_or(false)
};
if !(dest_field_ty == source_field.ty || is_subtype) {
has_invalid_types = true;
}
for dest_arg in &dest_args {
let name = &dest_arg.name;
let Some(source_arg) = source_args.iter().find(|arg| &arg.name == name) else {
invalid_args_presence.insert(name.clone());
continue;
};
let arg_is_subtype = self
.is_strict_subtype(&source_arg.ty, &dest_arg.ty)
.unwrap_or(false);
if dest_arg.ty != source_arg.ty && !arg_is_subtype {
invalid_args_types.insert(name.clone());
}
if dest_arg.default_value != source_arg.default_value {
invalid_args_defaults.insert(name.clone());
}
}
}
if has_invalid_types {
self.error_reporter.report_mismatch_error(
CompositionError::ExternalTypeMismatch {
message: format!(
"Type of field \"{dest}\" is incompatible across subgraphs (where marked @external): it has ",
),
},
dest_field,
sources,
&self.subgraphs,
|d| Some(format!("type \"{}\"", d.ty)),
|s, idx| s.try_get(self.subgraphs[idx].schema().schema()).map(|f| format!("type \"{}\"", f.ty)),
);
}
for arg_name in &invalid_args_presence {
let argument_pos = ObjectFieldArgumentDefinitionPosition {
type_name: dest.type_name().clone(),
field_name: dest.field_name().clone(),
argument_name: arg_name.clone(),
};
self.error_reporter.report_mismatch_error_with_specifics(
CompositionError::ExternalArgumentMissing {
message: format!(
"Field \"{dest}\" is missing argument \"{argument_pos}\" in some subgraphs where it is marked @external: "
),
},
&argument_pos,
&self.argument_sources(sources, arg_name)?,
&self.subgraphs,
|_| Some("present".to_string()),
|_, _idx| Some("present".to_string()),
|_, names| format!("argument \"{argument_pos}\" is declared in {} ", names.unwrap_or("undefined".to_string())),
|_, names| format!("but not in {names} (where \"{dest}\" is @external)."),
true,
);
}
for arg_name in &invalid_args_types {
let argument_pos = ObjectFieldArgumentDefinitionPosition {
type_name: dest.type_name().clone(),
field_name: dest.field_name().clone(),
argument_name: arg_name.clone(),
};
self.error_reporter.report_mismatch_error(
CompositionError::ExternalArgumentTypeMismatch {
message: format!(
"Type of argument \"{argument_pos}\" is incompatible across subgraphs (where \"{dest}\" is marked @external): it has ",
),
},
&argument_pos,
&self.argument_sources(sources, arg_name)?,
&self.subgraphs,
|d| d.try_get(self.merged.schema()).map(|a| format!("type \"{}\"", a.ty)),
|s, idx| s.try_get(self.subgraphs[idx].schema().schema()).map(|a| format!("type \"{}\"", a.ty)),
);
}
for arg_name in &invalid_args_defaults {
let argument_pos = ObjectFieldArgumentDefinitionPosition {
type_name: dest.type_name().clone(),
field_name: dest.field_name().clone(),
argument_name: arg_name.clone(),
};
self.error_reporter.report_mismatch_error(
CompositionError::ExternalArgumentDefaultMismatch {
message: format!(
"Argument \"{argument_pos}\" has incompatible defaults across subgraphs (where \"{dest}\" is marked @external): it has ",
),
},
&argument_pos,
&self.argument_sources(sources, arg_name)?,
&self.subgraphs,
|d| d.try_get(self.merged.schema())
.and_then(|f| Some(format!("default value {}", f.default_value.as_ref()?))),
|s, idx| s.try_get(self.subgraphs[idx].schema().schema())
.map(|f| if let Some(def) = &f.default_value {
format!("default value {}", def)
} else {
"no default value".to_string()
})
,
);
}
Ok(())
}
fn argument_sources(
&self,
sources: &Sources<FieldDefinitionPosition>,
dest_arg_name: &Name,
) -> Result<Sources<ObjectFieldArgumentDefinitionPosition>, FederationError> {
let mut arg_sources = IndexMap::with_capacity_and_hasher(sources.len(), Default::default());
for (source_idx, source_field_pos) in sources.iter() {
let arg_position = if let Some(field_pos) = source_field_pos {
let field_def =
field_pos.get(self.subgraphs[*source_idx].validated_schema().schema())?;
if field_def
.arguments
.iter()
.any(|arg| &arg.name == dest_arg_name)
{
Some(ObjectFieldArgumentDefinitionPosition {
type_name: field_pos.type_name().clone(),
field_name: field_pos.field_name().clone(),
argument_name: dest_arg_name.clone(),
})
} else {
None
}
} else {
None
};
arg_sources.insert(*source_idx, arg_position);
}
Ok(arg_sources)
}
pub(crate) fn validate_field_sharing(
&mut self,
sources: &Sources<ObjectOrInterfaceFieldDefinitionPosition>,
dest: &ObjectOrInterfaceFieldDefinitionPosition,
merge_context: &FieldMergeContext,
) -> Result<(), FederationError> {
let mut shareable_sources: Vec<SubgraphWithIndex> = Vec::with_capacity(sources.len());
let mut non_shareable_sources: Vec<SubgraphWithIndex> = Vec::with_capacity(sources.len());
let mut all_resolving: Vec<SubgraphField> = Vec::with_capacity(sources.len());
let mut categorize_field =
|idx: usize, subgraph: String, field: &ObjectOrInterfaceFieldDefinitionPosition| {
let field = field.clone().into();
if !self.subgraphs[idx]
.metadata()
.is_field_fully_external(&field)
{
all_resolving.push(SubgraphField {
subgraph: SubgraphWithIndex {
subgraph: subgraph.clone(),
idx,
},
field: field.clone(),
});
if self.subgraphs[idx].metadata().is_field_shareable(&field) {
shareable_sources.push(SubgraphWithIndex { subgraph, idx });
} else {
non_shareable_sources.push(SubgraphWithIndex { subgraph, idx });
}
}
};
for (idx, source) in sources.iter() {
match source {
None => {
let itf_object_fields =
self.fields_in_source_if_abstracted_by_interface_object(dest, *idx)?;
for field in itf_object_fields {
let subgraph_str = format!(
"{} (through @interfaceObject field \"{}.{}\")",
self.names[*idx], field.type_name, field.field_name
);
categorize_field(*idx, subgraph_str, &field.into());
}
}
Some(source) => {
if !merge_context.is_used_overridden(*idx)
&& !merge_context.is_unused_overridden(*idx)
{
let subgraph = self.names[*idx].clone();
categorize_field(*idx, subgraph, source);
}
}
}
}
fn print_subgraphs<T: HasSubgraph>(arr: &[T]) -> String {
human_readable_subgraph_names(arr.iter().map(|s| s.subgraph()))
}
if !non_shareable_sources.is_empty()
&& (!shareable_sources.is_empty() || non_shareable_sources.len() > 1)
{
let resolving_subgraphs = print_subgraphs(&all_resolving);
let non_shareables = if shareable_sources.is_empty() {
"all of them".to_string()
} else {
print_subgraphs(&non_shareable_sources)
};
let subgraph_with_targetless_override = non_shareable_sources
.iter()
.find(|s| merge_context.has_override_with_unknown_target(s.idx));
let extra_hint = if let Some(s) = subgraph_with_targetless_override {
format!(
" (please note that \"{}.{}\" has an @override directive in \"{}\" that targets an unknown subgraph so this could be due to misspelling the @override(from:) argument)",
dest.type_name(),
dest.field_name(),
s.subgraph,
)
} else {
"".to_string()
};
self.error_reporter.add_error(CompositionError::InvalidFieldSharing {
message: format!(
"Non-shareable field \"{}.{}\" is resolved from multiple subgraphs: it is resolved from {} and defined as non-shareable in {}{}",
dest.type_name(),
dest.field_name(),
resolving_subgraphs,
non_shareables,
extra_hint,
),
locations: all_resolving.iter().flat_map(|field|
field.locations(self)
).collect(),
});
}
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub(crate) struct FieldMergeContextProperties {
pub used_overridden: bool,
pub unused_overridden: bool,
pub override_with_unknown_target: bool,
pub override_label: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct FieldMergeContext {
props: HashMap<usize, FieldMergeContextProperties>,
}
impl FieldMergeContext {
pub(crate) fn new<I: IntoIterator<Item = usize>>(indices: I) -> Self {
let mut props = HashMap::new();
for i in indices {
props.insert(i, FieldMergeContextProperties::default());
}
FieldMergeContext { props }
}
pub(crate) fn is_used_overridden(&self, idx: usize) -> bool {
self.props
.get(&idx)
.map(|p| p.used_overridden)
.unwrap_or(false)
}
pub(crate) fn is_unused_overridden(&self, idx: usize) -> bool {
self.props
.get(&idx)
.map(|p| p.unused_overridden)
.unwrap_or(false)
}
pub(crate) fn set_used_overridden(&mut self, idx: usize) {
if let Some(p) = self.props.get_mut(&idx) {
p.used_overridden = true;
}
}
pub(crate) fn set_unused_overridden(&mut self, idx: usize) {
if let Some(p) = self.props.get_mut(&idx) {
p.unused_overridden = true;
}
}
pub(crate) fn set_override_with_unknown_target(&mut self, idx: usize) {
if let Some(p) = self.props.get_mut(&idx) {
p.override_with_unknown_target = true;
}
}
pub(crate) fn set_override_label(&mut self, idx: usize, label: String) {
if let Some(p) = self.props.get_mut(&idx) {
p.override_label = Some(label);
}
}
pub(crate) fn override_label(&self, idx: usize) -> Option<&str> {
self.props
.get(&idx)
.and_then(|p| p.override_label.as_deref())
}
pub(crate) fn has_override_with_unknown_target(&self, idx: usize) -> bool {
self.props
.get(&idx)
.map(|p| p.override_with_unknown_target)
.unwrap_or(false)
}
pub(crate) fn some<F>(&self, mut predicate: F) -> bool
where
F: FnMut(&FieldMergeContextProperties, usize) -> bool,
{
self.props.iter().any(|(&i, p)| predicate(p, i))
}
}
enum JoinableField<'a> {
Output(&'a FieldDefinition),
Input(&'a InputValueDefinition),
}
impl<'a> JoinableField<'a> {
fn ty(&self) -> &Type {
match self {
JoinableField::Output(field) => &field.ty,
JoinableField::Input(input) => &input.ty,
}
}
fn directives(&self) -> &DirectiveList {
match self {
JoinableField::Output(field) => &field.directives,
JoinableField::Input(input) => &input.directives,
}
}
fn arguments(&self) -> Vec<Node<InputValueDefinition>> {
match self {
JoinableField::Output(field) => field.arguments.clone(),
JoinableField::Input(input) => vec![Node::new((*input).clone())],
}
}
}
impl Merger {
pub(crate) fn add_join_field<T>(
&mut self,
sources: &Sources<T>,
dest: &T,
all_types_equal: bool,
merge_context: &FieldMergeContext,
) -> Result<(), FederationError>
where
T: Clone + Into<DirectiveTargetPosition>,
{
let dest = dest.clone().into();
let parent_name = match &dest {
DirectiveTargetPosition::ObjectField(pos) => pos.type_name.clone(),
DirectiveTargetPosition::InterfaceField(pos) => pos.type_name.clone(),
DirectiveTargetPosition::InputObjectField(pos) => pos.type_name.clone(),
_ => {
bail!("Invalid DirectiveTargetPosition for join field: {:?}", dest);
}
};
trace!("Checking if join__field is needed for field {dest}");
match self.needs_join_field(sources, &parent_name, all_types_equal, merge_context) {
Ok(needs) if !needs => {
trace!("Field {dest} does not need join__field");
return Ok(());
} Err(_) => {
trace!("Error implies parent of {dest} does not exist, skipping join__field");
return Ok(());
} Ok(_) => {} }
let sources_with_override = sources.iter().filter_map(|(&idx, source_opt)| {
let used_overridden = merge_context.is_used_overridden(idx);
let unused_overridden = merge_context.is_unused_overridden(idx);
let override_label = merge_context.override_label(idx);
match source_opt {
None => None,
Some(_) if unused_overridden && override_label.is_none() => None,
Some(source) => Some((idx, source, used_overridden, override_label)),
}
});
trace!(
"Found {} sources with override",
sources_with_override.clone().count()
);
for (idx, source, used_overridden, override_label) in sources_with_override {
let Some(graph_name) = self.subgraph_names_to_join_spec_name.get(&self.names[idx])
else {
trace!(
"Skipping join__field for subgraph index {} as it has no graph enum value",
idx
);
continue;
};
let graph_value = Value::Enum(graph_name.clone());
let source = source.clone().into();
let field_def = match &source {
DirectiveTargetPosition::ObjectField(pos) => {
let def = pos
.get(self.subgraphs[idx].schema().schema())
.map_err(|err| {
FederationError::internal(format!(
"Cannot find object field definition for subgraph {}: {}",
self.subgraphs[idx].name, err
))
})?;
JoinableField::Output(def)
}
DirectiveTargetPosition::InterfaceField(pos) => {
let def = pos
.get(self.subgraphs[idx].schema().schema())
.map_err(|err| {
FederationError::internal(format!(
"Cannot find interface field definition for subgraph {}: {}",
self.subgraphs[idx].name, err
))
})?;
JoinableField::Output(def)
}
DirectiveTargetPosition::InputObjectField(pos) => {
let def = pos
.get(self.subgraphs[idx].schema().schema())
.map_err(|err| {
FederationError::internal(format!(
"Cannot find input object field definition for subgraph {}: {}",
self.subgraphs[idx].name, err
))
})?;
JoinableField::Input(def)
}
_ => {
trace!("Skipping join__field for non-field position: {:?}", source);
continue;
}
};
let type_string = field_def.ty().to_string();
let subgraph = &self.subgraphs[idx];
let external = source
.try_into()
.is_ok_and(|pos| self.is_field_external(idx, &pos));
let requires = self.get_field_set(
&field_def,
subgraph.requires_directive_name().ok().flatten().as_ref(),
);
let provides = self.get_field_set(
&field_def,
subgraph.provides_directive_name().ok().flatten().as_ref(),
);
let has_unknown_target = merge_context.has_override_with_unknown_target(idx);
let override_from = if has_unknown_target {
None
} else {
self.get_override_from(
&field_def,
subgraph.override_directive_name().ok().flatten().as_ref(),
)
};
let context_arguments = self.extract_context_arguments(idx, &field_def)?;
let mut builder = JoinFieldBuilder::new()
.arg(&FEDERATION_GRAPH_ARGUMENT_NAME, graph_value)
.maybe_bool_arg(&FEDERATION_EXTERNAL_DIRECTIVE_NAME_IN_SPEC, external)
.maybe_arg(&FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC, requires)
.maybe_arg(&FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC, provides)
.maybe_arg(&FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC, override_from)
.maybe_arg(&JOIN_OVERRIDE_LABEL_ARGUMENT_NAME, override_label)
.maybe_bool_arg(&FEDERATION_USED_OVERRIDEN_ARGUMENT_NAME, used_overridden)
.maybe_arg(&FEDERATION_CONTEXT_ARGUMENT_NAME, context_arguments.clone());
if !all_types_equal && !type_string.is_empty() {
builder = builder.arg(&FEDERATION_TYPE_ARGUMENT_NAME, type_string);
}
let directive = builder.build();
dest.insert_directive(&mut self.merged, directive)?;
}
Ok(())
}
#[allow(dead_code)]
pub(crate) fn needs_join_field<T>(
&self,
sources: &Sources<T>,
parent_name: &Name,
all_types_equal: bool,
merge_context: &FieldMergeContext,
) -> Result<bool, FederationError>
where
T: Clone + Into<DirectiveTargetPosition>,
{
if !all_types_equal {
return Ok(true);
}
if merge_context.some(|props, _| props.used_overridden || props.override_label.is_some()) {
return Ok(true);
}
let sources = map_sources(sources, |source| source.clone().map(|s| s.into()));
for (&idx, source_opt) in &sources {
if let Some(source_pos) = source_opt
&& let Some(subgraph) = self.subgraphs.get(idx)
&& let Ok(Some(override_directive_name)) = subgraph.override_directive_name()
{
let has_override = match source_pos {
DirectiveTargetPosition::ObjectField(pos) => !pos
.get_applied_directives(subgraph.schema(), &override_directive_name)
.is_empty(),
DirectiveTargetPosition::InterfaceField(pos) => !pos
.get_applied_directives(subgraph.schema(), &override_directive_name)
.is_empty(),
_ => false,
};
if has_override {
return Ok(true);
}
}
}
for source in sources.values().flatten() {
match source {
DirectiveTargetPosition::ObjectField(obj_field) => {
if self
.fields_with_from_context
.object_fields
.contains(obj_field)
{
return Ok(true);
}
}
DirectiveTargetPosition::InterfaceField(intf_field) => {
if self
.fields_with_from_context
.interface_fields
.contains(intf_field)
{
return Ok(true);
}
}
_ => continue, }
}
let has_interface_object = self.subgraphs.iter().any(|subgraph| {
let obj_pos = ObjectTypeDefinitionPosition {
type_name: parent_name.clone(),
};
subgraph.is_interface_object_type(&obj_pos.into())
});
if has_interface_object {
return Ok(true);
}
for (&idx, source_opt) in &sources {
let subgraph = &self.subgraphs[idx];
let provides_directive_name = subgraph.provides_directive_name().ok().flatten();
let requires_directive_name = subgraph.requires_directive_name().ok().flatten();
let overridden = merge_context.is_unused_overridden(idx);
match source_opt {
Some(source_pos) => {
if !overridden {
let is_external = match source_pos {
DirectiveTargetPosition::ObjectField(pos) => self.is_field_external(
idx,
&FieldDefinitionPosition::Object(pos.clone()),
),
DirectiveTargetPosition::InterfaceField(pos) => self.is_field_external(
idx,
&FieldDefinitionPosition::Interface(pos.clone()),
),
_ => false, };
if is_external {
return Ok(true);
}
if provides_directive_name.is_some_and(|provides| {
!source_pos
.get_applied_directives(subgraph.schema(), &provides)
.is_empty()
}) {
return Ok(true);
}
if requires_directive_name.is_some_and(|requires| {
!source_pos
.get_applied_directives(subgraph.schema(), &requires)
.is_empty()
}) {
return Ok(true);
}
}
let field_name = match source_pos {
DirectiveTargetPosition::ObjectField(pos) => &pos.field_name,
DirectiveTargetPosition::InterfaceField(pos) => &pos.field_name,
DirectiveTargetPosition::InputObjectField(pos) => &pos.field_name,
_ => {
continue;
}
};
for subgraph in self.subgraphs.iter() {
let parent_ty = subgraph.schema().schema().types.get(parent_name);
if let Some(ExtendedType::Object(obj)) = parent_ty
&& !obj.fields.contains_key(field_name)
{
return Ok(true);
}
if let Some(ExtendedType::Interface(intf)) = parent_ty
&& !intf.fields.contains_key(field_name)
{
return Ok(true);
}
}
}
None => {
if subgraph
.schema()
.try_get_type(parent_name.clone())
.is_some()
{
return Ok(true);
}
}
}
}
Ok(false)
}
#[allow(dead_code)]
fn extract_context_arguments(
&self,
idx: usize,
source: &JoinableField,
) -> Result<Option<Value>, FederationError> {
let subgraph_name = self.subgraphs[idx].name.clone();
let Ok(Some(from_context_name)) = self.subgraphs[idx].from_context_directive_name() else {
return Ok(None);
};
let directive_name = &from_context_name;
let mut context_args: Vec<Node<Value>> = vec![];
for arg in source.arguments().iter() {
let Some(directive) = arg.directives.get(directive_name) else {
continue;
};
let Some(field) = directive
.specified_argument_by_name(&FEDERATION_FIELD_ARGUMENT_NAME)
.and_then(|v| v.as_str())
else {
continue;
};
let (Some(context), Some(selection)) = parse_context(field) else {
continue;
};
let prefixed_context = format!("{subgraph_name}__{context}");
context_args.push(Node::new(Value::Object(vec![
(name!("context"), Node::new(Value::String(prefixed_context))),
(
FEDERATION_NAME_ARGUMENT_NAME,
Node::new(Value::String(arg.name.to_string())),
),
(
FEDERATION_TYPE_ARGUMENT_NAME,
Node::new(Value::String(arg.ty.to_string())),
),
(
FEDERATION_SELECTION_ARGUMENT_NAME,
Node::new(Value::String(selection)),
),
])));
}
Ok((!context_args.is_empty()).then(|| Value::List(context_args)))
}
#[allow(dead_code)]
fn get_field_set(
&self,
field_def: &JoinableField,
directive_name: Option<&Name>,
) -> Option<String> {
let directive_name = directive_name?;
field_def
.directives()
.get(directive_name)?
.specified_argument_by_name(&FEDERATION_FIELDS_ARGUMENT_NAME)?
.as_str()
.map(|v| v.to_string())
}
#[allow(dead_code)]
fn get_override_from(
&self,
field_def: &JoinableField,
directive_name: Option<&Name>,
) -> Option<String> {
let directive_name = directive_name?;
Some(
field_def
.directives()
.get(directive_name)?
.specified_argument_by_name(&FEDERATION_FROM_ARGUMENT_NAME)?
.as_str()?
.to_string(),
)
}
}
#[allow(dead_code)]
pub(crate) struct JoinFieldBuilder {
arguments: Vec<Node<Argument>>,
}
#[allow(dead_code)]
impl JoinFieldBuilder {
pub(crate) fn new() -> Self {
Self {
arguments: Vec::new(),
}
}
pub(crate) fn arg<T: Into<Value>>(mut self, key: &Name, value: T) -> Self {
self.arguments.push(Node::new(Argument {
name: key.clone(),
value: Node::new(value.into()),
}));
self
}
pub(crate) fn maybe_arg<T: Into<Value>>(self, key: &Name, value: Option<T>) -> Self {
if let Some(v) = value {
self.arg(key, v)
} else {
self
}
}
pub(crate) fn maybe_bool_arg(self, key: &Name, condition: bool) -> Self {
if condition {
self.arg(key, Value::Boolean(true))
} else {
self
}
}
pub(crate) fn build(self) -> Directive {
Directive {
name: name!("join__field"),
arguments: self.arguments,
}
}
}