use std::fmt::Display;
use std::iter::once;
use std::ops::Range;
use apollo_compiler::Node;
use apollo_compiler::ast::FieldDefinition;
use apollo_compiler::ast::Value;
use apollo_compiler::collections::IndexSet;
use apollo_compiler::parser::LineColumn;
use apollo_compiler::schema::ExtendedType;
use itertools::Itertools;
use shape::Shape;
use shape::ShapeCase;
use shape::location::Location;
use shape::location::SourceId;
use self::variables::VariableResolver;
use super::Code;
use super::Message;
use super::Name;
use crate::connectors::ConnectSpec;
use crate::connectors::JSONSelection;
use crate::connectors::Namespace;
use crate::connectors::PathSelection;
use crate::connectors::SubSelection;
use crate::connectors::expand::visitors::FieldVisitor;
use crate::connectors::expand::visitors::GroupVisitor;
use crate::connectors::id::ConnectedElement;
use crate::connectors::json_selection::NamedSelection;
use crate::connectors::json_selection::Ranged;
use crate::connectors::json_selection::VarPaths;
use crate::connectors::schema_type_ref::SchemaTypeRef;
use crate::connectors::spec::connect::CONNECT_SELECTION_ARGUMENT_NAME;
use crate::connectors::validation::coordinates::ConnectDirectiveCoordinate;
use crate::connectors::validation::coordinates::SelectionCoordinate;
use crate::connectors::validation::expression::MappingArgument;
use crate::connectors::validation::expression::parse_mapping_argument;
use crate::connectors::validation::graphql::SchemaInfo;
use crate::connectors::validation::graphql::subslice_location;
use crate::connectors::variable::Phase;
use crate::connectors::variable::Target;
use crate::connectors::variable::VariableContext;
mod variables;
pub(super) struct Selection<'schema> {
parsed: JSONSelection,
node: Node<Value>,
coordinate: SelectionCoordinate<'schema>,
}
impl<'schema> Selection<'schema> {
pub(super) fn parse(
connect_directive: ConnectDirectiveCoordinate<'schema>,
schema: &'schema SchemaInfo<'schema>,
) -> Result<Self, Message> {
let coordinate = SelectionCoordinate::from(connect_directive);
let selection_arg = connect_directive
.directive
.arguments
.iter()
.find(|arg| arg.name == CONNECT_SELECTION_ARGUMENT_NAME)
.ok_or_else(|| Message {
code: Code::GraphQLError,
message: format!("{coordinate} is required."),
locations: connect_directive
.directive
.line_column_range(&schema.sources)
.into_iter()
.collect(),
})?;
let MappingArgument { expression, node } = parse_mapping_argument(
&selection_arg.value,
coordinate,
Code::InvalidSelection,
schema,
)?;
Ok(Self {
parsed: expression.expression,
node,
coordinate,
})
}
pub(super) fn type_check(self, schema: &SchemaInfo) -> Result<Vec<(Name, Name)>, Message> {
let coordinate = self.coordinate.connect;
let context = VariableContext::new(&coordinate.element, Phase::Response, Target::Body);
validate_selection_variables(
&VariableResolver::new(context.clone(), schema),
self.coordinate,
&self.node,
schema,
context,
self.parsed.external_var_paths(),
&self.parsed.local_var_names(),
)?;
if schema.connect_link.spec < ConnectSpec::V0_4 {
self.type_check_legacy(schema)
} else {
self.type_check_shape_based(schema)
}
}
fn type_check_shape_based(self, schema: &SchemaInfo) -> Result<Vec<(Name, Name)>, Message> {
let coordinate = self.coordinate.connect;
let shape = self.parsed.shape();
match coordinate.element {
ConnectedElement::Field {
parent_type,
field_def,
..
} => {
let return_type_name = field_def.ty.inner_named_type();
let Some(return_type_ref) = SchemaTypeRef::new(schema, return_type_name) else {
return Ok(Vec::new());
};
let concrete_type_refs = match return_type_ref.extended() {
ExtendedType::Object(_) => vec![return_type_ref],
ExtendedType::Interface(interface_type) => schema
.types
.values()
.filter_map(|t| {
if let ExtendedType::Object(o) = t {
if o.implements_interfaces.contains(&interface_type.name) {
SchemaTypeRef::new(schema, &o.name)
} else {
None
}
} else {
None
}
})
.collect(),
ExtendedType::Union(union_type) => union_type
.members
.iter()
.filter_map(|member_name| SchemaTypeRef::new(schema, member_name))
.collect(),
_ => {
return Ok(Vec::new());
}
};
if concrete_type_refs.is_empty() {
return Ok(Vec::new());
}
let mut validator = SelectionValidator::new(
schema,
PathPart::Root(parent_type),
&self.node,
self.coordinate,
);
validator.path.push(PathPart::Field {
definition: field_def,
ty: parent_type,
});
validator.seen_fields.clear();
let mut all_resolved_fields = Vec::new();
for concrete_type_ref in concrete_type_refs {
match validator.walk_selection_with_shape(concrete_type_ref, &shape) {
Ok(mut fields) => all_resolved_fields.append(&mut fields),
Err(err) => return Err(err),
}
}
Ok(all_resolved_fields)
}
ConnectedElement::Type { type_ref } => {
if self.parsed.next_subselection().is_none() {
return Ok(Vec::new());
}
if !type_ref.is_object() {
return Err(Message {
code: Code::GraphQLError,
message: format!(
"Root type definition {} is not an object type",
type_ref.name()
),
locations: vec![],
});
}
let mut validator = SelectionValidator::new(
schema,
PathPart::Root(type_ref),
&self.node,
self.coordinate,
);
validator.seen_fields.clear();
validator.walk_selection_with_shape(type_ref, &shape)
}
}
}
pub(super) fn variables(&self) -> impl Iterator<Item = Namespace> + '_ {
self.parsed
.variable_references()
.map(|var_ref| var_ref.namespace.namespace)
}
fn type_check_legacy(self, schema: &SchemaInfo) -> Result<Vec<(Name, Name)>, Message> {
let coordinate = self.coordinate.connect;
match coordinate.element {
ConnectedElement::Field {
parent_type,
field_def,
..
} => {
let field_type_name = field_def.ty.inner_named_type();
let Some(return_type) = schema.types.get(field_type_name) else {
return Ok(Vec::new());
};
let ExtendedType::Object(return_type_obj) = return_type else {
return Ok(Vec::new());
};
let Some(sub_selection) = self.parsed.next_subselection() else {
return Ok(Vec::new());
};
let ExtendedType::Object(parent_type_obj) = parent_type.extended() else {
return Err(Message {
code: Code::GraphQLError,
message: "Parent type is not an object type".to_string(),
locations: vec![],
});
};
let group = LegacyGroup {
selection: sub_selection,
ty: return_type_obj,
definition: Some(field_def),
};
LegacySelectionValidator::new(
schema,
LegacyPathPart::Root(parent_type_obj),
&self.node,
self.coordinate,
)
.walk(group)
.map(|validator| validator.seen_fields)
}
ConnectedElement::Type { type_ref } => {
let Some(sub_selection) = self.parsed.next_subselection() else {
return Ok(Vec::new());
};
let ExtendedType::Object(type_obj) = type_ref.extended() else {
return Err(Message {
code: Code::GraphQLError,
message: "Type definition is not an object type".to_string(),
locations: vec![],
});
};
let group = LegacyGroup {
selection: sub_selection,
ty: type_obj,
definition: None,
};
LegacySelectionValidator::new(
schema,
LegacyPathPart::Root(type_obj),
&self.node,
self.coordinate,
)
.walk(group)
.map(|validator| validator.seen_fields)
}
}
}
}
pub(super) fn validate_selection_variables<'a>(
variable_resolver: &VariableResolver,
coordinate: impl Display,
selection_str: &Node<Value>,
schema: &SchemaInfo,
context: VariableContext,
variable_paths: impl IntoIterator<Item = &'a PathSelection>,
local_var_names: &IndexSet<String>,
) -> Result<(), Message> {
for path in variable_paths {
if let Some(reference) = path.variable_reference() {
variable_resolver
.resolve(&reference, selection_str)
.map_err(|mut err| {
err.message = format!("In {coordinate}: {message}", message = err.message);
err
})?;
} else if let Some(reference) = path.variable_reference::<String>() {
let locals_suffix = {
let local_var_vec = local_var_names.iter().cloned().collect::<Vec<_>>();
if local_var_vec.is_empty() {
"".to_string()
} else {
format!(", {}", local_var_vec.join(", "))
}
};
return Err(Message {
code: context.error_code(),
message: format!(
"In {coordinate}: unknown variable `{namespace}`, must be one of {available}{locals}",
namespace = reference.namespace.namespace.as_str(),
available = context.namespaces_joined(),
locals = locals_suffix,
),
locations: reference
.namespace
.location
.iter()
.flat_map(|range| subslice_location(selection_str, range.clone(), schema))
.collect(),
});
}
}
Ok(())
}
struct SelectionValidator<'schema> {
schema: &'schema SchemaInfo<'schema>,
root: PathPart<'schema>,
path: Vec<PathPart<'schema>>,
node: &'schema Node<Value>,
coordinate: SelectionCoordinate<'schema>,
seen_fields: Vec<(Name, Name)>,
}
impl<'schema> SelectionValidator<'schema> {
const fn new(
schema: &'schema SchemaInfo<'schema>,
root: PathPart<'schema>,
node: &'schema Node<Value>,
coordinate: SelectionCoordinate<'schema>,
) -> Self {
Self {
schema,
root,
path: Vec::new(),
node,
coordinate,
seen_fields: Vec::new(),
}
}
}
impl<'schema> SelectionValidator<'schema> {
fn check_for_circular_reference(
&self,
field_def: &Node<FieldDefinition>,
current_ty: SchemaTypeRef<'schema>,
) -> Result<(), Message> {
for (depth, seen_part) in self.path_with_root().enumerate() {
let (seen_type, ancestor_field) = match seen_part {
PathPart::Root(root) => (root, None),
PathPart::Field { ty, definition } => (ty, Some(definition)),
};
if seen_type == current_ty {
return Err(Message {
code: Code::CircularReference,
message: format!(
"Circular reference detected in {coordinate}: type `{type_name}` appears more than once in `{selection_path}`. For more information, see https://go.apollo.dev/connectors/limitations#circular-references",
coordinate = &self.coordinate,
selection_path = self
.path_with_root()
.map(|part| match part {
PathPart::Root(ty) => ty.name().as_str(),
PathPart::Field { definition, .. } => definition.name.as_str(),
})
.join("."),
type_name = current_ty.name(),
),
locations: if depth > 1 {
ancestor_field
.and_then(|def| def.line_column_range(&self.schema.sources))
.into_iter()
.chain(field_def.line_column_range(&self.schema.sources))
.collect()
} else {
field_def
.line_column_range(&self.schema.sources)
.into_iter()
.collect()
},
});
}
}
Ok(())
}
fn get_shape_locations<'a>(
&self,
shape_locations: impl IntoIterator<Item = &'a Location>,
) -> Vec<Range<LineColumn>> {
shape_locations
.into_iter()
.filter_map(|location| match &location.source_id {
SourceId::GraphQL(file_id) => self
.schema
.sources
.get(file_id)
.and_then(|source| source.get_line_column_range(location.span.clone())),
SourceId::Other(_) => {
subslice_location(self.node, location.span.clone(), self.schema)
}
})
.collect()
}
fn path_with_root(&self) -> impl Iterator<Item = PathPart<'_>> {
once(self.root).chain(self.path.iter().copied())
}
fn walk_selection_with_shape(
&mut self,
type_ref: SchemaTypeRef<'schema>,
shape: &Shape,
) -> Result<Vec<(Name, Name)>, Message> {
match shape.case() {
ShapeCase::Object { fields, .. } => {
for (field_name, field_shape) in fields.iter() {
if field_name == "__typename" {
continue;
}
let fields_by_type_name = type_ref.get_fields(field_name.as_str());
if fields_by_type_name.is_empty() {
return Err(Message {
code: Code::SelectedFieldNotFound,
message: format!(
"{} contains field `{field_name}`, which does not exist on `{}`.",
self.coordinate,
type_ref.name(),
),
locations: self.get_shape_locations(field_shape.locations()),
});
}
for (type_name, field_component) in fields_by_type_name.iter() {
let field_def = &field_component.node;
let Some(type_ref) = SchemaTypeRef::new(self.schema, type_name) else {
continue;
};
self.path.push(PathPart::Field {
definition: field_def,
ty: type_ref,
});
let inner_type_name = field_def.ty.inner_named_type();
if let Some(field_type_ref) =
SchemaTypeRef::new(self.schema, inner_type_name)
{
self.check_for_circular_reference(field_def, field_type_ref)?;
}
if !field_def.arguments.is_empty() {
let mut locations = self.get_shape_locations(field_shape.locations());
if let Some(def_location) =
field_def.line_column_range(&self.schema.sources)
{
locations.push(def_location);
}
return Err(Message {
code: Code::ConnectorsFieldWithArguments,
message: format!(
"{coordinate} selects field `{parent_type}.{field_name}`, which has arguments. Only fields with a connector can have arguments.",
coordinate = &self.coordinate,
parent_type = type_ref.name(),
field_name = field_name,
),
locations,
});
}
self.seen_fields
.push((type_ref.name().clone(), field_def.name.clone()));
let has_subselection = match field_shape.case() {
ShapeCase::Object { .. } => true,
ShapeCase::Array { .. } => true,
_ => false, };
if let Some(field_type_ref) =
SchemaTypeRef::new(self.schema, inner_type_name)
{
match (field_type_ref.extended(), has_subselection) {
(ExtendedType::Object(_), true) => {
self.walk_selection_with_shape(field_type_ref, field_shape)?;
}
(_, true) => {
return Err(Message {
code: Code::GroupSelectionIsNotObject,
message: format!(
"{} selects a group `{} {{}}`, but `{}.{}` is of type `{}` which is not an object.",
self.coordinate,
field_name,
type_ref.name(),
field_name,
inner_type_name,
),
locations: self
.get_shape_locations(field_shape.locations()),
});
}
_ => {
}
}
}
self.path.pop();
}
}
Ok(self.seen_fields.clone())
}
ShapeCase::One(shapes) => {
let mut all_seen_fields = Vec::new();
let mut all_errors = Vec::new();
for (index, member_shape) in shapes.iter().enumerate() {
match self.walk_selection_with_shape(type_ref, member_shape) {
Ok(mut fields) => all_seen_fields.append(&mut fields),
Err(e) => all_errors.push((index, e)),
}
}
if !all_seen_fields.is_empty() || all_errors.is_empty() {
Ok(all_seen_fields)
} else {
Err(Message {
code: Code::GraphQLError,
message: format!(
"No matching shape found for selection. Attempted {} different shape variations and all failed.",
all_errors.len()
),
locations: all_errors
.first()
.map(|(_, e)| e.locations.clone())
.unwrap_or_default(),
})
}
}
_ => Ok(Vec::new()), }
}
}
#[derive(Clone, Copy, Debug)]
enum PathPart<'a> {
Root(SchemaTypeRef<'a>),
Field {
definition: &'a Node<FieldDefinition>,
ty: SchemaTypeRef<'a>,
},
}
use apollo_compiler::schema::ObjectType;
struct LegacySelectionValidator<'schema> {
schema: &'schema SchemaInfo<'schema>,
root: LegacyPathPart<'schema>,
path: Vec<LegacyPathPart<'schema>>,
node: &'schema Node<Value>,
coordinate: SelectionCoordinate<'schema>,
seen_fields: Vec<(Name, Name)>,
}
impl<'schema> LegacySelectionValidator<'schema> {
const fn new(
schema: &'schema SchemaInfo<'schema>,
root: LegacyPathPart<'schema>,
node: &'schema Node<Value>,
coordinate: SelectionCoordinate<'schema>,
) -> Self {
Self {
schema,
root,
path: Vec::new(),
node,
coordinate,
seen_fields: Vec::new(),
}
}
fn check_for_circular_reference(
&self,
field: LegacyField,
object: &Node<ObjectType>,
) -> Result<(), Message> {
for (depth, seen_part) in self.path_with_root().enumerate() {
let (seen_type, ancestor_field) = match seen_part {
LegacyPathPart::Root(root) => (root, None),
LegacyPathPart::Field { ty, definition } => (ty, Some(definition)),
};
if seen_type == object {
return Err(Message {
code: Code::CircularReference,
message: format!(
"Circular reference detected in {coordinate}: type `{new_object_name}` appears more than once in `{selection_path}`. For more information, see https://go.apollo.dev/connectors/limitations#circular-references",
coordinate = &self.coordinate,
selection_path = self.path_string(field.definition),
new_object_name = object.name,
),
locations: self
.get_range_location(field.inner_range())
.chain(if depth > 1 {
ancestor_field
.and_then(|def| def.line_column_range(&self.schema.sources))
} else {
None
})
.chain(field.definition.line_column_range(&self.schema.sources))
.collect(),
});
}
}
Ok(())
}
fn get_selection_location(
&self,
selection: &impl Ranged,
) -> impl Iterator<Item = Range<LineColumn>> {
selection
.range()
.and_then(|range| subslice_location(self.node, range, self.schema))
.into_iter()
}
fn get_range_location(
&self,
selection: Option<Range<usize>>,
) -> impl Iterator<Item = Range<LineColumn>> {
selection
.as_ref()
.and_then(|range| subslice_location(self.node, range.clone(), self.schema))
.into_iter()
}
fn path_with_root(&self) -> impl Iterator<Item = LegacyPathPart<'_>> {
once(self.root).chain(self.path.iter().copied())
}
fn path_string(&self, tail: &FieldDefinition) -> String {
self.path_with_root()
.map(|part| match part {
LegacyPathPart::Root(ty) => ty.name.as_str(),
LegacyPathPart::Field { definition, .. } => definition.name.as_str(),
})
.chain(once(tail.name.as_str()))
.join(".")
}
fn last_field(&self) -> &LegacyPathPart<'_> {
self.path.last().unwrap_or(&self.root)
}
}
#[derive(Clone, Copy, Debug)]
struct LegacyField<'schema> {
selection: &'schema NamedSelection,
definition: &'schema Node<FieldDefinition>,
}
impl<'schema> LegacyField<'schema> {
fn next_subselection(&self) -> Option<&'schema SubSelection> {
self.selection.next_subselection()
}
fn inner_range(&self) -> Option<Range<usize>> {
self.selection.range()
}
}
#[derive(Clone, Copy, Debug)]
enum LegacyPathPart<'a> {
Root(&'a Node<ObjectType>),
Field {
definition: &'a Node<FieldDefinition>,
ty: &'a Node<ObjectType>,
},
}
impl LegacyPathPart<'_> {
const fn ty(&self) -> &Node<ObjectType> {
match self {
LegacyPathPart::Root(ty) => ty,
LegacyPathPart::Field { ty, .. } => ty,
}
}
}
#[derive(Clone, Debug)]
struct LegacyGroup<'schema> {
selection: &'schema SubSelection,
ty: &'schema Node<ObjectType>,
definition: Option<&'schema Node<FieldDefinition>>,
}
impl<'schema> GroupVisitor<LegacyGroup<'schema>, LegacyField<'schema>>
for LegacySelectionValidator<'schema>
{
fn try_get_group_for_field(
&self,
field: &LegacyField<'schema>,
) -> Result<Option<LegacyGroup<'schema>>, Self::Error> {
let Some(selection) = field.next_subselection() else {
return Ok(None);
};
let Some(ty) = self
.schema
.get_object(field.definition.ty.inner_named_type())
else {
return Ok(None);
};
Ok(Some(LegacyGroup {
selection,
ty,
definition: Some(field.definition),
}))
}
fn enter_group(
&mut self,
group: &LegacyGroup<'schema>,
) -> Result<Vec<LegacyField<'schema>>, Self::Error> {
if let Some(definition) = group.definition {
self.path.push(LegacyPathPart::Field {
definition,
ty: group.ty,
});
}
group.selection.selections_iter().flat_map(|selection| {
let mut results = Vec::new();
for field_name in selection.names() {
if let Some(definition) = group.ty.fields.get(field_name) {
results.push(Ok(LegacyField {
selection,
definition,
}));
} else if field_name != "__typename" {
results.push(Err(Message {
code: Code::SelectedFieldNotFound,
message: format!(
"{coordinate} contains field `{field_name}`, which does not exist on `{parent_type}`.",
coordinate = &self.coordinate,
parent_type = group.ty.name,
),
locations: self.get_selection_location(selection).collect(),
}));
}
}
results
}).collect()
}
fn exit_group(&mut self) -> Result<(), Self::Error> {
self.path.pop();
Ok(())
}
}
impl<'schema> FieldVisitor<LegacyField<'schema>> for LegacySelectionValidator<'schema> {
type Error = Message;
fn visit(&mut self, field: LegacyField<'schema>) -> Result<(), Self::Error> {
let field_name = field.definition.name.as_str();
let type_name = field.definition.ty.inner_named_type();
let coordinate = self.coordinate;
let field_type = self.schema.types.get(type_name).ok_or_else(|| Message {
code: Code::GraphQLError,
message: format!(
"{coordinate} contains field `{field_name}`, which has undefined type `{type_name}`.",
),
locations: self.get_range_location(field.inner_range()).collect(),
})?;
let is_group = field.next_subselection().is_some();
self.seen_fields.push((
self.last_field().ty().name.clone(),
field.definition.name.clone(),
));
if !field.definition.arguments.is_empty() {
return Err(Message {
code: Code::ConnectorsFieldWithArguments,
message: format!(
"{coordinate} selects field `{parent_type}.{field_name}`, which has arguments. Only fields with a connector can have arguments.",
parent_type = self.last_field().ty().name,
),
locations: self
.get_range_location(field.inner_range())
.chain(field.definition.line_column_range(&self.schema.sources))
.collect(),
});
}
match (field_type, is_group) {
(ExtendedType::Object(object), true) => {
self.check_for_circular_reference(field, object)
}
(_, true) => Err(Message {
code: Code::GroupSelectionIsNotObject,
message: format!(
"{coordinate} selects a group `{field_name}{{}}`, but `{parent_type}.{field_name}` is of type `{type_name}` which is not an object.",
parent_type = self.last_field().ty().name,
),
locations: self
.get_range_location(field.inner_range())
.chain(field.definition.line_column_range(&self.schema.sources))
.collect(),
}),
(ExtendedType::Object(_), false) => Err(Message {
code: Code::GroupSelectionRequiredForObject,
message: format!(
"`{parent_type}.{field_name}` is an object, so {coordinate} must select a group `{field_name}{{}}`.",
parent_type = self.last_field().ty().name,
),
locations: self
.get_range_location(field.inner_range())
.chain(field.definition.line_column_range(&self.schema.sources))
.collect(),
}),
(_, false) => Ok(()),
}
}
}