The basic idea for implementing filter in wundergraph is not that complex. You have a a bunch of traits build the filter structure from the provided graphql request and map that to diesels dsl.
The basic building block are structs like this one
pub struct Eq<T, C>(Option<T>, PhantomData<C>);
where T is the actual type of the provided filter value and C the column type the filter should be applied.
Then there is this trait impl:
impl<C, T, DB> BuildFilter<DB> for Eq<T, C>
where
C: ExpressionMethods + NonAggregate + Column + QueryFragment<DB> + Default + 'static,
T: AsExpression<C::SqlType> + ToSql<<C as Expression>::SqlType, DB>,
T::Expression: NonAggregate + AppearsOnTable<C::Table> + QueryFragment<DB> + 'static,
DB: Backend + HasSqlType<<C as Expression>::SqlType> + 'static,
C::Table: 'static,
operators::Eq<C, <T as AsExpression<C::SqlType>>::Expression>:
AppearsOnTable<C::Table, SqlType = Bool>,
{
type Ret = Box<dyn BoxableFilter<C::Table, DB, SqlType = Bool>>;
fn into_filter(self) -> Option<Self::Ret> {
let Self(filter, _) = self;
filter.map(|v| Box::new(C::default().eq(v)) as Box<_>)
}
}
which describe how to build a diesel expression out of the struct.
Those simple operation structs are then aggregated into a single high level wrapping struct here
pub struct FilterOption<T, C>
where
T: FilterValue<C>,
{
eq: Eq<T::RawValue, C>,
neq: NotEq<T::RawValue, C>,
eq_any: EqAny<T::RawValue, C>,
additional: T::AdditionalFilter,
}
where FilterValue is just a helper trait that allows to use a different type for building the operations and allows to provide more operations specific for the type via the additional field.
Those filters are then aggregated here into a final filter expression by chaining them via and. This trait impl is used to construct this data structure from the provided graphql ast.
Those generic data structures are then used to build a specific filters like:
struct FooFilter {
first_field: FilterOption<RustFieldType, foo::first_column>,
second_field: FilterOption<RustFieldType, foo::second_column>,
}
(This requires then a manual “deserialize” implementation that maps the provided query string to the corresponding fields (by just matching fields), as pseudo-code something like this:
fn deserialize(value: serde_json::Value) -> FooFilter {
if let Some(object) = value {
let mut ret = Self::default();
for (key, value) in object {
match key {
"first_field" => {
ret.first_field =FilterOption::<RustFieldType, foo::first_column>::deserialize(value);
},
"second_field" => {
ret.second_field =FilterOption::<RustFieldType, foo::second_column>::deserialize(value);
},
_ => todo!("Handle column mismatch")
}
}
} else {
todo!("Handle error")
}
}
This implementation works with query strings like: filter: {"first_field": {"eq": 42}, "second_field": {"neq": "Foo"}} where everything is expected to be statically typed.