apollo_compiler/introspection/mod.rs
1//! [Execution](https://spec.graphql.org/draft/#sec-Execution) engine
2//! for the [schema introspection](https://spec.graphql.org/draft/#sec-Schema-Introspection)
3//! portion of a query
4//!
5//! The main entry point is [`partial_execute`].
6
7use crate::collections::HashMap;
8use crate::executable::Operation;
9#[cfg(doc)]
10use crate::executable::OperationMap;
11use crate::execution::engine::execute_selection_set;
12use crate::execution::engine::ExecutionMode;
13use crate::execution::engine::PropagateNull;
14#[cfg(doc)]
15use crate::request::coerce_variable_values;
16use crate::request::RequestError;
17use crate::response::ExecutionResponse;
18use crate::response::JsonMap;
19use crate::schema::Implementers;
20use crate::validation::Valid;
21use crate::ExecutableDocument;
22use crate::Name;
23use crate::Schema;
24
25mod max_depth;
26mod resolvers;
27
28/// Check that the nesting level of some list fields does not exceed a fixed depth limit.
29///
30/// Since [the schema-introspection schema][s] is recursive,
31/// a malicious query could cause huge responses that grow exponentially to the nesting depth.
32///
33/// An error result is a [request error](https://spec.graphql.org/draft/#request-error):
34/// execution must not run at all,
35/// and the GraphQL response must not have a `data` key (which is different from `data: null`).
36///
37/// The exact criteria may change in future apollo-compiler versions.
38///
39/// [s]: https://spec.graphql.org/draft/#sec-Schema-Introspection.Schema-Introspection-Schema
40pub fn check_max_depth(
41 document: &Valid<ExecutableDocument>,
42 operation: &Operation,
43) -> Result<(), RequestError> {
44 let initial_depth = 0;
45 max_depth::check_selection_set(
46 document,
47 &mut HashMap::default(),
48 initial_depth,
49 &operation.selection_set,
50 )
51 .map(drop)
52}
53
54/// Excecutes the [schema introspection](https://spec.graphql.org/draft/#sec-Schema-Introspection)
55/// portion of a query and returns a partial response.
56///
57/// * Consider calling [`check_max_depth`] before this function
58/// * `implementers_map` is expected to be form
59/// [`schema.implementers_map()`][Schema::implementers_map],
60/// allowing it to be computed once and reused for many queries
61/// * `operation` is expected to be from
62/// [`document.operations.get(operation_name)?`][OperationMap::get]
63/// * `operation` is expected to be a query,
64/// check [`operation.operation_type.is_query()`][Operation::operation_type]
65/// * `variable_values` is expected to be from [`coerce_variable_values`]
66///
67/// Concrete [root fields][Operation::root_fields] (those with an explicit definition in the schema)
68/// are **_silently ignored_**.
69///
70/// Only introspection meta-fields are executed:
71/// `__typename` (at the response root), `__type`, and `__schema`.
72/// If the operation also contains concrete fields,
73/// the caller can execute them separately and merge the two partial responses.
74/// To categorize which kinds of root fields are present, consider using code like:
75///
76/// ```
77/// # use apollo_compiler::executable as exe;
78/// # fn categorize_fields(document: &exe::ExecutableDocument, operation: &exe::Operation) {
79/// let mut has_schema_introspection_fields = false;
80/// let mut has_root_typename_fields = false;
81/// let mut has_concrete_fields = false;
82/// for root_field in operation.root_fields(document) {
83/// match root_field.name.as_str() {
84/// "__type" | "__schema" => has_schema_introspection_fields = true,
85/// "__typename" => has_root_typename_fields = true,
86/// _ => has_concrete_fields = true,
87/// }
88/// }
89/// # }
90/// ```
91pub fn partial_execute(
92 schema: &Valid<Schema>,
93 implementers_map: &HashMap<Name, Implementers>,
94 document: &Valid<ExecutableDocument>,
95 operation: &Operation,
96 variable_values: &Valid<JsonMap>,
97) -> Result<ExecutionResponse, RequestError> {
98 let object_type_name = operation.object_type();
99 let Some(root_operation_object_type_def) = schema.get_object(object_type_name) else {
100 return Err(RequestError {
101 message: "Undefined root operation type".to_owned(),
102 location: object_type_name.location(),
103 is_suspected_validation_bug: true,
104 });
105 };
106
107 let initial_value =
108 &resolvers::IntrospectionRootResolver(resolvers::SchemaWithImplementersMap {
109 schema,
110 implementers_map,
111 });
112 let mut errors = Vec::new();
113 let path = None;
114 let data = execute_selection_set(
115 schema,
116 document,
117 variable_values,
118 &mut errors,
119 path,
120 ExecutionMode::Normal,
121 root_operation_object_type_def,
122 initial_value,
123 &operation.selection_set.selections,
124 )
125 // What `.ok()` below converts to `None` is a field error on a non-null field
126 // propagated all the way to the root, so that the response JSON should contain `"data": null`.
127 //
128 // No-op to witness the error type:
129 .inspect_err(|_: &PropagateNull| {})
130 .ok();
131 Ok(ExecutionResponse { data, errors })
132}