apollo_compiler/execution/
engine.rs1use 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#[derive(Debug, Copy, Clone)]
28pub(crate) enum ExecutionMode {
29 Normal,
31 #[allow(unused)]
33 Sequential,
34}
35
36pub(crate) struct PropagateNull;
40
41pub(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#[allow(clippy::too_many_arguments)] pub(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 }
80 }
81
82 let mut response_map = JsonMap::with_capacity(grouped_field_set.len());
83 for (&response_key, fields) in &grouped_field_set {
84 let field_name = &fields[0].name;
86 let Ok(field_def) = schema.type_field(&object_type.name, field_name) else {
87 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#[allow(clippy::too_many_arguments)] fn 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
186fn 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 _ => 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#[allow(clippy::too_many_arguments)] fn 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
272pub(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}