Remove skipped fields from the document before executing the query.
This commit is contained in:
parent
e2ef677519
commit
eb9cda4c80
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Remove skipped fields from the document before executing the query.
|
||||||
|
|
||||||
## [3.0.4] 2021-11-18
|
## [3.0.4] 2021-11-18
|
||||||
|
|
||||||
- Remove `OutputJson` because `Json` can replace it.
|
- Remove `OutputJson` because `Json` can replace it.
|
||||||
|
|
|
@ -14,7 +14,7 @@ use serde::Serialize;
|
||||||
|
|
||||||
use crate::extensions::Extensions;
|
use crate::extensions::Extensions;
|
||||||
use crate::parser::types::{
|
use crate::parser::types::{
|
||||||
Directive, Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
|
Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
|
||||||
};
|
};
|
||||||
use crate::schema::SchemaEnv;
|
use crate::schema::SchemaEnv;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -497,42 +497,6 @@ impl<'a, T> ContextBase<'a, T> {
|
||||||
.node
|
.node
|
||||||
.into_const_with(|name| self.var_value(&name, pos))
|
.into_const_with(|name| self.var_value(&name, pos))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn is_ifdef(&self, directives: &[Positioned<Directive>]) -> bool {
|
|
||||||
directives
|
|
||||||
.iter()
|
|
||||||
.any(|directive| directive.node.name.node == "ifdef")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn is_skip(&self, directives: &[Positioned<Directive>]) -> ServerResult<bool> {
|
|
||||||
for directive in directives {
|
|
||||||
let include = match &*directive.node.name.node {
|
|
||||||
"skip" => false,
|
|
||||||
"include" => true,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let condition_input = directive
|
|
||||||
.node
|
|
||||||
.get_argument("if")
|
|
||||||
.ok_or_else(|| ServerError::new(format!(r#"Directive @{} requires argument `if` of type `Boolean!` but it was not provided."#, if include { "include" } else { "skip" }),Some(directive.pos)))?
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let pos = condition_input.pos;
|
|
||||||
let condition_input = self.resolve_input_value(condition_input)?;
|
|
||||||
|
|
||||||
if include
|
|
||||||
!= <bool as InputType>::parse(Some(condition_input))
|
|
||||||
.map_err(|e| e.into_server_error(pos))?
|
|
||||||
{
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ContextBase<'a, &'a Positioned<SelectionSet>> {
|
impl<'a> ContextBase<'a, &'a Positioned<SelectionSet>> {
|
||||||
|
@ -734,17 +698,6 @@ impl<'a> Iterator for SelectionFieldsIter<'a> {
|
||||||
loop {
|
loop {
|
||||||
let it = self.iter.last_mut()?;
|
let it = self.iter.last_mut()?;
|
||||||
let item = it.next();
|
let item = it.next();
|
||||||
if let Some(item) = item {
|
|
||||||
// ignore any items that are skipped (i.e. @skip/@include)
|
|
||||||
if self
|
|
||||||
.context
|
|
||||||
.is_skip(&item.node.directives())
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
// TODO: should we throw errors here? they will be caught later in execution and it'd cause major backwards compatibility issues
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
Some(selection) => match &selection.node {
|
Some(selection) => match &selection.node {
|
||||||
|
|
|
@ -110,15 +110,6 @@ fn filter<'a>(
|
||||||
context: &'a Context<'a>,
|
context: &'a Context<'a>,
|
||||||
) {
|
) {
|
||||||
for item in &selection_set.items {
|
for item in &selection_set.items {
|
||||||
// doing this imperatively is a bit nasty, but using iterators would
|
|
||||||
// require a boxed return type (I believe) as its recusive
|
|
||||||
|
|
||||||
// ignore any items that are skipped (i.e. @skip/@include)
|
|
||||||
if context.is_skip(&item.node.directives()).unwrap_or(false) {
|
|
||||||
// TODO: should we throw errors here? they will be caught later in execution and it'd cause major backwards compatibility issues
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match &item.node {
|
match &item.node {
|
||||||
Selection::Field(field) => {
|
Selection::Field(field) => {
|
||||||
if field.node.name.node == name {
|
if field.node.name.node == name {
|
||||||
|
|
|
@ -5,7 +5,6 @@ use indexmap::IndexMap;
|
||||||
|
|
||||||
use crate::extensions::ResolveInfo;
|
use crate::extensions::ResolveInfo;
|
||||||
use crate::parser::types::Selection;
|
use crate::parser::types::Selection;
|
||||||
use crate::registry::MetaType;
|
|
||||||
use crate::{Context, ContextSelectionSet, Name, OutputType, ServerError, ServerResult, Value};
|
use crate::{Context, ContextSelectionSet, Name, OutputType, ServerError, ServerResult, Value};
|
||||||
|
|
||||||
/// Represents a GraphQL container object.
|
/// Represents a GraphQL container object.
|
||||||
|
@ -139,10 +138,6 @@ impl<'a> Fields<'a> {
|
||||||
root: &'a T,
|
root: &'a T,
|
||||||
) -> ServerResult<()> {
|
) -> ServerResult<()> {
|
||||||
for selection in &ctx.item.node.items {
|
for selection in &ctx.item.node.items {
|
||||||
if ctx.is_skip(&selection.node.directives())? {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match &selection.node {
|
match &selection.node {
|
||||||
Selection::Field(field) => {
|
Selection::Field(field) => {
|
||||||
if field.node.name.node == "__typename" {
|
if field.node.name.node == "__typename" {
|
||||||
|
@ -157,16 +152,6 @@ impl<'a> Fields<'a> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.is_ifdef(&field.node.directives) {
|
|
||||||
if let Some(MetaType::Object { fields, .. }) =
|
|
||||||
ctx.schema_env.registry.types.get(T::type_name().as_ref())
|
|
||||||
{
|
|
||||||
if !fields.contains_key(field.node.name.node.as_str()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.0.push(Box::pin({
|
self.0.push(Box::pin({
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
async move {
|
async move {
|
||||||
|
|
|
@ -8,8 +8,8 @@ use indexmap::map::IndexMap;
|
||||||
use crate::context::{Data, QueryEnvInner};
|
use crate::context::{Data, QueryEnvInner};
|
||||||
use crate::extensions::{ExtensionFactory, Extensions};
|
use crate::extensions::{ExtensionFactory, Extensions};
|
||||||
use crate::model::__DirectiveLocation;
|
use crate::model::__DirectiveLocation;
|
||||||
use crate::parser::parse_query;
|
use crate::parser::types::{Directive, DocumentOperations, OperationType, Selection, SelectionSet};
|
||||||
use crate::parser::types::{DocumentOperations, OperationType};
|
use crate::parser::{parse_query, Positioned};
|
||||||
use crate::registry::{MetaDirective, MetaInputValue, Registry};
|
use crate::registry::{MetaDirective, MetaInputValue, Registry};
|
||||||
use crate::resolver_utils::{resolve_container, resolve_container_serial};
|
use crate::resolver_utils::{resolve_container, resolve_container_serial};
|
||||||
use crate::subscription::collect_subscription_streams;
|
use crate::subscription::collect_subscription_streams;
|
||||||
|
@ -17,7 +17,7 @@ use crate::types::QueryRoot;
|
||||||
use crate::validation::{check_rules, ValidationMode};
|
use crate::validation::{check_rules, ValidationMode};
|
||||||
use crate::{
|
use crate::{
|
||||||
BatchRequest, BatchResponse, CacheControl, ContextBase, InputType, ObjectType, OutputType,
|
BatchRequest, BatchResponse, CacheControl, ContextBase, InputType, ObjectType, OutputType,
|
||||||
QueryEnv, Request, Response, ServerError, SubscriptionType, ID,
|
QueryEnv, Request, Response, ServerError, SubscriptionType, Variables, ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Schema builder
|
/// Schema builder
|
||||||
|
@ -312,13 +312,6 @@ where
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.add_directive(MetaDirective {
|
|
||||||
name: "ifdef",
|
|
||||||
description: Some("Directs the executor to query only when the field exists."),
|
|
||||||
locations: vec![__DirectiveLocation::FIELD],
|
|
||||||
args: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// register scalars
|
// register scalars
|
||||||
<bool as InputType>::create_type_info(&mut registry);
|
<bool as InputType>::create_type_info(&mut registry);
|
||||||
<i32 as InputType>::create_type_info(&mut registry);
|
<i32 as InputType>::create_type_info(&mut registry);
|
||||||
|
@ -391,7 +384,7 @@ where
|
||||||
extensions.attach_query_data(query_data.clone());
|
extensions.attach_query_data(query_data.clone());
|
||||||
|
|
||||||
let request = extensions.prepare_request(request).await?;
|
let request = extensions.prepare_request(request).await?;
|
||||||
let document = {
|
let mut document = {
|
||||||
let query = &request.query;
|
let query = &request.query;
|
||||||
let fut_parse = async { parse_query(&query).map_err(Into::<ServerError>::into) };
|
let fut_parse = async { parse_query(&query).map_err(Into::<ServerError>::into) };
|
||||||
futures_util::pin_mut!(fut_parse);
|
futures_util::pin_mut!(fut_parse);
|
||||||
|
@ -454,7 +447,13 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (operation_name, operation) = operation.map_err(|err| vec![err])?;
|
let (operation_name, mut operation) = operation.map_err(|err| vec![err])?;
|
||||||
|
|
||||||
|
// remove skipped fields
|
||||||
|
for fragment in document.fragments.values_mut() {
|
||||||
|
remove_skipped_selection(&mut fragment.node.selection_set.node, &request.variables);
|
||||||
|
}
|
||||||
|
remove_skipped_selection(&mut operation.node.selection_set.node, &request.variables);
|
||||||
|
|
||||||
let env = QueryEnvInner {
|
let env = QueryEnvInner {
|
||||||
extensions,
|
extensions,
|
||||||
|
@ -599,3 +598,51 @@ where
|
||||||
self.execute_stream_with_session_data(request.into(), Default::default())
|
self.execute_stream_with_session_data(request.into(), Default::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_skipped_selection(selection_set: &mut SelectionSet, variables: &Variables) {
|
||||||
|
fn is_skipped(directives: &[Positioned<Directive>], variables: &Variables) -> bool {
|
||||||
|
for directive in directives {
|
||||||
|
let include = match &*directive.node.name.node {
|
||||||
|
"skip" => false,
|
||||||
|
"include" => true,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(condition_input) = directive.node.get_argument("if") {
|
||||||
|
let value = condition_input
|
||||||
|
.node
|
||||||
|
.clone()
|
||||||
|
.into_const_with(|name| variables.get(&name).cloned().ok_or(()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let value: bool = InputType::parse(Some(value)).unwrap_or_default();
|
||||||
|
if include != value {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
selection_set
|
||||||
|
.items
|
||||||
|
.retain(|selection| !is_skipped(selection.node.directives(), variables));
|
||||||
|
|
||||||
|
for selection in &mut selection_set.items {
|
||||||
|
selection.node.directives_mut().retain(|directive| {
|
||||||
|
directive.node.name.node != "skip" && directive.node.name.node != "include"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for selection in &mut selection_set.items {
|
||||||
|
match &mut selection.node {
|
||||||
|
Selection::Field(field) => {
|
||||||
|
remove_skipped_selection(&mut field.node.selection_set.node, variables);
|
||||||
|
}
|
||||||
|
Selection::FragmentSpread(_) => {}
|
||||||
|
Selection::InlineFragment(inline_fragment) => {
|
||||||
|
remove_skipped_selection(&mut inline_fragment.node.selection_set.node, variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -43,9 +43,6 @@ pub(crate) fn collect_subscription_streams<'a, T: SubscriptionType + 'static>(
|
||||||
streams: &mut Vec<BoxFieldStream<'a>>,
|
streams: &mut Vec<BoxFieldStream<'a>>,
|
||||||
) -> ServerResult<()> {
|
) -> ServerResult<()> {
|
||||||
for selection in &ctx.item.node.items {
|
for selection in &ctx.item.node.items {
|
||||||
if ctx.is_skip(selection.node.directives())? {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match &selection.node {
|
match &selection.node {
|
||||||
Selection::Field(field) => streams.push(Box::pin({
|
Selection::Field(field) => streams.push(Box::pin({
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
|
|
|
@ -12,20 +12,37 @@ pub async fn test_directive_skip() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
|
let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
|
||||||
let resp = schema
|
let data = schema
|
||||||
.execute(
|
.execute(
|
||||||
r#"
|
r#"
|
||||||
{
|
fragment A on QueryRoot {
|
||||||
|
value5: value @skip(if: true)
|
||||||
|
value6: value @skip(if: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
query {
|
||||||
value1: value @skip(if: true)
|
value1: value @skip(if: true)
|
||||||
value2: value @skip(if: false)
|
value2: value @skip(if: false)
|
||||||
|
... @skip(if: true) {
|
||||||
|
value3: value
|
||||||
|
}
|
||||||
|
... @skip(if: false) {
|
||||||
|
value4: value
|
||||||
|
}
|
||||||
|
... A
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.into_result()
|
||||||
|
.unwrap()
|
||||||
|
.data;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.data,
|
data,
|
||||||
value!({
|
value!({
|
||||||
"value2": 10,
|
"value2": 10,
|
||||||
|
"value4": 10,
|
||||||
|
"value6": 10,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,59 +76,3 @@ pub async fn test_directive_include() {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
pub async fn test_directive_ifdef() {
|
|
||||||
struct QueryRoot;
|
|
||||||
|
|
||||||
#[Object]
|
|
||||||
impl QueryRoot {
|
|
||||||
pub async fn value1(&self) -> i32 {
|
|
||||||
10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MutationRoot;
|
|
||||||
|
|
||||||
#[Object]
|
|
||||||
impl MutationRoot {
|
|
||||||
pub async fn action1(&self) -> i32 {
|
|
||||||
10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let schema = Schema::new(QueryRoot, MutationRoot, EmptySubscription);
|
|
||||||
let resp = schema
|
|
||||||
.execute(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
value1 @ifdef
|
|
||||||
value2 @ifdef
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(
|
|
||||||
resp.data,
|
|
||||||
value!({
|
|
||||||
"value1": 10,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let resp = schema
|
|
||||||
.execute(
|
|
||||||
r#"
|
|
||||||
mutation {
|
|
||||||
action1 @ifdef
|
|
||||||
action2 @ifdef
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(
|
|
||||||
resp.data,
|
|
||||||
value!({
|
|
||||||
"action1": 10,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user