Skip to main content

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}