filterable alternatives and similar packages
Based on the "Framework Components" category.
Alternatively, view filterable alternatives based on common mentions on social networks and blogs.
-
ex_admin
ExAdmin is an auto administration package for Elixir and the Phoenix Framework -
phoenix_html
Phoenix.HTML functions for working with HTML strings and templates -
phoenix_ecto
Phoenix and Ecto integration with support for concurrent acceptance testing -
absinthe_plug
Plug support for Absinthe, the GraphQL toolkit for Elixir -
react_phoenix
Make rendering React.js components in Phoenix easy -
phoenix_live_reload
Provides live-reload functionality for Phoenix -
params
Easy parameters validation/casting with Ecto.Schema, akin to Rails' strong parameters. -
phoenix_pubsub_redis
The Redis PubSub adapter for the Phoenix framework -
dayron
A repository `similar` to Ecto.Repo that maps to an underlying http client, sending requests to an external rest api instead of a database -
rummage_phoenix
Full Phoenix Support for Rummage. It can be used for searching, sorting and paginating collections in phoenix. -
phoenix_token_auth
Token authentication solution for Phoenix. Useful for APIs for e.g. single page apps. -
sentinel
DEPRECATED - Phoenix Authentication library that wraps Guardian for extra functionality -
plug_rails_cookie_session_store
Rails compatible Plug session store -
phx_component_helpers
Extensible Phoenix liveview components, without boilerplate -
multiverse
Elixir package that allows to add compatibility layers via API gateways. -
access pass
provides a full user authentication experience for an API. Includes login,logout,register,forgot password, forgot username, confirmation email and all that other good stuff. Includes plug for checking for authenticated users and macro for generating the required routes. -
better_params
Cleaner request parameters in Elixir web applications ๐ -
scrivener_headers
Scrivener pagination with headers and web linking -
phoenix_pubsub_rabbitmq
RabbitMQ adapter for Phoenix's PubSub layer -
plug_checkup
PlugCheckup provides a Plug for adding simple health checks to your app -
plug_rest
REST behaviour and Plug router for hypermedia web applications in Elixir -
trailing_format_plug
An elixir plug to support legacy APIs that use a rails-like trailing format: http://api.dev/resources.json -
Votex
Implements vote / like / follow functionality for Ecto models in Elixir. Inspired from Acts as Votable gem in Ruby on Rails -
phoenix_html_simplified_helpers
Some helpers for phoenix html( truncate, time_ago_in_words, number_with_delimiter, url_for, current_page? ) -
plug_canonical_host
PlugCanonicalHost ensures that all requests are served by a single canonical host.
Elixir and Phoenix Application Security Platform
Do you think we are missing an alternative of filterable or a related project?
README
Filterable
Filterable allows to map incoming query parameters to filter functions. The goal is to provide minimal and easy to use DSL for building composable queries using incoming parameters. Filterable doesn't depend on external libraries or frameworks and can be used in Phoenix or pure Elixir projects. Inspired by has_scope.
Installation
Add filterable
to your mix.exs.
{:filterable, "~> 0.7.4"}
Usage
Phoenix controller
Put use Filterable.Phoenix.Controller
inside Phoenix controller or add it into web.ex
.
It will extend controller module with filterable
macro which allows to define filters.
Then use apply_filters
function inside controller action to filter using defined filters:
defmodule MyApp.PostController do
use MyApp.Web, :controller
use Filterable.Phoenix.Controller
filterable do
filter author(query, value, _conn) do
query |> where(author_name: ^value)
end
@options param: :q
filter search(query, value, _conn) do
query |> where([u], ilike(u.title, ^"%#{value}%"))
end
@options cast: :integer
filter year(query, value, _conn) do
query |> where(year: ^value)
end
end
# /posts?q=castle&author=Kafka&year=1926
def index(conn, params) do
with {:ok, query, filter_values} <- apply_filters(Post, conn),
posts <- Repo.all(query),
do: render(conn, "index.json", posts: posts, meta: filter_values)
end
end
If you prefer to handle errors with exceptions then use apply_filters!
:
def index(conn, params) do
{query, filter_values} = apply_filters!(Post, conn)
render(conn, "index.json", posts: Repo.all(posts), meta: filter_values)
end
Phoenix model
Put use Filterable.Phoenix.Model
inside Ecto model module and define filters using filterable
macro:
defmodule MyApp.Post do
use MyApp.Web, :model
use Filterable.Phoenix.Model
filterable do
filter author(query, value, _conn) do
query |> where(author_name: ^value)
end
end
schema "posts" do
...
end
end
Then call apply_filters
function from model module:
# /posts?author=Tom
def index(conn, params, conn) do
with {:ok, query, filter_values} <- Post.apply_filters(conn),
posts <- Repo.all(query),
do: render(conn, "index.json", posts: posts, meta: filter_values)
end
Separate module
Filters could be defined in separate module, just use Filterable.DSL
inside module to make it filterable:
defmodule PostFilters do
use Filterable.DSL
use Filterable.Ecto.Helpers
field :author
field :title
paginateable per_page: 10
@options param: :q
filter search(query, value, _conn) do
query |> where([u], ilike(u.title, ^"%#{value}%"))
end
@options cast: :integer
filter year(query, value, _conn) do
query |> where(author_name: ^value)
end
end
defmodule MyApp.PostController do
use MyApp.Web, :controller
use Filterable.Phoenix.Controller
filterable PostFilters
# /posts?q=castle&author=Kafka&year=1926
def index(conn, params) do
with {:ok, query, filter_values} <- apply_filters(Post, conn),
posts <- Repo.all(query),
do: render(conn, "index.json", posts: posts, meta: filter_values)
end
end
Defining filters
Each defined filter can be tuned with @options
module attribute.
Just set @options
attribute before filter definition. Available options are:
:param
- allows to set query parameter name, by default same as filter name. Accepts Atom
, List
, and Keyword
values:
# /posts?q=castle
# => #Ecto.Query<from p in Post, where: ilike(u.title, ^"%castle%")>
@options param: :q
filter search(query, value, _conn) do
query |> where([u], ilike(u.title, ^"%#{value}%"))
end
# /posts?sort=name&order=desc
# => #Ecto.Query<from p in Post, order_by: [desc: p.name]>
@options param: [:sort, :order], cast: :integer
filter search(query, %{sort: field, order: order}, _conn) do
query |> order_by([{^order, ^field}])
end
# /posts?sort[field]=name&sort[order]=desc
# => #Ecto.Query<from p in Post, order_by: [desc: p.name]>
@options param: [sort: [:field, :order]], cast: :integer
filter search(query, %{field: field, order: order}, _conn) do
query |> order_by([{^order, ^field}])
end
:default
- allows to set default filter value:
# /posts
# => #Ecto.Query<from p in Post, limit: 20>
@options default: 20, cast: :integer
filter limit(query, value, _conn) do
query |> limit(^value)
end
# /posts
# => #Ecto.Query<from p in Post, order_by: [desc: p.inserted_at]>
@options param: [:sort, :order], default: [sort: :inserted_at, order: :desc], cast: :atom_unchecked
filter search(query, %{sort: field, order: order}, _conn) do
query |> order_by([{^order, ^field}])
end
:allow_blank
- when true
then it allows to trigger filter with blank value (""
, []
, {}
, %{}
). false
by default, so all blank values will be converted to nil
:
# /posts?title=""
# => #Ecto.Query<from p in Post>
@options allow_blank: false
filter title(query, value, _conn) do
query |> where(title: ^value)
end
# /posts?title=""
# => #Ecto.Query<from p in Post, where: p.title == "">
@options allow_blank: true
filter title(query, value, _conn) do
query |> where(title: ^value)
end
:allow_nil
- when true
then it allows to trigger filter with nil
value, false
by default:
# /posts?title=""
# => #Ecto.Query<from p in Post, where: is_nil(p.title)>
# /posts?title=Casle
# => #Ecto.Query<from p in Post, where: p.title == "Casle">
@options allow_nil: true
filter title(query, nil, _conn) do
query |> where([q], is_nil(q.title))
end
filter title(query, value, _conn) do
query |> where(title: ^value)
end
:trim
- allows to remove leading and trailing whitespaces from string values, true
by default:
# /posts?title=" Casle "
# => #Ecto.Query<from p in Post, where: p.title == "Casle">
filter title(query, value, _conn) do
query |> where(title: ^value)
end
# /posts?title=" Casle "
# => #Ecto.Query<from p in Post, where: p.title == " Casle ">
@options trim: false
filter title(query, value, _conn) do
query |> where(title: ^value)
end
:cast
- allows to convert value to specific type. Available types are: :integer
, :float
, :string
, {:atom, [...]}
, :boolean
, :date
, :datetime
, :atom_unchecked
. Casting to atoms is a special case, as atoms are never garbage collected. It is therefore important to give a list of valid atoms. Casting will only work if the given value is in the list of atoms.
Also can accept pointer to function:
# /posts?limit=20
# => #Ecto.Query<from p in Post, limit: 20>
@options cast: :integer
filter limit(query, value, _conn) do
query |> limit(^value)
end
# /posts?title=Casle
# => #Ecto.Query<from p in Post, where: p.title == "casle">
@options cast: &String.downcase/1
filter title(query, value, _conn) do
query |> where(title: ^value)
end
:cast_errors
- accepts true
(default) or false
. If true
then it returns error if value can't be caster to specific type. If false
- it skips filter if filter value can't be casted:
# /posts?inserted_at=Casle
# => {:error, "Unable to cast \"Casle\" to datetime"}
@options cast: :datetime
filter inserted_at(query, value, _conn) do
query |> where(inserted_at: ^value)
end
# /posts?inserted_at=Casle
# => #Ecto.Query<from p in Post>
@options cast: :datetime, cast_errors: false
filter inserted_at(query, value, _conn) do
query |> where(inserted_at: ^value)
end
:share
- allows to set shared value. When false
then filter function will be triggered without shared value argument:
@options share: false
filter title(query, value) do
query |> where(title: ^value)
end
All these options can be specified in apply_filters
function or filterable
macro. Then they will take affect on all defined filters:
filterable share: false, cast_errors: false do
field :title
end
# or
filterable PostFilters, share: false, cast_errors: false
# or
{:ok, query, filter_values} = apply_filters(conn, share: false, cast_errors: false)
Ecto helpers
Filterable.Ecto.Helpers
module provides macros which allows to define some popular filters:
field/2
- expands to simple Ecto.Query.where
filter:
filterable do
field :title
field :stars, cast: :integer
end
Same filters could be built with filter
macro:
filterable do
filter title(query, value, _conn) do
query |> where(title: ^value)
end
@options cast: :integer
filter stars(query, value, _conn) do
query |> where(stars: ^value)
end
end
paginateable/1
- provides pagination logic, Default amount of records per page could be tuned with per_page
option. By default it's set to 20:
filterable do
# /posts?page=3
# => #Ecto.Query<from p in Post, limit: 10, offset: 20>
paginateable per_page: 10
end
limitable/1
- provides limit/offset logic:
filterable do
# /posts?limit=3offset=10
# => #Ecto.Query<from p in Post, limit: 3, offset: 10>
limitable limit: 10
end
orderable/1
- provides sorting logic, accepts list of atoms:
filterable do
# /posts?sort=inserted_at&order=asc
# => #Ecto.Query<from p in Post, order_by: [asc: p.inserted_at]>
orderable [:title, :inserted_at]
end
Common usage
Filterable
also can be used in non Ecto/Phoenix projects.
Put use Filterable.DSL
inside module to start defining filters:
defmodule RepoFilters do
use Filterable.DSL
filter name(list, value) do
list |> Enum.filter(& &1.name == value)
end
@options cast: :integer
filter stars(list, value) do
list |> Enum.filter(& &1.stars >= value)
end
end
Then filter collection using apply_filters
function:
repos = [%{name: "phoenix", stars: 8565}, %{name: "ecto", start: 2349}]
{:ok, result, filter_values} = RepoFilters.apply_filters(repos, %{name: "phoenix", stars: "8000"})
# or
{:ok, result, filter_values} = Filterable.apply_filters(repos, %{name: "phoenix", stars: "8000"}, RepoFilters)
Code formatter
filter
macro and phoenix helpers like orderable
, paginateable
are the part fo DSL so there is no need to wrap them in parentheses.
Just add the following line into formatter configs:
[
# ...
import_deps: [:filterable]
]
Similar packages
Contribution
Feel free to send your PR with proposals, improvements or corrections ๐