{"id":13507996,"url":"https://github.com/omohokcoj/filterable","last_synced_at":"2025-10-21T18:45:47.778Z","repository":{"id":57501383,"uuid":"68450722","full_name":"omohokcoj/filterable","owner":"omohokcoj","description":"Filtering from incoming params in Elixir/Ecto/Phoenix with easy to use DSL.","archived":false,"fork":false,"pushed_at":"2023-02-05T12:05:34.000Z","size":179,"stargazers_count":106,"open_issues_count":1,"forks_count":6,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-11-01T07:33:27.403Z","etag":null,"topics":["dynamic-queries","ecto","elixir","filter","filterable","pagination","phoenix","query-builder","searching","sorting"],"latest_commit_sha":null,"homepage":"https://hex.pm/packages/filterable","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/omohokcoj.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-09-17T12:26:16.000Z","updated_at":"2024-07-15T10:05:37.000Z","dependencies_parsed_at":"2023-02-18T23:30:39.791Z","dependency_job_id":null,"html_url":"https://github.com/omohokcoj/filterable","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omohokcoj%2Ffilterable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omohokcoj%2Ffilterable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omohokcoj%2Ffilterable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omohokcoj%2Ffilterable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/omohokcoj","download_url":"https://codeload.github.com/omohokcoj/filterable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246301963,"owners_count":20755512,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dynamic-queries","ecto","elixir","filter","filterable","pagination","phoenix","query-builder","searching","sorting"],"created_at":"2024-08-01T02:00:45.167Z","updated_at":"2025-10-21T18:45:47.700Z","avatar_url":"https://github.com/omohokcoj.png","language":"Elixir","readme":"# Filterable\n\n[![Build Status](https://travis-ci.org/omohokcoj/filterable.svg?branch=master)](https://travis-ci.org/omohokcoj/filterable)\n[![Code Climate](https://codeclimate.com/github/omohokcoj/filterable/badges/gpa.svg)](https://codeclimate.com/github/omohokcoj/filterable)\n[![Coverage Status](https://coveralls.io/repos/github/omohokcoj/filterable/badge.svg?branch=master)](https://coveralls.io/github/omohokcoj/filterable?branch=master)\n[![Inline docs](http://inch-ci.org/github/omohokcoj/filterable.svg?branch=master)](http://inch-ci.org/github/omohokcoj/filterable)\n[![Hex.pm](https://img.shields.io/hexpm/v/filterable.svg)](https://hex.pm/packages/filterable)\n\nFilterable allows to map incoming query parameters to filter functions.\nThe goal is to provide minimal and easy to use DSL for building composable queries using incoming parameters.\nFilterable doesn't depend on external libraries or frameworks and can be used in Phoenix or pure Elixir projects.\nInspired by [has_scope](https://github.com/plataformatec/has_scope).\n\n## Installation\n\nAdd `filterable` to your mix.exs.\n\n```elixir\n{:filterable, \"~\u003e 0.7.4\"}\n```\n\n## Usage\n\n### Phoenix controller\n\nPut `use Filterable.Phoenix.Controller` inside Phoenix controller or add it into `web.ex`.\nIt will extend controller module with `filterable` macro which allows to define filters.\nThen use `apply_filters` function inside controller action to filter using defined filters:\n\n```elixir\ndefmodule MyApp.PostController do\n  use MyApp.Web, :controller\n  use Filterable.Phoenix.Controller\n\n  filterable do\n    filter author(query, value, _conn) do\n      query |\u003e where(author_name: ^value)\n    end\n\n    @options param: :q\n    filter search(query, value, _conn) do\n      query |\u003e where([u], ilike(u.title, ^\"%#{value}%\"))\n    end\n\n    @options cast: :integer\n    filter year(query, value, _conn) do\n      query |\u003e where(year: ^value)\n    end\n  end\n\n  # /posts?q=castle\u0026author=Kafka\u0026year=1926\n  def index(conn, params) do\n    with {:ok, query, filter_values} \u003c- apply_filters(Post, conn),\n         posts                       \u003c- Repo.all(query),\n     do: render(conn, \"index.json\", posts: posts, meta: filter_values)\n  end\nend\n```\n\nIf you prefer to handle errors with exceptions then use `apply_filters!`:\n\n```elixir\ndef index(conn, params) do\n  {query, filter_values} = apply_filters!(Post, conn)\n  render(conn, \"index.json\", posts: Repo.all(posts), meta: filter_values)\nend\n```\n\n### Phoenix model\n\nPut `use Filterable.Phoenix.Model` inside Ecto model module and define filters using `filterable` macro:\n\n```elixir\ndefmodule MyApp.Post do\n  use MyApp.Web, :model\n  use Filterable.Phoenix.Model\n\n  filterable do\n    filter author(query, value, _conn) do\n      query |\u003e where(author_name: ^value)\n    end\n  end\n\n  schema \"posts\" do\n    ...\n  end\nend\n```\n\nThen call `apply_filters` function from model module:\n\n```elixir\n# /posts?author=Tom\ndef index(conn, params, conn) do\n  with {:ok, query, filter_values} \u003c- Post.apply_filters(conn),\n       posts                       \u003c- Repo.all(query),\n   do: render(conn, \"index.json\", posts: posts, meta: filter_values)\nend\n```\n\n### Separate module\n\nFilters could be defined in separate module, just `use Filterable.DSL` inside module to make it filterable:\n\n```elixir\ndefmodule PostFilters do\n  use Filterable.DSL\n  use Filterable.Ecto.Helpers\n\n  field :author\n  field :title\n\n  paginateable per_page: 10\n\n  @options param: :q\n  filter search(query, value, _conn) do\n    query |\u003e where([u], ilike(u.title, ^\"%#{value}%\"))\n  end\n\n  @options cast: :integer\n  filter year(query, value, _conn) do\n    query |\u003e where(author_name: ^value)\n  end\nend\n\ndefmodule MyApp.PostController do\n  use MyApp.Web, :controller\n  use Filterable.Phoenix.Controller\n\n  filterable PostFilters\n\n  # /posts?q=castle\u0026author=Kafka\u0026year=1926\n  def index(conn, params) do\n    with {:ok, query, filter_values} \u003c- apply_filters(Post, conn),\n         posts                       \u003c- Repo.all(query),\n     do: render(conn, \"index.json\", posts: posts, meta: filter_values)\n  end\nend\n```\n\n## Defining filters\n\nEach defined filter can be tuned with `@options` module attribute.\nJust set `@options` attribute before filter definition. Available options are:\n\n`:param` - allows to set query parameter name, by default same as filter name. Accepts `Atom`, `List`, and `Keyword` values:\n\n```elixir\n# /posts?q=castle\n# =\u003e #Ecto.Query\u003cfrom p in Post, where: ilike(u.title, ^\"%castle%\")\u003e\n@options param: :q\nfilter search(query, value, _conn) do\n  query |\u003e where([u], ilike(u.title, ^\"%#{value}%\"))\nend\n\n# /posts?sort=name\u0026order=desc\n# =\u003e #Ecto.Query\u003cfrom p in Post, order_by: [desc: p.name]\u003e\n@options param: [:sort, :order], cast: :integer\nfilter search(query, %{sort: field, order: order}, _conn) do\n  query |\u003e order_by([{^order, ^field}])\nend\n\n# /posts?sort[field]=name\u0026sort[order]=desc\n# =\u003e #Ecto.Query\u003cfrom p in Post, order_by: [desc: p.name]\u003e\n@options param: [sort: [:field, :order]], cast: :integer\nfilter search(query, %{field: field, order: order}, _conn) do\n  query |\u003e order_by([{^order, ^field}])\nend\n```\n\n`:default` - allows to set default filter value:\n\n```elixir\n# /posts\n# =\u003e #Ecto.Query\u003cfrom p in Post, limit: 20\u003e\n@options default: 20, cast: :integer\nfilter limit(query, value, _conn) do\n  query |\u003e limit(^value)\nend\n\n# /posts\n# =\u003e #Ecto.Query\u003cfrom p in Post, order_by: [desc: p.inserted_at]\u003e\n@options param: [:sort, :order], default: [sort: :inserted_at, order: :desc], cast: :atom_unchecked\nfilter search(query, %{sort: field, order: order}, _conn) do\n  query |\u003e order_by([{^order, ^field}])\nend\n```\n\n`: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`:\n\n```elixir\n# /posts?title=\"\"\n# =\u003e #Ecto.Query\u003cfrom p in Post\u003e\n@options allow_blank: false\nfilter title(query, value, _conn) do\n  query |\u003e where(title: ^value)\nend\n\n# /posts?title=\"\"\n# =\u003e #Ecto.Query\u003cfrom p in Post, where: p.title == \"\"\u003e\n@options allow_blank: true\nfilter title(query, value, _conn) do\n  query |\u003e where(title: ^value)\nend\n```\n\n`:allow_nil` - when `true` then it allows to trigger filter with `nil` value, `false` by default:\n\n```elixir\n# /posts?title=\"\"\n# =\u003e #Ecto.Query\u003cfrom p in Post, where: is_nil(p.title)\u003e\n# /posts?title=Casle\n# =\u003e #Ecto.Query\u003cfrom p in Post, where: p.title == \"Casle\"\u003e\n@options allow_nil: true\nfilter title(query, nil, _conn) do\n  query |\u003e where([q], is_nil(q.title))\nend\nfilter title(query, value, _conn) do\n  query |\u003e where(title: ^value)\nend\n```\n\n`:trim` - allows to remove leading and trailing whitespaces from string values, `true` by default:\n\n```elixir\n# /posts?title=\"   Casle  \"\n# =\u003e #Ecto.Query\u003cfrom p in Post, where: p.title == \"Casle\"\u003e\nfilter title(query, value, _conn) do\n  query |\u003e where(title: ^value)\nend\n\n# /posts?title=\"   Casle  \"\n# =\u003e #Ecto.Query\u003cfrom p in Post, where: p.title == \"   Casle  \"\u003e\n@options trim: false\nfilter title(query, value, _conn) do\n  query |\u003e where(title: ^value)\nend\n```\n\n`: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.\n\nAlso can accept pointer to function:\n\n```elixir\n# /posts?limit=20\n# =\u003e #Ecto.Query\u003cfrom p in Post, limit: 20\u003e\n@options cast: :integer\nfilter limit(query, value, _conn) do\n  query |\u003e limit(^value)\nend\n\n# /posts?title=Casle\n# =\u003e #Ecto.Query\u003cfrom p in Post, where: p.title == \"casle\"\u003e\n@options cast: \u0026String.downcase/1\nfilter title(query, value, _conn) do\n  query |\u003e where(title: ^value)\nend\n```\n\n`: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:\n\n```elixir\n# /posts?inserted_at=Casle\n# =\u003e {:error, \"Unable to cast \\\"Casle\\\" to datetime\"}\n@options cast: :datetime\nfilter inserted_at(query, value, _conn) do\n  query |\u003e where(inserted_at: ^value)\nend\n\n# /posts?inserted_at=Casle\n# =\u003e #Ecto.Query\u003cfrom p in Post\u003e\n@options cast: :datetime, cast_errors: false\nfilter inserted_at(query, value, _conn) do\n  query |\u003e where(inserted_at: ^value)\nend\n```\n\n`:share` - allows to set shared value. When `false` then filter function will be triggered without shared value argument:\n\n```elixir\n@options share: false\nfilter title(query, value) do\n  query |\u003e where(title: ^value)\nend\n```\n\nAll these options can be specified in `apply_filters` function or `filterable` macro. Then they will take affect on all defined filters:\n\n```elixir\nfilterable share: false, cast_errors: false do\n  field :title\nend\n\n# or\n\nfilterable PostFilters, share: false, cast_errors: false\n\n# or\n\n{:ok, query, filter_values} = apply_filters(conn, share: false, cast_errors: false)\n```\n\n## Ecto helpers\n\n`Filterable.Ecto.Helpers` module provides macros which allows to define some popular filters:\n\n`field/2` - expands to simple `Ecto.Query.where` filter:\n\n```elixir\nfilterable do\n  field :title\n  field :stars, cast: :integer\nend\n```\n\nSame filters could be built with `filter` macro:\n\n```elixir\nfilterable do\n  filter title(query, value, _conn) do\n    query |\u003e where(title: ^value)\n  end\n\n  @options cast: :integer\n  filter stars(query, value, _conn) do\n    query |\u003e where(stars: ^value)\n  end\nend\n```\n\n`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:\n\n```elixir\nfilterable do\n  # /posts?page=3\n  # =\u003e #Ecto.Query\u003cfrom p in Post, limit: 10, offset: 20\u003e\n  paginateable per_page: 10\nend\n```\n\n`limitable/1` - provides limit/offset logic:\n\n```elixir\nfilterable do\n  # /posts?limit=3offset=10\n  # =\u003e #Ecto.Query\u003cfrom p in Post, limit: 3, offset: 10\u003e\n  limitable limit: 10\nend\n```\n\n`orderable/1` - provides sorting logic, accepts list of atoms:\n\n```elixir\nfilterable do\n  # /posts?sort=inserted_at\u0026order=asc\n  # =\u003e #Ecto.Query\u003cfrom p in Post, order_by: [asc: p.inserted_at]\u003e\n  orderable [:title, :inserted_at]\nend\n```\n\n## Common usage\n\n`Filterable` also can be used in non Ecto/Phoenix projects.\nPut `use Filterable.DSL` inside module to start defining filters:\n\n```elixir\ndefmodule RepoFilters do\n  use Filterable.DSL\n\n  filter name(list, value) do\n    list |\u003e Enum.filter(\u0026 \u00261.name == value)\n  end\n\n  @options cast: :integer\n  filter stars(list, value) do\n    list |\u003e Enum.filter(\u0026 \u00261.stars \u003e= value)\n  end\nend\n```\n\nThen filter collection using `apply_filters` function:\n\n```elixir\nrepos = [%{name: \"phoenix\", stars: 8565}, %{name: \"ecto\", start: 2349}]\n\n{:ok, result, filter_values} = RepoFilters.apply_filters(repos, %{name: \"phoenix\", stars: \"8000\"})\n# or\n{:ok, result, filter_values} = Filterable.apply_filters(repos, %{name: \"phoenix\", stars: \"8000\"}, RepoFilters)\n```\n\n## Code formatter\n`filter` macro and phoenix helpers like `orderable`, `paginateable` are the part fo DSL so there is no need to wrap them in parentheses.\n\nJust add the following line into formatter configs:\n```elixir\n[\n  # ...\n\n  import_deps: [:filterable]\n]\n```\n\n## Similar packages\n- [filterex](https://github.com/rcdilorenzo/filtrex)\n- [rumage_ecto](https://github.com/Excipients/rummage_ecto)\n- [inquisitor](https://github.com/DockYard/inquisitor)\n- [ex_sieve](https://github.com/valyukov/ex_sieve)\n\n## Contribution\n\nFeel free to send your PR with proposals, improvements or corrections 😉\n","funding_links":[],"categories":["Framework Components"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomohokcoj%2Ffilterable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fomohokcoj%2Ffilterable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomohokcoj%2Ffilterable/lists"}