# Filtery
**`Filtery` help you to build the query using a syntax which is similar to Mongo**
This is super useful when you want to build filter from request params.
```elixir
filter = %{
status: "active",
email: {:not, nil},
role: ["admin", "moderator"]
}
Filtery.apply(User, filter)
```
## Installation
Add `filtery` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:filtery, "~> 0.2"}
]
end
```
Documentation is published here [https://hexdocs.pm/filtery](https://hexdocs.pm/filtery).
**Table of Contents**
- [Installation](#installation)
- [I. Usage](#i-usage)
- [II. Syntax](#ii-syntax)
- [III. Supported operator](#iii-supported-operator)
- [1. Comparition operator](#1-comparition-operator)
- [2. Logical operator](#2-logical-operator)
- [3. Extra operator](#3-extra-operator)
- [4. Check `NULL` and skip `nil` filter](#check-null-and-skip-nil-filter)
- [IV. Define your operators](#iv-define-your-operators)
- [V. Joining tables](#v-joining-tables)
<!-- markdown-toc end -->
## I. Usage
`Filtery` help you to build the query using a similar syntax with MongoDB like this:
```elixir
filter = %{
status: "active",
email: {:not, nil},
role: ["admin", "moderator"]
}
Filtery.apply(User, filter)
```
The result is a query like this:
```elixir
from(u in User, where: u.status == "active" and not is_nil(u.email) and u.role in ["admin", "moderator"])
```
## II. Syntax
You can use `<field>: <value>` expressions to specify the equality condition and query operator expressions.
```
%{
<field1>: <value1>,
<field2>: { <operator>, <value> },
...
}
```
**Notes: all operator belows are reserved keywords and cannot be used as field name**
## III. Supported operator
### 1. Comparition operator
| `:eq` | Matches values that are equal to a specified value. |
| ------ | :----------------------------------------------------------- |
| `:gt` | Matches values that are greater than a specified value. |
| `:gte` | Matches values that are greater than or equal to a specified value. |
| `:in` | Matches any of the values specified in an array. |
| `:lt` | Matches values that are less than a specified value. |
| `:lte` | Matches values that are less than or equal to a specified value. |
| `:ne` | Matches all values that are not equal to a specified value. |
| `:nin` | Matches none of the values specified in an array. |
### 2. Logical operator
| Name | Description |
| ------ | ------------------------------------------------------------ |
| `:and` | Joins query clauses with a logical `AND` returns all documents that match the conditions of both clauses. |
| `:not` | Inverts the effect of a query expression and returns documents that do *not* match the query expression. |
| | |
| `:or` | Joins query clauses with a logical `OR` returns all documents that match the conditions of either clause. |
#### `AND` operator
By default, if a map or keyword list is given, `Filtery` will join all field condition of that map using `AND`
```elixir
Filtery.apply(User, %{status: "active", age: {:gt, 20}})
# same with
Filtery.apply(User, %{and:
%{status: "active", age: {:gt, 20}}
})
# same with
Filtery.apply(User, %{and:
[status: "active", age: {:gt, 20]}
})
# same with
from(u in User, where: u.status == "active" and u.age > 20)
```
#### `OR` operator
The `:or` operator performs a logical `OR` operation on an array of *two or more* `<expressions>`
```elixir
Filtery.apply(Product, %{or: %{
price: {:gt, 20},
category: "sport"
}})
```
#### `NOT` operator
Performs a logical `NOT` operation on the specified `<operator-expression>`
*Syntax*: `%{ field: %{ not: <operator-expression> } }`
```elixir
Filtery.apply(Product, %{or: %{
price: {:gt, 20},
category: {:not: "sport"}
}})
Filtery.apply(Product, %{or: %{
price: {:not, {:gt, 20}},
category: "sport"
}})
```
### 3. Extra operator
`Filtery` provides some more useful operators to work with text and range.
| Name | Description |
| -------------------- | ------------------------------------------------------------ |
| `:between` | Matches values `>` lower bound and `<` upper bound |
| `:ibetween` | Matches values `>=` lower bound and `<=`upper bound |
| `like`, `contains` | Match values which contains specific value |
| `ilike`, `icontains` | Case insensitive version of `like` |
| `has` | For array type column, Matches array which has specific value |
#### Syntax
- `between` | `ibetween`
*Syntax*: `field: {:between, [lower_value, upper_value]}`
### Check `NULL` and skip `nil` filter
By 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
```elixir
Filtery.apply(query, filter, skip_nil: true)
```
In 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:
```elixir
Filter.apply(query, %{email: :is_nil}, skip_nil: true)
```
## IV. Define your operators
You can extend `Filtery` and define your own operator. For example, here I define a new operatory `equal`
```elixir
defmodule MyFiltery do
use Filtery.Base
def filter(column, {:equal, value}) do
dynamic([q], field(q, ^column) == ^value)
end
end
```
To support a filter, you must follow this spec
```elixir
@spec filter(column::atom(), {operator::atom(), value::any()}) :: Ecto.Query.dynamic()
```
Within the body of `filter/2` function using `dynamic` to compose your condtion and return a `dynamic`
## V. Joining tables
**`Filtery` defines a special operator `ref` to join table**
*Syntax*: `<field>: {:ref, <qualifier>, <filter on joined table>}`
If `qualifier` is skipped, then `:inner` join is used by default.
```elixir
query = Filtery.apply(Post, %{comments: {:ref, %{
approved: true,
content: {:like, "filtery"}
}}})
```
And then you can use **Name binding** to do further query
```elixir
query = where(query, [comments: c], c.published_at > ^xday_ago)
```
**Qualifiers**
By 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.
### **You can filter with nested `ref`**
```elixir
Filtery.apply(Post, %{comments: {:ref, %{
approved: true,
user: {:ref, %{
name: {:like, "Tom"}
}}
}}})
```
### Important Notes on `ref` operator
- Field name must be the association name in your schema because `Filtery` use `assoc` to build join query.
In the above example, `Post` schema must define association `has_many: :comments, Comment`
- Not allow 2 ref with same name because the name is used as alias `:as` in join query, so it can only use one.