Skip to content

Commit 3c29d4f

Browse files
committed
graph, graphql: Handle selection/leaf mismatch
Make sure that we handle queries that have a selection from a scalar field by ignoring the selection or that have no selection for a non-scalar field by ignoring that field. The latter differs from the previous behavior where the result would contain an entry for such a field, but the data for the field would be an empty object or a list of empty objects.
1 parent 33db777 commit 3c29d4f

File tree

3 files changed

+94
-6
lines changed

3 files changed

+94
-6
lines changed

graph/src/data/graphql/ext.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ pub trait DocumentExt {
6666
fn get_named_type(&self, name: &str) -> Option<&TypeDefinition>;
6767

6868
fn scalar_value_type(&self, field_type: &Type) -> ValueType;
69+
70+
/// Return `true` if the type does not allow selection of child fields.
71+
///
72+
/// # Panics
73+
///
74+
/// If `field_type` names an unknown type
75+
fn is_leaf_type(&self, field_type: &Type) -> bool;
6976
}
7077

7178
impl DocumentExt for Document {
@@ -219,6 +226,19 @@ impl DocumentExt for Document {
219226
Type::ListType(inner) => self.scalar_value_type(inner),
220227
}
221228
}
229+
230+
fn is_leaf_type(&self, field_type: &Type) -> bool {
231+
match self
232+
.get_named_type(field_type.get_base_type())
233+
.expect("names of field types have been validated")
234+
{
235+
TypeDefinition::Enum(_) | TypeDefinition::Scalar(_) => true,
236+
TypeDefinition::Object(_)
237+
| TypeDefinition::Interface(_)
238+
| TypeDefinition::Union(_)
239+
| TypeDefinition::InputObject(_) => false,
240+
}
241+
}
222242
}
223243

224244
pub trait TypeExt {

graphql/src/execution/query.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use graph::data::graphql::DocumentExt as _;
12
use graph::data::value::Object;
23
use graphql_parser::Pos;
34
use graphql_tools::validation::rules::*;
@@ -18,7 +19,7 @@ use graph::prelude::{info, o, q, r, s, BlockNumber, CheapClone, Logger, TryFromV
1819

1920
use crate::execution::ast as a;
2021
use crate::query::{ast as qast, ext::BlockConstraint};
21-
use crate::schema::ast as sast;
22+
use crate::schema::ast::{self as sast};
2223
use crate::values::coercion;
2324
use crate::{execution::get_field, schema::api::ErrorPolicy};
2425

@@ -861,21 +862,53 @@ impl Transform {
861862
selection_set,
862863
} = field;
863864

865+
// Short-circuit '__typename' since it is not a real field
866+
if name == "__typename" {
867+
return Ok(Some(a::Field {
868+
position,
869+
alias,
870+
name,
871+
arguments: vec![],
872+
directives: vec![],
873+
selection_set: a::SelectionSet::new(vec![]),
874+
}));
875+
}
876+
877+
let field_type = parent_type.field(&name).ok_or_else(|| {
878+
vec![QueryExecutionError::UnknownField(
879+
position,
880+
parent_type.name().to_string(),
881+
name.clone(),
882+
)]
883+
})?;
884+
864885
let (directives, skip) = self.interpolate_directives(directives)?;
865886
if skip {
866887
return Ok(None);
867888
}
868889

869890
let mut arguments = self.interpolate_arguments(arguments, &position)?;
870891
self.coerce_argument_values(&mut arguments, parent_type, &name)?;
892+
893+
let is_leaf_type = self.schema.document().is_leaf_type(&field_type.field_type);
871894
let selection_set = if selection_set.items.is_empty() {
895+
if !is_leaf_type {
896+
// see: graphql-bug-compat
897+
// Field requires selection, ignore this field
898+
return Ok(None);
899+
}
872900
a::SelectionSet::new(vec![])
873901
} else {
874-
let field_type = parent_type.field(&name).expect("field names are valid");
875-
let ty = field_type.field_type.get_base_type();
876-
let type_set = a::ObjectTypeSet::from_name(&self.schema, ty)?;
877-
let ty = self.schema.object_or_interface(ty).unwrap();
878-
self.expand_selection_set(selection_set, &type_set, ty)?
902+
if is_leaf_type {
903+
// see: graphql-bug-compat
904+
// Field does not allow selections, ignore selections
905+
a::SelectionSet::new(vec![])
906+
} else {
907+
let ty = field_type.field_type.get_base_type();
908+
let type_set = a::ObjectTypeSet::from_name(&self.schema, ty)?;
909+
let ty = self.schema.object_or_interface(ty).unwrap();
910+
self.expand_selection_set(selection_set, &type_set, ty)?
911+
}
879912
};
880913

881914
Ok(Some(a::Field {

graphql/tests/query.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,41 @@ fn ignores_invalid_field_arguments() {
14521452
})
14531453
}
14541454

1455+
// see: graphql-bug-compat
1456+
#[test]
1457+
fn leaf_selection_mismatch() {
1458+
run_test_sequentially(|store| async move {
1459+
let deployment = setup(store.as_ref());
1460+
let result = execute_query_document(
1461+
&deployment.hash,
1462+
// 'name' is a string and doesn't admit a selection
1463+
graphql_parser::parse_query("query { musician(id: \"m1\") { id name { wat }} } ")
1464+
.expect("invalid test query")
1465+
.into_static(),
1466+
)
1467+
.await;
1468+
let exp = object! {
1469+
musician: object! {
1470+
id: "m1",
1471+
name: "John"
1472+
}
1473+
};
1474+
let data = extract_data!(result).unwrap();
1475+
assert_eq!(exp, data);
1476+
1477+
let result = execute_query_document(
1478+
&deployment.hash,
1479+
// 'mainBand' is an object and requires a selection; it is ignored
1480+
graphql_parser::parse_query("query { musician(id: \"m1\") { id name mainBand } } ")
1481+
.expect("invalid test query")
1482+
.into_static(),
1483+
)
1484+
.await;
1485+
let data = extract_data!(result).unwrap();
1486+
assert_eq!(exp, data);
1487+
})
1488+
}
1489+
14551490
async fn check_musicians_at(
14561491
id: &DeploymentHash,
14571492
query: &str,

0 commit comments

Comments
 (0)