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)

 ...