Skip to content

Commit 9e6435b

Browse files
committed
graphql: Treat missing variables as null values
Instead of immediately reporting an error, treat missing variables as nulls and let the rest of the execution logic deal with nulls
1 parent 3c29d4f commit 9e6435b

File tree

2 files changed

+74
-32
lines changed

2 files changed

+74
-32
lines changed

graphql/src/execution/query.rs

+35-32
Original file line numberDiff line numberDiff line change
@@ -704,54 +704,57 @@ struct Transform {
704704
}
705705

706706
impl Transform {
707-
fn variable(&self, name: &str, pos: &Pos) -> Result<r::Value, QueryExecutionError> {
707+
/// Look up the value of the variable `name`. If the variable is not
708+
/// defined, return `r::Value::Null`
709+
// graphql-bug-compat: Once queries are fully validated, all variables
710+
// will be defined
711+
fn variable(&self, name: &str) -> r::Value {
708712
self.variables
709713
.get(name)
710714
.map(|value| value.clone())
711-
.ok_or_else(|| QueryExecutionError::MissingVariableError(pos.clone(), name.to_string()))
715+
.unwrap_or(r::Value::Null)
712716
}
713717

714718
/// Interpolate variable references in the arguments `args`
715719
fn interpolate_arguments(
716720
&self,
717721
args: Vec<(String, q::Value)>,
718722
pos: &Pos,
719-
) -> Result<Vec<(String, r::Value)>, QueryExecutionError> {
723+
) -> Vec<(String, r::Value)> {
720724
args.into_iter()
721-
.map(|(name, val)| self.interpolate_value(val, pos).map(|val| (name, val)))
725+
.map(|(name, val)| {
726+
let val = self.interpolate_value(val, pos);
727+
(name, val)
728+
})
722729
.collect()
723730
}
724731

725732
/// Turn `value` into an `r::Value` by resolving variable references
726-
fn interpolate_value(
727-
&self,
728-
value: q::Value,
729-
pos: &Pos,
730-
) -> Result<r::Value, QueryExecutionError> {
733+
fn interpolate_value(&self, value: q::Value, pos: &Pos) -> r::Value {
731734
match value {
732-
q::Value::Variable(var) => self.variable(&var, pos),
733-
q::Value::Int(ref num) => Ok(r::Value::Int(
734-
num.as_i64().expect("q::Value::Int contains an i64"),
735-
)),
736-
q::Value::Float(f) => Ok(r::Value::Float(f)),
737-
q::Value::String(s) => Ok(r::Value::String(s)),
738-
q::Value::Boolean(b) => Ok(r::Value::Boolean(b)),
739-
q::Value::Null => Ok(r::Value::Null),
740-
q::Value::Enum(s) => Ok(r::Value::Enum(s)),
735+
q::Value::Variable(var) => self.variable(&var),
736+
q::Value::Int(ref num) => {
737+
r::Value::Int(num.as_i64().expect("q::Value::Int contains an i64"))
738+
}
739+
q::Value::Float(f) => r::Value::Float(f),
740+
q::Value::String(s) => r::Value::String(s),
741+
q::Value::Boolean(b) => r::Value::Boolean(b),
742+
q::Value::Null => r::Value::Null,
743+
q::Value::Enum(s) => r::Value::Enum(s),
741744
q::Value::List(vals) => {
742-
let vals: Vec<_> = vals
745+
let vals = vals
743746
.into_iter()
744747
.map(|val| self.interpolate_value(val, pos))
745-
.collect::<Result<Vec<_>, _>>()?;
746-
Ok(r::Value::List(vals))
748+
.collect();
749+
r::Value::List(vals)
747750
}
748751
q::Value::Object(map) => {
749752
let mut rmap = BTreeMap::new();
750753
for (key, value) in map.into_iter() {
751-
let value = self.interpolate_value(value, pos)?;
754+
let value = self.interpolate_value(value, pos);
752755
rmap.insert(key, value);
753756
}
754-
Ok(r::Value::object(rmap))
757+
r::Value::object(rmap)
755758
}
756759
}
757760
}
@@ -763,22 +766,22 @@ impl Transform {
763766
&self,
764767
dirs: Vec<q::Directive>,
765768
) -> Result<(Vec<a::Directive>, bool), QueryExecutionError> {
766-
let dirs = dirs
769+
let dirs: Vec<_> = dirs
767770
.into_iter()
768771
.map(|dir| {
769772
let q::Directive {
770773
name,
771774
position,
772775
arguments,
773776
} = dir;
774-
self.interpolate_arguments(arguments, &position)
775-
.map(|arguments| a::Directive {
776-
name,
777-
position,
778-
arguments,
779-
})
777+
let arguments = self.interpolate_arguments(arguments, &position);
778+
a::Directive {
779+
name,
780+
position,
781+
arguments,
782+
}
780783
})
781-
.collect::<Result<Vec<_>, _>>()?;
784+
.collect();
782785
let skip = dirs.iter().any(|dir| dir.skip());
783786
Ok((dirs, skip))
784787
}
@@ -887,7 +890,7 @@ impl Transform {
887890
return Ok(None);
888891
}
889892

890-
let mut arguments = self.interpolate_arguments(arguments, &position)?;
893+
let mut arguments = self.interpolate_arguments(arguments, &position);
891894
self.coerce_argument_values(&mut arguments, parent_type, &name)?;
892895

893896
let is_leaf_type = self.schema.document().is_leaf_type(&field_type.field_type);

graphql/tests/query.rs

+39
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,45 @@ fn leaf_selection_mismatch() {
14871487
})
14881488
}
14891489

1490+
// see: graphql-bug-compat
1491+
#[test]
1492+
fn missing_variable() {
1493+
run_test_sequentially(|store| async move {
1494+
let deployment = setup(store.as_ref());
1495+
let result = execute_query_document(
1496+
&deployment.hash,
1497+
// '$first' is not defined, use its default from the schema
1498+
graphql_parser::parse_query("query { musicians(first: $first, skip: $skip) { id } }")
1499+
.expect("invalid test query")
1500+
.into_static(),
1501+
)
1502+
.await;
1503+
// We silently set `$first` to 100 and `$skip` to 0, and therefore
1504+
// get everything
1505+
let exp = object! {
1506+
musicians: vec![
1507+
object! { id: "m1" },
1508+
object! { id: "m2" },
1509+
object! { id: "m3" },
1510+
object! { id: "m4" },
1511+
]
1512+
};
1513+
let data = extract_data!(result).unwrap();
1514+
assert_eq!(exp, data);
1515+
1516+
let result = execute_query_document(
1517+
&deployment.hash,
1518+
// '$where' is not defined but nullable, ignore the argument
1519+
graphql_parser::parse_query("query { musicians(where: $where) { id } }")
1520+
.expect("invalid test query")
1521+
.into_static(),
1522+
)
1523+
.await;
1524+
let data = extract_data!(result).unwrap();
1525+
assert_eq!(exp, data);
1526+
})
1527+
}
1528+
14901529
async fn check_musicians_at(
14911530
id: &DeploymentHash,
14921531
query: &str,

0 commit comments

Comments
 (0)