Skip to content

Refactor filtering, ordering and pagination#1761

Open
Flo0807 wants to merge 30 commits intodevelopfrom
feature/refactor-filters
Open

Refactor filtering, ordering and pagination#1761
Flo0807 wants to merge 30 commits intodevelopfrom
feature/refactor-filters

Conversation

@Flo0807
Copy link
Collaborator

@Flo0807 Flo0807 commented Jan 16, 2026

closes #311, closes #331, closes #348, closes #349

Filters

Core

  • Created Backpex.FilterValidation for changeset-based validation
    • build_changeset/3: builds schemaless changeset from URL params
    • valid_values/1: extracts only validated filter values
    • build_types/2: builds type map from filter configurations

New Filter Callbacks

  • Added type/1: specifies Ecto type for casting URL params
  • Added changeset/3: allows custom validation logic per filter
  • Added validate/2: public API for testing validation

Existing Filter Updates

  • Boolean Filter: validates selected keys exist in options/1
  • Select Filter: validates selected value exists in options/1
  • MultiSelect Filter: validates all selected values exist in options/1
  • Range Filter: validates date/number formats and that start <= end

LiveResource

  • Updated index.ex to use FilterValidation.build_changeset/3
  • Added @filter_form assign (Phoenix form from changeset)
  • Added @filter_values assign (only valid, casted values)
  • Invalid filters are not applied to queries: users see unfiltered results with inline errors

Ecto Adapter

  • Changed apply_filters/4 to apply_filters/3 (removed empty_filter_key parameter)
  • Filters now receive validated, casted values

Ordering & Pagination

  • Created lib/backpex/query_options_validation.ex for validating URL parameters
    • Uses Ecto changesets for integer validation (page, per_page)
    • Safe atom conversion for order_by/order_direction
    • Invalid values fall back to defaults instead of crashing
  • Invalid values silently fall back to defaults

@Flo0807 Flo0807 self-assigned this Jan 16, 2026
@Flo0807 Flo0807 marked this pull request as draft January 16, 2026 14:25
@Flo0807 Flo0807 added feature New feature breaking-change A breaking change labels Jan 16, 2026
@Flo0807 Flo0807 changed the title Refactor filters Refactor filtering, ordering and pagination Jan 16, 2026
|> validate_number(:page, greater_than: 0)
|> validate_inclusion(:per_page, per_page_options)

extract_valid_values(changeset, defaults)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe do not apply filtering at all if we have invalid values (?)

@Flo0807 Flo0807 marked this pull request as ready for review February 11, 2026 13:56
@Flo0807 Flo0807 requested a review from Copilot February 11, 2026 13:57
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +411 to 443
# 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)

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@Flo0807 Flo0807 requested a review from pehbehbeh February 11, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change A breaking change feature New feature

Projects

None yet

3 participants