When we added support for complex filtering in Buttondown, I spent a long time trying to come up with a schema for filters that felt sufficiently ergonomic and future-proof. I had a few constraints, all of which were reasonable:
- It needed to be JSON-serializable, and trivially parsable by both the front-end and back-end.
- It needed to be arbitrarily extendible across a number of domains (you could filter subscribers, but also you might want to filter emails or other models.)
- It needed to be able to handle both and and or logic (folks tagged foo and bar as well as folded tagged foo or bar).
- It needed to handle nested logic (folks tagged foo and folks tagged bar or baz.)
The solution I landed upon is not, I’m sure, a novel one, but googling “recursive filter schema” was unsuccessful and I am really happy with the result so here it is in case you need something like this:
@dataclass
class FilterGroup:
filters: list[Filter]
groups: list[FilterGroup]
predicate: "and" | "or"
@dataclass
class Filter:
field: str
operator: "less_than" | "greater_than" | "equals" | "not_equals" | "contains" | "not_contains"
value: str
And there you have it. Simple, easily serializable/type-safe, can handle everything you throw at it.
For example, a filter for all folks younger than 18 or older than 60 and retired:
FilterGroup(
predicate="or",
filters=[
Field(
field="age",
operator="less_than",
value="18"
)
],
groups=[
FilterGroup(
predicate="and",
filters=[
Field(
field="age",
operator="greater_than",
value="60"
),
Field(
field="status",
operator="equals",
value="retired"
)
]
groups=[],
)
]
)