apollo_compiler/execution/
engine.rs

1use crate::ast::Value;
2use crate::collections::HashSet;
3use crate::collections::IndexMap;
4use crate::executable::Field;
5use crate::executable::Selection;
6use crate::execution::input_coercion::coerce_argument_values;
7use crate::execution::resolver::ObjectValue;
8use crate::execution::resolver::ResolverError;
9use crate::execution::result_coercion::complete_value;
10use crate::parser::SourceMap;
11use crate::parser::SourceSpan;
12use crate::response::GraphQLError;
13use crate::response::JsonMap;
14use crate::response::JsonValue;
15use crate::response::ResponseDataPathSegment;
16use crate::schema::ExtendedType;
17use crate::schema::FieldDefinition;
18use crate::schema::ObjectType;
19use crate::schema::Type;
20use crate::validation::SuspectedValidationBug;
21use crate::validation::Valid;
22use crate::ExecutableDocument;
23use crate::Name;
24use crate::Schema;
25
26/// <https://spec.graphql.org/October2021/#sec-Normal-and-Serial-Execution>
27#[derive(Debug, Copy, Clone)]
28pub(crate) enum ExecutionMode {
29    /// Allowed to resolve fields in any order, including in parellel
30    Normal,
31    /// Top-level fields of a mutation operation must be executed in order
32    #[allow(unused)]
33    Sequential,
34}
35
36/// Return in `Err` when a field error occurred at some non-nullable place
37///
38/// <https://spec.graphql.org/October2021/#sec-Handling-Field-Errors>
39pub(crate) struct PropagateNull;
40
41/// Linked-list version of `Vec<PathElement>`, taking advantage of the call stack
42pub(crate) type LinkedPath<'a> = Option<&'a LinkedPathElement<'a>>;
43
44pub(crate) struct LinkedPathElement<'a> {
45    pub(crate) element: ResponseDataPathSegment,
46    pub(crate) next: LinkedPath<'a>,
47}
48
49/// <https://spec.graphql.org/October2021/#ExecuteSelectionSet()>
50#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
51pub(crate) fn execute_selection_set<'a>(
52    schema: &Valid<Schema>,
53    document: &'a Valid<ExecutableDocument>,
54    variable_values: &Valid<JsonMap>,
55    errors: &mut Vec<GraphQLError>,
56    path: LinkedPath<'_>,
57    mode: ExecutionMode,
58    object_type: &ObjectType,
59    object_value: &ObjectValue<'_>,
60    selections: impl IntoIterator<Item = &'a Selection>,
61) -> Result<JsonMap, PropagateNull> {
62    let mut grouped_field_set = IndexMap::default();
63    collect_fields(
64        schema,
65        document,
66        variable_values,
67        object_type,
68        object_value,
69        selections,
70        &mut HashSet::default(),
71        &mut grouped_field_set,
72    );
73
74    match mode {
75        ExecutionMode::Normal => {}
76        ExecutionMode::Sequential => {
77            // If we want parallelism, use `futures::future::join_all` (async)
78            // or Rayon’s `par_iter` (sync) here.
79        }
80    }
81
82    let mut response_map = JsonMap::with_capacity(grouped_field_set.len());
83    for (&response_key, fields) in &grouped_field_set {
84        // Indexing should not panic: `collect_fields` only creates a `Vec` to push to it
85        let field_name = &fields[0].name;
86        let Ok(field_def) = schema.type_field(&object_type.name, field_name) else {
87            // TODO: Return a `validation_bug`` field error here?
88            // The spec specifically has a “If fieldType is defined” condition,
89            // but it being undefined would make the request invalid, right?
90            continue;
91        };
92        let value = if field_name == "__typename" {
93            JsonValue::from(object_type.name.as_str())
94        } else {
95            let field_path = LinkedPathElement {
96                element: ResponseDataPathSegment::Field(response_key.clone()),
97                next: path,
98            };
99            execute_field(
100                schema,
101                document,
102                variable_values,
103                errors,
104                Some(&field_path),
105                mode,
106                object_value,
107                field_def,
108                fields,
109            )?
110        };
111        response_map.insert(response_key.as_str(), value);
112    }
113    Ok(response_map)
114}
115
116/// <https://spec.graphql.org/October2021/#CollectFields()>
117#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
118fn collect_fields<'a>(
119    schema: &Schema,
120    document: &'a ExecutableDocument,
121    variable_values: &Valid<JsonMap>,
122    object_type: &ObjectType,
123    object_value: &ObjectValue<'_>,
124    selections: impl IntoIterator<Item = &'a Selection>,
125    visited_fragments: &mut HashSet<&'a Name>,
126    grouped_fields: &mut IndexMap<&'a Name, Vec<&'a Field>>,
127) {
128    for selection in selections {
129        if eval_if_arg(selection, "skip", variable_values).unwrap_or(false)
130            || !eval_if_arg(selection, "include", variable_values).unwrap_or(true)
131        {
132            continue;
133        }
134        match selection {
135            Selection::Field(field) => {
136                if !object_value.skip_field(&field.name) {
137                    grouped_fields
138                        .entry(field.response_key())
139                        .or_default()
140                        .push(field.as_ref())
141                }
142            }
143            Selection::FragmentSpread(spread) => {
144                let new = visited_fragments.insert(&spread.fragment_name);
145                if !new {
146                    continue;
147                }
148                let Some(fragment) = document.fragments.get(&spread.fragment_name) else {
149                    continue;
150                };
151                if !does_fragment_type_apply(schema, object_type, fragment.type_condition()) {
152                    continue;
153                }
154                collect_fields(
155                    schema,
156                    document,
157                    variable_values,
158                    object_type,
159                    object_value,
160                    &fragment.selection_set.selections,
161                    visited_fragments,
162                    grouped_fields,
163                )
164            }
165            Selection::InlineFragment(inline) => {
166                if let Some(condition) = &inline.type_condition {
167                    if !does_fragment_type_apply(schema, object_type, condition) {
168                        continue;
169                    }
170                }
171                collect_fields(
172                    schema,
173                    document,
174                    variable_values,
175                    object_type,
176                    object_value,
177                    &inline.selection_set.selections,
178                    visited_fragments,
179                    grouped_fields,
180                )
181            }
182        }
183    }
184}
185
186/// <https://spec.graphql.org/October2021/#DoesFragmentTypeApply()>
187fn does_fragment_type_apply(
188    schema: &Schema,
189    object_type: &ObjectType,
190    fragment_type: &Name,
191) -> bool {
192    match schema.types.get(fragment_type) {
193        Some(ExtendedType::Object(_)) => *fragment_type == object_type.name,
194        Some(ExtendedType::Interface(_)) => {
195            object_type.implements_interfaces.contains(fragment_type)
196        }
197        Some(ExtendedType::Union(def)) => def.members.contains(&object_type.name),
198        // Undefined or not an output type: validation should have caught this
199        _ => false,
200    }
201}
202
203fn eval_if_arg(
204    selection: &Selection,
205    directive_name: &str,
206    variable_values: &Valid<JsonMap>,
207) -> Option<bool> {
208    match selection
209        .directives()
210        .get(directive_name)?
211        .specified_argument_by_name("if")?
212        .as_ref()
213    {
214        Value::Boolean(value) => Some(*value),
215        Value::Variable(var) => variable_values.get(var.as_str())?.as_bool(),
216        _ => None,
217    }
218}
219
220/// <https://spec.graphql.org/October2021/#ExecuteField()>
221#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
222fn execute_field(
223    schema: &Valid<Schema>,
224    document: &Valid<ExecutableDocument>,
225    variable_values: &Valid<JsonMap>,
226    errors: &mut Vec<GraphQLError>,
227    path: LinkedPath<'_>,
228    mode: ExecutionMode,
229    object_value: &ObjectValue<'_>,
230    field_def: &FieldDefinition,
231    fields: &[&Field],
232) -> Result<JsonValue, PropagateNull> {
233    let field = fields[0];
234    let argument_values = match coerce_argument_values(
235        schema,
236        document,
237        variable_values,
238        errors,
239        path,
240        field_def,
241        field,
242    ) {
243        Ok(argument_values) => argument_values,
244        Err(PropagateNull) => return try_nullify(&field_def.ty, Err(PropagateNull)),
245    };
246    let resolved_result = object_value.resolve_field(&field.name, &argument_values);
247    let completed_result = match resolved_result {
248        Ok(resolved) => complete_value(
249            schema,
250            document,
251            variable_values,
252            errors,
253            path,
254            mode,
255            field.ty(),
256            resolved,
257            fields,
258        ),
259        Err(ResolverError { message }) => {
260            errors.push(GraphQLError::field_error(
261                format!("resolver error: {message}"),
262                path,
263                field.name.location(),
264                &document.sources,
265            ));
266            Err(PropagateNull)
267        }
268    };
269    try_nullify(&field_def.ty, completed_result)
270}
271
272/// Try to insert a propagated null if possible, or keep propagating it.
273///
274/// <https://spec.graphql.org/October2021/#sec-Handling-Field-Errors>
275pub(crate) fn try_nullify(
276    ty: &Type,
277    result: Result<JsonValue, PropagateNull>,
278) -> Result<JsonValue, PropagateNull> {
279    match result {
280        Ok(json) => Ok(json),
281        Err(PropagateNull) => {
282            if ty.is_non_null() {
283                Err(PropagateNull)
284            } else {
285                Ok(JsonValue::Null)
286            }
287        }
288    }
289}
290
291pub(crate) fn path_to_vec(mut link: LinkedPath<'_>) -> Vec<ResponseDataPathSegment> {
292    let mut path = Vec::new();
293    while let Some(node) = link {
294        path.push(node.element.clone());
295        link = node.next;
296    }
297    path.reverse();
298    path
299}
300
301impl GraphQLError {
302    pub(crate) fn field_error(
303        message: impl Into<String>,
304        path: LinkedPath<'_>,
305        location: Option<SourceSpan>,
306        sources: &SourceMap,
307    ) -> Self {
308        let mut err = Self::new(message, location, sources);
309        err.path = path_to_vec(path);
310        err
311    }
312}
313
314impl SuspectedValidationBug {
315    pub(crate) fn into_field_error(
316        self,
317        sources: &SourceMap,
318        path: LinkedPath<'_>,
319    ) -> GraphQLError {
320        let Self { message, location } = self;
321        let mut err = GraphQLError::field_error(message, path, location, sources);
322        err.extensions
323            .insert("APOLLO_SUSPECTED_VALIDATION_BUG", true.into());
324        err
325    }
326}