{"version":3,"file":"filter-query.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-query.ts"],"sourcesContent":["// Server-side half of the filter model: the same `Filter[]` the bar produces,\n// encoded the way a paginated backend expects to read it. Kept apart from\n// `filtersToSearchParams` (filter-model.ts) because the two answer different\n// questions — that one round-trips the UI state through the URL, this one talks\n// to an API and its shape is the API's, not ours.\n\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\nimport { orderRange } from \"./filter-apply\";\n\n/**\n * Operator suffix per filter operator, following the `<column>__<op>` convention\n * of `tempest-fastapi-sdk`.\n *\n * Ported from `build_filter_condition` in\n * `tempest_fastapi_sdk/db/expressions.py`, which is what `BaseRepository`\n * dict filters and `Q` both resolve through. An empty suffix means the bare\n * column name, which that function resolves to plain equality.\n *\n * `contains` maps to `icontains` (case-insensitive, and the backend escapes the\n * `LIKE` wildcards) and both emptiness operators map to `isnull`, whose value\n * carries the direction.\n */\nconst DEFAULT_OPERATOR_SUFFIX: Record<FilterOperator, string> = {\n    eq: \"\",\n    ne: \"__ne\",\n    contains: \"__icontains\",\n    gt: \"__gt\",\n    gte: \"__gte\",\n    lt: \"__lt\",\n    lte: \"__lte\",\n    between: \"__between\",\n    in: \"__in\",\n    empty: \"__isnull\",\n    notEmpty: \"__isnull\",\n};\n\n/**\n * Columns the backend treats as a substring search when they carry no operator\n * suffix.\n *\n * Ported from `build_filter_condition`, which special-cases a string `name` into\n * `ILIKE %value%`. Sending a bare `name=` for an `eq` filter would therefore ask\n * for \"contains\" while the chip says \"is\", so `eq` on such a column is emitted as\n * `name__iexact` instead.\n */\nconst DEFAULT_SUBSTRING_COLUMNS: ReadonlySet<string> = new Set([\"name\"]);\n\n/** Overrides for the backend dialect {@link filtersToQueryParams} encodes into. */\nexport interface FiltersToQueryParamsOptions {\n    /**\n     * Columns whose `eq` is emitted as `<column>__iexact` rather than as the bare\n     * column name. Default `[\"name\"]`, which is what `build_filter_condition`\n     * special-cases.\n     *\n     * Pass the columns **your** backend treats that way — `nome`, `titulo`,\n     * `razao_social` — or `[]` when it treats none of them specially and a bare\n     * `eq` should stay bare.\n     */\n    substringColumns?: readonly string[];\n    /**\n     * Operator suffixes, merged over the `tempest-fastapi-sdk` dialect, so an\n     * override names only the operators that differ.\n     *\n     * @example\n     * // A DRF-style backend\n     * filtersToQueryParams(filters, {\n     *     substringColumns: [],\n     *     operatorSuffix: { contains: \"__icontains\", ne: \"__exclude\" },\n     * });\n     */\n    operatorSuffix?: Partial<Record<FilterOperator, string>>;\n}\n\n/**\n * Encode filters as query params for a paginated backend.\n *\n * The counterpart of {@link applyFilters}: same filter set, evaluated by the\n * database instead of by the browser, which is the only option once the list is\n * paginated on the server and the page in memory is not the whole result.\n *\n * The encoding is the `<column>__<op>` convention `tempest-fastapi-sdk` already\n * reads (`BaseRepository._apply_filters` → `build_filter_condition`):\n *\n * | Operator | Param |\n * | --- | --- |\n * | `eq` | `field` (or `field__iexact` for the `name` column) |\n * | `ne` | `field__ne` |\n * | `contains` | `field__icontains` |\n * | `gt` `gte` `lt` `lte` | `field__gt` … `field__lte` |\n * | `between` | `field__between` twice, low value first |\n * | `in` | `field__in` once per value |\n * | `empty` / `notEmpty` | `field__isnull=true` / `=false` |\n *\n * Returns `URLSearchParams` rather than a plain object because `between` carries\n * a pair and `in` carries a list: an object would keep only the last value of\n * each, silently narrowing the filter. Repeated params are also how FastAPI\n * receives a `list[str]` declared with `Query`.\n *\n * Two things the backend has to hold up its end of, or the filter fails quietly:\n *\n * - **Every key must be declared.** `BasePaginationFilterSchema.get_conditions()`\n *   only forwards fields the subclass declares, so a `status__ne` the schema\n *   never mentions is dropped by FastAPI before the repository sees it — no\n *   error, no filtering.\n * - **`isnull` matches `NULL` only.** A column that stores `\"\"` for \"no value\"\n *   will not answer an `empty` filter, while {@link applyFilters} treats blank\n *   text as empty.\n *\n * The defaults are that dialect, not a universal truth. A backend that names its\n * substring column something other than `name`, or that spells its operators\n * differently, passes `options` — the alternative was an encoder whose special\n * cases were a wall for anyone not on the Tempest stack.\n *\n * @example\n * const params = filtersToQueryParams(filters);\n * params.set(\"page\", String(page));\n * const data = await api.get(`/orders?${params}`);\n *\n * @example\n * // A backend whose searchable column is `razao_social` and that spells `ne`\n * // the Django way.\n * const params = filtersToQueryParams(filters, {\n *     substringColumns: [\"razao_social\"],\n *     operatorSuffix: { ne: \"__exclude\" },\n * });\n *\n * @param filters - Applied filters; incomplete ones are ignored.\n * @param options - Dialect overrides. Defaults to the `tempest-fastapi-sdk` one.\n * @returns Params ready to append to a request URL.\n */\nexport function filtersToQueryParams(\n    filters: readonly Filter[],\n    options: FiltersToQueryParamsOptions = {},\n): URLSearchParams {\n    const suffixes = options.operatorSuffix\n        ? { ...DEFAULT_OPERATOR_SUFFIX, ...options.operatorSuffix }\n        : DEFAULT_OPERATOR_SUFFIX;\n    const substringColumns = options.substringColumns\n        ? new Set(options.substringColumns)\n        : DEFAULT_SUBSTRING_COLUMNS;\n\n    const params = new URLSearchParams();\n\n    for (const filter of filters) {\n        if (!isComplete(filter)) continue;\n\n        const suffix = suffixes[filter.operator];\n        const key =\n            filter.operator === \"eq\" && substringColumns.has(filter.field)\n                ? `${filter.field}__iexact`\n                : `${filter.field}${suffix}`;\n\n        if (filter.operator === \"empty\" || filter.operator === \"notEmpty\") {\n            params.append(key, filter.operator === \"empty\" ? \"true\" : \"false\");\n            continue;\n        }\n\n        const values = Array.isArray(filter.value)\n            ? [...filter.value]\n            : [String(filter.value ?? \"\")];\n\n        if (filter.operator === \"between\") {\n            for (const value of orderRange(values)) params.append(key, value);\n            continue;\n        }\n\n        for (const value of values) params.append(key, value);\n    }\n\n    return params;\n}\n"],"mappings":"sEAuBA,IAAM,EAA0D,CAC5D,GAAI,GACJ,GAAI,OACJ,SAAU,cACV,GAAI,OACJ,IAAK,QACL,GAAI,OACJ,IAAK,QACL,QAAS,YACT,GAAI,OACJ,MAAO,WACP,SAAU,UACd,EAWM,EAAiD,IAAI,IAAI,CAAC,MAAM,CAAC,EAqFvE,SAAgB,EACZ,EACA,EAAuC,CAAC,EACzB,CACf,IAAM,EAAW,EAAQ,eACnB,CAAE,GAAG,EAAyB,GAAG,EAAQ,cAAe,EACxD,EACA,EAAmB,EAAQ,iBAC3B,IAAI,IAAI,EAAQ,gBAAgB,EAChC,EAEA,EAAS,IAAI,gBAEnB,IAAK,IAAM,KAAU,EAAS,CAC1B,GAAI,CAAC,EAAA,WAAW,CAAM,EAAG,SAEzB,IAAM,EAAS,EAAS,EAAO,UACzB,EACF,EAAO,WAAa,MAAQ,EAAiB,IAAI,EAAO,KAAK,EACvD,GAAG,EAAO,MAAM,UAChB,GAAG,EAAO,QAAQ,IAE5B,GAAI,EAAO,WAAa,SAAW,EAAO,WAAa,WAAY,CAC/D,EAAO,OAAO,EAAK,EAAO,WAAa,QAAU,OAAS,OAAO,EACjE,QACJ,CAEA,IAAM,EAAS,MAAM,QAAQ,EAAO,KAAK,EACnC,CAAC,GAAG,EAAO,KAAK,EAChB,CAAC,OAAO,EAAO,OAAS,EAAE,CAAC,EAEjC,GAAI,EAAO,WAAa,UAAW,CAC/B,IAAK,IAAM,KAAS,EAAA,WAAW,CAAM,EAAG,EAAO,OAAO,EAAK,CAAK,EAChE,QACJ,CAEA,IAAK,IAAM,KAAS,EAAQ,EAAO,OAAO,EAAK,CAAK,CACxD,CAEA,OAAO,CACX"}