use std::ops::Range;
use apollo_compiler::Name;
use apollo_compiler::Schema;
use apollo_compiler::ast::Directive;
use apollo_compiler::collections::IndexSet;
use apollo_compiler::executable::FieldSet;
use apollo_compiler::executable::Selection;
use apollo_compiler::name;
use apollo_compiler::parser::LineColumn;
use apollo_compiler::parser::Parser;
use apollo_compiler::schema::Component;
use apollo_compiler::schema::ExtendedType;
use apollo_compiler::schema::ObjectType;
use apollo_compiler::validation::Valid;
use hashbrown::HashSet;
use indexmap::IndexMap;
use itertools::Itertools;
use shape::Shape;
use shape::ShapeCase;
use shape::ShapeVisitor;
use self::keys::EntityKeyChecker;
use self::keys::field_set_error;
pub(crate) use self::keys::field_set_is_subset;
use crate::connectors::Connector;
use crate::connectors::EntityResolver::TypeBatch;
use crate::connectors::Namespace::Batch;
use crate::connectors::json_selection::SelectionTrie;
use crate::connectors::validation::Code;
use crate::connectors::validation::Message;
use crate::connectors::validation::graphql::SchemaInfo;
use crate::link::Import;
use crate::link::Link;
use crate::link::federation_spec_definition::FEDERATION_FIELDS_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_RESOLVABLE_ARGUMENT_NAME;
use crate::link::spec::Identity;
use crate::subgraph::spec::CONTEXT_DIRECTIVE_NAME;
use crate::subgraph::spec::EXTERNAL_DIRECTIVE_NAME;
use crate::subgraph::spec::FROM_CONTEXT_DIRECTIVE_NAME;
mod keys;
pub(super) fn validate(
schema: &SchemaInfo,
file_name: &str,
fields_seen_by_connectors: Vec<(Name, Name)>,
) -> Vec<Message> {
let messages: Vec<Message> = check_for_disallowed_type_definitions(schema)
.chain(check_conflicting_directives(schema))
.collect();
if !messages.is_empty() {
return messages;
}
check_seen_fields(schema, fields_seen_by_connectors)
.chain(advanced_validations(schema, file_name))
.collect()
}
fn check_for_disallowed_type_definitions(schema: &SchemaInfo) -> impl Iterator<Item = Message> {
use apollo_compiler::parser::SourceSpan;
use crate::connectors::ConnectSpec;
let subscription_name = schema
.schema_definition
.subscription
.as_ref()
.map(|sub| &sub.name);
let spec = schema.connect_link.spec;
schema
.types
.values()
.filter_map(move |extended_type| match extended_type {
ExtendedType::Union(union_type) if spec < ConnectSpec::V0_4 => {
Some(Message {
code: Code::ConnectorsUnsupportedAbstractType,
message: "Abstract schema types, such as `union`, are not supported when using connectors.".to_string(),
locations: SourceSpan::recompose(union_type.location(), union_type.name.location())
.and_then(|location| location.line_column_range(&schema.sources))
.into_iter()
.collect(),
})
}
ExtendedType::Interface(interface_type) if spec < ConnectSpec::V0_4 => {
Some(Message {
code: Code::ConnectorsUnsupportedAbstractType,
message: "Abstract schema types, such as `interface`, are not supported when using connectors.".to_string(),
locations: SourceSpan::recompose(interface_type.location(), interface_type.name.location())
.and_then(|location| location.line_column_range(&schema.sources))
.into_iter()
.collect(),
})
}
ExtendedType::Object(obj) if subscription_name.is_some_and(|name| name == &obj.name) => {
Some(Message {
code: Code::SubscriptionInConnectors,
message: format!(
"A subscription root type is not supported when using `@{connect_directive_name}`.",
connect_directive_name = schema.connect_directive_name(),
),
locations: obj.name.line_column_range(&schema.sources).into_iter().collect(),
})
}
_ => None,
})
}
fn check_conflicting_directives(schema: &Schema) -> Vec<Message> {
let Some((fed_link, fed_link_directive)) =
Link::for_identity(schema, &Identity::federation_identity())
else {
return Vec::new();
};
let imports = fed_link_directive
.specified_argument_by_name(&name!("import"))
.and_then(|arg| arg.as_list())
.into_iter()
.flatten()
.filter_map(|value| Import::from_value(value).ok().map(|import| (value, import)))
.collect_vec();
let disallowed_imports = [CONTEXT_DIRECTIVE_NAME, FROM_CONTEXT_DIRECTIVE_NAME];
fed_link
.imports
.into_iter()
.filter_map(|import| {
disallowed_imports
.contains(&import.element)
.then(|| Message {
code: Code::ConnectorsUnsupportedFederationDirective,
message: format!(
"The directive `@{import}` is not supported when using connectors.",
import = import.alias.as_ref().unwrap_or(&import.element)
),
locations: imports
.iter()
.find_map(|(value, reparsed)| {
(*reparsed == *import).then(|| value.line_column_range(&schema.sources))
})
.flatten()
.into_iter()
.collect(),
})
})
.collect()
}
fn check_seen_fields(
schema: &SchemaInfo,
fields_seen_by_connectors: Vec<(Name, Name)>,
) -> impl Iterator<Item = Message> {
let federation = Link::for_identity(schema, &Identity::federation_identity());
let external_directive_name = federation.map_or(EXTERNAL_DIRECTIVE_NAME, |(link, _)| {
link.directive_name_in_schema(&EXTERNAL_DIRECTIVE_NAME)
});
let mut all_fields = IndexSet::default();
for extended_type in schema.types.values() {
if extended_type.is_built_in() {
continue;
}
if extended_type
.directives()
.iter()
.any(|dir| dir.name == external_directive_name)
{
continue;
}
match extended_type {
ExtendedType::Object(object) => {
for (field_name, field_def) in &object.fields {
if !field_def
.directives
.iter()
.any(|dir| dir.name == external_directive_name)
{
all_fields.insert((extended_type.name().clone(), field_name.clone()));
}
}
}
ExtendedType::Interface(interface) => {
for (type_name, implementing_type) in schema.types.iter() {
if let ExtendedType::Object(obj) = implementing_type
&& obj.implements_interfaces.contains(&interface.name)
{
for (field_name, field_def) in &obj.fields {
if !field_def
.directives
.iter()
.any(|dir| dir.name == external_directive_name)
{
all_fields.insert((type_name.clone(), field_name.clone()));
}
}
}
}
}
_ => {}
}
}
let mut seen_fields = fields_seen_by_resolvable_keys(schema);
seen_fields.extend(fields_seen_by_connectors);
(&all_fields - &seen_fields).into_iter().map(move |(parent_type, field_name)| {
let Ok(field_def) = schema.type_field(&parent_type, &field_name) else {
return Message {
code: Code::GraphQLError,
message: format!(
"Field `{parent_type}.{field_name}` is missing from the schema.",
),
locations: Vec::new(),
};
};
Message {
code: Code::ConnectorsUnresolvedField,
message: format!(
"No connector resolves field `{parent_type}.{field_name}`. It must have a `@{connect_directive_name}` directive or appear in `@{connect_directive_name}(selection:)`.",
connect_directive_name = schema.connect_directive_name()
),
locations: field_def.line_column_range(&schema.sources).into_iter().collect(),
}
})
}
fn fields_seen_by_resolvable_keys(schema: &SchemaInfo) -> IndexSet<(Name, Name)> {
let mut seen_fields = IndexSet::default();
let objects = schema.types.values().filter_map(|node| node.as_object());
let mut selections: Vec<(Name, Selection)> = objects
.clone()
.flat_map(|object| {
resolvable_key_fields(object, schema).flat_map(|(field_set, _)| {
field_set
.selection_set
.selections
.iter()
.map(|selection| (object.name.clone(), selection.clone()))
.collect::<Vec<_>>()
})
})
.collect();
while !selections.is_empty() {
if let Some((type_name, selection)) = selections.pop()
&& let Some(field) = selection.as_field()
{
let t = (type_name, field.name.clone());
if !seen_fields.contains(&t) {
seen_fields.insert(t);
field.selection_set.selections.iter().for_each(|selection| {
selections.push((field.ty().inner_named_type().clone(), selection.clone()));
});
}
}
}
seen_fields
}
fn resolvable_key_fields<'a>(
object: &'a ObjectType,
schema: &'a Schema,
) -> impl Iterator<Item = (FieldSet, &'a Component<Directive>)> {
object
.directives
.iter()
.filter(|directive| directive.name == FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC)
.filter(|directive| {
directive
.arguments
.iter()
.find(|arg| arg.name == FEDERATION_RESOLVABLE_ARGUMENT_NAME)
.and_then(|arg| arg.value.to_bool())
.unwrap_or(true)
})
.filter_map(|directive| {
directive
.arguments
.iter()
.find(|arg| arg.name == FEDERATION_FIELDS_ARGUMENT_NAME)
.map(|arg| &arg.value)
.and_then(|value| value.as_str())
.and_then(|fields_str| {
Parser::new()
.parse_field_set(
Valid::assume_valid_ref(schema),
object.name.clone(),
fields_str.to_string(),
"",
)
.ok()
.map(|field_set| (field_set, directive))
})
})
}
fn advanced_validations(schema: &SchemaInfo, subgraph_name: &str) -> Vec<Message> {
let mut messages = Vec::new();
let Ok(connectors) = Connector::from_schema(schema, subgraph_name) else {
return messages;
};
let mut entity_checker = EntityKeyChecker::default();
for (field_set, directive) in find_all_resolvable_keys(schema) {
entity_checker.add_key(&field_set, directive);
}
for connector in &connectors {
if connector.entity_resolver == Some(TypeBatch) {
let input_trie = compute_batch_input_trie(connector);
match SelectionSetWalker::new(connector.name(), schema, &input_trie)
.walk(&connector.selection.shape(), connector)
{
Ok(res) => messages.extend(res),
Err(err) => messages.push(err),
}
}
}
for connector in connectors {
match connector.resolvable_key(schema) {
Ok(None) => continue,
Err(_) => {
let variables = connector.variable_references().collect_vec();
messages.push(field_set_error(&variables, &connector, schema));
}
Ok(Some(field_set)) => {
entity_checker.add_connector(field_set, &connector.selection.shape());
}
}
}
if !messages.is_empty() {
return messages;
}
entity_checker.check_for_missing_entity_connectors(schema)
}
fn compute_batch_input_trie(connector: &Connector) -> SelectionTrie {
let mut trie = SelectionTrie::new();
connector
.variable_references()
.filter(|var| var.namespace.namespace == Batch)
.for_each(|var| {
let _ = &trie.extend(&var.selection);
});
trie
}
struct SelectionSetWalker<'walker> {
name: Name,
schema: &'walker SchemaInfo<'walker>,
trie: &'walker SelectionTrie,
unmapped_fields: HashSet<String>,
}
impl<'walker> SelectionSetWalker<'walker> {
fn new(name: Name, schema: &'walker SchemaInfo<'walker>, trie: &'walker SelectionTrie) -> Self {
SelectionSetWalker {
name,
schema,
trie,
unmapped_fields: HashSet::new(),
}
}
}
#[derive(Debug, thiserror::Error)]
enum ShapeVisitorError<'error> {
#[error(
"The `@connect` directive on `{connector}` specifies a `$batch` entity resolver, but the field `{unset}` could not be found in `@connect(selection: ...)`"
)]
BatchKeyNotSubsetOfOutputShape {
connector: String,
unset: &'error String,
locations: Vec<Range<LineColumn>>,
},
#[error("Attempted to resolve key on unexpected shape `{shape_str}`")]
UnexpectedKeyOnShape {
shape_str: String,
locations: Vec<Range<LineColumn>>,
},
#[error(
"`$batch` fields must be mapped from the API response body. Variables such as `$context` and `$this` are not supported"
)]
NonRootBatch(Vec<Range<LineColumn>>),
}
impl From<ShapeVisitorError<'_>> for Message {
fn from(value: ShapeVisitorError) -> Self {
match &value {
ShapeVisitorError::BatchKeyNotSubsetOfOutputShape { locations, .. } => Message {
code: Code::ConnectorsBatchKeyNotInSelection,
message: value.to_string(),
locations: locations.clone(),
},
ShapeVisitorError::UnexpectedKeyOnShape { locations, .. } => Message {
code: Code::ConnectorsUnresolvedField,
message: value.to_string(),
locations: locations.clone(),
},
ShapeVisitorError::NonRootBatch(locations) => Message {
code: Code::ConnectorsNonRootBatchKey,
message: value.to_string(),
locations: locations.clone(),
},
}
}
}
impl SelectionSetWalker<'_> {
const ROOT_SHAPE: &'static str = "$root";
fn walk(
mut self,
output_shape: &Shape,
connector: &Connector,
) -> Result<Vec<Message>, Message> {
output_shape.visit_shape(&mut self)?;
let mut vec = Vec::new();
for unset in &self.unmapped_fields {
vec.push(
ShapeVisitorError::BatchKeyNotSubsetOfOutputShape {
connector: connector.id.directive.simple_name(),
unset,
locations: self
.name
.line_column_range(&self.schema.sources)
.into_iter()
.collect(),
}
.into(),
);
}
Ok(vec)
}
}
impl<'walker> ShapeVisitor for SelectionSetWalker<'walker> {
type Error = ShapeVisitorError<'walker>;
type Output = ();
fn default(&mut self, shape: &Shape) -> Result<Self::Output, Self::Error> {
Err(ShapeVisitorError::UnexpectedKeyOnShape {
shape_str: shape.pretty_print(),
locations: self
.name
.line_column_range(&self.schema.sources)
.into_iter()
.collect(),
})
}
fn visit_object(
&mut self,
_: &Shape,
fields: &IndexMap<String, Shape>,
_: &Shape,
) -> Result<Self::Output, Self::Error> {
for (key, sub_selection) in self.trie.iter() {
let Some(next_shape) = fields.get(key) else {
self.unmapped_fields.insert(key.to_string());
continue;
};
if let ShapeCase::Name(name, _) = next_shape.case() {
let base_name = name.base_shape_name();
if base_name != Self::ROOT_SHAPE {
return Err(ShapeVisitorError::NonRootBatch(
self.name
.line_column_range(&self.schema.sources)
.into_iter()
.collect(),
));
}
}
if sub_selection.is_empty() {
continue;
}
let mut nested = SelectionSetWalker::new(self.name.clone(), self.schema, sub_selection);
next_shape.visit_shape(&mut nested)?;
self.unmapped_fields
.extend(nested.unmapped_fields.into_iter());
}
Ok(())
}
}
fn find_all_resolvable_keys(schema: &Schema) -> Vec<(FieldSet, &Component<Directive>)> {
schema
.types
.values()
.filter_map(|extended_type| extended_type.as_object())
.flat_map(|object| resolvable_key_fields(object, schema))
.collect()
}