Skip to content

Commit d97abbc

Browse files
authored
Add schema introspection support and execution-related APIs (#758)
- Add data structure in `apollo_compiler::execution` for a GraphQL response, its data, and errors. All (de)serializable with `serde`. - Add [`coerce_variable_values()`] in that same module. - Add `apollo_compiler::execution::SchemaIntrospection` providing full execution for the [schema introspection] parts of an operation and separating the rest to be executed separately. In order to support all kinds of introspection queries this actually includes a full execution engine where users provide objects with resolvable fields. At this time this engine is not exposed in the public API. If you’re interested in it [let us know] about your use case! - Add `ExecutableDocument::insert_operation` convenience method. [`coerce_variable_values()`]: https://spec.graphql.org/October2021/#sec-Coercing-Variable-Values [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection [let us know]: https://github.com/apollographql/apollo-rs/issues/new?assignees=&labels=triage&projects=&template=feature_request.md
1 parent fcc2ca8 commit d97abbc

21 files changed

+5046
-48
lines changed

crates/apollo-compiler/CHANGELOG.md

+16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2020
# [x.x.x] (unreleased) - 2023-xx-xx
2121

2222
## Features
23+
- **Add execution-related and introspection functionality - [SimonSapin], [pull/758]:**
24+
- Add data structure in `apollo_compiler::execution` for a GraphQL response, its data, and errors.
25+
All (de)serializable with `serde`.
26+
- Add [`coerce_variable_values()`] in that same module.
27+
- Add `apollo_compiler::execution::SchemaIntrospection`
28+
providing full execution for the [schema introspection] parts of an operation
29+
and separating the rest to be executed separately.
30+
In order to support all kinds of introspection queries this actually includes
31+
a full execution engine where users provide objects with resolvable fields.
32+
At this time this engine is not exposed in the public API.
33+
If you’re interested in it [let us know] about your use case!
34+
- Add `ExecutableDocument::insert_operation` convenience method.
2335
- **Add `NodeStr::from(Name)` - [goto-bus-stop], [pull/773]**
2436
- **Convenience accessors for `ast::Selection` enum - [SimonSapin], [pull/777]**
2537
`as_field`, `as_inline_fragment`, and `as_fragment_spread`; all returning `Option<&_>`.
@@ -30,9 +42,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3042

3143
[goto-bus-stop]: https://github.com/goto-bus-stop]
3244
[SimonSapin]: https://github.com/SimonSapin
45+
[pull/758]: https://github.com/apollographql/apollo-rs/pull/758
3346
[pull/773]: https://github.com/apollographql/apollo-rs/pull/773
3447
[pull/774]: https://github.com/apollographql/apollo-rs/pull/774
3548
[pull/777]: https://github.com/apollographql/apollo-rs/pull/777
49+
[`coerce_variable_values()`]: https://spec.graphql.org/October2021/#sec-Coercing-Variable-Values
50+
[schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
51+
[let us know]: https://github.com/apollographql/apollo-rs/issues/new?assignees=&labels=triage&projects=&template=feature_request.md
3652

3753
# [1.0.0-beta.10](https://crates.io/crates/apollo-compiler/1.0.0-beta.10) - 2023-12-04
3854

crates/apollo-compiler/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ indexmap = "2.0.0"
2424
rowan = "0.15.5"
2525
salsa = "0.16.1"
2626
serde = { version = "1.0", features = ["derive"] }
27+
serde_json_bytes = { version = "0.2.2", features = ["preserve_order"] }
2728
thiserror = "1.0.31"
2829
triomphe = "0.1.9"
2930
# TODO: replace `sptr` with standard library methods when available:

crates/apollo-compiler/src/executable/mod.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ impl ExecutableDocument {
241241

242242
/// Return the relevant operation for a request, or a request error
243243
///
244-
/// This the [GetOperation](https://spec.graphql.org/October2021/#GetOperation())
244+
/// This the [GetOperation()](https://spec.graphql.org/October2021/#GetOperation())
245245
/// algorithm in the _Executing Requests_ section of the specification.
246246
///
247247
/// A GraphQL request comes with a document (which may contain multiple operations)
@@ -292,6 +292,20 @@ impl ExecutableDocument {
292292
.ok_or(GetOperationError())
293293
}
294294

295+
/// Insert the given operation in either `named_operations` or `anonymous_operation`
296+
/// as appropriate, and return the old operation (if any) with that name (or lack thereof).
297+
pub fn insert_operation(
298+
&mut self,
299+
operation: impl Into<Node<Operation>>,
300+
) -> Option<Node<Operation>> {
301+
let operation = operation.into();
302+
if let Some(name) = &operation.name {
303+
self.named_operations.insert(name.clone(), operation)
304+
} else {
305+
self.anonymous_operation.replace(operation)
306+
}
307+
}
308+
295309
serialize_method!();
296310
}
297311

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

0 commit comments

Comments
 (0)