Skip to content

Commit

Permalink
Add schema introspection
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonSapin committed Nov 23, 2023
1 parent 192fc5a commit 52cde13
Show file tree
Hide file tree
Showing 15 changed files with 3,836 additions and 6 deletions.
20 changes: 15 additions & 5 deletions crates/apollo-compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,32 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## Features

- **Add `parse_and_validate` constructors for `Schema` and `ExecutableDocument` - [SimonSapin],
[pull/752]:**
when mutating isn’t needed after parsing,
this returns an immutable `Valid<_>` value in one step.
- **Add execution-related and introspection functionality - [SimonSapin],
- **Add [schema introspection] support and execution-related APIs - [SimonSapin],
[pull/FIXME]:**
- 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 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.
- Make `Name` and `NodeStr` (de)serializable with `serde`.
- Add `ExecutableDocument::insert_operation` convenience method.
- **Add `parse_and_validate` constructors for `Schema` and `ExecutableDocument` - [SimonSapin],
[pull/752]:**
when mutating isn’t needed after parsing,
this returns an immutable `Valid<_>` value in one step.

[SimonSapin]: https://github.com/SimonSapin
[issue/709]: https://github.com/apollographql/apollo-rs/issues/709
[issue/751]: https://github.com/apollographql/apollo-rs/issues/751
[pull/752]: https://github.com/apollographql/apollo-rs/pull/752
[`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.0.0-beta.7](https://crates.io/crates/apollo-compiler/1.0.0-beta.7) - 2023-11-17
Expand Down
253 changes: 253 additions & 0 deletions crates/apollo-compiler/src/executable/filtering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
use crate::ast;
use crate::executable::Field;
use crate::executable::Fragment;
use crate::executable::FragmentSpread;
use crate::executable::InlineFragment;
use crate::executable::Operation;
use crate::executable::Selection;
use crate::executable::SelectionSet;
use crate::execution::RequestError;
use crate::schema;
use crate::schema::Name;
use crate::validation::Valid;
use crate::ExecutableDocument;
use crate::Node;
use crate::Schema;
use indexmap::IndexMap;
use std::collections::HashSet;

type FragmentMap = IndexMap<Name, Node<Fragment>>;

pub(crate) struct FilteredDocumentBuilder<'doc, Predicate>
where
Predicate: FnMut(&Selection) -> bool,
{
document: &'doc Valid<ExecutableDocument>,
remove_selection: Predicate,
new_fragments: FragmentMap,

/// The contents of these fragments was filtered to nothing.
/// Corresonding fragment spreads should be removed.
emptied_fragments: HashSet<&'doc Name>,

/// Avoid infinite recursion
fragments_being_processed: HashSet<&'doc Name>,

/// Remove unused variables to satisfy the _All Variables Used_ validation rule.
/// This feels like busy work. How important is it to produce a fully valid document?
/// <https://spec.graphql.org/October2021/#sec-All-Variables-Used>
variables_used: HashSet<&'doc Name>,
}

impl<'doc, Predicate> FilteredDocumentBuilder<'doc, Predicate>
where
Predicate: FnMut(&Selection) -> bool,
{
/// Return a document with exactly one operation,
/// which is `operation` filtered according to `remove_selection`.
///
/// If a non-empty selection set becomes empty, its parent is removed.
/// Returns `None` if there is nothing left.
///
/// The returned document also contains fragments needed by the remaining selections.
/// Fragment definitions are filtered too.
pub(crate) fn single_operation(
schema: &Valid<Schema>,
document: &'doc Valid<ExecutableDocument>,
operation: &'doc Operation,
remove_selection: Predicate,
) -> Result<Option<Valid<ExecutableDocument>>, RequestError> {
let mut builder = Self {
document,
remove_selection,
new_fragments: FragmentMap::new(),
emptied_fragments: HashSet::new(),
fragments_being_processed: HashSet::new(),
variables_used: HashSet::new(),
};
let Some(new_operation) = builder.filter_operation(operation)? else {
return Ok(None);
};
let mut new_document = ExecutableDocument {
sources: document.sources.clone(),
anonymous_operation: None,
named_operations: IndexMap::new(),
fragments: builder.new_fragments,
};
new_document.insert_operation(new_operation);
let valid = if cfg!(debug_assertions) {
new_document
.validate(schema)
.expect("filtering a valid document should result in a valid document")
} else {
Valid::assume_valid(new_document)
};
Ok(Some(valid))
}

fn filter_operation(
&mut self,
operation: &'doc Operation,
) -> Result<Option<Operation>, RequestError> {
self.variables_used.clear();
for var in &operation.variables {
if let Some(default) = &var.default_value {
self.variables_in_value(default)
}
}
for directive in &operation.directives {
for arg in &directive.arguments {
self.variables_in_value(&arg.value)
}
}
let Some(selection_set) = self.filter_selection_set(&operation.selection_set)? else {
return Ok(None);
};
Ok(Some(Operation {
operation_type: operation.operation_type,
name: operation.name.clone(),
variables: operation
.variables
.iter()
.filter(|var| self.variables_used.contains(&var.name))
.cloned()
.collect(),
directives: operation.directives.clone(),
selection_set,
}))
}

fn filter_selection_set(
&mut self,
selection_set: &'doc SelectionSet,
) -> Result<Option<SelectionSet>, RequestError> {
let selections = selection_set
.selections
.iter()
.filter_map(|selection| self.filter_selection(selection).transpose())
.collect::<Result<Vec<_>, _>>()?;
if !selections.is_empty() {
Ok(Some(SelectionSet {
ty: selection_set.ty.clone(),
selections,
}))
} else {
Ok(None)
}
}

fn filter_selection(
&mut self,
selection: &'doc Selection,
) -> Result<Option<Selection>, RequestError> {
if (self.remove_selection)(selection) {
return Ok(None);
}
let new_selection = match selection {
Selection::Field(field) => {
let selection_set = if field.selection_set.selections.is_empty() {
// Keep a leaf field as-is
field.selection_set.clone()
} else {
// `?` removes a non-leaf field if its sub-selections becomes empty
let Some(set) = self.filter_selection_set(&field.selection_set)? else {
return Ok(None);
};
set
};
for arg in &field.arguments {
self.variables_in_value(&arg.value)
}
Selection::Field(field.same_location(Field {
definition: field.definition.clone(),
alias: field.alias.clone(),
name: field.name.clone(),
arguments: field.arguments.clone(),
directives: field.directives.clone(),
selection_set,
}))
}
Selection::InlineFragment(inline_fragment) => {
let Some(selection_set) =
self.filter_selection_set(&inline_fragment.selection_set)?
else {
return Ok(None);
};
Selection::InlineFragment(inline_fragment.same_location(InlineFragment {
type_condition: inline_fragment.type_condition.clone(),
directives: inline_fragment.directives.clone(),
selection_set,
}))
}
Selection::FragmentSpread(fragment_spread) => {
let name = &fragment_spread.fragment_name;
if self.emptied_fragments.contains(name) {
return Ok(None);
}
if self.fragments_being_processed.contains(name) {
return Err(RequestError::new("fragment spread cycle").validation_bug());
}
if !self.new_fragments.contains_key(name) {
let fragment_def =
self.document.fragments.get(name).ok_or_else(|| {
RequestError::new("undefined fragment").validation_bug()
})?;

let Some(selection_set) =
self.filter_selection_set(&fragment_def.selection_set)?
else {
self.emptied_fragments.insert(name);
return Ok(None);
};
for directive in &fragment_def.directives {
for arg in &directive.arguments {
self.variables_in_value(&arg.value)
}
}
self.new_fragments.insert(
fragment_def.name.clone(),
fragment_def.same_location(Fragment {
name: fragment_def.name.clone(),
directives: fragment_def.directives.clone(),
selection_set,
}),
);
}
Selection::FragmentSpread(fragment_spread.same_location(FragmentSpread {
fragment_name: name.clone(),
directives: fragment_spread.directives.clone(),
}))
}
};
for directive in selection.directives() {
for arg in &directive.arguments {
self.variables_in_value(&arg.value)
}
}
Ok(Some(new_selection))
}

fn variables_in_value(&mut self, value: &'doc ast::Value) {
match value {
schema::Value::Variable(name) => {
self.variables_used.insert(name);
}
schema::Value::List(list) => {
for value in list {
self.variables_in_value(value)
}
}
schema::Value::Object(object) => {
for (_name, value) in object {
self.variables_in_value(value)
}
}
schema::Value::Null
| schema::Value::Enum(_)
| schema::Value::String(_)
| schema::Value::Float(_)
| schema::Value::Int(_)
| schema::Value::Boolean(_) => {}
}
}
}
15 changes: 15 additions & 0 deletions crates/apollo-compiler/src/executable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use indexmap::IndexMap;
use std::collections::HashSet;
use std::path::Path;

pub(crate) mod filtering;
pub(crate) mod from_ast;
mod serialize;
pub(crate) mod validation;
Expand Down Expand Up @@ -288,6 +289,20 @@ impl ExecutableDocument {
.ok_or_else(|| RequestError::new("multiple operations but no `operationName`"))
}

/// Insert the given operation in either `named_operations` or `anonymous_operation`
/// as appropriate, and return the old operation (if any) with that name (or lack thereof).
pub fn insert_operation(
&mut self,
operation: impl Into<Node<Operation>>,
) -> Option<Node<Operation>> {
let operation = operation.into();
if let Some(name) = &operation.name {
self.named_operations.insert(name.clone(), operation)
} else {
self.anonymous_operation.replace(operation)
}
}

serialize_method!();
}

Expand Down
Loading

0 comments on commit 52cde13

Please sign in to comment.