Skip to content

Commit 304edcf

Browse files
authored
Reduce scope of execution / introspection APIs (#944)
The `execution` module was sort of aiming for comprehensive handling of GraphQL execution. For example, `execution::Response` was able to represent any possible GraphQL response including `extensions` `enum ResponseData { Absent, Null, Object(JsonMap) }`. However a real-world GraphQL server is unlikely to be satisfied with using that external type as its only representation of responses. In practice Apollo Router has a struct that carries additional data not serialized to JSON. This redesigns the public API and reduce it to what’s actually needed to support the main functionality we want to provide: introspection. Additionally, the introspection "splitting" functionality is removed, as Apollo Router doesn’t use it and I don’t have great confidence it’s correct. Splitting a document into two `Valid<ExecutableDocument>`s requires keeping track of which variables and fragments are used where, since producing a document with unused definitions would make it invalid. This is tricky to get right, and not particularly useful. Instead we now have introspection execution take the original unmodified document and ignore the fields it doesn’t know how to execute. It’s up to the caller to execute those fields separately and merge two partial responses. New public API below, replacing `apollo_compiler::execution`. See doc-comments in the PR diff. `apollo_compiler::request` module: ```rust pub fn coerce_variable_values( schema: &Valid<Schema>, operation: &Operation, values: &JsonMap, ) -> Result<Valid<JsonMap>, RequestError> {…} #[derive(Debug, Clone)] pub struct RequestError {…} impl RequestError { // Should this return a plain `&str`? // It’s a `String` underneath atm but we may want something more structured later pub fn message(&self) -> impl Display + '_ {…} pub fn location(&self) -> Option<SourceSpan> {…} pub fn to_graphql_error(&self, sources: &SourceMap) -> GraphQLError {…} } ``` `apollo_compiler::response` module: ```rust pub use serde_json_bytes; pub type JsonValue = serde_json_bytes::Value; pub type JsonMap = serde_json_bytes::Map<serde_json_bytes::ByteString, JsonValue>; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ExecutionResponse { pub errors: Vec<GraphQLError>, // Intentionally cannot represent request error (absent `data`), `Result` is used instead: pub data: Option<JsonMap>, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GraphQLError { pub message: String, pub locations: Vec<LineColumn>, pub path: Vec<ResponseDataPathSegment>, // Set to `{"APOLLO_SUSPECTED_VALIDATION_BUG": true}` for cases that "should never happen" in valid documents pub extensions: JsonMap, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ResponseDataPathSegment { Field(crate::Name), ListIndex(usize), } impl GraphQLError { pub fn new( message: impl Into<String>, location: Option<SourceSpan>, sources: &SourceMap, ) -> Self {…} } ``` `apollo_compiler::introspection` module: ```rust pub fn check_max_depth( document: &Valid<ExecutableDocument>, operation: &Operation, ) -> Result<(), RequestError> {…} pub fn partial_execute( schema: &Valid<Schema>, implementers_map: &HashMap<Name, Implementers>, document: &Valid<ExecutableDocument>, operation: &Operation, variable_values: &Valid<JsonMap>, ) -> Result<ExecutionResponse, RequestError> {…} ``` Fixes #937
1 parent 87d1d4d commit 304edcf

24 files changed

+538
-1555
lines changed

crates/apollo-compiler/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2121

2222
## BREAKING
2323

24+
- **Reduce scope of execution / introspection APIs - [SimonSapin], [pull/944]**
2425
- **Move `apollo_compiler::schema::ArgumentByNameError` into `apollo_compiler::ast` - [SimonSapin], [pull/942]**
2526

2627
## Features
@@ -41,6 +42,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
4142
[pull/925]: https://github.com/apollographql/apollo-rs/issues/925
4243
[pull/929]: https://github.com/apollographql/apollo-rs/pull/929
4344
[pull/942]: https://github.com/apollographql/apollo-rs/pull/942
45+
[pull/944]: https://github.com/apollographql/apollo-rs/pull/944
4446

4547

4648
# [1.0.0-beta.24](https://crates.io/crates/apollo-compiler/1.0.0-beta.24) - 2024-09-24

crates/apollo-compiler/examples/introspect.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use apollo_compiler::execution::SchemaIntrospectionQuery;
1+
use apollo_compiler::introspection;
22
use apollo_compiler::validation::Valid;
33
use apollo_compiler::ExecutableDocument;
44
use apollo_compiler::Schema;
@@ -20,7 +20,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
2020
return Err("Provide a query to execute".into());
2121
};
2222

23-
let query = if filename.starts_with('{') {
23+
let doc = if filename.starts_with('{') {
2424
ExecutableDocument::parse_and_validate(&schema, filename, "input.graphql")
2525
} else {
2626
ExecutableDocument::parse_and_validate(
@@ -31,17 +31,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3131
}
3232
.map_err(|err| err.errors.to_string())?;
3333

34-
let variables = Default::default();
35-
let response = SchemaIntrospectionQuery::split_and_execute(
34+
let response = introspection::partial_execute(
3635
&schema,
37-
&query,
38-
query
39-
.operations
36+
&schema.implementers_map(),
37+
&doc,
38+
doc.operations
4039
.get(None)
41-
.map_err(|_| "Provided query must be an anonymous introspection query")?,
42-
Valid::assume_valid_ref(&variables),
43-
|_| panic!("Provided query must not have non-introspection elements"),
44-
);
40+
.map_err(|_| "Must have exactly one operation")?,
41+
Valid::assume_valid_ref(&Default::default()),
42+
)
43+
.map_err(|e| e.message().to_string())?;
4544

4645
serde_json::to_writer_pretty(std::io::stdout().lock(), &response)?;
4746

crates/apollo-compiler/src/ast/impls.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,18 @@ impl OperationType {
713713
}
714714
}
715715

716+
pub fn is_query(&self) -> bool {
717+
matches!(self, Self::Query)
718+
}
719+
720+
pub fn is_mutation(&self) -> bool {
721+
matches!(self, Self::Mutation)
722+
}
723+
724+
pub fn is_subscription(&self) -> bool {
725+
matches!(self, Self::Subscription)
726+
}
727+
716728
serialize_method!();
717729
}
718730

crates/apollo-compiler/src/diagnostic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@
6767
//! }
6868
//! }
6969
//! ```
70-
use crate::execution::GraphQLError;
7170
use crate::parser::FileId;
7271
use crate::parser::LineColumn;
7372
use crate::parser::SourceFile;
7473
use crate::parser::SourceMap;
7574
use crate::parser::SourceSpan;
75+
use crate::response::GraphQLError;
7676
#[cfg(doc)]
7777
use crate::ExecutableDocument;
7878
#[cfg(doc)]

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

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub use crate::ast::OperationType;
7979
pub use crate::ast::Type;
8080
pub use crate::ast::Value;
8181
pub use crate::ast::VariableDefinition;
82+
use crate::request::RequestError;
8283
pub use crate::Name;
8384

8485
/// Executable definitions, annotated with type information
@@ -332,18 +333,6 @@ pub(crate) enum ExecutableDefinitionName {
332333
Fragment(Name),
333334
}
334335

335-
/// A request error returned by [`OperationMap::get`]
336-
///
337-
/// If `get_operation`’s `name_request` argument was `Some`, this error indicates
338-
/// that the document does not contain an operation with the requested name.
339-
///
340-
/// If `name_request` was `None`, the request is ambiguous
341-
/// because the document contains multiple operations
342-
/// (or zero, though the document would be invalid in that case).
343-
#[derive(Debug, Clone, PartialEq, Eq)]
344-
#[non_exhaustive]
345-
pub struct GetOperationError();
346-
347336
impl ExecutableDocument {
348337
/// Create an empty document, to be filled programatically
349338
pub fn new() -> Self {
@@ -432,52 +421,73 @@ impl OperationMap {
432421

433422
/// Return the relevant operation for a request, or a request error
434423
///
435-
/// This the [GetOperation()](https://spec.graphql.org/October2021/#GetOperation())
424+
/// This is the [_GetOperation()_](https://spec.graphql.org/October2021/#GetOperation())
436425
/// algorithm in the _Executing Requests_ section of the specification.
437426
///
438427
/// A GraphQL request comes with a document (which may contain multiple operations)
439428
/// an an optional operation name. When a name is given the request executes the operation
440429
/// with that name, which is expected to exist. When it is not given / null / `None`,
441430
/// the document is expected to contain a single operation (which may or may not be named)
442431
/// to avoid ambiguity.
443-
pub fn get(&self, name_request: Option<&str>) -> Result<&Node<Operation>, GetOperationError> {
432+
pub fn get(&self, name_request: Option<&str>) -> Result<&Node<Operation>, RequestError> {
444433
if let Some(name) = name_request {
445434
// Honor the request
446-
self.named.get(name)
447-
} else if let Some(op) = &self.anonymous {
448-
// No name request, return the anonymous operation if it’s the only operation
449-
self.named.is_empty().then_some(op)
450-
} else {
451-
// No name request or anonymous operation, return a named operation if it’s the only one
452435
self.named
453-
.values()
454-
.next()
455-
.and_then(|op| (self.named.len() == 1).then_some(op))
436+
.get(name)
437+
.ok_or_else(|| format!("No operation named '{name}'"))
438+
} else {
439+
// No name request (`operationName` unspecified or null)
440+
if let Some(op) = &self.anonymous {
441+
// Return the anonymous operation if it’s the only operation
442+
self.named.is_empty().then_some(op)
443+
} else {
444+
// No anonymous operation, return a named operation if it’s the only one
445+
self.named
446+
.values()
447+
.next()
448+
.and_then(|op| (self.named.len() == 1).then_some(op))
449+
}
450+
.ok_or_else(|| {
451+
"Ambiguous request: multiple operations but no specified `operationName`".to_owned()
452+
})
456453
}
457-
.ok_or(GetOperationError())
454+
.map_err(|message| RequestError {
455+
message,
456+
location: None,
457+
is_suspected_validation_bug: false,
458+
})
458459
}
459460

460461
/// Similar to [`get`][Self::get] but returns a mutable reference.
461-
pub fn get_mut(
462-
&mut self,
463-
name_request: Option<&str>,
464-
) -> Result<&mut Operation, GetOperationError> {
462+
pub fn get_mut(&mut self, name_request: Option<&str>) -> Result<&mut Operation, RequestError> {
465463
if let Some(name) = name_request {
466464
// Honor the request
467-
self.named.get_mut(name)
468-
} else if let Some(op) = &mut self.anonymous {
469-
// No name request, return the anonymous operation if it’s the only operation
470-
self.named.is_empty().then_some(op)
471-
} else {
472-
// No name request or anonymous operation, return a named operation if it’s the only one
473-
let len = self.named.len();
474465
self.named
475-
.values_mut()
476-
.next()
477-
.and_then(|op| (len == 1).then_some(op))
466+
.get_mut(name)
467+
.ok_or_else(|| format!("No operation named '{name}'"))
468+
} else {
469+
// No name request (`operationName` unspecified or null)
470+
if let Some(op) = &mut self.anonymous {
471+
// Return the anonymous operation if it’s the only operation
472+
self.named.is_empty().then_some(op)
473+
} else {
474+
// No anonymous operation, return a named operation if it’s the only one
475+
let len = self.named.len();
476+
self.named
477+
.values_mut()
478+
.next()
479+
.and_then(|op| (len == 1).then_some(op))
480+
}
481+
.ok_or_else(|| {
482+
"Ambiguous request: multiple operations but no specified `operationName`".to_owned()
483+
})
478484
}
479485
.map(Node::make_mut)
480-
.ok_or(GetOperationError())
486+
.map_err(|message| RequestError {
487+
message,
488+
location: None,
489+
is_suspected_validation_bug: false,
490+
})
481491
}
482492

483493
/// Insert the given operation in either `named_operations` or `anonymous_operation`
@@ -532,7 +542,8 @@ impl Operation {
532542
///
533543
/// This does **not** perform [field merging] nor fragment spreads de-duplication,
534544
/// so multiple items in this iterator may have the same response key,
535-
/// point to the same field definition, or even be the same field selection.
545+
/// point to the same field definition,
546+
/// or even be the same field selection (reached through different fragments).
536547
///
537548
/// [field merging]: https://spec.graphql.org/draft/#sec-Field-Selection-Merging
538549
pub fn root_fields<'doc>(

crates/apollo-compiler/src/execution/engine.rs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ use crate::execution::input_coercion::coerce_argument_values;
77
use crate::execution::resolver::ObjectValue;
88
use crate::execution::resolver::ResolverError;
99
use crate::execution::result_coercion::complete_value;
10-
use crate::execution::GraphQLError;
11-
use crate::execution::JsonMap;
12-
use crate::execution::JsonValue;
13-
use crate::execution::ResponseDataPathElement;
1410
use crate::parser::SourceMap;
1511
use crate::parser::SourceSpan;
12+
use crate::response::GraphQLError;
13+
use crate::response::JsonMap;
14+
use crate::response::JsonValue;
15+
use crate::response::ResponseDataPathSegment;
1616
use crate::schema::ExtendedType;
1717
use crate::schema::FieldDefinition;
1818
use crate::schema::ObjectType;
@@ -42,7 +42,7 @@ pub(crate) struct PropagateNull;
4242
pub(crate) type LinkedPath<'a> = Option<&'a LinkedPathElement<'a>>;
4343

4444
pub(crate) struct LinkedPathElement<'a> {
45-
pub(crate) element: ResponseDataPathElement,
45+
pub(crate) element: ResponseDataPathSegment,
4646
pub(crate) next: LinkedPath<'a>,
4747
}
4848

@@ -59,12 +59,13 @@ pub(crate) fn execute_selection_set<'a>(
5959
object_value: &ObjectValue<'_>,
6060
selections: impl IntoIterator<Item = &'a Selection>,
6161
) -> Result<JsonMap, PropagateNull> {
62-
let mut grouped_field_set = IndexMap::with_hasher(Default::default());
62+
let mut grouped_field_set = IndexMap::default();
6363
collect_fields(
6464
schema,
6565
document,
6666
variable_values,
6767
object_type,
68+
object_value,
6869
selections,
6970
&mut HashSet::default(),
7071
&mut grouped_field_set,
@@ -92,7 +93,7 @@ pub(crate) fn execute_selection_set<'a>(
9293
JsonValue::from(object_type.name.as_str())
9394
} else {
9495
let field_path = LinkedPathElement {
95-
element: ResponseDataPathElement::Field(response_key.clone()),
96+
element: ResponseDataPathSegment::Field(response_key.clone()),
9697
next: path,
9798
};
9899
execute_field(
@@ -119,6 +120,7 @@ fn collect_fields<'a>(
119120
document: &'a ExecutableDocument,
120121
variable_values: &Valid<JsonMap>,
121122
object_type: &ObjectType,
123+
object_value: &ObjectValue<'_>,
122124
selections: impl IntoIterator<Item = &'a Selection>,
123125
visited_fragments: &mut HashSet<&'a Name>,
124126
grouped_fields: &mut IndexMap<&'a Name, Vec<&'a Field>>,
@@ -130,10 +132,14 @@ fn collect_fields<'a>(
130132
continue;
131133
}
132134
match selection {
133-
Selection::Field(field) => grouped_fields
134-
.entry(field.response_key())
135-
.or_default()
136-
.push(field.as_ref()),
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+
}
137143
Selection::FragmentSpread(spread) => {
138144
let new = visited_fragments.insert(&spread.fragment_name);
139145
if !new {
@@ -150,6 +156,7 @@ fn collect_fields<'a>(
150156
document,
151157
variable_values,
152158
object_type,
159+
object_value,
153160
&fragment.selection_set.selections,
154161
visited_fragments,
155162
grouped_fields,
@@ -166,6 +173,7 @@ fn collect_fields<'a>(
166173
document,
167174
variable_values,
168175
object_type,
176+
object_value,
169177
&inline.selection_set.selections,
170178
visited_fragments,
171179
grouped_fields,
@@ -280,7 +288,7 @@ pub(crate) fn try_nullify(
280288
}
281289
}
282290

283-
pub(crate) fn path_to_vec(mut link: LinkedPath<'_>) -> Vec<ResponseDataPathElement> {
291+
pub(crate) fn path_to_vec(mut link: LinkedPath<'_>) -> Vec<ResponseDataPathSegment> {
284292
let mut path = Vec::new();
285293
while let Some(node) = link {
286294
path.push(node.element.clone());
@@ -309,8 +317,10 @@ impl SuspectedValidationBug {
309317
sources: &SourceMap,
310318
path: LinkedPath<'_>,
311319
) -> GraphQLError {
312-
let mut err = self.into_graphql_error(sources);
313-
err.path = path_to_vec(path);
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());
314324
err
315325
}
316326
}

0 commit comments

Comments
 (0)