Skip to content

Refactor GraphQL execution #3005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5302b94
graph, graphql, server: Encapsulate maps in Value in a new type
lutter Oct 28, 2021
fc78a18
graphql: Move complexity check/field validation into intermediate struct
lutter Oct 30, 2021
be2f07a
graph, graphql: Resolve variables before executing query
lutter Oct 30, 2021
8b3ccee
graphql: Do not pass variables around during execution
lutter Oct 30, 2021
d489ca2
graph, graphql: Use our own structs to represent the query AST
lutter Nov 15, 2021
69d5386
graphql: Restrict construction of selection sets
lutter Nov 15, 2021
1e43f7f
graphql: Make SelectionSet.items private
lutter Nov 15, 2021
a6482de
graphql: Move ObjectCondition to graphql::schema::ast
lutter Nov 16, 2021
334f322
graphql: Mix the introspection schema into the API schema
lutter Nov 19, 2021
455218b
graphql: Simplify constructing an ExecutionContext for introspection
lutter Nov 19, 2021
f9d51fc
graphql: Transform the query before executing it
lutter Nov 16, 2021
6645e3f
graphql: Clean up imports in schema::ast
lutter Nov 19, 2021
479e7dd
graphql: Rename ObjectCondition to ObjectType
lutter Nov 19, 2021
8ff30cb
graphql: Simplify CollectedAttributeNames and rename it
lutter Nov 19, 2021
93729aa
graphql: Remove unused CollectedResponseKey
lutter Nov 19, 2021
fc58734
graphql: Simplify Join type in prefetch a little
lutter Nov 19, 2021
bc4e200
graphql: Pass a schema, not the query, to coerce_argument_values
lutter Nov 23, 2021
af14ff7
graphql: Coerce arguments when constructing the query
lutter Nov 23, 2021
d20ad48
graphql: Keep original field order when grouping by block constraint
lutter Nov 23, 2021
4e9eff9
core: Do not rely on debug output in interface tests
lutter Nov 23, 2021
5125702
all: Change GraphQL output to follow the order of fields in the query
lutter Nov 24, 2021
b5336d1
graphql: Verify that root fragments are expanded
lutter Nov 24, 2021
393d0dc
graph, graphql: Use Arc<ObjctType> to reference object types
lutter Nov 24, 2021
22e0f21
graph, graphql: Encapsulate access to the underlying schema in ApiSchema
lutter Nov 24, 2021
12aba47
graphql: Remove unused functions
lutter Nov 29, 2021
6014541
graphql: Clarify/fix some comments
lutter Nov 29, 2021
383d5a6
graphql: Make sure that undefined arguments are ignored
lutter Jan 6, 2022
33db777
graphql: Change the default for running validations
lutter Jan 18, 2022
3c29d4f
graph, graphql: Handle selection/leaf mismatch
lutter Jan 18, 2022
9e6435b
graphql: Treat missing variables as null values
lutter Jan 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 62 additions & 73 deletions core/tests/interfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ async fn one_interface_zero_entities() {
.unwrap();

let data = extract_data!(res).unwrap();
assert_eq!(format!("{:?}", data), "Object({\"leggeds\": List([])})")
let exp = object! { leggeds: Vec::<r::Value>::new() };
assert_eq!(data, exp);
}

#[tokio::test]
Expand All @@ -87,21 +88,17 @@ async fn one_interface_one_entity() {
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"leggeds\": List([Object({\"legs\": Int(3)})])})"
);
let exp = object! { leggeds: vec![ object!{ legs: 3 }]};
assert_eq!(data, exp);

// Query by ID.
let query = "query { legged(id: \"1\") { legs } }";
let res = insert_and_query(subgraph_id, schema, vec![], query)
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"legged\": Object({\"legs\": Int(3)})})",
);
let exp = object! { legged: object! { legs: 3 }};
assert_eq!(data, exp);
}

#[tokio::test]
Expand All @@ -121,10 +118,8 @@ async fn one_interface_one_entity_typename() {
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"leggeds\": List([Object({\"__typename\": String(\"Animal\")})])})"
)
let exp = object! { leggeds: vec![ object!{ __typename: "Animal" } ]};
assert_eq!(data, exp);
}

#[tokio::test]
Expand All @@ -150,21 +145,17 @@ async fn one_interface_multiple_entities() {
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"leggeds\": List([Object({\"legs\": Int(3)}), Object({\"legs\": Int(4)})])})"
);
let exp = object! { leggeds: vec![ object! { legs: 3 }, object! { legs: 4 }]};
assert_eq!(data, exp);

// Test for support issue #32.
let query = "query { legged(id: \"2\") { legs } }";
let res = insert_and_query(subgraph_id, schema, vec![], query)
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"legged\": Object({\"legs\": Int(4)})})",
);
let exp = object! { legged: object! { legs: 4 }};
assert_eq!(data, exp);
}

#[tokio::test]
Expand All @@ -187,10 +178,8 @@ async fn reference_interface() {
.unwrap();

let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"leggeds\": List([Object({\"leg\": Object({\"id\": String(\"1\")})})])})"
)
let exp = object! { leggeds: vec![ object!{ leg: object! { id: "1" } }] };
assert_eq!(data, exp);
}

#[tokio::test]
Expand Down Expand Up @@ -253,13 +242,15 @@ async fn reference_interface_derived() {
.unwrap();

let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"events\": List([\
Object({\"id\": String(\"buy\"), \"transaction\": Object({\"id\": String(\"txn\")})}), \
Object({\"id\": String(\"gift\"), \"transaction\": Object({\"id\": String(\"txn\")})}), \
Object({\"id\": String(\"sell1\"), \"transaction\": Object({\"id\": String(\"txn\")})}), \
Object({\"id\": String(\"sell2\"), \"transaction\": Object({\"id\": String(\"txn\")})})])})");
let exp = object! {
events: vec![
object! { id: "buy", transaction: object! { id: "txn" } },
object! { id: "gift", transaction: object! { id: "txn" } },
object! { id: "sell1", transaction: object! { id: "txn" } },
object! { id: "sell2", transaction: object! { id: "txn" } }
]
};
assert_eq!(data, exp);
}

#[tokio::test]
Expand All @@ -278,14 +269,20 @@ async fn follow_interface_reference_invalid() {
.await
.unwrap();

// Depending on whether `ENABLE_GRAPHQL_VALIDATIONS` is set or not, we
// get different errors
match &res.to_result().unwrap_err()[0] {
QueryError::ExecutionError(QueryExecutionError::ValidationError(_, error_message)) => {
assert_eq!(
error_message,
"Cannot query field \"parent\" on type \"Legged\"."
);
}
e => panic!("error {} is not the expected one", e),
QueryError::ExecutionError(QueryExecutionError::UnknownField(_, type_name, field_name)) => {
assert_eq!(type_name, "Legged");
assert_eq!(field_name, "parent");
}
e => panic!("error `{}` is not the expected one", e),
}
}

Expand Down Expand Up @@ -323,10 +320,10 @@ async fn follow_interface_reference() {
.unwrap();

let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"legged\": Object({\"parent\": Object({\"id\": String(\"parent\")})})})"
)
let exp = object! {
legged: object! { parent: object! { id: "parent" } }
};
assert_eq!(data, exp)
}

#[tokio::test]
Expand Down Expand Up @@ -426,11 +423,11 @@ async fn two_interfaces() {
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
"Object({\"ibars\": List([Object({\"bar\": Int(100)}), Object({\"bar\": Int(200)})]), \
\"ifoos\": List([Object({\"foo\": String(\"bla\")}), Object({\"foo\": String(\"ble\")})])})"
);
let exp = object! {
ibars: vec![ object! { bar: 100 }, object! { bar: 200 }],
ifoos: vec![ object! { foo: "bla" }, object! { foo: "ble" } ]
};
assert_eq!(data, exp);
}

#[tokio::test]
Expand All @@ -454,21 +451,17 @@ async fn interface_non_inline_fragment() {
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
r#"Object({"leggeds": List([Object({"name": String("cow")})])})"#
);
let exp = object! { leggeds: vec![ object! { name: "cow" } ]};
assert_eq!(data, exp);

// Query the fragment and something else.
let query = "query { leggeds { legs, ...frag } } fragment frag on Animal { name }";
let res = insert_and_query(subgraph_id, schema, vec![], query)
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
r#"Object({"leggeds": List([Object({"legs": Int(3), "name": String("cow")})])})"#,
);
let exp = object! { leggeds: vec![ object!{ legs: 3, name: "cow" } ]};
assert_eq!(data, exp);
}

#[tokio::test]
Expand Down Expand Up @@ -501,10 +494,8 @@ async fn interface_inline_fragment() {
.await
.unwrap();
let data = extract_data!(res).unwrap();
assert_eq!(
format!("{:?}", data),
r#"Object({"leggeds": List([Object({"airspeed": Int(24)}), Object({"name": String("cow")})])})"#
);
let exp = object! { leggeds: vec![ object!{ airspeed: 24 }, object! { name: "cow" }]};
assert_eq!(data, exp);
}

#[tokio::test]
Expand Down Expand Up @@ -567,20 +558,11 @@ async fn interface_inline_fragment_with_subquery() {
.await
.unwrap();
let data = extract_data!(res).unwrap();

assert_eq!(
format!("{:?}", data),
"Object({\
\"leggeds\": List([\
Object({\
\"airspeed\": Int(5), \
\"legs\": Int(2), \
\"parent\": Object({\"id\": String(\"mama_bird\")})\
}), \
Object({\"legs\": Int(4)})\
])\
})"
);
let exp = object! {
leggeds: vec![ object!{ legs: 2, airspeed: 5, parent: object! { id: "mama_bird" } },
object!{ legs: 4 }]
};
assert_eq!(data, exp);
}

#[tokio::test]
Expand Down Expand Up @@ -1394,6 +1376,16 @@ async fn enum_list_filters() {

#[tokio::test]
async fn recursive_fragment() {
// Depending on whether `ENABLE_GRAPHQL_VALIDATIONS` is set or not, we
// get different error messages
const FOO_ERRORS: [&str; 2] = [
"Cannot spread fragment \"FooFrag\" within itself.",
"query has fragment cycle including `FooFrag`",
];
const FOO_BAR_ERRORS: [&str; 2] = [
"Cannot spread fragment \"BarFrag\" within itself via \"FooFrag\".",
"query has fragment cycle including `BarFrag`",
];
let subgraph_id = "RecursiveFragment";
let schema = "
type Foo @entity {
Expand Down Expand Up @@ -1426,7 +1418,7 @@ async fn recursive_fragment() {
.await
.unwrap();
let data = res.to_result().unwrap_err()[0].to_string();
assert_eq!(data, "Cannot spread fragment \"FooFrag\" within itself.");
assert!(FOO_ERRORS.contains(&data.as_str()));

let co_recursive = "
query {
Expand All @@ -1453,8 +1445,5 @@ async fn recursive_fragment() {
.await
.unwrap();
let data = res.to_result().unwrap_err()[0].to_string();
assert_eq!(
data,
"Cannot spread fragment \"BarFrag\" within itself via \"FooFrag\"."
);
assert!(FOO_BAR_ERRORS.contains(&data.as_str()));
}
7 changes: 3 additions & 4 deletions graph/src/components/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1713,12 +1713,11 @@ impl AttributeNames {
}
}

/// Adds a attribute name. Ignores meta fields.
pub fn add(&mut self, field: &q::Field) {
if Self::is_meta_field(&field.name) {
pub fn update(&mut self, field_name: &str) {
if Self::is_meta_field(field_name) {
return;
}
self.insert(&field.name)
self.insert(&field_name)
}

/// Adds a attribute name. Ignores meta fields.
Expand Down
55 changes: 55 additions & 0 deletions graph/src/data/graphql/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ pub trait DocumentExt {
fn get_named_type(&self, name: &str) -> Option<&TypeDefinition>;

fn scalar_value_type(&self, field_type: &Type) -> ValueType;

/// Return `true` if the type does not allow selection of child fields.
///
/// # Panics
///
/// If `field_type` names an unknown type
fn is_leaf_type(&self, field_type: &Type) -> bool;
}

impl DocumentExt for Document {
Expand Down Expand Up @@ -219,6 +226,19 @@ impl DocumentExt for Document {
Type::ListType(inner) => self.scalar_value_type(inner),
}
}

fn is_leaf_type(&self, field_type: &Type) -> bool {
match self
.get_named_type(field_type.get_base_type())
.expect("names of field types have been validated")
{
TypeDefinition::Enum(_) | TypeDefinition::Scalar(_) => true,
TypeDefinition::Object(_)
| TypeDefinition::Interface(_)
| TypeDefinition::Union(_)
| TypeDefinition::InputObject(_) => false,
}
}
}

pub trait TypeExt {
Expand Down Expand Up @@ -348,6 +368,41 @@ impl DirectiveFinder for Vec<Directive> {
}
}

pub trait TypeDefinitionExt {
fn name(&self) -> &str;

// Return `true` if this is the definition of a type from the
// introspection schema
fn is_introspection(&self) -> bool {
self.name().starts_with("__")
}
}

impl TypeDefinitionExt for TypeDefinition {
fn name(&self) -> &str {
match self {
TypeDefinition::Scalar(t) => &t.name,
TypeDefinition::Object(t) => &t.name,
TypeDefinition::Interface(t) => &t.name,
TypeDefinition::Union(t) => &t.name,
TypeDefinition::Enum(t) => &t.name,
TypeDefinition::InputObject(t) => &t.name,
}
}
}

pub trait FieldExt {
// Return `true` if this is the name of one of the query fields from the
// introspection schema
fn is_introspection(&self) -> bool;
}

impl FieldExt for Field {
fn is_introspection(&self) -> bool {
&self.name == "__schema" || &self.name == "__type"
}
}

#[cfg(test)]
mod directive_finder_tests {
use graphql_parser::parse_schema;
Expand Down
8 changes: 4 additions & 4 deletions graph/src/data/graphql/object_macro.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use crate::data::value::Object;
use crate::prelude::q;
use crate::prelude::r;
use std::collections::BTreeMap;
use std::iter::FromIterator;

/// Creates a `graphql_parser::query::Value::Object` from key/value pairs.
/// If you don't need to determine which keys are included dynamically at runtime
/// consider using the `object! {}` macro instead.
pub fn object_value(data: Vec<(&str, r::Value)>) -> r::Value {
r::Value::Object(BTreeMap::from_iter(
r::Value::Object(Object::from_iter(
data.into_iter().map(|(k, v)| (k.to_string(), v)),
))
}
Expand Down Expand Up @@ -83,12 +83,12 @@ macro_rules! impl_into_values {

impl_into_values![(String, String), (f64, Float), (bool, Boolean)];

/// Creates a `graphql_parser::query::Value::Object` from key/value pairs.
/// Creates a `data::value::Value::Object` from key/value pairs.
#[macro_export]
macro_rules! object {
($($name:ident: $value:expr,)*) => {
{
let mut result = ::std::collections::BTreeMap::new();
let mut result = $crate::data::value::Object::new();
$(
let value = $crate::data::graphql::object_macro::IntoValue::into_value($value);
result.insert(stringify!($name).to_string(), value);
Expand Down
Loading