Skip to content

Commit

Permalink
Fix/query interface with fragments (#6)
Browse files Browse the repository at this point in the history
* fix: query interface with fragments

* fix: query interface with fragments

* fix: no need to box interfaces

* fix: no need to box interfaces

* fix: integration tests

* fix: create interfaces in builder instead

* fix: create interfaces in builder instead

* fix: remove arc and take it from index

* fix: remove duplicate when __typename is requested

* chore(deps): update rust crate async-graphql to v7.0.13 (tailcallhq#3241)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate anyhow to v1.0.95 (tailcallhq#3242)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate async-trait to v0.1.85 (tailcallhq#3243)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate chrono to v0.4.39 (tailcallhq#3244)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate clap to v4.5.26 (tailcallhq#3245)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate hyper to v0.14.32 (tailcallhq#3246)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate moka to v0.12.10 (tailcallhq#3247)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate phonenumber to v0.3.7 (tailcallhq#3252)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate proc-macro2 to v1.0.93 (tailcallhq#3250)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate quote to v1.0.38 (tailcallhq#3248)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate rustls to v0.23.21 (tailcallhq#3249)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate rustls-pki-types to v1.10.1 (tailcallhq#3251)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate serde to v1.0.217 (tailcallhq#3253)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate serde_json to v1.0.135 (tailcallhq#3254)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update rust crate syn to v2.0.96 (tailcallhq#3255)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency @cloudflare/workers-types to v4.20250109.0 (tailcallhq#3256)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • Loading branch information
MedHeikelBouzayene and renovate[bot] authored Jan 14, 2025
1 parent 580002a commit 9548aca
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 182 deletions.
314 changes: 152 additions & 162 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/core/blueprint/index.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use indexmap::IndexMap;

use super::InputObjectTypeDefinition;
Expand Down Expand Up @@ -38,6 +40,16 @@ impl Index {
matches!(def, Some(Definition::Scalar(_))) || scalar::Scalar::is_predefined(type_name)
}

pub fn get_interfaces(&self) -> HashSet<String> {
self.map
.iter()
.filter_map(|(_, (def, _))| match def {
Definition::Interface(interface) => Some(interface.name.to_owned()),
_ => None,
})
.collect()
}

pub fn type_is_enum(&self, type_name: &str) -> bool {
let def = self.map.get(type_name).map(|(def, _)| def);

Expand Down
25 changes: 20 additions & 5 deletions src/core/jit/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub struct Builder<'a> {
impl<'a> Builder<'a> {
pub fn new(blueprint: &Blueprint, document: &'a ExecutableDocument) -> Self {
let index = Arc::new(blueprint.index());

Self {
document,
index,
Expand Down Expand Up @@ -128,6 +129,7 @@ impl<'a> Builder<'a> {
#[inline(always)]
fn iter(
&self,
parent_fragment: Option<&str>,
selection: &SelectionSet,
type_condition: &str,
fragments: &HashMap<&str, &FragmentDefinition>,
Expand Down Expand Up @@ -168,6 +170,7 @@ impl<'a> Builder<'a> {
.map(|(k, v)| (k.node.as_str().to_string(), v.node.to_owned()))
.collect::<HashMap<_, _>>();

let parent_fragment = parent_fragment.map(|s| s.to_owned());
// Check if the field is present in the schema index
if let Some(field_def) = self.index.get_field(type_condition, field_name) {
let mut args = Vec::with_capacity(request_args.len());
Expand Down Expand Up @@ -198,8 +201,12 @@ impl<'a> Builder<'a> {
let id = FieldId::new(self.field_id.next());

// Recursively gather child fields for the selection set
let child_fields =
self.iter(&gql_field.selection_set.node, type_of.name(), fragments);
let child_fields = self.iter(
None,
&gql_field.selection_set.node,
type_of.name(),
fragments,
);

let ir = match field_def {
QueryField::Field((field_def, _)) => field_def.resolver.clone(),
Expand All @@ -220,6 +227,7 @@ impl<'a> Builder<'a> {
let field = Field {
id,
selection: child_fields,
parent_fragment,
name: field_name.to_string(),
output_name: gql_field
.alias
Expand Down Expand Up @@ -252,6 +260,7 @@ impl<'a> Builder<'a> {
args: Vec::new(),
pos: selection.pos.into(),
selection: vec![], // __typename has no child selection
parent_fragment,
directives,
is_enum: false,
scalar: Some(scalar::Scalar::Empty),
Expand All @@ -265,6 +274,7 @@ impl<'a> Builder<'a> {
fragments.get(fragment_spread.fragment_name.node.as_str())
{
fields.extend(self.iter(
Some(fragment.type_condition.node.on.node.as_str()),
&fragment.selection_set.node,
fragment.type_condition.node.on.node.as_str(),
fragments,
Expand All @@ -277,8 +287,12 @@ impl<'a> Builder<'a> {
.as_ref()
.map(|cond| cond.node.on.node.as_str())
.unwrap_or(type_condition);

fields.extend(self.iter(&fragment.selection_set.node, type_of, fragments));
fields.extend(self.iter(
Some(type_of),
&fragment.selection_set.node,
type_of,
fragments,
));
}
}
}
Expand Down Expand Up @@ -334,7 +348,7 @@ impl<'a> Builder<'a> {
let name = self
.get_type(operation.ty)
.ok_or(BuildError::RootOperationTypeNotDefined { operation: operation.ty })?;
let fields = self.iter(&operation.selection_set.node, name, &fragments);
let fields = self.iter(None, &operation.selection_set.node, name, &fragments);

let is_introspection_query = operation.selection_set.node.items.iter().any(|f| {
if let Selection::Field(Positioned { node: gql_field, .. }) = &f.node {
Expand All @@ -351,6 +365,7 @@ impl<'a> Builder<'a> {
operation.ty,
self.index.clone(),
is_introspection_query,
Some(self.index.get_interfaces()),
);
Ok(plan)
}
Expand Down
8 changes: 7 additions & 1 deletion src/core/jit/model.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display, Formatter};
use std::num::NonZeroU64;
use std::sync::Arc;
Expand Down Expand Up @@ -187,6 +187,7 @@ pub struct Field<Input> {
pub include: Option<Variable>,
pub args: Vec<Arg<Input>>,
pub selection: Vec<Field<Input>>,
pub parent_fragment: Option<String>,
pub pos: Pos,
pub directives: Vec<Directive<Input>>,
pub is_enum: bool,
Expand Down Expand Up @@ -245,6 +246,7 @@ impl<Input> Field<Input> {
.into_iter()
.map(|f| f.try_map(map))
.collect::<Result<Vec<Field<Output>>, Error>>()?,
parent_fragment: None,
skip: self.skip,
include: self.include,
pos: self.pos,
Expand Down Expand Up @@ -315,6 +317,7 @@ pub struct OperationPlan<Input> {
pub min_cache_ttl: Option<NonZeroU64>,
pub selection: Vec<Field<Input>>,
pub before: Option<IR>,
pub interfaces: Option<HashSet<String>>,
}

impl<Input> OperationPlan<Input> {
Expand All @@ -339,6 +342,7 @@ impl<Input> OperationPlan<Input> {
is_protected: self.is_protected,
min_cache_ttl: self.min_cache_ttl,
before: self.before,
interfaces: None,
})
}
}
Expand All @@ -351,6 +355,7 @@ impl<Input> OperationPlan<Input> {
operation_type: OperationType,
index: Arc<Index>,
is_introspection_query: bool,
interfaces: Option<HashSet<String>>,
) -> Self
where
Input: Clone,
Expand All @@ -366,6 +371,7 @@ impl<Input> OperationPlan<Input> {
is_protected: false,
min_cache_ttl: None,
before: Default::default(),
interfaces,
}
}

Expand Down
65 changes: 57 additions & 8 deletions src/core/jit/transform/graphql.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::convert::Infallible;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
Expand All @@ -20,18 +21,25 @@ impl<A> GraphQL<A> {
}
}

fn compute_selection_set<A: Display + Debug + JsonLikeOwned>(base_field: &mut [Field<A>]) {
fn compute_selection_set<A: Display + Debug + JsonLikeOwned>(
base_field: &mut [Field<A>],
interfaces: &HashSet<String>,
) {
for field in base_field.iter_mut() {
if let Some(ir) = field.ir.as_mut() {
ir.modify_io(&mut |io| {
if let IO::GraphQL { req_template, .. } = io {
if let Some(v) = format_selection_set(field.selection.iter()) {
if let Some(v) = format_selection_set(
field.selection.iter(),
interfaces,
interfaces.contains(field.type_of.name()),
) {
req_template.selection = Some(Mustache::parse(&v).into());
}
}
});
}
compute_selection_set(field.selection.as_mut());
compute_selection_set(field.selection.as_mut(), interfaces);
}
}

Expand All @@ -40,15 +48,24 @@ impl<A: Display + Debug + JsonLikeOwned + Clone> Transform for GraphQL<A> {
type Error = Infallible;

fn transform(&self, mut plan: Self::Value) -> Valid<Self::Value, Self::Error> {
compute_selection_set(&mut plan.selection);
let interfaces = match plan.interfaces {
Some(ref interfaces) => interfaces,
None => &HashSet::new(),
};
compute_selection_set(&mut plan.selection, interfaces);

Valid::succeed(plan)
}
}

fn format_selection_set<'a, A: 'a + Display + JsonLikeOwned>(
selection_set: impl Iterator<Item = &'a Field<A>>,
interfaces: &HashSet<String>,
is_parent_interface: bool,
) -> Option<String> {
let mut fragments_fields = HashMap::new();
let mut normal_fields = vec![];
let mut is_typename_requested = false;
let set = selection_set
.filter(|field| !matches!(&field.ir, Some(IR::IO(_)) | Some(IR::Dynamic(_))))
.map(|field| {
Expand All @@ -58,20 +75,52 @@ fn format_selection_set<'a, A: 'a + Display + JsonLikeOwned>(
} else {
field.name.to_string()
};
format_selection_field(field, &field_name)
let is_this_field_interface = interfaces.contains(field.type_of.name());
let formatted_selection_fields =
format_selection_field(field, &field_name, interfaces, is_this_field_interface);
is_typename_requested = is_typename_requested || field_name == "__typename";
match &field.parent_fragment {
Some(fragment) if is_parent_interface => {
fragments_fields
.entry(fragment.to_owned())
.or_insert_with(Vec::new)
.push(formatted_selection_fields);
}
_ => {
normal_fields.push(formatted_selection_fields);
}
}
})
.collect::<Vec<_>>();

if set.is_empty() {
return None;
}

Some(format!("{{ {} }}", set.join(" ")))
let fragments_set: Vec<String> = fragments_fields
.into_iter()
.map(|(fragment_name, fields)| {
format!("... on {} {{ {} }}", fragment_name, fields.join(" "))
})
.collect();

//Don't force user to query the type and get it automatically
if is_parent_interface && !is_typename_requested {
normal_fields.push("__typename".to_owned());
}
normal_fields.extend(fragments_set);
Some(format!("{{ {} }}", normal_fields.join(" ")))
}

fn format_selection_field<A: Display + JsonLikeOwned>(field: &Field<A>, name: &str) -> String {
fn format_selection_field<A: Display + JsonLikeOwned>(
field: &Field<A>,
name: &str,
interfaces: &HashSet<String>,
is_parent_interface: bool,
) -> String {
let arguments = format_selection_field_arguments(field);
let selection_set = format_selection_set(field.selection.iter());
let selection_set =
format_selection_set(field.selection.iter(), interfaces, is_parent_interface);

let mut output = format!("{}{}", name, arguments);

Expand Down
1 change: 1 addition & 0 deletions src/core/jit/transform/input_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ where
is_const: self.plan.is_const,
is_protected: self.plan.is_protected,
min_cache_ttl: self.plan.min_cache_ttl,
interfaces: None,
selection,
before: self.plan.before,
})
Expand Down
12 changes: 6 additions & 6 deletions tailcall-cloudflare/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions tests/core/snapshots/graphql-with-fragments.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"queryNodeC": [
{
"name": "nodeA",
"__typename": "NodeA",
"nodeA_id": "nodeA_id"
},
{
"name": "nodeB",
"__typename": "NodeB",
"nodeB_id": "nodeB_id"
}
]
}
}
}
28 changes: 28 additions & 0 deletions tests/core/snapshots/graphql-with-fragments.md_client.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: tests/core/spec.rs
expression: formatted
snapshot_kind: text
---
type NodeA implements NodeC {
name: String
nodeA_id: String
}

type NodeB implements NodeC {
name: String
nodeB_id: String
}

interface NodeC {
name: String
}

type Query {
queryNodeA: [NodeA!]
queryNodeB: [NodeB!]
queryNodeC: [NodeC!]
}

schema {
query: Query
}
Loading

0 comments on commit 9548aca

Please sign in to comment.