apollo_compiler/resolvers/mod.rs
1//! GraphQL [execution](https://spec.graphql.org/September2025/#sec-Execution)
2//! based on callbacks resolving one field at a time.
3//!
4//! Start with [`Execution::new`],
5//! then use builder-pattern methods to configure,
6//! then use either the [`execute_sync`][Execution::execute_sync]
7//! or [`execute_async`][Execution::execute_async] method.
8//! They take an initial object value
9//! (implementing the [`ObjectValue`] or [`AsyncObjectValue`] trait respectively)
10//! that represents an instance of the root operation type (such as `Query`).
11//! Trait methods are called as needed to resolve object fields,
12//! which may in turn return more objects.
13//!
14//! How to implement the trait is up to the user:
15//! there could be a separate Rust struct per GraphQL object type,
16//! or a single Rust enum with a variants per GraphQL object type,
17//! or some other strategy.
18//!
19//! # Execution errors and sibling cancellation
20//!
21//! When an [execution error] propagates through non-null response positions,
22//! execution of the remaining sibling fields (or remaining list items)
23//! in the affected selection set is canceled, like in graphql-js:
24//! their resolvers are not called,
25//! and no additional errors are reported for them.
26//! Response data is unaffected,
27//! since the enclosing response position is discarded either way.
28//!
29//! [execution error]: https://spec.graphql.org/September2025/#sec-Handling-Execution-Errors
30//!
31//! # Example
32//!
33//! ```
34#![doc = include_str!("../../examples/async_resolvers.rs")]
35//! ```
36
37use crate::collections::HashMap;
38use crate::executable;
39use crate::executable::Operation;
40#[cfg(doc)]
41use crate::introspection;
42use crate::request::coerce_variable_values;
43use crate::request::RequestError;
44use crate::resolvers::execution::execute_selection_set;
45use crate::resolvers::execution::ExecutionContext;
46use crate::resolvers::execution::ExecutionMode;
47use crate::resolvers::execution::MaybeLazy;
48use crate::resolvers::execution::PropagateNull;
49use crate::response::ExecutionResponse;
50use crate::response::JsonMap;
51use crate::response::JsonValue;
52use crate::schema;
53use crate::schema::Implementers;
54use crate::validation::Valid;
55use crate::ExecutableDocument;
56use crate::Name;
57use crate::Schema;
58use futures::future::BoxFuture;
59use futures::stream::BoxStream;
60use futures::FutureExt as _;
61use std::sync::OnceLock;
62
63mod execution;
64pub(crate) mod input_coercion;
65mod result_coercion;
66
67/// Builder for configuring GraphQL execution
68///
69/// See [module-level documentation][self].
70pub struct Execution<'a> {
71 schema: &'a Valid<Schema>,
72 document: &'a Valid<ExecutableDocument>,
73 operation: Option<&'a Operation>,
74 implementers_map: Option<&'a HashMap<Name, Implementers>>,
75 variable_values: Option<VariableValues<'a>>,
76 enable_schema_introspection: Option<bool>,
77}
78
79/// Default to disabled:
80/// https://www.apollographql.com/blog/why-you-should-disable-graphql-introspection-in-production/
81const DEFAULT_ENABLE_SCHEMA_INTROSPECTION: bool = false;
82
83enum VariableValues<'a> {
84 Raw(&'a JsonMap),
85 Coerced(&'a Valid<JsonMap>),
86}
87
88#[derive(Clone, Copy)]
89pub(crate) enum MaybeAsync<A, S> {
90 Async(A),
91 Sync(S),
92}
93
94pub(crate) type MaybeAsyncObject<'a> = MaybeAsync<&'a dyn AsyncObjectValue, &'a dyn ObjectValue>;
95
96pub(crate) type MaybeAsyncResolved<'a> = MaybeAsync<AsyncResolvedValue<'a>, ResolvedValue<'a>>;
97
98/// Information passed to [`ObjectValue::resolve_field`] or [`AsyncObjectValue::resolve_field`].
99pub struct ResolveInfo<'a> {
100 pub(crate) schema: &'a Valid<Schema>,
101 pub(crate) implementers_map: MaybeLazy<'a, HashMap<Name, Implementers>>,
102 pub(crate) document: &'a Valid<ExecutableDocument>,
103 pub(crate) fields: &'a [&'a executable::Field],
104 pub(crate) arguments: &'a JsonMap,
105}
106
107/// An error returned by [`ObjectValue::resolve_field`] or [`AsyncObjectValue::resolve_field`],
108/// which will become an [execution error](https://spec.graphql.org/September2025/#sec-Errors.Execution-Errors)
109/// (a.k.a. execution error) in the GraphQL response,
110/// with path and locations filled in.
111pub struct ExecutionError {
112 pub message: String,
113}
114
115/// A concrete GraphQL object whose fields can be resolved during execution.
116pub trait ObjectValue {
117 /// Returns the name of the concrete object type
118 ///
119 /// That name expected to be that of an object type defined in the schema.
120 fn type_name(&self) -> &str;
121
122 /// Resolves a concrete field of this object
123 ///
124 /// The resolved value is expected to match the type of the corresponding field definition
125 /// in the schema.
126 ///
127 /// This is _not_ called for [introspection](https://spec.graphql.org/September2025/#sec-Introspection)
128 /// meta-fields `__typename`, `__type`, or `__schema`: those are handled separately.
129 ///
130 /// A typical implementation might look like:
131 ///
132 /// ```ignore
133 /// match info.field_name() {
134 /// "field1" => Ok(ResolvedValue::leaf(self.resolve_field1())),
135 /// "field2" => Ok(ResolvedValue::list(self.resolve_field2())),
136 /// _ => Err(self.unknown_field_error(info)),
137 /// }
138 /// ```
139 fn resolve_field<'a>(
140 &'a self,
141 info: &'a ResolveInfo<'a>,
142 ) -> Result<ResolvedValue<'a>, ExecutionError>;
143
144 /// Generate a resolve error for `resolve_field` to return in case of an unexpected field name.
145 ///
146 /// In many cases this should never happen,
147 /// such as when writing resolvers for a fixed, known schema.
148 /// Still, this method generates a GraphQL execution error without Rust panick in case of a bug.
149 fn unknown_field_error(&self, info: &ResolveInfo<'_>) -> ExecutionError {
150 ExecutionError::unknown_field(info.field_name(), self.type_name())
151 }
152}
153
154/// A concrete GraphQL object whose fields can be resolved asynchronously during execution.
155pub trait AsyncObjectValue: Send {
156 /// Returns the name of the concrete object type
157 ///
158 /// That name expected to be that of an object type defined in the schema.
159 fn type_name(&self) -> &str;
160
161 /// Resolves a concrete field of this object
162 ///
163 /// The resolved value is expected to match the type of the corresponding field definition
164 /// in the schema.
165 ///
166 /// This is _not_ called for [introspection](https://spec.graphql.org/September2025/#sec-Introspection)
167 /// meta-fields `__typename`, `__type`, or `__schema`: those are handled separately.
168 ///
169 /// A typical implementation might look like:
170 ///
171 /// ```ignore
172 /// Box::pin(async move {
173 /// match info.field_name() {
174 /// "field1" => Ok(AsyncResolvedValue::leaf(self.resolve_field1().await)),
175 /// "field2" => Ok(AsyncResolvedValue::list(self.resolve_field2().await)),
176 /// _ => Err(self.unknown_field_error(info)),
177 /// }
178 /// })
179 /// ```
180 fn resolve_field<'a>(
181 &'a self,
182 info: &'a ResolveInfo<'a>,
183 ) -> BoxFuture<'a, Result<AsyncResolvedValue<'a>, ExecutionError>>;
184
185 /// Generate a resolve error for `resolve_field` to return in case of an unexpected field name.
186 ///
187 /// In many cases this should never happen,
188 /// such as when writing resolvers for a fixed, known schema.
189 /// Still, this method generates a GraphQL execution error without Rust panick in case of a bug.
190 fn unknown_field_error(&self, info: &ResolveInfo<'_>) -> ExecutionError {
191 ExecutionError::unknown_field(info.field_name(), self.type_name())
192 }
193}
194
195/// The successful return type of [`ObjectValue::resolve_field`].
196pub enum ResolvedValue<'a> {
197 /// * JSON null represents GraphQL null
198 /// * A GraphQL enum value is represented as a JSON string
199 /// * GraphQL built-in scalars are coerced according to their respective *Result Coercion* spec
200 /// * For custom scalars, any JSON value is passed through as-is (including array or object)
201 Leaf(JsonValue),
202
203 /// Expected where the GraphQL type is an object, interface, or union type
204 Object(Box<dyn ObjectValue + 'a>),
205
206 /// Expected for GraphQL list types
207 List(Box<dyn Iterator<Item = Result<Self, ExecutionError>> + 'a>),
208
209 /// Skip this field as if the selection had `@skip(if: true)`:
210 /// do not insert null nor emit an error.
211 ///
212 /// This causes the eventual response data to be incomplete.
213 /// It can be useful to have some fields executed with per-field resolvers by this API
214 /// and other fields with some other execution model such as Apollo Federation,
215 /// with the two response `data` maps merged before sending the response.
216 ///
217 /// This is used by [`introspection::partial_execute`].
218 SkipForPartialExecution,
219}
220
221/// The successful return type of [`AsyncObjectValue::resolve_field`].
222pub enum AsyncResolvedValue<'a> {
223 /// * JSON null represents GraphQL null
224 /// * A GraphQL enum value is represented as a JSON string
225 /// * GraphQL built-in scalars are coerced according to their respective *Result Coercion* spec
226 /// * For custom scalars, any JSON value is passed through as-is (including array or object)
227 Leaf(JsonValue),
228
229 /// Expected where the GraphQL type is an object, interface, or union type
230 Object(Box<dyn AsyncObjectValue + 'a>),
231
232 /// Expected for GraphQL list types
233 List(BoxStream<'a, Result<Self, ExecutionError>>),
234
235 /// Skip this field as if the selection had `@skip(if: true)`:
236 /// do not insert null nor emit an error.
237 ///
238 /// This causes the eventual response data to be incomplete.
239 /// This can be useful to have some fields executed with per-field resolvers by this API
240 /// and other fields with some other execution model such as Apollo Federation,
241 /// with the two response `data` maps merged before sending the response.
242 ///
243 /// This is used by [`introspection::partial_execute`].
244 SkipForPartialExecution,
245}
246
247impl<'a> Execution<'a> {
248 /// Create a new builder for configuring GraphQL execution
249 ///
250 /// See [module-level documentation][self].
251 pub fn new(schema: &'a Valid<Schema>, document: &'a Valid<ExecutableDocument>) -> Self {
252 Self {
253 schema,
254 document,
255 operation: None,
256 implementers_map: None,
257 variable_values: None,
258 enable_schema_introspection: None,
259 }
260 }
261
262 /// Sets the operation to execute.
263 ///
264 /// Mutually exclusive with [`operation_name`][Self::operation_name].
265 pub fn operation(mut self, operation: &'a Operation) -> Self {
266 assert!(
267 self.operation.is_none(),
268 "operation to execute already provided"
269 );
270 self.operation = Some(operation);
271 self
272 }
273
274 /// Sets the operation to execute.
275 ///
276 /// Mutually exclusive with [`operation`][Self::operation].
277 ///
278 /// If neither is called or if `None` is passed here,
279 /// the document is expected to contain exactly one operation.
280 /// See [`document.operations.get()``][executable::OperationMap::get].
281 pub fn operation_name(mut self, operation_name: Option<&str>) -> Result<Self, RequestError> {
282 assert!(
283 self.operation.is_none(),
284 "operation to execute already provided"
285 );
286 self.operation = Some(self.document.operations.get(operation_name)?);
287 Ok(self)
288 }
289
290 /// Provide a pre-computed result of [`Schema::implementers_map`].
291 ///
292 /// If not provided here, it will be computed lazily on demand
293 /// and cached for the duration of execution.
294 pub fn implementers_map(mut self, implementers_map: &'a HashMap<Name, Implementers>) -> Self {
295 assert!(
296 self.implementers_map.is_none(),
297 "implementers map already provided"
298 );
299 self.implementers_map = Some(implementers_map);
300 self
301 }
302
303 /// Provide values of the request’s variables,
304 /// having already gone through [`coerce_variable_values`].
305 ///
306 /// Mutually exclusive with [`raw_variable_values`][Self::raw_variable_values].
307 ///
308 /// If neither is used, an empty map is assumed.
309 pub fn coerced_variable_values(mut self, variable_values: &'a Valid<JsonMap>) -> Self {
310 assert!(
311 self.variable_values.is_none(),
312 "variable values already provided"
313 );
314 self.variable_values = Some(VariableValues::Coerced(variable_values));
315 self
316 }
317
318 /// Provide values of the request’s variables.
319 ///
320 /// Mutually exclusive with [`coerced_variable_values`][Self::coerced_variable_values].
321 ///
322 /// If neither is used, an empty map is assumed.
323 pub fn raw_variable_values(mut self, variable_values: &'a JsonMap) -> Self {
324 assert!(
325 self.variable_values.is_none(),
326 "variable values already provided"
327 );
328 self.variable_values = Some(VariableValues::Raw(variable_values));
329 self
330 }
331
332 /// By default, schema introspection is _disabled_ per the [recommendation] to do so in production:
333 /// the meta-field `__schema` and `__type` return an execution error.
334 /// (`__typename` is not affected, as it is always available.)
335 ///
336 /// Setting this configuration to `true` makes execution
337 /// generate the appropriate response data for those fields.
338 ///
339 /// [`ObjectValue::resolve_field`] or [`AsyncObjectValue::resolve_field`] is never called
340 /// for meta-fields `__typename`, `__schema`, or `__type`.
341 /// They are always handled implicitly.
342 ///
343 /// [recommendation]: https://www.apollographql.com/blog/why-you-should-disable-graphql-introspection-in-production/
344 pub fn enable_schema_introspection(mut self, enable_schema_introspection: bool) -> Self {
345 assert!(
346 self.enable_schema_introspection.is_none(),
347 "schema introspection already configured"
348 );
349 self.enable_schema_introspection = Some(enable_schema_introspection);
350 self
351 }
352
353 /// Perform execution with synchronous resolvers
354 pub fn execute_sync(
355 &self,
356 initial_value: &dyn ObjectValue,
357 ) -> Result<ExecutionResponse, RequestError> {
358 // To avoid code duplication, we call the same `async fn`s here as in `execute_async`.
359 let future = self.execute_common(MaybeAsync::Sync(initial_value));
360
361 // An `async fn` returns a future whose `poll` method returns:
362 //
363 // * `Poll::Ready(R)` when the function returns
364 // * `Poll::Pending` when it `.await`s an inner future that returns `Poll::Pending`
365 //
366 // When we use `MaybeAsync::Sync`, there are no manually-written implementations
367 // of the `Future` trait involved at all, only `async fn`s that call each other.
368 // Therefore we expect `Poll::Pending` to never be generated.
369 // Instead all futures should be immediately ready,
370 // and this `expect` should therefore never panic.
371 future
372 .now_or_never()
373 .expect("expected async fn with sync resolvers to never be pending")
374 }
375
376 /// Perform execution with asynchronous resolvers
377 pub async fn execute_async(
378 &self,
379 initial_value: &dyn AsyncObjectValue,
380 ) -> Result<ExecutionResponse, RequestError> {
381 self.execute_common(MaybeAsync::Async(initial_value)).await
382 }
383
384 async fn execute_common(
385 &self,
386 initial_value: MaybeAsyncObject<'_>,
387 ) -> Result<ExecutionResponse, RequestError> {
388 let operation = if let Some(op) = self.operation {
389 op
390 } else {
391 self.document.operations.get(None)?
392 };
393
394 let object_type_name = operation.object_type();
395 let Some(root_operation_object_type_def) = self.schema.get_object(object_type_name) else {
396 return Err(RequestError {
397 message: "Undefined root operation type".to_owned(),
398 location: object_type_name.location(),
399 is_suspected_validation_bug: true,
400 });
401 };
402
403 let map;
404 let variable_values = match self.variable_values {
405 None => {
406 map = Valid::assume_valid(JsonMap::new());
407 &map
408 }
409 Some(VariableValues::Raw(v)) => {
410 map = coerce_variable_values(self.schema, operation, v)?;
411 &map
412 }
413 Some(VariableValues::Coerced(v)) => v,
414 };
415 let lock;
416 let implementers_map = match self.implementers_map {
417 None => {
418 lock = OnceLock::new();
419 MaybeLazy::Lazy(&lock)
420 }
421 Some(map) => MaybeLazy::Eager(map),
422 };
423 let enable_schema_introspection = self
424 .enable_schema_introspection
425 .unwrap_or(DEFAULT_ENABLE_SCHEMA_INTROSPECTION);
426 let mut errors = Vec::new();
427 let mut context = ExecutionContext {
428 schema: self.schema,
429 document: self.document,
430 variable_values,
431 errors: &mut errors,
432 implementers_map,
433 enable_schema_introspection,
434 };
435 let mode = match operation.operation_type {
436 executable::OperationType::Query | executable::OperationType::Subscription => {
437 ExecutionMode::Normal
438 }
439 executable::OperationType::Mutation => ExecutionMode::Sequential,
440 };
441 let result = execute_selection_set(
442 &mut context,
443 None,
444 mode,
445 root_operation_object_type_def,
446 initial_value,
447 &operation.selection_set.selections,
448 )
449 .await;
450 let data = result
451 // If `Result::ok` converts an error to `None` that’s an execution error on a non-null,
452 // field propagated all the way to the root,
453 // so that the JSON response should contain `"data": null`.
454 //
455 // No-op to witness the error type:
456 .inspect_err(|_: &PropagateNull| {})
457 .ok();
458 Ok(ExecutionResponse { data, errors })
459 }
460}
461
462impl<'a> ResolveInfo<'a> {
463 // https://github.com/graphql/graphql-js/blob/v16.11.0/src/type/definition.ts#L980-L991
464
465 /// The schema originally passed to [`Execution::new`]
466 pub fn schema(&self) -> &'a Valid<Schema> {
467 self.schema
468 }
469
470 pub fn implementers_map(&self) -> &'a HashMap<Name, Implementers> {
471 match self.implementers_map {
472 MaybeLazy::Eager(map) => map,
473 MaybeLazy::Lazy(cell) => cell.get_or_init(|| self.schema.implementers_map()),
474 }
475 }
476
477 /// The executable document originally passed to [`Execution::new`]
478 pub fn document(&self) -> &'a Valid<ExecutableDocument> {
479 self.document
480 }
481
482 /// The name of the field being resolved
483 pub fn field_name(&self) -> &'a str {
484 &self.fields[0].name
485 }
486
487 /// The field definition in the schema
488 pub fn field_definition(&self) -> &'a schema::FieldDefinition {
489 &self.fields[0].definition
490 }
491
492 /// The field selections being resolved.
493 ///
494 /// There is always at least one, but there may be more in case of
495 /// [field merging](https://spec.graphql.org/September2025/#sec-Field-Selection-Merging).
496 pub fn field_selections(&self) -> &'a [&'a executable::Field] {
497 self.fields
498 }
499
500 /// The arguments passed to this field, after
501 /// [`CoerceArgumentValues()`](https://spec.graphql.org/September2025/#sec-Coercing-Field-Arguments):
502 /// this matches the argument definitions in the schema.
503 pub fn arguments(&self) -> &'a JsonMap {
504 self.arguments
505 }
506}
507
508impl<'a> ResolvedValue<'a> {
509 /// Construct a null leaf resolved value
510 pub fn null() -> Self {
511 Self::Leaf(JsonValue::Null)
512 }
513
514 /// Construct a leaf resolved value from something that is convertible to JSON
515 pub fn leaf(json: impl Into<JsonValue>) -> Self {
516 Self::Leaf(json.into())
517 }
518
519 /// Construct an object resolved value
520 pub fn object(object: impl ObjectValue + 'a) -> Self {
521 Self::Object(Box::new(object))
522 }
523
524 /// Construct an object resolved value or null
525 pub fn nullable_object(opt_object: Option<impl ObjectValue + 'a>) -> Self {
526 match opt_object {
527 Some(object) => Self::Object(Box::new(object)),
528 None => Self::null(),
529 }
530 }
531
532 /// Construct a list resolved value from an iterator
533 ///
534 /// If errors can happen during iteration,
535 /// construct the [`ResolvedValue::List`] enum variant directly instead.
536 pub fn list<I>(iter: I) -> Self
537 where
538 I: IntoIterator<Item = Self>,
539 I::IntoIter: 'a,
540 {
541 Self::List(Box::new(iter.into_iter().map(Ok)))
542 }
543}
544
545impl<'a> AsyncResolvedValue<'a> {
546 /// Construct a null leaf resolved value
547 pub fn null() -> Self {
548 Self::Leaf(JsonValue::Null)
549 }
550
551 /// Construct a leaf resolved value from something that is convertible to JSON
552 pub fn leaf(json: impl Into<JsonValue>) -> Self {
553 Self::Leaf(json.into())
554 }
555
556 /// Construct an object resolved value
557 pub fn object(object: impl AsyncObjectValue + 'a) -> Self {
558 Self::Object(Box::new(object))
559 }
560
561 /// Construct an object resolved value or null
562 pub fn nullable_object(opt_object: Option<impl AsyncObjectValue + 'a>) -> Self {
563 match opt_object {
564 Some(object) => Self::Object(Box::new(object)),
565 None => Self::null(),
566 }
567 }
568
569 /// Construct a list resolved value from an iterator
570 ///
571 /// If errors can happen during iteration,
572 /// construct the [`ResolvedValue::List`] enum variant directly instead.
573 pub fn list<I>(iter: I) -> Self
574 where
575 I: IntoIterator<Item = Self>,
576 I::IntoIter: 'a + Send,
577 {
578 Self::List(Box::pin(futures::stream::iter(iter.into_iter().map(Ok))))
579 }
580}
581
582impl MaybeAsync<Box<dyn AsyncObjectValue + '_>, Box<dyn ObjectValue + '_>> {
583 pub(crate) fn type_name(&self) -> &str {
584 match self {
585 MaybeAsync::Async(obj) => obj.type_name(),
586 MaybeAsync::Sync(obj) => obj.type_name(),
587 }
588 }
589}
590
591impl ExecutionError {
592 fn unknown_field(field_name: &str, type_name: &str) -> Self {
593 Self {
594 message: format!("unexpected field name: {field_name} in type {type_name}"),
595 }
596 }
597}