Remove skipped fields from the document before executing the query.

This commit is contained in:
Sunli 2021-11-18 19:37:10 +08:00
parent e2ef677519
commit eb9cda4c80
7 changed files with 85 additions and 147 deletions

View File

@ -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/),
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
- Remove `OutputJson` because `Json` can replace it.

View File

@ -14,7 +14,7 @@ use serde::Serialize;
use crate::extensions::Extensions;
use crate::parser::types::{
Directive, Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
};
use crate::schema::SchemaEnv;
use crate::{
@ -497,42 +497,6 @@ impl<'a, T> ContextBase<'a, T> {
.node
.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>> {
@ -734,17 +698,6 @@ impl<'a> Iterator for SelectionFieldsIter<'a> {
loop {
let it = self.iter.last_mut()?;
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 {
Some(selection) => match &selection.node {

View File

@ -110,15 +110,6 @@ fn filter<'a>(
context: &'a Context<'a>,
) {
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 {
Selection::Field(field) => {
if field.node.name.node == name {

View File

@ -5,7 +5,6 @@ use indexmap::IndexMap;
use crate::extensions::ResolveInfo;
use crate::parser::types::Selection;
use crate::registry::MetaType;
use crate::{Context, ContextSelectionSet, Name, OutputType, ServerError, ServerResult, Value};
/// Represents a GraphQL container object.
@ -139,10 +138,6 @@ impl<'a> Fields<'a> {
root: &'a T,
) -> ServerResult<()> {
for selection in &ctx.item.node.items {
if ctx.is_skip(&selection.node.directives())? {
continue;
}
match &selection.node {
Selection::Field(field) => {
if field.node.name.node == "__typename" {
@ -157,16 +152,6 @@ impl<'a> Fields<'a> {
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({
let ctx = ctx.clone();
async move {

View File

@ -8,8 +8,8 @@ use indexmap::map::IndexMap;
use crate::context::{Data, QueryEnvInner};
use crate::extensions::{ExtensionFactory, Extensions};
use crate::model::__DirectiveLocation;
use crate::parser::parse_query;
use crate::parser::types::{DocumentOperations, OperationType};
use crate::parser::types::{Directive, DocumentOperations, OperationType, Selection, SelectionSet};
use crate::parser::{parse_query, Positioned};
use crate::registry::{MetaDirective, MetaInputValue, Registry};
use crate::resolver_utils::{resolve_container, resolve_container_serial};
use crate::subscription::collect_subscription_streams;
@ -17,7 +17,7 @@ use crate::types::QueryRoot;
use crate::validation::{check_rules, ValidationMode};
use crate::{
BatchRequest, BatchResponse, CacheControl, ContextBase, InputType, ObjectType, OutputType,
QueryEnv, Request, Response, ServerError, SubscriptionType, ID,
QueryEnv, Request, Response, ServerError, SubscriptionType, Variables, ID,
};
/// 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
<bool 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());
let request = extensions.prepare_request(request).await?;
let document = {
let mut document = {
let query = &request.query;
let fut_parse = async { parse_query(&query).map_err(Into::<ServerError>::into) };
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 {
extensions,
@ -599,3 +598,51 @@ where
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);
}
}
}
}

View File

@ -43,9 +43,6 @@ pub(crate) fn collect_subscription_streams<'a, T: SubscriptionType + 'static>(
streams: &mut Vec<BoxFieldStream<'a>>,
) -> ServerResult<()> {
for selection in &ctx.item.node.items {
if ctx.is_skip(selection.node.directives())? {
continue;
}
match &selection.node {
Selection::Field(field) => streams.push(Box::pin({
let ctx = ctx.clone();

View File

@ -12,20 +12,37 @@ pub async fn test_directive_skip() {
}
let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
let resp = schema
let data = schema
.execute(
r#"
{
fragment A on QueryRoot {
value5: value @skip(if: true)
value6: value @skip(if: false)
}
query {
value1: value @skip(if: true)
value2: value @skip(if: false)
... @skip(if: true) {
value3: value
}
... @skip(if: false) {
value4: value
}
... A
}
"#,
)
.await;
.await
.into_result()
.unwrap()
.data;
assert_eq!(
resp.data,
data,
value!({
"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,
})
);
}