Refactor filtering, ordering and pagination#1761
Conversation
| |> validate_number(:page, greater_than: 0) | ||
| |> validate_inclusion(:per_page, per_page_options) | ||
|
|
||
| extract_valid_values(changeset, defaults) |
There was a problem hiding this comment.
Maybe do not apply filtering at all if we have invalid values (?)
There was a problem hiding this comment.
Pull request overview
This PR refactors Backpex’s index query parameter handling to validate filters, ordering, and pagination via Ecto changesets, preventing crashes from malformed URL params and enabling inline filter error display in the UI.
Changes:
- Introduces changeset-based filter validation (
Backpex.FilterValidation) and adds new filter callbacks (type/1,changeset/3,validate/2). - Adds URL param validation for pagination/ordering (
Backpex.PaginationValidation) with safe fallbacks. - Updates LiveResource index pipeline, Ecto adapter filter application, UI rendering, docs, and expands unit/integration test coverage.
Reviewed changes
Copilot reviewed 49 out of 49 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
lib/backpex/filter_validation.ex |
New module to build a schemaless changeset from filter URL params and extract validated values. |
lib/backpex/filters/filter.ex |
Extends filter behaviour with type/1, changeset/3, validate/2; updates macro defaults. |
lib/backpex/filters/select.ex |
Adds default type/1/changeset/3 and inline error display for select filters. |
lib/backpex/filters/multi_select.ex |
Adds default type/1/changeset/3 and inline error display + validation for multi-select. |
lib/backpex/filters/boolean.ex |
Adds default type/1/changeset/3 and inline error display + validation for boolean filter. |
lib/backpex/filters/range.ex |
Adds range validation via changeset/3 and displays inline errors in the UI. |
lib/backpex/pagination_validation.ex |
New module to validate page/per_page/order_by/order_direction safely and clamp page after count. |
lib/backpex/adapters/ecto.ex |
Refactors apply_filters to accept validated filter_values + configs and skip unknowns. |
lib/backpex/live_resource/index.ex |
Uses new validation modules; stores raw filter params for form display and validated values for queries/badges. |
lib/backpex/live_resource.ex |
Updates criteria building to use filter_values/filter_configs; simplifies filter option retrieval. |
lib/backpex/html/resource.ex |
Renders badges from validated values and passes per-field error messages to filter form components. |
lib/backpex_web.ex |
Imports form error component for filters to display inline validation messages. |
mix.exs |
Adds new/updated docs pages to the generated documentation list. |
test/backpex/filter_validation_test.exs |
New unit tests for filter changeset building and validated-value extraction. |
test/backpex/pagination_validation_test.exs |
New unit tests for pagination/ordering validation and atom-safety. |
test/adapters/ecto_test.exs |
Updates adapter filter tests for new validated filter pipeline. |
test/filters/select_test.exs |
Adds tests for select filter validation via changeset/3. |
test/filters/multi_select_test.exs |
Adds tests for multi-select validation via changeset/3. |
test/filters/boolean_test.exs |
Adds tests for boolean validation via changeset/3. |
test/filters/range_test.exs |
Adds tests for range validation for number/date/datetime types. |
guides/live_resource/pagination.md |
New pagination guide describing params + validation/clamping. |
guides/live_resource/ordering.md |
Documents ordering URL params and validation behavior. |
guides/filter/filter-validation.md |
New guide describing filter validation system and error display. |
guides/upgrading/v0.18.md |
Upgrade notes for the validation refactor and adapter/criteria changes. |
guides/filter/what-is-a-filter.md |
Updates filter overview to include validation/error behavior. |
guides/filter/how-to-add-a-filter.md |
Updates wording/examples and references validation guide. |
guides/filter/filter-presets.md |
Notes presets also go through validation and expected formats. |
guides/filter/custom-filter.md |
Updates custom filter docs to include type/1 and changeset-based validation. |
demo/test/demo_web/live/**/pagination_live_test.exs |
Adds LiveView coverage for pagination defaults, invalid params, and clamping across demo resources. |
demo/test/demo_web/live/**/ordering_live_test.exs |
Adds LiveView coverage for ordering defaults, invalid params, and non-orderable fallback. |
demo/test/demo_web/live/post/filter_live_test.exs |
Adds LiveView coverage for filtering behavior + invalid filter behavior. |
demo/test/demo_web/live/product/filter_live_test.exs |
Splits product filter tests into a dedicated file. |
demo/test/demo_web/live/product/index_live_test.exs |
Removes older range filter tests now covered elsewhere. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Build filter changeset from URL params and extract valid values | ||
| raw_filter_params = Map.get(params, "filters", %{}) | ||
| filter_changeset = FilterValidation.build_changeset(raw_filter_params, filters, socket.assigns) | ||
| filter_values = FilterValidation.valid_values(filter_changeset) | ||
| filter_form = to_form(filter_changeset, as: :filters) | ||
|
|
||
| # Validate pagination and sorting params (page clamping happens after we know item_count) | ||
| query_options = | ||
| PaginationValidation.build(params, | ||
| per_page_default: per_page_default, | ||
| per_page_options: per_page_options, | ||
| orderable_fields: orderable_fields, | ||
| init_order: init_order | ||
| ) | ||
|
|
||
| count_criteria = [ | ||
| search: LiveResource.search_options(params, fields, schema), | ||
| filters: LiveResource.filter_options(valid_filter_params, filters) | ||
| filter_values: filter_values, | ||
| filter_configs: filters | ||
| ] | ||
|
|
||
| {:ok, item_count} = Resource.count(count_criteria, fields, socket.assigns, live_resource) | ||
| total_pages = LiveResource.calculate_total_pages(item_count, query_options.per_page) | ||
|
|
||
| per_page = | ||
| params | ||
| |> LiveResource.parse_integer("per_page", per_page_default) | ||
| |> LiveResource.value_in_permitted_or_default(per_page_options, per_page_default) | ||
|
|
||
| total_pages = LiveResource.calculate_total_pages(item_count, per_page) | ||
|
|
||
| page = params |> LiveResource.parse_integer("page", 1) |> LiveResource.validate_page(total_pages) | ||
| page_options = %{page: page, per_page: per_page} | ||
|
|
||
| order_options = LiveResource.order_options_by_params(params, fields, init_order, socket.assigns) | ||
| # Clamp page to valid range now that we know total_pages | ||
| page = PaginationValidation.clamp_page(query_options.page, total_pages) | ||
| query_options = %{query_options | page: page} | ||
|
|
||
| query_options = | ||
| page_options | ||
| |> Map.merge(order_options) | ||
| query_options | ||
| |> maybe_put_search(params) | ||
| |> Map.put(:filters, Map.get(valid_filter_params, "filters", %{})) | ||
| |> Map.put(:filters, raw_filter_params) | ||
|
|
There was a problem hiding this comment.
raw_filter_params can be a non-map when the URL contains an unexpected filters value (e.g. ?filters=abc). In that case query_options.filters becomes a string, and later rendering/change-filter will crash (e.g. Map.get/2 on a non-map). Consider normalizing here (or earlier) so raw_filter_params is always a map (fallback to %{} when not a map) before storing it in query_options.
closes #311, closes #331, closes #348, closes #349
Filters
Core
Backpex.FilterValidationfor changeset-based validationbuild_changeset/3: builds schemaless changeset from URL paramsvalid_values/1: extracts only validated filter valuesbuild_types/2: builds type map from filter configurationsNew Filter Callbacks
type/1: specifies Ecto type for casting URL paramschangeset/3: allows custom validation logic per filtervalidate/2: public API for testing validationExisting Filter Updates
options/1LiveResource
index.exto useFilterValidation.build_changeset/3@filter_formassign (Phoenix form from changeset)@filter_valuesassign (only valid, casted values)Ecto Adapter
apply_filters/4toapply_filters/3(removedempty_filter_keyparameter)Ordering & Pagination
lib/backpex/query_options_validation.exfor validating URL parameters