{"id":20080620,"url":"https://github.com/bluzky/filtery","last_synced_at":"2025-05-05T23:31:18.822Z","repository":{"id":48377750,"uuid":"387650287","full_name":"bluzky/filtery","owner":"bluzky","description":"Build Ecto query using MongoDB-like query style.","archived":false,"fork":false,"pushed_at":"2021-10-15T08:13:08.000Z","size":46,"stargazers_count":20,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-24T10:44:49.884Z","etag":null,"topics":["ecto","elixir","phoenix","query-builder"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bluzky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-07-20T02:35:19.000Z","updated_at":"2025-01-09T23:43:43.000Z","dependencies_parsed_at":"2022-09-02T19:40:10.494Z","dependency_job_id":null,"html_url":"https://github.com/bluzky/filtery","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluzky%2Ffiltery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluzky%2Ffiltery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluzky%2Ffiltery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluzky%2Ffiltery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bluzky","download_url":"https://codeload.github.com/bluzky/filtery/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252593010,"owners_count":21773386,"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":["ecto","elixir","phoenix","query-builder"],"created_at":"2024-11-13T15:29:20.979Z","updated_at":"2025-05-05T23:31:18.385Z","avatar_url":"https://github.com/bluzky.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Filtery\n\n[![Build Status](https://github.com/bluzky/filtery/workflows/Elixir%20CI/badge.svg)](https://github.com/bluzky/filtery/actions) [![Coverage Status](https://coveralls.io/repos/github/bluzky/filtery/badge.svg?branch=master)](https://coveralls.io/github/bluzky/filtery?branch=master) [![Hex Version](https://img.shields.io/hexpm/v/filtery.svg)](https://hex.pm/packages/filtery) [![docs](https://img.shields.io/badge/docs-hexpm-blue.svg)](https://hexdocs.pm/filtery/)\n\n**`Filtery` help you to build the query using a syntax which is similar to Mongo**\nThis is super useful when you want to build filter from request params.\n\n```elixir\nfilter = %{\n\tstatus: \"active\",\n\temail: {:not, nil},\n\trole: [\"admin\", \"moderator\"]\n}\nFiltery.apply(User, filter)\n```\n\n## Installation\n\nAdd `filtery` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:filtery, \"~\u003e 0.2\"}\n  ]\nend\n```\n\nDocumentation is published here [https://hexdocs.pm/filtery](https://hexdocs.pm/filtery).\n\n**Table of Contents**\n\n  - [Installation](#installation)\n  - [I. Usage](#i-usage)\n  - [II. Syntax](#ii-syntax)\n  - [III. Supported operator](#iii-supported-operator)\n      - [1. Comparition operator](#1-comparition-operator)\n      - [2. Logical operator](#2-logical-operator)\n      - [3. Extra operator](#3-extra-operator)\n      - [4. Check `NULL` and skip `nil` filter](#check-null-and-skip-nil-filter)\n  - [IV. Define your operators](#iv-define-your-operators)\n  - [V. Joining tables](#v-joining-tables)\n\u003c!-- markdown-toc end --\u003e\n\n\n## I. Usage\n\n`Filtery` help you to build the query using a similar syntax with MongoDB like this:\n\n```elixir\nfilter = %{\n\tstatus: \"active\",\n\temail: {:not, nil},\n\trole: [\"admin\", \"moderator\"]\n}\nFiltery.apply(User, filter)\n```\n\n\n\nThe result is a query like this:\n\n```elixir\nfrom(u in User, where: u.status == \"active\" and not is_nil(u.email) and u.role in [\"admin\", \"moderator\"])\n```\n\n\n\n## II. Syntax \n\nYou can use `\u003cfield\u003e: \u003cvalue\u003e` expressions to specify the equality condition and query operator expressions.\n\n```\n%{\n  \u003cfield1\u003e: \u003cvalue1\u003e,\n  \u003cfield2\u003e: { \u003coperator\u003e, \u003cvalue\u003e },\n  ...\n}\n```\n\n\n\n**Notes: all operator belows are reserved keywords and cannot be used as field name**\n\n\n\n## III. Supported operator \n\n\n\n### 1. Comparition operator\n\n| `:eq`  | Matches values that are equal to a specified value.          |\n| ------ | :----------------------------------------------------------- |\n| `:gt`  | Matches values that are greater than a specified value.      |\n| `:gte` | Matches values that are greater than or equal to a specified value. |\n| `:in`  | Matches any of the values specified in an array.             |\n| `:lt`  | Matches values that are less than a specified value.         |\n| `:lte` | Matches values that are less than or equal to a specified value. |\n| `:ne`  | Matches all values that are not equal to a specified value.  |\n| `:nin` | Matches none of the values specified in an array.            |\n\n\n\n### 2. Logical operator\n\n| Name   | Description                                                  |\n| ------ | ------------------------------------------------------------ |\n| `:and` | Joins query clauses with a logical `AND` returns all documents that match the conditions of both clauses. |\n| `:not` | Inverts the effect of a query expression and returns documents that do *not* match the query expression. |\n|        |                                                              |\n| `:or`  | Joins query clauses with a logical `OR` returns all documents that match the conditions of either clause. |\n\n\n\n#### `AND` operator\n\nBy default, if  a map or keyword list is given, `Filtery` will join all field condition of that map using `AND`\n\n\n\n```elixir\nFiltery.apply(User, %{status: \"active\", age: {:gt, 20}})\n\n# same with\nFiltery.apply(User, %{and:\n                      %{status: \"active\", age: {:gt, 20}}\n                      })\n\n# same with\nFiltery.apply(User, %{and:\n                      [status: \"active\", age: {:gt, 20]}\n                     })\n                     \n# same with\nfrom(u in User, where: u.status == \"active\" and u.age \u003e 20)\n```\n\n\n\n#### `OR` operator\n\nThe `:or` operator performs a logical `OR` operation on an array of *two or more* `\u003cexpressions\u003e` \n\n\n\n\n\n```elixir\nFiltery.apply(Product, %{or: %{\n                           price: {:gt, 20},\n                           category: \"sport\"\n                         }})\n```\n\n\n\n#### `NOT` operator\n\nPerforms a logical `NOT` operation on the specified `\u003coperator-expression\u003e` \n\n*Syntax*: `%{ field: %{ not:  \u003coperator-expression\u003e  } }`\n\n\n\n```elixir\nFiltery.apply(Product, %{or: %{\n                           price: {:gt, 20},\n                           category: {:not: \"sport\"}\n                         }})\n\nFiltery.apply(Product, %{or: %{\n                           price: {:not, {:gt, 20}},\n                           category: \"sport\"\n                         }})\n```\n\n\n\n\n\n### 3. Extra operator\n\n`Filtery` provides some more useful operators to work with text and range.\n\n\n\n| Name                 | Description                                                  |\n| -------------------- | ------------------------------------------------------------ |\n| `:between`           | Matches values `\u003e` lower bound and `\u003c` upper bound           |\n| `:ibetween`          | Matches values `\u003e=` lower bound and `\u003c=`upper  bound         |\n| `like`, `contains`   | Match values which contains specific value                   |\n| `ilike`, `icontains` | Case insensitive version of `like`                           |\n| `has`                | For array type column, Matches array which has specific value |\n\n\n\n#### Syntax\n\n- `between` | `ibetween`\n\n  *Syntax*:  `field: {:between, [lower_value, upper_value]}`\n\n\n\n\n\n### Check `NULL` and skip `nil` filter\n\nBy default is a value in the filter is `nil`, `Filtery` applies `is_nil` to check `NULL` value. You can tell `Filtery` to ignore all `nil` field by passing `skip_nil: true` to the options\n\n```elixir\nFiltery.apply(query, filter, skip_nil: true)\n```\n\n\n\nIn that case, if you want to check field which is `NULL` or `NOT NULL` you use `:is_nil` instead of `nil` when passing value to the filter:\n\n```elixir\nFilter.apply(query, %{email: :is_nil}, skip_nil: true)\n```\n\n\n\n## IV. Define your operators \n\nYou can extend `Filtery` and define your own operator. For example, here I define a new operatory `equal`  \n\n```elixir\ndefmodule MyFiltery do\n\tuse Filtery.Base\n\t\n\tdef filter(column, {:equal, value}) do\n  \tdynamic([q], field(q, ^column) == ^value)\n\tend\nend\n```\n\n\nTo support a filter, you must follow this spec\n\n```elixir\n@spec filter(column::atom(), {operator::atom(), value::any()}) :: Ecto.Query.dynamic()\n```\n\nWithin the body of `filter/2` function using `dynamic` to compose your condtion and return a `dynamic`\n\n\n## V. Joining tables \n\n**`Filtery` defines a special operator `ref` to join table**\n\n*Syntax*: `\u003cfield\u003e: {:ref, \u003cqualifier\u003e, \u003cfilter on joined table\u003e}`\n\nIf `qualifier` is skipped, then `:inner` join is used by default.\n\n```elixir\nquery = Filtery.apply(Post, %{comments: {:ref, %{\n                                    approved: true,\n                                    content: {:like, \"filtery\"}\n                                 }}})\n```\n\nAnd then you can use **Name binding** to do further query\n\n```elixir\nquery = where(query, [comments: c], c.published_at \u003e ^xday_ago)\n```\n\n\n\n**Qualifiers**\n\nBy default `Filtery` join using `:inner` qualifier. You can use one of ``:inner`, `:left`, `:right`, `:cross`, `:full`, `:inner_lateral` or `:left_lateral` qualifier as defined by Ecto.\n\n\n\n### **You can filter with nested `ref`**\n\n```elixir\nFiltery.apply(Post, %{comments: {:ref, %{\n                                    approved: true,\n                                    user: {:ref, %{\n                                              name: {:like, \"Tom\"}\n                                           }}\n                                 }}})\n```\n\n\n### Important Notes on `ref` operator\n\n- Field name must be the association name in your schema because `Filtery` use `assoc` to build join query.\n\n  In the above example, `Post` schema must define association `has_many: :comments, Comment`\n\n- Not allow 2 ref with same name because the name is used as alias `:as` in join query, so it can only use one.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluzky%2Ffiltery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbluzky%2Ffiltery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluzky%2Ffiltery/lists"}