import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; export interface FacetBucket { value: string; count: number; } /** * One `GROUP BY` query per faceted column, each with every OTHER active * filter applied but its OWN predicate omitted. That omission is the point: * with `status=SUBMITTED` selected, the status facet still reports * `APPROVED: 8` so the user can switch, while the freightType facet reflects * only the SUBMITTED-scoped set. Omit `search` from nothing — it's a scope, * not a pill, and stays applied in every facet. * * Capped at 50 buckets per column — FK-id facets (warehouseId, yardId) can * have real cardinality; beyond 50 the frontend should fall back to a * typeahead instead of a checkbox list. Never facet a column whose popover * would need its own search box (references, plate numbers, free text). * * @param base builds a FRESH query builder (soft-delete guard only, * no filters) — called once per facet column. * @param applyFilters applies every filter to `qb`, using `omit` to skip * one column's own predicate. * @param columns facet key -> "alias.column" SQL reference. */ export async function computeFacets( base: () => SelectQueryBuilder, applyFilters: (qb: SelectQueryBuilder, omit?: string) => void, columns: Record, ): Promise> { const entries = await Promise.all( Object.entries(columns).map(async ([key, column]) => { const qb = base(); applyFilters(qb, key); const rows = await qb .select(column, 'value') .addSelect('COUNT(*)::int', 'count') .andWhere(`${column} IS NOT NULL`) .groupBy(column) .orderBy('count', 'DESC') .limit(50) .getRawMany<{ value: string; count: number }>(); return [key, rows.map((r) => ({ value: String(r.value), count: Number(r.count) }))] as const; }), ); return Object.fromEntries(entries); }