Skip to content

Commit 461a827

Browse files
committed
feat(graph,graphql): allow to filter by child entity fields
1 parent 42c017e commit 461a827

File tree

6 files changed

+446
-91
lines changed

6 files changed

+446
-91
lines changed

graph/src/components/store/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ pub enum EntityFilter {
192192
NotEndsWith(Attribute, Value),
193193
NotEndsWithNoCase(Attribute, Value),
194194
ChangeBlockGte(BlockNumber),
195+
Child(String, EntityType, Box<EntityFilter>),
195196
}
196197

197198
// A somewhat concise string representation of a filter
@@ -235,6 +236,7 @@ impl fmt::Display for EntityFilter {
235236
NotEndsWith(a, v) => write!(f, "{a} !~ *{v}$"),
236237
NotEndsWithNoCase(a, v) => write!(f, "{a} !~ *{v}$i"),
237238
ChangeBlockGte(b) => write!(f, "block >= {b}"),
239+
Child(a, et, cf) => write!(f, "join {et} by {a} ({})", cf.to_string()),
238240
}
239241
}
240242
}

graphql/src/schema/api.rs

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,15 @@ fn field_filter_input_values(
238238
// `where: { others: ["some-id", "other-id"] }`. In both cases,
239239
// we allow ID strings as the values to be passed to these
240240
// filters.
241-
field_scalar_filter_input_values(
241+
let mut input_values = field_scalar_filter_input_values(
242242
schema,
243243
field,
244244
&ScalarType::new(String::from("String")),
245-
)
245+
);
246+
247+
extend_with_child_filter_input_value(field, name, &mut input_values);
248+
249+
input_values
246250
}
247251
}
248252
TypeDefinition::Scalar(ref t) => field_scalar_filter_input_values(schema, field, t),
@@ -306,6 +310,19 @@ fn field_scalar_filter_input_values(
306310
.collect()
307311
}
308312

313+
/// Appends a child filter to input values
314+
fn extend_with_child_filter_input_value(
315+
field: &Field,
316+
field_type_name: &String,
317+
input_values: &mut Vec<InputValue>,
318+
) {
319+
input_values.push(input_value(
320+
&format!("{}_", field.name),
321+
"",
322+
Type::NamedType(format!("{}_filter", field_type_name)),
323+
));
324+
}
325+
309326
/// Generates `*_filter` input values for the given enum field.
310327
fn field_enum_filter_input_values(
311328
_schema: &Document,
@@ -338,40 +355,51 @@ fn field_list_filter_input_values(
338355
// Decide what type of values can be passed to the filter. In the case
339356
// one-to-many or many-to-many object or interface fields that are not
340357
// derived, we allow ID strings to be passed on.
341-
let input_field_type = match typedef {
342-
TypeDefinition::Interface(_) | TypeDefinition::Object(_) => {
358+
let (input_field_type, parent_type_name) = match typedef {
359+
TypeDefinition::Object(parent) => {
343360
if ast::get_derived_from_directive(field).is_some() {
344361
return None;
345362
} else {
346-
Type::NamedType("String".into())
363+
(Type::NamedType("String".into()), Some(parent.name.clone()))
347364
}
348365
}
349-
TypeDefinition::Scalar(ref t) => Type::NamedType(t.name.to_owned()),
350-
TypeDefinition::Enum(ref t) => Type::NamedType(t.name.to_owned()),
366+
TypeDefinition::Interface(parent) => {
367+
if ast::get_derived_from_directive(field).is_some() {
368+
return None;
369+
} else {
370+
(Type::NamedType("String".into()), Some(parent.name.clone()))
371+
}
372+
}
373+
TypeDefinition::Scalar(ref t) => (Type::NamedType(t.name.to_owned()), None),
374+
TypeDefinition::Enum(ref t) => (Type::NamedType(t.name.to_owned()), None),
351375
TypeDefinition::InputObject(_) | TypeDefinition::Union(_) => return None,
352376
};
353377

354-
Some(
355-
vec![
356-
"",
357-
"not",
358-
"contains",
359-
"contains_nocase",
360-
"not_contains",
361-
"not_contains_nocase",
362-
]
363-
.into_iter()
364-
.map(|filter_type| {
365-
input_value(
366-
&field.name,
367-
filter_type,
368-
Type::ListType(Box::new(Type::NonNullType(Box::new(
369-
input_field_type.clone(),
370-
)))),
371-
)
372-
})
373-
.collect(),
374-
)
378+
let mut input_values: Vec<InputValue> = vec![
379+
"",
380+
"not",
381+
"contains",
382+
"contains_nocase",
383+
"not_contains",
384+
"not_contains_nocase",
385+
]
386+
.into_iter()
387+
.map(|filter_type| {
388+
input_value(
389+
&field.name,
390+
filter_type,
391+
Type::ListType(Box::new(Type::NonNullType(Box::new(
392+
input_field_type.clone(),
393+
)))),
394+
)
395+
})
396+
.collect();
397+
398+
if let Some(parent) = parent_type_name {
399+
extend_with_child_filter_input_value(field, &parent, &mut input_values);
400+
}
401+
402+
Some(input_values)
375403
})
376404
}
377405

@@ -916,6 +944,7 @@ mod tests {
916944
"pets_contains_nocase",
917945
"pets_not_contains",
918946
"pets_not_contains_nocase",
947+
"pets_",
919948
"favoriteFurType",
920949
"favoriteFurType_not",
921950
"favoriteFurType_in",
@@ -940,6 +969,7 @@ mod tests {
940969
"favoritePet_ends_with_nocase",
941970
"favoritePet_not_ends_with",
942971
"favoritePet_not_ends_with_nocase",
972+
"favoritePet_",
943973
"_change_block"
944974
]
945975
.iter()
@@ -1170,4 +1200,56 @@ type Gravatar @entity {
11701200
}
11711201
.expect("\"metadata\" field is missing on Query type");
11721202
}
1203+
1204+
#[test]
1205+
fn api_schema_contains_child_entity_filter_on_lists() {
1206+
let input_schema = parse_schema(
1207+
r#"
1208+
type Note @entity {
1209+
id: ID!
1210+
text: String!
1211+
author: User! @derived(field: "id")
1212+
}
1213+
1214+
type User @entity {
1215+
id: ID!
1216+
dateOfBirth: String
1217+
country: String
1218+
notes: [Note!]!
1219+
}
1220+
"#,
1221+
)
1222+
.expect("Failed to parse input schema");
1223+
let schema = api_schema(&input_schema).expect("Failed to derived API schema");
1224+
1225+
println!("{}", schema);
1226+
1227+
let user_filter = schema
1228+
.get_named_type("User_filter")
1229+
.expect("User_filter type is missing in derived API schema");
1230+
1231+
let filter_type = match user_filter {
1232+
TypeDefinition::InputObject(t) => Some(t),
1233+
_ => None,
1234+
}
1235+
.expect("User_filter type is not an input object");
1236+
1237+
let user_notes_filter_field = filter_type
1238+
.fields
1239+
.iter()
1240+
.find_map(|field| {
1241+
if field.name == "notes_" {
1242+
Some(field)
1243+
} else {
1244+
None
1245+
}
1246+
})
1247+
.expect("notes_ field is missing in the User_filter input object");
1248+
1249+
assert_eq!(user_notes_filter_field.name, "notes_");
1250+
assert_eq!(
1251+
user_notes_filter_field.value_type,
1252+
Type::NamedType(String::from("Note_filter"))
1253+
);
1254+
}
11731255
}

graphql/src/schema/ast.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub(crate) enum FilterOp {
3333
NotEndsWith,
3434
NotEndsWithNoCase,
3535
Equal,
36+
Child,
3637
}
3738

3839
/// Split a "name_eq" style name into an attribute ("name") and a filter op (`Equal`).
@@ -65,6 +66,7 @@ pub(crate) fn parse_field_as_filter(key: &str) -> (String, FilterOp) {
6566
}
6667
k if k.ends_with("_ends_with") => ("_ends_with", FilterOp::EndsWith),
6768
k if k.ends_with("_ends_with_nocase") => ("_ends_with_nocase", FilterOp::EndsWithNoCase),
69+
k if k.ends_with("_") => ("_", FilterOp::Child),
6870
_ => ("", FilterOp::Equal),
6971
};
7072

graphql/src/store/prefetch.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,7 @@ fn execute_field(
629629
resolver.store.as_ref(),
630630
parents,
631631
join,
632+
ctx.query.schema.as_ref(),
632633
field,
633634
multiplicity,
634635
ctx.query.schema.types_for_interface(),
@@ -649,6 +650,7 @@ fn fetch(
649650
store: &(impl QueryStore + ?Sized),
650651
parents: &[&mut Node],
651652
join: &Join<'_>,
653+
schema: &ApiSchema,
652654
field: &a::Field,
653655
multiplicity: ChildMultiplicity,
654656
types_for_interface: &BTreeMap<EntityType, Vec<s::ObjectType>>,
@@ -666,6 +668,7 @@ fn fetch(
666668
max_first,
667669
max_skip,
668670
selected_attrs,
671+
schema,
669672
)?;
670673
query.query_id = Some(query_id);
671674

0 commit comments

Comments
 (0)