Group By Count
The pickers' query — distinct values of one field with counts, over Lucid GROUP BY or a custom adapter.
A filter dropdown should list what the data actually contains, not ask the operator to type blind:
the distinct values of one field with counts, over the rows the active filters select. That is
groupByCount — the terminal aggregation behind value pickers — over Lucid models and custom
backends alike.
The client
import { filterQuery } from '@adonis-agora/filter-client'
const qs = filterQuery()
.where('tenant', 'acme')
.groupByCount('tag', { limit: 20 })
.toQueryString()
// filter[tenant]=acme&groupByCount[field]=tag&groupByCount[limit]=20The scope rides the usual where/search; the axis and its bounds ride groupByCount[field],
groupByCount[limit], groupByCount[offset], groupByCount[search]. limit bounds the groups
(highest count first — tag cardinality grows with the data, so the unbounded answer is a listing);
offset pages that bound; search narrows to values containing the text, server-side, so a rare
value outside the first page stays reachable by typing.
Over Lucid
import { groupByCountFromRequest } from '@adonis-agora/filter'
export default class UsersController {
async facets(ctx: HttpContext) {
return groupByCountFromRequest(User.query(), UserFilter, ctx)
}
}The server scope (tenant, default filters) plus the request filters and search apply on the
builder, then a GROUP BY replaces entity-row output — most groups first, so the answer is
pageable. The grouping field clears the same allow-list sort answers to; anything else is a
400, never SQL. Listing sort/pagination do not apply: this mode replaces entity rows.
Over a custom backend
Pass a draft and an adapter instead of a builder — the custom filter narrows the draft over the same scope, then the adapter counts it:
const rows = await groupByCountFromRequest(draft, RunFilter, ctx, {
adapter: {
async groupByCount(field, draft, opts) {
return engine.runValueFacets(axisFor(field), draft.facetQuery(), opts)
},
},
})Ordering is fixed (count desc, value asc) on every path, because a pageable answer over an unordered listing repeats and skips rows. The Lucid execution pins it in SQL; a custom adapter is expected to return the same order.