Skip to content

Commit

Permalink
Filter by child
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Jan 20, 2022
1 parent 3727335 commit c288e18
Show file tree
Hide file tree
Showing 6 changed files with 518 additions and 107 deletions.
18 changes: 1 addition & 17 deletions graph/src/components/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,27 +177,11 @@ pub enum EntityFilter {
NotStartsWith(Attribute, Value),
EndsWith(Attribute, Value),
NotEndsWith(Attribute, Value),
Child(String, EntityType, Box<EntityFilter>),
}

// Define some convenience methods
impl EntityFilter {
pub fn new_equal(
attribute_name: impl Into<Attribute>,
attribute_value: impl Into<Value>,
) -> Self {
EntityFilter::Equal(attribute_name.into(), attribute_value.into())
}

pub fn new_in(
attribute_name: impl Into<Attribute>,
attribute_values: Vec<impl Into<Value>>,
) -> Self {
EntityFilter::In(
attribute_name.into(),
attribute_values.into_iter().map(Into::into).collect(),
)
}

pub fn and_maybe(self, other: Option<Self>) -> Self {
use EntityFilter as f;
match other {
Expand Down
124 changes: 103 additions & 21 deletions graphql/src/schema/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,11 +376,15 @@ fn field_filter_input_values(
// `where: { others: ["some-id", "other-id"] }`. In both cases,
// we allow ID strings as the values to be passed to these
// filters.
field_scalar_filter_input_values(
let mut input_values = field_scalar_filter_input_values(
schema,
field,
&ScalarType::new(String::from("String")),
)
);

extend_with_child_filter_input_value(field, name, &mut input_values);

input_values
}
}
TypeDefinition::Scalar(ref t) => field_scalar_filter_input_values(schema, field, t),
Expand Down Expand Up @@ -438,6 +442,19 @@ fn field_scalar_filter_input_values(
.collect()
}

/// Appends a child filter to input values
fn extend_with_child_filter_input_value(
field: &Field,
field_type_name: &String,
input_values: &mut Vec<InputValue>,
) {
input_values.push(input_value(
&format!("{}_", field.name),
"",
Type::NamedType(format!("{}_filter", field_type_name)),
));
}

/// Generates `*_filter` input values for the given enum field.
fn field_enum_filter_input_values(
_schema: &Document,
Expand Down Expand Up @@ -470,33 +487,44 @@ fn field_list_filter_input_values(
// Decide what type of values can be passed to the filter. In the case
// one-to-many or many-to-many object or interface fields that are not
// derived, we allow ID strings to be passed on.
let input_field_type = match typedef {
TypeDefinition::Interface(_) | TypeDefinition::Object(_) => {
let (input_field_type, parent_type_name) = match typedef {
TypeDefinition::Object(parent) => {
if ast::get_derived_from_directive(field).is_some() {
return None;
} else {
(Type::NamedType("String".into()), Some(parent.name.clone()))
}
}
TypeDefinition::Interface(parent) => {
if ast::get_derived_from_directive(field).is_some() {
return None;
} else {
Type::NamedType("String".into())
(Type::NamedType("String".into()), Some(parent.name.clone()))
}
}
TypeDefinition::Scalar(ref t) => Type::NamedType(t.name.to_owned()),
TypeDefinition::Enum(ref t) => Type::NamedType(t.name.to_owned()),
TypeDefinition::Scalar(ref t) => (Type::NamedType(t.name.to_owned()), None),
TypeDefinition::Enum(ref t) => (Type::NamedType(t.name.to_owned()), None),
TypeDefinition::InputObject(_) | TypeDefinition::Union(_) => return None,
};

Some(
vec!["", "not", "contains", "not_contains"]
.into_iter()
.map(|filter_type| {
input_value(
&field.name,
filter_type,
Type::ListType(Box::new(Type::NonNullType(Box::new(
input_field_type.clone(),
)))),
)
})
.collect(),
)
let mut input_values: Vec<InputValue> = vec!["", "not", "contains", "not_contains"]
.into_iter()
.map(|filter_type| {
input_value(
&field.name,
filter_type,
Type::ListType(Box::new(Type::NonNullType(Box::new(
input_field_type.clone(),
)))),
)
})
.collect();

if let Some(parent) = parent_type_name {
extend_with_child_filter_input_value(field, &parent, &mut input_values);
}

Some(input_values)
})
}

Expand Down Expand Up @@ -1020,6 +1048,7 @@ mod tests {
"pets_not",
"pets_contains",
"pets_not_contains",
"pets_",
"favoriteFurType",
"favoriteFurType_not",
"favoriteFurType_in",
Expand All @@ -1038,6 +1067,7 @@ mod tests {
"favoritePet_not_starts_with",
"favoritePet_ends_with",
"favoritePet_not_ends_with",
"favoritePet_",
]
.iter()
.map(ToString::to_string)
Expand Down Expand Up @@ -1249,4 +1279,56 @@ type Gravatar @entity {
}
.expect("\"metadata\" field is missing on Query type");
}

#[test]
fn api_schema_contains_child_entity_filter_on_lists() {
let input_schema = parse_schema(
r#"
type Note @entity {
id: ID!
text: String!
author: User! @derived(field: "id")
}
type User @entity {
id: ID!
dateOfBirth: String
country: String
notes: [Note!]!
}
"#,
)
.expect("Failed to parse input schema");
let schema = api_schema(&input_schema).expect("Failed to derived API schema");

println!("{}", schema);

let user_filter = schema
.get_named_type("User_filter")
.expect("User_filter type is missing in derived API schema");

let filter_type = match user_filter {
TypeDefinition::InputObject(t) => Some(t),
_ => None,
}
.expect("User_filter type is not an input object");

let user_notes_filter_field = filter_type
.fields
.iter()
.find_map(|field| {
if field.name == "notes_" {
Some(field)
} else {
None
}
})
.expect("notes_ field is missing in the User_filter input object");

assert_eq!(user_notes_filter_field.name, "notes_");
assert_eq!(
user_notes_filter_field.value_type,
Type::NamedType(String::from("Note_filter"))
);
}
}
2 changes: 2 additions & 0 deletions graphql/src/schema/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub(crate) enum FilterOp {
EndsWith,
NotEndsWith,
Equal,
Child,
}

/// Split a "name_eq" style name into an attribute ("name") and a filter op (`Equal`).
Expand All @@ -46,6 +47,7 @@ pub(crate) fn parse_field_as_filter(key: &str) -> (String, FilterOp) {
k if k.ends_with("_not_ends_with") => ("_not_ends_with", FilterOp::NotEndsWith),
k if k.ends_with("_starts_with") => ("_starts_with", FilterOp::StartsWith),
k if k.ends_with("_ends_with") => ("_ends_with", FilterOp::EndsWith),
k if k.ends_with("_") => ("_", FilterOp::Child),
_ => ("", FilterOp::Equal),
};

Expand Down
3 changes: 3 additions & 0 deletions graphql/src/store/prefetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ fn execute_field(
resolver.store.as_ref(),
parents,
&join,
ctx.query.schema.as_ref(),
field,
multiplicity,
ctx.query.schema.types_for_interface(),
Expand All @@ -653,6 +654,7 @@ fn fetch(
store: &(impl QueryStore + ?Sized),
parents: &Vec<&mut Node>,
join: &Join<'_>,
schema: &ApiSchema,
field: &a::Field,
multiplicity: ChildMultiplicity,
types_for_interface: &BTreeMap<EntityType, Vec<s::ObjectType>>,
Expand All @@ -670,6 +672,7 @@ fn fetch(
max_first,
max_skip,
selected_attrs,
schema,
)?;
query.query_id = Some(query_id);

Expand Down
Loading

0 comments on commit c288e18

Please sign in to comment.