Skip to content

Commit 2a77b5d

Browse files
committed
Filter by child
1 parent 4575c39 commit 2a77b5d

File tree

6 files changed

+539
-104
lines changed

6 files changed

+539
-104
lines changed

graph/src/components/store/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ pub enum EntityFilter {
160160
EndsWithNoCase(Attribute, Value),
161161
NotEndsWith(Attribute, Value),
162162
NotEndsWithNoCase(Attribute, Value),
163+
Child(String, EntityType, Box<EntityFilter>),
163164
}
164165

165166
// Define some convenience methods

graphql/src/schema/api.rs

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,15 @@ fn field_filter_input_values(
233233
// `where: { others: ["some-id", "other-id"] }`. In both cases,
234234
// we allow ID strings as the values to be passed to these
235235
// filters.
236-
field_scalar_filter_input_values(
236+
let mut input_values = field_scalar_filter_input_values(
237237
schema,
238238
field,
239239
&ScalarType::new(String::from("String")),
240-
)
240+
);
241+
242+
extend_with_child_filter_input_value(field, name, &mut input_values);
243+
244+
input_values
241245
}
242246
}
243247
TypeDefinition::Scalar(ref t) => field_scalar_filter_input_values(schema, field, t),
@@ -301,6 +305,19 @@ fn field_scalar_filter_input_values(
301305
.collect()
302306
}
303307

308+
/// Appends a child filter to input values
309+
fn extend_with_child_filter_input_value(
310+
field: &Field,
311+
field_type_name: &String,
312+
input_values: &mut Vec<InputValue>,
313+
) {
314+
input_values.push(input_value(
315+
&format!("{}_", field.name),
316+
"",
317+
Type::NamedType(format!("{}_filter", field_type_name)),
318+
));
319+
}
320+
304321
/// Generates `*_filter` input values for the given enum field.
305322
fn field_enum_filter_input_values(
306323
_schema: &Document,
@@ -333,40 +350,51 @@ fn field_list_filter_input_values(
333350
// Decide what type of values can be passed to the filter. In the case
334351
// one-to-many or many-to-many object or interface fields that are not
335352
// derived, we allow ID strings to be passed on.
336-
let input_field_type = match typedef {
337-
TypeDefinition::Interface(_) | TypeDefinition::Object(_) => {
353+
let (input_field_type, parent_type_name) = match typedef {
354+
TypeDefinition::Object(parent) => {
338355
if ast::get_derived_from_directive(field).is_some() {
339356
return None;
340357
} else {
341-
Type::NamedType("String".into())
358+
(Type::NamedType("String".into()), Some(parent.name.clone()))
342359
}
343360
}
344-
TypeDefinition::Scalar(ref t) => Type::NamedType(t.name.to_owned()),
345-
TypeDefinition::Enum(ref t) => Type::NamedType(t.name.to_owned()),
361+
TypeDefinition::Interface(parent) => {
362+
if ast::get_derived_from_directive(field).is_some() {
363+
return None;
364+
} else {
365+
(Type::NamedType("String".into()), Some(parent.name.clone()))
366+
}
367+
}
368+
TypeDefinition::Scalar(ref t) => (Type::NamedType(t.name.to_owned()), None),
369+
TypeDefinition::Enum(ref t) => (Type::NamedType(t.name.to_owned()), None),
346370
TypeDefinition::InputObject(_) | TypeDefinition::Union(_) => return None,
347371
};
348372

349-
Some(
350-
vec![
351-
"",
352-
"not",
353-
"contains",
354-
"contains_nocase",
355-
"not_contains",
356-
"not_contains_nocase",
357-
]
358-
.into_iter()
359-
.map(|filter_type| {
360-
input_value(
361-
&field.name,
362-
filter_type,
363-
Type::ListType(Box::new(Type::NonNullType(Box::new(
364-
input_field_type.clone(),
365-
)))),
366-
)
367-
})
368-
.collect(),
369-
)
373+
let mut input_values: Vec<InputValue> = vec![
374+
"",
375+
"not",
376+
"contains",
377+
"contains_nocase",
378+
"not_contains",
379+
"not_contains_nocase",
380+
]
381+
.into_iter()
382+
.map(|filter_type| {
383+
input_value(
384+
&field.name,
385+
filter_type,
386+
Type::ListType(Box::new(Type::NonNullType(Box::new(
387+
input_field_type.clone(),
388+
)))),
389+
)
390+
})
391+
.collect();
392+
393+
if let Some(parent) = parent_type_name {
394+
extend_with_child_filter_input_value(field, &parent, &mut input_values);
395+
}
396+
397+
Some(input_values)
370398
})
371399
}
372400

@@ -900,6 +928,7 @@ mod tests {
900928
"pets_contains_nocase",
901929
"pets_not_contains",
902930
"pets_not_contains_nocase",
931+
"pets_",
903932
"favoriteFurType",
904933
"favoriteFurType_not",
905934
"favoriteFurType_in",
@@ -924,6 +953,7 @@ mod tests {
924953
"favoritePet_ends_with_nocase",
925954
"favoritePet_not_ends_with",
926955
"favoritePet_not_ends_with_nocase",
956+
"favoritePet_",
927957
]
928958
.iter()
929959
.map(ToString::to_string)
@@ -1135,4 +1165,56 @@ type Gravatar @entity {
11351165
}
11361166
.expect("\"metadata\" field is missing on Query type");
11371167
}
1168+
1169+
#[test]
1170+
fn api_schema_contains_child_entity_filter_on_lists() {
1171+
let input_schema = parse_schema(
1172+
r#"
1173+
type Note @entity {
1174+
id: ID!
1175+
text: String!
1176+
author: User! @derived(field: "id")
1177+
}
1178+
1179+
type User @entity {
1180+
id: ID!
1181+
dateOfBirth: String
1182+
country: String
1183+
notes: [Note!]!
1184+
}
1185+
"#,
1186+
)
1187+
.expect("Failed to parse input schema");
1188+
let schema = api_schema(&input_schema).expect("Failed to derived API schema");
1189+
1190+
println!("{}", schema);
1191+
1192+
let user_filter = schema
1193+
.get_named_type("User_filter")
1194+
.expect("User_filter type is missing in derived API schema");
1195+
1196+
let filter_type = match user_filter {
1197+
TypeDefinition::InputObject(t) => Some(t),
1198+
_ => None,
1199+
}
1200+
.expect("User_filter type is not an input object");
1201+
1202+
let user_notes_filter_field = filter_type
1203+
.fields
1204+
.iter()
1205+
.find_map(|field| {
1206+
if field.name == "notes_" {
1207+
Some(field)
1208+
} else {
1209+
None
1210+
}
1211+
})
1212+
.expect("notes_ field is missing in the User_filter input object");
1213+
1214+
assert_eq!(user_notes_filter_field.name, "notes_");
1215+
assert_eq!(
1216+
user_notes_filter_field.value_type,
1217+
Type::NamedType(String::from("Note_filter"))
1218+
);
1219+
}
11381220
}

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
@@ -623,6 +623,7 @@ fn execute_field(
623623
resolver.store.as_ref(),
624624
parents,
625625
join,
626+
ctx.query.schema.as_ref(),
626627
field,
627628
multiplicity,
628629
ctx.query.schema.types_for_interface(),
@@ -643,6 +644,7 @@ fn fetch(
643644
store: &(impl QueryStore + ?Sized),
644645
parents: &[&mut Node],
645646
join: &Join<'_>,
647+
schema: &ApiSchema,
646648
field: &a::Field,
647649
multiplicity: ChildMultiplicity,
648650
types_for_interface: &BTreeMap<EntityType, Vec<s::ObjectType>>,
@@ -660,6 +662,7 @@ fn fetch(
660662
max_first,
661663
max_skip,
662664
selected_attrs,
665+
schema,
663666
)?;
664667
query.query_id = Some(query_id);
665668

0 commit comments

Comments
 (0)