react query builder basic filters
by alfredopacino
CSS
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
text-align: center;
}
TypeScript
// at least 1 key has to be present
type RangeTerm = Partial<{ lt: number; lte: number; gt: number; gte: number; eq: number }> &
({ lt: number } | { lte: number } | { gt: number } | { gte: number } | { eq: number });
type QueryTermValue = string | string[] | RangeTerm | null;
type QueryTerm = { field: string; value: QueryTermValue; queryConfig?: { extra?: QueryTermValue } };
type NestedFilter = {
fullPath: string;
value: QueryTermValue;
};
type NestedGroups = Record<string, NestedFilter[]>;
type TermQuery = { term: Record<string, QueryTermValue> };
type TermsQuery = { terms: Record<string, string[]> };
type RangeQuery = { range: Record<string, RangeTerm> };
type NestedQuery = { nested: { path: string; query: unknown } };
type QueryClause = TermQuery | TermsQuery | RangeQuery | NestedQuery;
const queryBuilder = (filters: QueryTerm[]) => {
const must: QueryClause[] = [];
const nestedGroups: NestedGroups = {};
const isRangeTerm = (v: QueryTermValue): v is RangeTerm =>
typeof v === 'object' &&
v !== null &&
!Array.isArray(v) &&
['lt', 'lte', 'gt', 'gte', 'eq'].some((key) => key in v);
filters.forEach(({ field, value }) => {
const fieldParts = field.split('.');
if (fieldParts.length === 1) {
// Top-level field (not nested)
if (Array.isArray(value)) {
must.push({ terms: { [field]: value } });
} else {
if (isRangeTerm(value)) {
must.push({ range: { [field]: value } });
} else {
must.push({ term: { [field]: value } });
}
}
} else {
// Group by nested path
const nestedPath = fieldParts.slice(0, -1).join('.');
if (!nestedGroups[nestedPath]) {
nestedGroups[nestedPath] = [];
}
nestedGroups[nestedPath].push({
fullPath: field,
value,
});
}
});
console.log("nestedGroups", nestedGroups)
...