Skip to content

Commit 1d6c889

Browse files
saihajkamilkisiela
andcommitted
graphql/store: AND/OR filter
Co-authored-by: Kamil Kisiela <[email protected]>
1 parent 013c2c9 commit 1d6c889

File tree

4 files changed

+308
-83
lines changed

4 files changed

+308
-83
lines changed

graphql/src/schema/api.rs

+24-2
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,24 @@ fn add_filter_type(
188188
let mut generated_filter_fields = field_input_values(schema, fields)?;
189189
generated_filter_fields.push(block_changed_filter_argument());
190190

191+
generated_filter_fields.push(InputValue {
192+
position: Pos::default(),
193+
description: None,
194+
name: "and".to_string(),
195+
value_type: Type::ListType(Box::new(Type::NamedType(filter_type_name.to_owned()))),
196+
default_value: None,
197+
directives: vec![],
198+
});
199+
200+
generated_filter_fields.push(InputValue {
201+
position: Pos::default(),
202+
description: None,
203+
name: "or".to_string(),
204+
value_type: Type::ListType(Box::new(Type::NamedType(filter_type_name.to_owned()))),
205+
default_value: None,
206+
directives: vec![],
207+
});
208+
191209
let typedef = TypeDefinition::InputObject(InputObjectType {
192210
position: Pos::default(),
193211
description: None,
@@ -969,7 +987,9 @@ mod tests {
969987
"favoritePet_",
970988
"leastFavoritePet_",
971989
"mostFavoritePets_",
972-
"_change_block"
990+
"_change_block",
991+
"and",
992+
"or"
973993
]
974994
.iter()
975995
.map(ToString::to_string)
@@ -1170,7 +1190,9 @@ mod tests {
11701190
"favoritePet_not_ends_with",
11711191
"favoritePet_not_ends_with_nocase",
11721192
"favoritePet_",
1173-
"_change_block"
1193+
"_change_block",
1194+
"and",
1195+
"or"
11741196
]
11751197
.iter()
11761198
.map(ToString::to_string)

graphql/src/schema/ast.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pub(crate) enum FilterOp {
3434
NotEndsWithNoCase,
3535
Equal,
3636
Child,
37+
And,
38+
Or,
3739
}
3840

3941
/// Split a "name_eq" style name into an attribute ("name") and a filter op (`Equal`).
@@ -67,11 +69,17 @@ pub(crate) fn parse_field_as_filter(key: &str) -> (String, FilterOp) {
6769
k if k.ends_with("_ends_with") => ("_ends_with", FilterOp::EndsWith),
6870
k if k.ends_with("_ends_with_nocase") => ("_ends_with_nocase", FilterOp::EndsWithNoCase),
6971
k if k.ends_with("_") => ("_", FilterOp::Child),
72+
k if k.eq("and") => ("and", FilterOp::And),
73+
k if k.eq("or") => ("or", FilterOp::Or),
7074
_ => ("", FilterOp::Equal),
7175
};
7276

73-
// Strip the operator suffix to get the attribute.
74-
(key.trim_end_matches(suffix).to_owned(), op)
77+
return match op {
78+
FilterOp::And => (key.to_owned(), op),
79+
FilterOp::Or => (key.to_owned(), op),
80+
// Strip the operator suffix to get the attribute.
81+
_ => (key.trim_end_matches(suffix).to_owned(), op),
82+
};
7583
}
7684

7785
/// An `ObjectType` with `Hash` and `Eq` derived from the name.

graphql/src/store/query.rs

+156-79
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use graph::prelude::*;
99
use graph::{components::store::EntityType, data::graphql::ObjectOrInterface};
1010

1111
use crate::execution::ast as a;
12-
use crate::schema::ast as sast;
12+
use crate::schema::ast::{self as sast, FilterOp};
1313

1414
use super::prefetch::SelectedAttributes;
1515

@@ -118,7 +118,7 @@ fn build_filter(
118118
) -> Result<Option<EntityFilter>, QueryExecutionError> {
119119
match field.argument_value("where") {
120120
Some(r::Value::Object(object)) => match build_filter_from_object(entity, object, schema) {
121-
Ok(filter) => Ok(Some(filter)),
121+
Ok(filter) => Ok(Some(EntityFilter::And(filter))),
122122
Err(e) => Err(e),
123123
},
124124
Some(r::Value::Null) => Ok(None),
@@ -161,91 +161,164 @@ fn parse_change_block_filter(value: &r::Value) -> Result<BlockNumber, QueryExecu
161161
}
162162
}
163163

164+
/// Parses a GraphQL Filter Value into an EntityFilter.
165+
fn build_entity_filter(
166+
field_name: String,
167+
operation: FilterOp,
168+
store_value: Value,
169+
) -> Result<EntityFilter, QueryExecutionError> {
170+
return match operation {
171+
FilterOp::Not => Ok(EntityFilter::Not(field_name, store_value)),
172+
FilterOp::GreaterThan => Ok(EntityFilter::GreaterThan(field_name, store_value)),
173+
FilterOp::LessThan => Ok(EntityFilter::LessThan(field_name, store_value)),
174+
FilterOp::GreaterOrEqual => Ok(EntityFilter::GreaterOrEqual(field_name, store_value)),
175+
FilterOp::LessOrEqual => Ok(EntityFilter::LessOrEqual(field_name, store_value)),
176+
FilterOp::In => Ok(EntityFilter::In(
177+
field_name,
178+
list_values(store_value, "_in")?,
179+
)),
180+
FilterOp::NotIn => Ok(EntityFilter::NotIn(
181+
field_name,
182+
list_values(store_value, "_not_in")?,
183+
)),
184+
FilterOp::Contains => Ok(EntityFilter::Contains(field_name, store_value)),
185+
FilterOp::ContainsNoCase => Ok(EntityFilter::ContainsNoCase(field_name, store_value)),
186+
FilterOp::NotContains => Ok(EntityFilter::NotContains(field_name, store_value)),
187+
FilterOp::NotContainsNoCase => Ok(EntityFilter::NotContainsNoCase(field_name, store_value)),
188+
FilterOp::StartsWith => Ok(EntityFilter::StartsWith(field_name, store_value)),
189+
FilterOp::StartsWithNoCase => Ok(EntityFilter::StartsWithNoCase(field_name, store_value)),
190+
FilterOp::NotStartsWith => Ok(EntityFilter::NotStartsWith(field_name, store_value)),
191+
FilterOp::NotStartsWithNoCase => {
192+
Ok(EntityFilter::NotStartsWithNoCase(field_name, store_value))
193+
}
194+
FilterOp::EndsWith => Ok(EntityFilter::EndsWith(field_name, store_value)),
195+
FilterOp::EndsWithNoCase => Ok(EntityFilter::EndsWithNoCase(field_name, store_value)),
196+
FilterOp::NotEndsWith => Ok(EntityFilter::NotEndsWith(field_name, store_value)),
197+
FilterOp::NotEndsWithNoCase => Ok(EntityFilter::NotEndsWithNoCase(field_name, store_value)),
198+
FilterOp::Equal => Ok(EntityFilter::Equal(field_name, store_value)),
199+
_ => unreachable!(),
200+
};
201+
}
202+
164203
/// Parses a GraphQL input object into an EntityFilter, if present.
165204
fn build_filter_from_object(
166205
entity: ObjectOrInterface,
167206
object: &Object,
168207
schema: &ApiSchema,
169-
) -> Result<EntityFilter, QueryExecutionError> {
170-
Ok(EntityFilter::And({
171-
object
172-
.iter()
173-
.map(|(key, value)| {
174-
// Special handling for _change_block input filter since its not a
175-
// standard entity filter that is based on entity structure/fields
176-
if key == "_change_block" {
177-
return match parse_change_block_filter(value) {
178-
Ok(block_number) => Ok(EntityFilter::ChangeBlockGte(block_number)),
179-
Err(e) => Err(e),
180-
};
181-
}
182-
183-
use self::sast::FilterOp::*;
184-
let (field_name, op) = sast::parse_field_as_filter(key);
185-
186-
let field = sast::get_field(entity, &field_name).ok_or_else(|| {
187-
QueryExecutionError::EntityFieldError(
188-
entity.name().to_owned(),
189-
field_name.clone(),
190-
)
191-
})?;
192-
193-
let ty = &field.field_type;
208+
) -> Result<Vec<EntityFilter>, QueryExecutionError> {
209+
Ok(object
210+
.iter()
211+
.map(|(key, value)| {
212+
// Special handling for _change_block input filter since its not a
213+
// standard entity filter that is based on entity structure/fields
214+
if key == "_change_block" {
215+
return match parse_change_block_filter(value) {
216+
Ok(block_number) => Ok(EntityFilter::ChangeBlockGte(block_number)),
217+
Err(e) => Err(e),
218+
};
219+
}
220+
use self::sast::FilterOp::*;
221+
let (field_name, op) = sast::parse_field_as_filter(key);
194222

195-
Ok(match op {
196-
Child => match value {
197-
DataValue::Object(obj) => {
198-
build_child_filter_from_object(entity, field_name, obj, schema)?
199-
}
200-
_ => {
201-
return Err(QueryExecutionError::AttributeTypeError(
202-
value.to_string(),
203-
ty.to_string(),
204-
))
205-
}
206-
},
223+
Ok(match op {
224+
And => {
225+
// AND input has a List of Objects
226+
let filters = object
227+
.iter()
228+
.map(|(_, value)| {
229+
return match value {
230+
// Iterate over the list and generate an EntityFilter from it
231+
r::Value::List(list) => Ok(list
232+
.iter()
233+
.map(|item| {
234+
return match item {
235+
r::Value::Object(object) => Ok(
236+
build_filter_from_object(entity, object, schema)?,
237+
),
238+
_ => Err(QueryExecutionError::InvalidFilterError),
239+
};
240+
})
241+
.collect::<Result<Vec<Vec<EntityFilter>>, QueryExecutionError>>(
242+
)?
243+
// Flatten all different EntityFilters into one list
244+
.into_iter()
245+
.flatten()
246+
.collect::<Vec<EntityFilter>>()),
247+
_ => Err(QueryExecutionError::InvalidFilterError),
248+
};
249+
})
250+
.collect::<Result<Vec<Vec<EntityFilter>>, QueryExecutionError>>()?
251+
.into_iter()
252+
// We iterate an object so all entity filters are flattened into one list
253+
.flatten()
254+
.collect::<Vec<EntityFilter>>();
255+
return Ok(EntityFilter::And(filters));
256+
}
257+
Or => {
258+
// OR input has a List of Objects
259+
let filters = object
260+
.iter()
261+
.map(|(_, value)| {
262+
return match value {
263+
// Iterate over the list and generate an EntityFilter from it
264+
r::Value::List(list) => Ok(list
265+
.iter()
266+
.map(|item| {
267+
return match item {
268+
r::Value::Object(object) => Ok(
269+
build_filter_from_object(entity, object, schema)?,
270+
),
271+
_ => Err(QueryExecutionError::InvalidFilterError),
272+
};
273+
})
274+
.collect::<Result<Vec<Vec<EntityFilter>>, QueryExecutionError>>(
275+
)?
276+
// Flatten all different EntityFilters into one list
277+
.into_iter()
278+
.flatten()
279+
.collect::<Vec<EntityFilter>>()),
280+
_ => Err(QueryExecutionError::InvalidFilterError),
281+
};
282+
})
283+
.collect::<Result<Vec<Vec<EntityFilter>>, QueryExecutionError>>()?
284+
.into_iter()
285+
// We iterate an object so all entity filters are flattened into one list
286+
.flatten()
287+
.collect::<Vec<EntityFilter>>();
288+
return Ok(EntityFilter::Or(filters));
289+
}
290+
Child => match value {
291+
DataValue::Object(obj) => {
292+
build_child_filter_from_object(entity, field_name, obj, schema)?
293+
}
207294
_ => {
208-
let store_value = Value::from_query_value(value, ty)?;
209-
210-
match op {
211-
Not => EntityFilter::Not(field_name, store_value),
212-
GreaterThan => EntityFilter::GreaterThan(field_name, store_value),
213-
LessThan => EntityFilter::LessThan(field_name, store_value),
214-
GreaterOrEqual => EntityFilter::GreaterOrEqual(field_name, store_value),
215-
LessOrEqual => EntityFilter::LessOrEqual(field_name, store_value),
216-
In => EntityFilter::In(field_name, list_values(store_value, "_in")?),
217-
NotIn => EntityFilter::NotIn(
218-
field_name,
219-
list_values(store_value, "_not_in")?,
220-
),
221-
Contains => EntityFilter::Contains(field_name, store_value),
222-
ContainsNoCase => EntityFilter::ContainsNoCase(field_name, store_value),
223-
NotContains => EntityFilter::NotContains(field_name, store_value),
224-
NotContainsNoCase => {
225-
EntityFilter::NotContainsNoCase(field_name, store_value)
226-
}
227-
StartsWith => EntityFilter::StartsWith(field_name, store_value),
228-
StartsWithNoCase => {
229-
EntityFilter::StartsWithNoCase(field_name, store_value)
230-
}
231-
NotStartsWith => EntityFilter::NotStartsWith(field_name, store_value),
232-
NotStartsWithNoCase => {
233-
EntityFilter::NotStartsWithNoCase(field_name, store_value)
234-
}
235-
EndsWith => EntityFilter::EndsWith(field_name, store_value),
236-
EndsWithNoCase => EntityFilter::EndsWithNoCase(field_name, store_value),
237-
NotEndsWith => EntityFilter::NotEndsWith(field_name, store_value),
238-
NotEndsWithNoCase => {
239-
EntityFilter::NotEndsWithNoCase(field_name, store_value)
240-
}
241-
Equal => EntityFilter::Equal(field_name, store_value),
242-
_ => unreachable!(),
243-
}
295+
let field = sast::get_field(entity, &field_name).ok_or_else(|| {
296+
QueryExecutionError::EntityFieldError(
297+
entity.name().to_owned(),
298+
field_name.clone(),
299+
)
300+
})?;
301+
let ty = &field.field_type;
302+
return Err(QueryExecutionError::AttributeTypeError(
303+
value.to_string(),
304+
ty.to_string(),
305+
));
244306
}
245-
})
307+
},
308+
_ => {
309+
let field = sast::get_field(entity, &field_name).ok_or_else(|| {
310+
QueryExecutionError::EntityFieldError(
311+
entity.name().to_owned(),
312+
field_name.clone(),
313+
)
314+
})?;
315+
let ty = &field.field_type;
316+
let store_value = Value::from_query_value(value, ty)?;
317+
return build_entity_filter(field_name, op, store_value);
318+
}
246319
})
247-
.collect::<Result<Vec<EntityFilter>, QueryExecutionError>>()?
248-
}))
320+
})
321+
.collect::<Result<Vec<EntityFilter>, QueryExecutionError>>()?)
249322
}
250323

251324
fn build_child_filter_from_object(
@@ -261,7 +334,11 @@ fn build_child_filter_from_object(
261334
let child_entity = schema
262335
.object_or_interface(type_name)
263336
.ok_or(QueryExecutionError::InvalidFilterError)?;
264-
let filter = Box::new(build_filter_from_object(child_entity, object, schema)?);
337+
let filter = Box::new(EntityFilter::And(build_filter_from_object(
338+
child_entity,
339+
object,
340+
schema,
341+
)?));
265342
let derived = field.is_derived();
266343
let attr = match derived {
267344
true => sast::get_derived_from_field(child_entity, field)

0 commit comments

Comments
 (0)