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.