diff --git a/src/core/jit/model.rs b/src/core/jit/model.rs
index 58859d50c7..b0220b175e 100644
--- a/src/core/jit/model.rs
+++ b/src/core/jit/model.rs
@@ -4,9 +4,13 @@ use std::fmt::{Debug, Display, Formatter};
use std::num::NonZeroU64;
use std::sync::Arc;
-use async_graphql::parser::types::{ConstDirective, OperationType};
+use async_graphql::parser::types::{
+ ConstDirective, Directive as GqlDirective, DocumentOperations, ExecutableDocument,
+ Field as GqlField, InlineFragment, OperationDefinition, OperationType, Selection, SelectionSet,
+ TypeCondition,
+};
use async_graphql::{Name, Positioned as AsyncPositioned, ServerError};
-use async_graphql_value::ConstValue;
+use async_graphql_value::{ConstValue, Value};
use serde::{Deserialize, Serialize};
use super::Error;
@@ -444,6 +448,174 @@ impl OperationPlan {
}
}
+impl OperationPlan
+where
+ Input: Clone + Into,
+{
+ /// Builds an executable GraphQL document from this operation plan.
+ pub fn to_doc(&self) -> ExecutableDocument {
+ ExecutableDocument {
+ operations: DocumentOperations::Single(AsyncPositioned::new(
+ OperationDefinition {
+ ty: self.operation_type,
+ variable_definitions: vec![],
+ directives: vec![],
+ selection_set: self.selection_set_to_doc(self.selection.iter(), true),
+ },
+ Default::default(),
+ )),
+ fragments: HashMap::new(),
+ }
+ }
+
+ fn selection_set_to_doc<'a>(
+ &self,
+ fields: impl IntoIterator- >,
+ wrap_fragments: bool,
+ ) -> AsyncPositioned
+ where
+ Input: 'a,
+ {
+ let fields = fields.into_iter().collect::>();
+ let pos = fields
+ .first()
+ .map(|field| field.pos.into())
+ .unwrap_or_default();
+ let mut items = Vec::new();
+ let mut fragment_groups: Vec<(&str, Vec<&Field>)> = Vec::new();
+
+ for field in fields {
+ if wrap_fragments {
+ if let Some(fragment) = field.parent_fragment.as_deref() {
+ if let Some((_, fields)) = fragment_groups
+ .iter_mut()
+ .find(|(name, _)| *name == fragment)
+ {
+ fields.push(field);
+ } else {
+ fragment_groups.push((fragment, vec![field]));
+ }
+ continue;
+ }
+ }
+
+ items.push(self.field_selection_to_doc(field));
+ }
+
+ for (fragment, fields) in fragment_groups {
+ let pos = fields
+ .first()
+ .map(|field| field.pos.into())
+ .unwrap_or_default();
+
+ items.push(AsyncPositioned::new(
+ Selection::InlineFragment(AsyncPositioned::new(
+ InlineFragment {
+ type_condition: Some(AsyncPositioned::new(
+ TypeCondition { on: AsyncPositioned::new(Name::new(fragment), pos) },
+ pos,
+ )),
+ directives: vec![],
+ selection_set: self.selection_set_to_doc(fields, false),
+ },
+ pos,
+ )),
+ pos,
+ ));
+ }
+
+ AsyncPositioned::new(SelectionSet { items }, pos)
+ }
+
+ fn field_selection_to_doc(&self, field: &Field) -> AsyncPositioned {
+ let pos = field.pos.into();
+ AsyncPositioned::new(
+ Selection::Field(AsyncPositioned::new(self.field_to_doc(field), pos)),
+ pos,
+ )
+ }
+
+ fn field_to_doc(&self, field: &Field) -> GqlField {
+ let pos = field.pos.into();
+ GqlField {
+ alias: (field.output_name != field.name)
+ .then(|| AsyncPositioned::new(Name::new(&field.output_name), pos)),
+ name: AsyncPositioned::new(Name::new(&field.name), pos),
+ arguments: field
+ .args
+ .iter()
+ .filter_map(|arg| {
+ arg.value.as_ref().map(|value| {
+ (
+ AsyncPositioned::new(Name::new(&arg.name), pos),
+ AsyncPositioned::new(value.clone().into(), pos),
+ )
+ })
+ })
+ .collect(),
+ directives: self.field_directives_to_doc(field),
+ selection_set: self.selection_set_to_doc(field.selection.iter(), true),
+ }
+ }
+
+ fn field_directives_to_doc(&self, field: &Field) -> Vec> {
+ let pos = field.pos.into();
+ let mut directives = field
+ .directives
+ .iter()
+ .map(|directive| {
+ AsyncPositioned::new(
+ GqlDirective {
+ name: AsyncPositioned::new(Name::new(&directive.name), pos),
+ arguments: directive
+ .arguments
+ .iter()
+ .map(|(name, value)| {
+ (
+ AsyncPositioned::new(Name::new(name), pos),
+ AsyncPositioned::new(value.clone().into(), pos),
+ )
+ })
+ .collect(),
+ },
+ pos,
+ )
+ })
+ .collect::>();
+
+ if let Some(skip) = &field.skip {
+ directives.push(Self::condition_directive_to_doc("skip", skip.as_str(), pos));
+ }
+
+ if let Some(include) = &field.include {
+ directives.push(Self::condition_directive_to_doc(
+ "include",
+ include.as_str(),
+ pos,
+ ));
+ }
+
+ directives
+ }
+
+ fn condition_directive_to_doc(
+ name: &str,
+ variable: &str,
+ pos: async_graphql::Pos,
+ ) -> AsyncPositioned {
+ AsyncPositioned::new(
+ GqlDirective {
+ name: AsyncPositioned::new(Name::new(name), pos),
+ arguments: vec![(
+ AsyncPositioned::new(Name::new("if"), pos),
+ AsyncPositioned::new(Value::Variable(Name::new(variable)), pos),
+ )],
+ },
+ pos,
+ )
+ }
+}
+
#[derive(Clone, Debug)]
pub struct Directive {
pub name: String,
@@ -580,9 +752,9 @@ impl From for Positioned {
#[cfg(test)]
mod test {
- use async_graphql::parser::types::ConstDirective;
+ use async_graphql::parser::types::{ConstDirective, DocumentOperations, Selection};
use async_graphql::Request;
- use async_graphql_value::ConstValue;
+ use async_graphql_value::{ConstValue, Value};
use super::{Directive, OperationPlan};
use crate::core::blueprint::Blueprint;
@@ -631,4 +803,65 @@ mod test {
assert!(actual.is_dedupe);
}
+
+ #[test]
+ fn test_operation_plan_to_doc_preserves_selection_fields() {
+ let actual = plan(
+ r#"
+ query($includeName: Boolean! = true) {
+ users {
+ id @options(paging: $includeName)
+ displayName: name @include(if: $includeName)
+ }
+ }
+ "#,
+ );
+
+ let doc = actual.to_doc();
+ let operation = match &doc.operations {
+ DocumentOperations::Single(operation) => &operation.node,
+ DocumentOperations::Multiple(_) => panic!("expected single operation"),
+ };
+
+ assert!(doc.fragments.is_empty());
+ assert!(operation.variable_definitions.is_empty());
+ assert_eq!(
+ operation.ty,
+ async_graphql::parser::types::OperationType::Query
+ );
+
+ let users = match &operation.selection_set.node.items[0].node {
+ Selection::Field(field) => &field.node,
+ _ => panic!("expected users field"),
+ };
+ assert_eq!(users.name.node.as_str(), "users");
+ assert!(users.alias.is_none());
+
+ let id = match &users.selection_set.node.items[0].node {
+ Selection::Field(field) => &field.node,
+ _ => panic!("expected id field"),
+ };
+ assert_eq!(id.name.node.as_str(), "id");
+ assert_eq!(id.directives.len(), 1);
+ assert_eq!(id.directives[0].node.name.node.as_str(), "options");
+ assert_eq!(id.directives[0].node.arguments[0].0.node.as_str(), "paging");
+ assert!(matches!(
+ &id.directives[0].node.arguments[0].1.node,
+ Value::Variable(name) if name.as_str() == "includeName"
+ ));
+
+ let name = match &users.selection_set.node.items[1].node {
+ Selection::Field(field) => &field.node,
+ _ => panic!("expected name field"),
+ };
+ assert_eq!(name.name.node.as_str(), "name");
+ assert_eq!(name.alias.as_ref().unwrap().node.as_str(), "displayName");
+ assert_eq!(name.directives.len(), 1);
+ assert_eq!(name.directives[0].node.name.node.as_str(), "include");
+ assert_eq!(name.directives[0].node.arguments[0].0.node.as_str(), "if");
+ assert!(matches!(
+ &name.directives[0].node.arguments[0].1.node,
+ Value::Variable(include) if include.as_str() == "includeName"
+ ));
+ }
}