Landing-Page Filter Groups (OR / AND)¶
Date: 2026-06-02
Problem / motivation¶
Landing-page queries (findLandingPages, findLandingPageKpis) only supported a
flat list of filters that were always AND-ed together. There was no way to
express alternatives such as "invoices that are DRAFT or SENT" or
compound logic like (status = DRAFT OR status = SENT) AND total > 100. Users
filtering reports and landing pages needed OR semantics, not just AND.
In scope¶
- A new optional
filterGroupsargument on the landing-page queries that lets a caller pass one or more groups, each combining its own filters withANDorOR. - Groups are AND-ed with each other and with the existing top-level
filters, enabling(a OR b) AND (c OR d)style logic (one level of nesting). - Full backward compatibility: the existing flat
filtersargument keeps its exact AND semantics; callers that don't sendfilterGroupsare unaffected.
Out of scope¶
- Arbitrary-depth nesting (a recursive group tree). Deferred — one level of grouping covers the reporting cases we have today. See Future additions.
- Chat-agent / NL query support for groups. The conversation agent still
emits flat
filtersonly; teaching it to emitfilterGroupsis a fast follow. - Frontend UI work in
cifras-admin(a separate change against this contract).
Data model changes¶
None. This is a pure GraphQL query-surface change — no tables, columns, or migrations.
GraphQL surface¶
New enum:
New input:
Changed query signatures (both gain an optional filterGroups: [FilterGroup!]):
findLandingPages(
sourceType: LandingSourceType!
filters: [Filter!]
filterGroups: [FilterGroup!]
orderBy: [OrderBy!]
limit: Int = 10
offset: Int = 0
generateCsv: Boolean! = false
): GenericLandingPage!
findLandingPageKpis(
sourceType: LandingSourceType!
filters: [Filter!]
filterGroups: [FilterGroup!]
): [LandingPageKpiResponse!]!
Semantics¶
- Each
Filterinside aFilterGroupis built into a condition exactly the way top-level filters are (same operators, same value parsing, same skip rules for blank/unparseable values). - The group's conditions are combined with the group
operator(OR→or_,AND→and_, defaultAND). - Every group is then AND-ed with the top-level
filtersand the other groups:
WHERE <top-level filters AND-ed>
AND (<group 1 conditions combined with its operator>)
AND (<group 2 conditions combined with its operator>)
- A group whose filters all resolve to nothing (e.g. every value is blank) is skipped and adds no clause.
Example¶
(status = DRAFT OR status = SENT) AND total > 100:
{
"sourceType": "INVOICES",
"filterGroups": [
{
"operator": "OR",
"filters": [
{ "columnName": "status", "operator": "EQ", "value": "DRAFT" },
{ "columnName": "status", "operator": "EQ", "value": "SENT" }
]
},
{
"operator": "AND",
"filters": [
{ "columnName": "total", "operator": "GT", "value": "100" }
]
}
]
}
The two groups are AND-ed, so the second single-filter group acts as a plain
top-level condition; you could equivalently pass total > 100 in the flat
filters list.
What's being implemented¶
- filter_types.py
— added the
LogicalOperatorenum and theFilterGroupinput. - filters.py
— extracted
build_filter_condition(single-filter → SQLAlchemy condition) out of the old loop, lifted the operation map to a module constant, and reworkedapply_filtersto acceptfiltersandfilter_groups. - search_types.py
— both
get_pagination_windowmethods now accept and forwardfilter_groups. - landing_report_service.py
—
find_landing_pagesandfind_landing_page_kpisaccept and forwardfilter_groups. - landing_queries.py
— both resolvers expose the
filter_groupsargument. - tests/graphql/common/test_filters.py — unit tests for AND-only, OR group, AND group, base+group combination, empty group skip, and unknown-column handling.
Frontend impact¶
findLandingPages/findLandingPageKpisgain an optionalfilterGroupsargument. Existing calls that only sendfilterscontinue to behave exactly as before (everything AND-ed) — no migration required.- To use OR logic, send
filterGroups: [{ operator: OR, filters: [...] }]. EachFilterinside a group has the same shape as today (columnName,operator,value/values). - Groups AND with each other and with any top-level
filters. To build(a OR b) AND (c OR d), send two groups, each withoperator: OR. - Only one level of grouping is supported; there is no nested-group field yet.
Future additions¶
- Recursive group tree — arbitrary nesting (
react-querybuilder-style) if a use case needs((a OR b) AND c) OR d. Deferred because one level covers current reporting needs and keeps the frontend builder simple. - Chat-agent support — extend
GraphQLFilter/ the conversation-agent prompt to emitfilterGroupsso natural-language queries can use OR logic. Fast follow.