Skip to content

Commit a1bf82e

Browse files
fix: query interface with fragments (#3238)
1 parent c4cefad commit a1bf82e

File tree

9 files changed

+243
-14
lines changed

9 files changed

+243
-14
lines changed

src/core/blueprint/index.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashSet;
2+
13
use indexmap::IndexMap;
24

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

43+
pub fn get_interfaces(&self) -> HashSet<String> {
44+
self.map
45+
.iter()
46+
.filter_map(|(_, (def, _))| match def {
47+
Definition::Interface(interface) => Some(interface.name.to_owned()),
48+
_ => None,
49+
})
50+
.collect()
51+
}
52+
4153
pub fn type_is_enum(&self, type_name: &str) -> bool {
4254
let def = self.map.get(type_name).map(|(def, _)| def);
4355

src/core/jit/builder.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub struct Builder<'a> {
7575
impl<'a> Builder<'a> {
7676
pub fn new(blueprint: &Blueprint, document: &'a ExecutableDocument) -> Self {
7777
let index = Arc::new(blueprint.index());
78+
7879
Self {
7980
document,
8081
index,
@@ -128,6 +129,7 @@ impl<'a> Builder<'a> {
128129
#[inline(always)]
129130
fn iter(
130131
&self,
132+
parent_fragment: Option<&str>,
131133
selection: &SelectionSet,
132134
type_condition: &str,
133135
fragments: &HashMap<&str, &FragmentDefinition>,
@@ -168,6 +170,7 @@ impl<'a> Builder<'a> {
168170
.map(|(k, v)| (k.node.as_str().to_string(), v.node.to_owned()))
169171
.collect::<HashMap<_, _>>();
170172

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

200203
// Recursively gather child fields for the selection set
201-
let child_fields =
202-
self.iter(&gql_field.selection_set.node, type_of.name(), fragments);
204+
let child_fields = self.iter(
205+
None,
206+
&gql_field.selection_set.node,
207+
type_of.name(),
208+
fragments,
209+
);
203210

204211
let ir = match field_def {
205212
QueryField::Field((field_def, _)) => field_def.resolver.clone(),
@@ -220,6 +227,7 @@ impl<'a> Builder<'a> {
220227
let field = Field {
221228
id,
222229
selection: child_fields,
230+
parent_fragment,
223231
name: field_name.to_string(),
224232
output_name: gql_field
225233
.alias
@@ -252,6 +260,7 @@ impl<'a> Builder<'a> {
252260
args: Vec::new(),
253261
pos: selection.pos.into(),
254262
selection: vec![], // __typename has no child selection
263+
parent_fragment,
255264
directives,
256265
is_enum: false,
257266
scalar: Some(scalar::Scalar::Empty),
@@ -265,6 +274,7 @@ impl<'a> Builder<'a> {
265274
fragments.get(fragment_spread.fragment_name.node.as_str())
266275
{
267276
fields.extend(self.iter(
277+
Some(fragment.type_condition.node.on.node.as_str()),
268278
&fragment.selection_set.node,
269279
fragment.type_condition.node.on.node.as_str(),
270280
fragments,
@@ -277,8 +287,12 @@ impl<'a> Builder<'a> {
277287
.as_ref()
278288
.map(|cond| cond.node.on.node.as_str())
279289
.unwrap_or(type_condition);
280-
281-
fields.extend(self.iter(&fragment.selection_set.node, type_of, fragments));
290+
fields.extend(self.iter(
291+
Some(type_of),
292+
&fragment.selection_set.node,
293+
type_of,
294+
fragments,
295+
));
282296
}
283297
}
284298
}
@@ -334,7 +348,7 @@ impl<'a> Builder<'a> {
334348
let name = self
335349
.get_type(operation.ty)
336350
.ok_or(BuildError::RootOperationTypeNotDefined { operation: operation.ty })?;
337-
let fields = self.iter(&operation.selection_set.node, name, &fragments);
351+
let fields = self.iter(None, &operation.selection_set.node, name, &fragments);
338352

339353
let is_introspection_query = operation.selection_set.node.items.iter().any(|f| {
340354
if let Selection::Field(Positioned { node: gql_field, .. }) = &f.node {
@@ -351,6 +365,7 @@ impl<'a> Builder<'a> {
351365
operation.ty,
352366
self.index.clone(),
353367
is_introspection_query,
368+
Some(self.index.get_interfaces()),
354369
);
355370
Ok(plan)
356371
}

src/core/jit/model.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::borrow::Cow;
2-
use std::collections::HashMap;
2+
use std::collections::{HashMap, HashSet};
33
use std::fmt::{Debug, Display, Formatter};
44
use std::num::NonZeroU64;
55
use std::sync::Arc;
@@ -187,6 +187,7 @@ pub struct Field<Input> {
187187
pub include: Option<Variable>,
188188
pub args: Vec<Arg<Input>>,
189189
pub selection: Vec<Field<Input>>,
190+
pub parent_fragment: Option<String>,
190191
pub pos: Pos,
191192
pub directives: Vec<Directive<Input>>,
192193
pub is_enum: bool,
@@ -245,6 +246,7 @@ impl<Input> Field<Input> {
245246
.into_iter()
246247
.map(|f| f.try_map(map))
247248
.collect::<Result<Vec<Field<Output>>, Error>>()?,
249+
parent_fragment: None,
248250
skip: self.skip,
249251
include: self.include,
250252
pos: self.pos,
@@ -315,6 +317,7 @@ pub struct OperationPlan<Input> {
315317
pub min_cache_ttl: Option<NonZeroU64>,
316318
pub selection: Vec<Field<Input>>,
317319
pub before: Option<IR>,
320+
pub interfaces: Option<HashSet<String>>,
318321
}
319322

320323
impl<Input> OperationPlan<Input> {
@@ -339,6 +342,7 @@ impl<Input> OperationPlan<Input> {
339342
is_protected: self.is_protected,
340343
min_cache_ttl: self.min_cache_ttl,
341344
before: self.before,
345+
interfaces: None,
342346
})
343347
}
344348
}
@@ -351,6 +355,7 @@ impl<Input> OperationPlan<Input> {
351355
operation_type: OperationType,
352356
index: Arc<Index>,
353357
is_introspection_query: bool,
358+
interfaces: Option<HashSet<String>>,
354359
) -> Self
355360
where
356361
Input: Clone,
@@ -366,6 +371,7 @@ impl<Input> OperationPlan<Input> {
366371
is_protected: false,
367372
min_cache_ttl: None,
368373
before: Default::default(),
374+
interfaces,
369375
}
370376
}
371377

src/core/jit/transform/graphql.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::collections::{HashMap, HashSet};
23
use std::convert::Infallible;
34
use std::fmt::{Debug, Display};
45
use std::marker::PhantomData;
@@ -20,18 +21,25 @@ impl<A> GraphQL<A> {
2021
}
2122
}
2223

23-
fn compute_selection_set<A: Display + Debug + JsonLikeOwned>(base_field: &mut [Field<A>]) {
24+
fn compute_selection_set<A: Display + Debug + JsonLikeOwned>(
25+
base_field: &mut [Field<A>],
26+
interfaces: &HashSet<String>,
27+
) {
2428
for field in base_field.iter_mut() {
2529
if let Some(ir) = field.ir.as_mut() {
2630
ir.modify_io(&mut |io| {
2731
if let IO::GraphQL { req_template, .. } = io {
28-
if let Some(v) = format_selection_set(field.selection.iter()) {
32+
if let Some(v) = format_selection_set(
33+
field.selection.iter(),
34+
interfaces,
35+
interfaces.contains(field.type_of.name()),
36+
) {
2937
req_template.selection = Some(Mustache::parse(&v).into());
3038
}
3139
}
3240
});
3341
}
34-
compute_selection_set(field.selection.as_mut());
42+
compute_selection_set(field.selection.as_mut(), interfaces);
3543
}
3644
}
3745

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

4250
fn transform(&self, mut plan: Self::Value) -> Valid<Self::Value, Self::Error> {
43-
compute_selection_set(&mut plan.selection);
51+
let interfaces = match plan.interfaces {
52+
Some(ref interfaces) => interfaces,
53+
None => &HashSet::new(),
54+
};
55+
compute_selection_set(&mut plan.selection, interfaces);
4456

4557
Valid::succeed(plan)
4658
}
4759
}
4860

4961
fn format_selection_set<'a, A: 'a + Display + JsonLikeOwned>(
5062
selection_set: impl Iterator<Item = &'a Field<A>>,
63+
interfaces: &HashSet<String>,
64+
is_parent_interface: bool,
5165
) -> Option<String> {
66+
let mut fragments_fields = HashMap::new();
67+
let mut normal_fields = vec![];
68+
let mut is_typename_requested = false;
5269
let set = selection_set
5370
.filter(|field| !matches!(&field.ir, Some(IR::IO(_)) | Some(IR::Dynamic(_))))
5471
.map(|field| {
@@ -58,20 +75,52 @@ fn format_selection_set<'a, A: 'a + Display + JsonLikeOwned>(
5875
} else {
5976
field.name.to_string()
6077
};
61-
format_selection_field(field, &field_name)
78+
let is_this_field_interface = interfaces.contains(field.type_of.name());
79+
let formatted_selection_fields =
80+
format_selection_field(field, &field_name, interfaces, is_this_field_interface);
81+
is_typename_requested = is_typename_requested || field_name == "__typename";
82+
match &field.parent_fragment {
83+
Some(fragment) if is_parent_interface => {
84+
fragments_fields
85+
.entry(fragment.to_owned())
86+
.or_insert_with(Vec::new)
87+
.push(formatted_selection_fields);
88+
}
89+
_ => {
90+
normal_fields.push(formatted_selection_fields);
91+
}
92+
}
6293
})
6394
.collect::<Vec<_>>();
6495

6596
if set.is_empty() {
6697
return None;
6798
}
6899

69-
Some(format!("{{ {} }}", set.join(" ")))
100+
let fragments_set: Vec<String> = fragments_fields
101+
.into_iter()
102+
.map(|(fragment_name, fields)| {
103+
format!("... on {} {{ {} }}", fragment_name, fields.join(" "))
104+
})
105+
.collect();
106+
107+
//Don't force user to query the type and get it automatically
108+
if is_parent_interface && !is_typename_requested {
109+
normal_fields.push("__typename".to_owned());
110+
}
111+
normal_fields.extend(fragments_set);
112+
Some(format!("{{ {} }}", normal_fields.join(" ")))
70113
}
71114

72-
fn format_selection_field<A: Display + JsonLikeOwned>(field: &Field<A>, name: &str) -> String {
115+
fn format_selection_field<A: Display + JsonLikeOwned>(
116+
field: &Field<A>,
117+
name: &str,
118+
interfaces: &HashSet<String>,
119+
is_parent_interface: bool,
120+
) -> String {
73121
let arguments = format_selection_field_arguments(field);
74-
let selection_set = format_selection_set(field.selection.iter());
122+
let selection_set =
123+
format_selection_set(field.selection.iter(), interfaces, is_parent_interface);
75124

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

src/core/jit/transform/input_resolver.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ where
8484
is_const: self.plan.is_const,
8585
is_protected: self.plan.is_protected,
8686
min_cache_ttl: self.plan.min_cache_ttl,
87+
interfaces: None,
8788
selection,
8889
before: self.plan.before,
8990
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
source: tests/core/spec.rs
3+
expression: response
4+
snapshot_kind: text
5+
---
6+
{
7+
"status": 200,
8+
"headers": {
9+
"content-type": "application/json"
10+
},
11+
"body": {
12+
"data": {
13+
"queryNodeC": [
14+
{
15+
"name": "nodeA",
16+
"__typename": "NodeA",
17+
"nodeA_id": "nodeA_id"
18+
},
19+
{
20+
"name": "nodeB",
21+
"__typename": "NodeB",
22+
"nodeB_id": "nodeB_id"
23+
}
24+
]
25+
}
26+
}
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
source: tests/core/spec.rs
3+
expression: formatted
4+
snapshot_kind: text
5+
---
6+
type NodeA implements NodeC {
7+
name: String
8+
nodeA_id: String
9+
}
10+
11+
type NodeB implements NodeC {
12+
name: String
13+
nodeB_id: String
14+
}
15+
16+
interface NodeC {
17+
name: String
18+
}
19+
20+
type Query {
21+
queryNodeA: [NodeA!]
22+
queryNodeB: [NodeB!]
23+
queryNodeC: [NodeC!]
24+
}
25+
26+
schema {
27+
query: Query
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
source: tests/core/spec.rs
3+
expression: formatter
4+
snapshot_kind: text
5+
---
6+
schema @server @upstream @link(src: "schema_0.graphql", type: Config) {
7+
query: Query
8+
}
9+
10+
interface NodeC {
11+
name: String
12+
}
13+
14+
type NodeA implements NodeC {
15+
name: String
16+
nodeA_id: String
17+
}
18+
19+
type NodeB implements NodeC {
20+
name: String
21+
nodeB_id: String
22+
}
23+
24+
type Query {
25+
queryNodeA: [NodeA!] @graphQL(url: "http://upstream/graphql", name: "nodeA")
26+
queryNodeB: [NodeB!] @graphQL(url: "http://upstream/graphql", name: "nodeB")
27+
queryNodeC: [NodeC!] @graphQL(url: "http://upstream/graphql", name: "nodeC")
28+
}

0 commit comments

Comments
 (0)