Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fltr

Fltr lets you define and compose reusable filters for Ecto queries, with optional parsing for external input.

Usage

Define the supported filters and their argument counts, then implement one to_expr clause for each filter:

defmodule TeamFilter do
  use Fltr, filters: [active: 0, id: 1, name: 1]

  def to_expr(:active), do: dynamic([team], team.active)
  def to_expr(:id, id), do: dynamic([team], team.id == ^id)
  def to_expr(:name, name), do: dynamic([team], team.name == ^name)
end

The argument count does not include the filter name. Here, :active maps to to_expr/1, while :id and :name map to to_expr/2. Fltr verifies the required callback arities when the module compiles.

A single filter

Pass a canonical filter directly to to_expr/1, then interpolate the resulting dynamic expression into an Ecto query:

import Ecto.Query

filter = TeamFilter.to_expr({:active})

Team
|> where(^filter)
|> Repo.all()

Boolean groups

Use :all to require every child filter and :any to require at least one. Groups can contain other groups:

filter =
  {:all,
   [
     {:active},
     {:any, [{:id, 7}, {:name, "Rovers"}]}
   ]}

filter = TeamFilter.to_expr(filter)

Team
|> where(^filter)
|> Repo.all()

External filters

When a filter comes from outside the application, pass it through the parse/1 function generated by use Fltr before compiling it:

import Ecto.Query

def list_teams(filter) do
  with {:ok, filter} <- TeamFilter.parse(filter) do
    filter = TeamFilter.to_expr(filter)

    Team
    |> where(^filter)
    |> Repo.all()
  end
end

External filters use strings and lists:

list_teams([
  "all",
  [
    ["active"],
    ["any", [["id", 7], ["name", "Rovers"]]]
  ]
])

Arguments pass through unchanged by default. Define parse/2 in the filter module when an argument needs validation or normalization:

def parse(:id, id) when is_binary(id) do
  case Integer.parse(id) do
    {id, ""} -> {:ok, id}
    _other -> :error
  end
end

def parse(:id, _id), do: :error

Lists and string-named tuples are treated as external input and invoke parse/2. Atom-named tuples are trusted canonical expressions: Fltr checks their argument count but does not parse their values again. Always call parse/1 before converting external input into an atom-named tuple.

parse/1 returns an error describing invalid input:

{:error, {:invalid_filter, input}}
{:error, {:unknown_filter, name}}
{:error, {:invalid_arguments, name, arguments}}

About

Composable filters for Ecto queries.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages