{"id":13507759,"url":"https://github.com/rcdilorenzo/filtrex","last_synced_at":"2025-10-21T18:59:43.103Z","repository":{"id":3175272,"uuid":"48511144","full_name":"rcdilorenzo/filtrex","owner":"rcdilorenzo","description":"A library for performing and validating complex filters from a client (e.g. smart filters)","archived":false,"fork":false,"pushed_at":"2024-07-25T07:56:10.000Z","size":240,"stargazers_count":198,"open_issues_count":9,"forks_count":27,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-10-21T18:59:27.691Z","etag":null,"topics":["comparator","database","ecto","filter","parsing","parsing-filters","url-parameters"],"latest_commit_sha":null,"homepage":"https://hex.pm/packages/filtrex","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/rcdilorenzo.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-12-23T21:09:42.000Z","updated_at":"2025-03-27T17:23:21.000Z","dependencies_parsed_at":"2023-07-05T18:49:15.822Z","dependency_job_id":"d2ae3d1d-9809-44ab-9bc5-8d9d4a26d232","html_url":"https://github.com/rcdilorenzo/filtrex","commit_stats":{"total_commits":70,"total_committers":10,"mean_commits":7.0,"dds":0.5142857142857142,"last_synced_commit":"392a87aa5eed99a2987dd6e615f27536714d4ad2"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/rcdilorenzo/filtrex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcdilorenzo%2Ffiltrex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcdilorenzo%2Ffiltrex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcdilorenzo%2Ffiltrex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcdilorenzo%2Ffiltrex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rcdilorenzo","download_url":"https://codeload.github.com/rcdilorenzo/filtrex/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rcdilorenzo%2Ffiltrex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280317282,"owners_count":26309998,"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","status":"online","status_checked_at":"2025-10-21T02:00:06.614Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["comparator","database","ecto","filter","parsing","parsing-filters","url-parameters"],"created_at":"2024-08-01T02:00:38.675Z","updated_at":"2025-10-21T18:59:43.072Z","avatar_url":"https://github.com/rcdilorenzo.png","language":"Elixir","funding_links":[],"categories":["Date and Time"],"sub_categories":[],"readme":"![Banner](resources/filtrex-banner.png)\n\n# Filtrex\n[![Join the chat at https://gitter.im/filtrex-elixir/Lobby](https://badges.gitter.im/filtrex-elixir/Lobby.svg)](https://gitter.im/filtrex-elixir/Lobby?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n[![Hex.pm](https://img.shields.io/hexpm/v/filtrex.svg)](https://hex.pm/packages/filtrex)\n[![Build Status](https://travis-ci.org/rcdilorenzo/filtrex.svg?branch=master)](https://travis-ci.org/rcdilorenzo/filtrex)\n[![Docs Status](http://inch-ci.org/github/rcdilorenzo/filtrex.svg?branch=master)](http://inch-ci.org/github/rcdilorenzo/filtrex)\n\nFiltrex aims to make filtering database results with [Ecto](https://hex.pm/packages/ecto) a breeze. This library attempts to help you solve problems such as...\n\n- Building database query filters from URL parameters\n- Establishing a readable, consistent convention for filter parameters\n- Validating filters do not expose unintended data or allow unauthorized access\n- Saving filters for later use (e.g. a smart filter feature)\n\nCheck out the [docs](https://hexdocs.pm/filtrex/) or read on for a quick start on how to use.\n\n_Note that this library does not require using a web dependency such as [Phoenix](https://hex.pm/packages/phoenix) but is geared towards web applications. It also has only been tested with a Postgres backing but may work with other database adapters._\n\n## Installation\n\nThe package is available on [Hex](https://hex.pm) and can be installed by adding it to your list of dependencies:\n\n```elixir\ndef deps do\n  [{:filtrex, \"~\u003e 0.4.3\"}]\nend\n```\n\n## Outline\n\n- [Example Usage](#example-usage)\n- [Filtering from parameters](#filtering-from-parameters) (and an [example with phoenix](#example-with-phoenix))\n- [Storing filters](#storing-filters)\n- [Configuration](#configuration)\n- [Filter Types](#filter-types)\n\n# Example Usage\n\nHere is a simple example of how to filter incoming parameters with filtrex:\n\n```elixir\n# in controller\ndefmodule MyApp.CommentController do\n  use MyAppWeb, :controller\n\n  def index(conn, params) do\n    # step 1: convert and validate incoming parameters\n    {:ok, filter} = MyApp.FilterConfig.comments()\n      |\u003e Filtrex.parse_params(params)\n\n    # step 2: build query\n    query = MyApp.Comment\n      |\u003e where([c], c.user_id == ^conn.assigns[:user_id])\n      |\u003e Filtrex.query(filter)\n\n    json conn, Repo.all(query)\n  end\nend\n\n# in lib/my_app/filter_config.ex\ndefmodule MyApp.FilterConfig do\n  import Filtrex.Type.Config\n\n  # create configuration for transforming / validating parameters\n  def comments() do\n    defconfig do\n      text [:title, :comments]\n      date :posted_at, format: \"{0M}-{0D}-{YYYY}\"\n    end\n  end\nend\n\n# reference model\ndefmodule MyApp.Comment do\n  use Ecto.Schema\n\n  schema \"comments\" do\n    field :title\n    field :comments\n    field :posted_at, :date\n  end\nend\n```\n\nWith this example, here are some of the query parameters that can be used:\n\n| Example Key                         | Example Value                              |\n|-------------------------------------|--------------------------------------------|\n| `comments_contains`                 | Chris McCord                               |\n| `title`                             | Upcoming Phoenix Features                  |\n| `posted_at_between` (nested value)  | start: \"01-01-2013\" \u003cbr\u003e end: \"12-31-2017\" |\n| `filter_union` (any \\| all \\| none) | any                                        |\n\n## Filtering Across Associations\nHere is a more complex example for filtering across an association:\n\n```elixir\n# in controller\ndefmodule MyApp.CommentController do\n  use MyAppWeb, :controller\n\n  def index(conn, params) do\n    # step 1: convert and validate incoming parameters\n    {:ok, user_filter} = Filtrex.parse_params(user_filter(), params[\"user\"] || %{})\n    {:ok, profile_filter} = Filtrex.parse_params(profile_filter(), params[\"profile\"] || %{})\n\n    # step 2: build query\n    user_query =\n      User\n      |\u003e Filtrex.query(user_filter)\n\n    profile_query =\n      Profile\n      |\u003e Filtrex.query(profile_filter)\n\n    # step 3: combine queries\n    query =\n      from(parent in user_query,\n        join: pi in ^profile_query,\n        on: pi.parent_id == parent.id\n      )\n\n    results = query\n    |\u003e preload([:profile])\n    |\u003e Repo.all(params)\n\n    json conn, results\n  end\nend\n\n# in lib/my_app/filter_config.ex\ndefmodule MyApp.FilterConfig do\n  import Filtrex.Type.Config\n\n  # create configuration for transforming / validating parameters\n  def user_filter() do\n    defconfig do\n      text [:email]\n    end\n  end\n\n  def profile_filter() do\n    defconfig do\n      text [:first_name]\n      text [:last_name]\n    end\n  end\nend\n```\n# Filtering From Parameters\n\nFiltering from parameters is often a tedious process of special keys and validation. Filtrex standardizes this process by using a human-readable format for columns. Consider these examples:\n\n| Query Key                | Column     | Intention     |\n|--------------------------|------------|---------------|\n| `comments_is_not`        | `comments` | `!=`          |\n| `title_contains`         | `title`    | includes text |\n| `rating_greater_than_or` | `rating`   | `\u003e=`          |\n| `posted_on_or_before`    | `posted`   | `\u003c=`          |\n\nAssuming that these keys are in a map along with the associated values, the parameters can be passed into filtrex with a simple configuration DSL that will validate and parse them effectively. See [Configuration](#configuration) for more details on how to create the appropriate config.\n\n```elixir\n# create necesary configuration\nimport Filtrex.Type.Config\nconfig = defconfig do\n  text [:title, :comments]\n  date :posted\n  number :rating, allow_decimal: true\nend\n\n# convert params into a validated filter\ncase Filtrex.parse_params(config, params) do\n  {:ok, filter} -\u003e\n    # use filter to create query\n    query = Filtrex.query(MyApp.Comment, filter)\n  {:error, error} -\u003e\n    # e.g. {:error, \"Unknown filter key 'title_means'\"}\nend\n```\n\n## Example with Phoenix\n\nHere is an example of how filtrex might be used within an elixir app that uses [phoenix](http://phoenixframework.org):\n\n\n```elixir\n# in controller\ndef index(conn, params = %{\"user_id\" =\u003e user_id}) do\n  # remove keys that are not filtered against\n  filter_params = Map.drop(params, ~w(user_id))\n\n  # create base query\n  base_query = from(c in MyApp.Comment, c.user_id == ^user_id)\n\n  # create relevant configuration\n  config = MyApp.Comment.filter_options(:admin)\n\n  # parse filter parameters\n  case Filtrex.parse_params(config, filter_params) do\n    {:ok, filter} -\u003e\n      # retrieve from database\n      render conn, \"index.json\", data: Filtrex.query(base_query, filter) |\u003e Repo.all\n\n    {:error, error} -\u003e\n      # render filter error\n      render conn, \"errors.json\", data: [error]\n  end\nend\n\n# in model-level module\ndefmodule MyApp.Comment do\n  import Filtrex.Type.Config\n  # ...\n\n  def filter_options(:admin) do\n    defconfig do\n      text :title\n      date :published\n      number :upvotes\n      number :rating, allow_decimal: true\n      boolean :flag\n      datetime [:updated_at, :inserted_at]\n    end\n  end\nend\n```\n\nWith this example, a query such as this one would filter comments:\n\n```elixir\n%{\n  \"title_contains\" =\u003e \"Conf\",\n  \"published_on_or_before\" =\u003e \"2016-04-01\",\n  \"upvotes_greater_than\" =\u003e 45,\n  \"flag\" =\u003e true,\n  \"updated_at_after\" =\u003e \"2016-04-01T12:34:56Z\",\n  \"rating_greater_than_or\" =\u003e 90.5,\n\n  \"filter_union\" =\u003e \"any\"\n  # ^ filter based on any of the columns\n}\n```\n\n# Storing Filters\n\nIn addition to parsing parameters, filtrex also enables parsing from a map syntax that is easily encodable to and from JSON. This feature allows storing a filter for future use (e.g. routinely checking for comments that mention \"ElixirConf\" or todos that are not completed).\n\n```elixir\n# create validation options for keys and formats\nimport Filtrex.Type.Config\nconfig = defconfig do\n  text [:title, :comments]\n  date :due_date\n  boolean :flag\nend\n\n# parse a filter from map syntax\n{:ok, filter} = Filtrex.parse(config, %{\n  \"filter\" =\u003e %{\n    \"type\" =\u003e \"all\",               # all | any | none\n    \"conditions\" =\u003e [\n      %{\"column\" =\u003e \"title\", \"comparator\" =\u003e \"contains\", \"value\" =\u003e \"Buy\", \"type\" =\u003e \"text\"},\n      %{\"column\" =\u003e \"title\", \"comparator\" =\u003e \"does not contain\", \"value\" =\u003e \"Milk\", \"type\" =\u003e \"text\"},\n      %{\"column\" =\u003e \"flag\", \"comparator\" =\u003e \"equals\", \"value\" =\u003e \"false\", \"type\" =\u003e \"boolean\"}\n    ],\n    \"sub_filters\" =\u003e [%{\n      \"filter\" =\u003e %{\n        \"type\" =\u003e \"any\",\n        \"conditions\" =\u003e [\n          %{\"column\" =\u003e \"due_date\", \"comparator\" =\u003e \"equals\", \"value\" =\u003e \"2016-03-26\", \"type\" =\u003e \"date\"}\n        ]\n      }\n    }]\n  }\n})\n\n# Encode filter structure into where clause on Ecto query\nquery = from(m in MyApp.Todo, where: m.rating \u003e 90)\n  |\u003e Filtrex.query(filter)  # =\u003e #Ecto.Query\u003c...\n\n```\n\nFor more details on the acceptable structure of this map, feel free to take a look at the [example json schema](http://jeremydorn.com/json-editor/?schema=N4IgJgpgZglgdjALjA9nAziAXKAYjAG0QgCdtRlECJsR8jSQAaERATwAcasQUAjAFYQAxomYgOJFFxLIImHCFgMyi9l1r8ho8ZOmk5Cip27GNPdIhLwA5uIhwArgFtsAbRABDAgXGe4bOJwaDQAugC+LMJoYEioGOSsJrSeJCSegSxIEM5GSea8giJiLHoyhonRBC5wiercIJbWcHaRINHOHKmeiCiqZg1NtiBt9XXJFlbDLA4u7iB8KCjU/uLEAB4l4D00LGA7yM67IE7OfIwRLABu3o6m+YNTLSPhryzojnwA+srEJHljHipdKZEAkCAAR0cMHBYGwUG86AgWWIuUSABJwVBaABiAD0kFgCGQaHQePofxer2pLEx0FxBOg8DipPJhEp4SAAA==\u0026value=N4IgZglgNgLgpgJxALlDAngBzikBDKKEAGhAGMB7AOwBMIYJqBnFAbVEqgFcBbK3BjCg5SlHpjwI8MCkmTlqMPBCotSGbALgAPGCRAA3AlxzyAQl3QgAvsQ4VufAfWH6xEqTLkgaFOEwACKgoYAMoqJRV9DVMQeF19I25YgFloAGsbOwVHfnkwKDwAczcKcUlpWVw4AEcuAjU4rFiAIwoHODx+UiSTXDAGnGsAXVImLhaAfUhYRBZkdnBoeDk0Ztwuq1FqOgZmNntc3BoTSZppEQVyzyr5WvqoRpjji8TjWIAmAAYARgA2AC0XwAzACPn8bMNrCNoUAAAA==\u0026theme=bootstrap2\u0026iconlib=fontawesome4\u0026object_layout=grid\u0026show_errors=interaction) or the raw [JSON schema config](resources/schema.json).\n\n# Configuration\n\nEach of the methods of creating a filter requires passing a configuration that filtrex then validates against. This data structure is really just a list of type configs.\n\n```elixir\n[%Filtrex.Type.Config{keys: [\"title\", \"description\"], options: %{}, type: :text},\n %Filtrex.Type.Config{keys: [\"published\"],  options: %{}, type: :boolean},\n %Filtrex.Type.Config{keys: [\"posted_at\"],  options: %{format: \"{YYYY}-{0M}-{0D}\"}, type: :date},\n %Filtrex.Type.Config{keys: [\"updated_at\"], options: %{}, type: :datetime},\n %Filtrex.Type.Config{keys: [\"views\"],      options: %{allow_decimal: false}, type: :number}]\n```\n\nHowever, for convenience and for validating the filter types, this DSL can be used to generate that exact data structure.\n\n```elixir\nimport Filtrex.Type.Config\n\ndefconfig do\n  # multiple text keys\n  text [:title, :description]\n\n  # boolean type\n  boolean :published\n\n  # date type with options\n  date :posted_at, format: \"{YYYY}-{0M}-{0D}\"\n\n  # simple datetime\n  datetime :updated_at\n\n  # integer value\n  number :views, allow_decimal: false\nend\n```\n\nThe options passed to each type gives the individual condition types more information to validate the filter against and is a required argument. See [filter types](#filter-types) for details on the available options.\n\n\n# Filter Types\n\nThe following condition types and comparators are supported.\n\n* [Filtrex.Condition.Boolean](http://rcdilorenzo.github.io/filtrex/Filtrex.Condition.Boolean.html)\n    * equals, does not equal\n* [Filtrex.Condition.Text](http://rcdilorenzo.github.io/filtrex/Filtrex.Condition.Text.html)\n    * equals, does not equal, contains, does not contain\n* [Filtrex.Condition.Date](http://rcdilorenzo.github.io/filtrex/Filtrex.Condition.Date.html)\n    * after, on or after, before, on or before, between, not between, equals, does not equal\n    * options: format (default: `{YYYY}-{0M}-{0D}`)\n* [Filtrex.Condition.DateTime](http://rcdilorenzo.github.io/filtrex/Filtrex.Condition.DateTime.html)\n    * after, on or after, before, on or before, equals, does not equal\n    * options: format (default: `{ISOz}`)\n* [Filtrex.Condition.Number](http://rcdilorenzo.github.io/filtrex/Filtrex.Condition.Number.html)\n    * equals, does not equal, greater than, less than or, greater than or, less than\n    * options: allow_decimal (default: false), allowed_values (default: nil)\n\n## License\n\nCopyright (c) 2015-2019 Christian Di Lorenzo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frcdilorenzo%2Ffiltrex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frcdilorenzo%2Ffiltrex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frcdilorenzo%2Ffiltrex/lists"}