Current section

Files

Jump to
fat_ecto README.md
Raw

README.md

# FatEcto: Supercharge Your Ecto Queries with Ease! πŸš€
[![Build Status](https://github.com/tanweerdev/fat_ecto/actions/workflows/fat_ecto.yml/badge.svg)](https://github.com/tanweerdev/fat_ecto/actions)
[![Coverage Status](https://coveralls.io/repos/github/tanweerdev/fat_ecto/badge.svg)](https://coveralls.io/github/tanweerdev/fat_ecto)
[![hex.pm version](https://img.shields.io/hexpm/v/fat_ecto.svg)](https://hex.pm/packages/fat_ecto)
[![hex.pm downloads](https://img.shields.io/hexpm/dt/fat_ecto.svg)](https://hex.pm/packages/fat_ecto)
[![hex.pm license](https://img.shields.io/hexpm/l/fat_ecto.svg)](https://github.com/tanweerdev/fat_ecto/blob/master/LICENSE)
[![Last Updated](https://img.shields.io/github/last-commit/tanweerdev/fat_ecto.svg)](https://github.com/tanweerdev/fat_ecto/commits/master)
---
## Description
FatEcto is an Elixir package designed to make your life easier when working with Ecto. It simplifies query building, filtering, sorting, and paginationβ€”so you can focus on what truly matters: building amazing applications. With FatEcto, writing complex repeating queries becomes effortless, flexible, and powerful! πŸ’ͺ
---
## Installation
Add `fat_ecto` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
# Check https://hexdocs.pm/fat_ecto for the latest version
{:fat_ecto, "~> 1.2"}
]
end
```
Then, run `mix deps.get` to install the package.
---
## Features & Modules
### πŸ›  FatEcto.Query.Dynamics.Buildable – Dynamic Filtering Made Easy
Tired of writing repetitive query filters? The `Whereable` module lets you dynamically filter records using flexible conditions passed from your web or mobile clientsβ€”with little to no effort! And the best part? You stay in control. πŸš€
#### Usage
```elixir
defmodule FatEcto.HospitalDynamicsBuilder do
use FatEcto.Query.Dynamics.Buildable,
filterable: [
id: ["$EQUAL", "$NOT_EQUAL"]
],
overrideable: ["name", "phone"],
ignoreable: [
name: ["%%", "", [], nil],
phone: ["%%", "", [], nil]
]
import Ecto.Query
@impl true
# You can implement override_buildable for your custom filters
def override_buildable("name", "$ILIKE", value) do
dynamic([r], ilike(fragment("(?)::TEXT", r.name), ^value))
end
def override_buildable(_field, _operator, _value), do: nil
end
```
---
#### Example Usage
Here are some practical examples of how to use `FatEcto.HospitalDynamicsBuilder` to dynamically build queries:
##### Example 1: Basic Filtering by ID
```elixir
# Filter hospitals with ID equal to 1
params = %{"id" => %{"$EQUAL" => 1}}
dynamics = FatEcto.HospitalDynamicsBuilder.build(params)
# Use the dynamics in a query
import Ecto.Query
query = where(FatEcto.FatHospital, ^dynamics)
# Resulting query:
# from(h in FatEcto.FatHospital, where: h.id == 1)
```
##### Example 2: Case-Insensitive Name Search
```elixir
# Filter hospitals with names containing "St. Mary"
params = %{"name" => %{"$ILIKE" => "%St. Mary%"}}
dynamics = FatEcto.HospitalDynamicsBuilder.build(params)
# Use the dynamics in a query
import Ecto.Query
query = where(FatEcto.FatHospital, ^dynamics)
# Resulting query:
# from(h in FatEcto.FatHospital, where: ilike(fragment("(?)::TEXT", h.name), ^"%St. Mary%"))
```
##### Example 3: Combining Multiple Filters
```elixir
# Filter hospitals with ID not equal to 2 AND name containing "General"
params = %{
"id" => %{"$NOT_EQUAL" => 2},
"name" => %{"$ILIKE" => "%General%"}
}
dynamics = FatEcto.HospitalDynamicsBuilder.build(params)
# Use the dynamics in a query
import Ecto.Query
query = where(FatEcto.FatHospital, ^dynamics)
# Resulting query:
# from(h in FatEcto.FatHospital, where: h.id != 2 and ilike(fragment("(?)::TEXT", h.name), ^"%General%"))
```
##### Example 4: Ignoring Empty or Invalid Values
```elixir
# Filter hospitals with a name, but ignore empty or invalid values
params = %{"name" => %{"$ILIKE" => "%%"}} # Empty value is ignored
dynamics = FatEcto.HospitalDynamicsBuilder.build(params)
# Use the dynamics in a query
import Ecto.Query
query = where(FatEcto.FatHospital, ^dynamics)
# Resulting query:
# from(h in FatEcto.FatHospital) # No filtering applied for name
```
##### Example 5: Even Complex Nested conditions
```elixir
# Filter hospitals with a name, but ignore empty or invalid values
params = %{
"$OR" => [
%{
"name" => %{"$ILIKE" => "%John%"},
"$OR" => %{"rating" => %{"$GT" => 18}, "location" => "New York"}
},
%{
"start_date" => "2023-01-01",
"$AND" => [
%{"rating" => %{"$GT" => 4}},
%{"email" => "fat_ecto@example.com"}
]
}
]
}
dynamics = DoctorFilter.build(params)
# Resulting dynamic:
dynamic(
[q],
((q.location == ^"New York" or q.rating > ^18) and ilike(fragment("(?)::TEXT", q.name), ^"%John%")) or
(q.rating > ^4 and q.email == ^"fat_ecto@example.com" and q.start_date == ^"2023-01-01")
)
# You can now apply the result on where just like above examples
```
##### Example 6: Global Configuration for Default Dynamics
Configure FatEcto to return `dynamic([q], true)` instead of `nil` when no filters are applied:
```elixir
# config/config.exs
config :fat_ecto, :default_dynamic, :return_true
# Now all Buildable modules return dynamic([q], true) when filters are empty
dynamics = FatEcto.HospitalBuilder.build(%{})
# Returns: dynamic([q], true) instead of nil
```
##### Example 7: Filtering on Joined Associations
Use nested `filterable` to filter on joined tables. The nested key (e.g., `doctors:`) becomes the expected `as:` binding name in your query.
```elixir
defmodule FatEcto.HospitalDynamicsBuilder do
use FatEcto.Query.Dynamics.Buildable,
filterable: [
name: ["$ILIKE"], # Direct filter on hospitals table
doctors: [ # Join filter: fields use :doctors binding
specialty: ["$EQUAL"],
rating: ["$GTE"]
]
]
def override_buildable(_field, _operator, _value), do: nil
end
import Ecto.Query
# Filter params use field names directly - "specialty" maps to :doctors binding
# because it's defined under `doctors:` in filterable config
params = %{"name" => %{"$ILIKE" => "%General%"}, "specialty" => %{"$EQUAL" => "Cardiology"}}
dynamics = FatEcto.HospitalDynamicsBuilder.build(params)
# Query must include the join with matching `as:` binding
FatEcto.FatHospital
|> join(:inner, [h], d in assoc(h, :doctors), as: :doctors)
|> where(^dynamics)
|> Repo.all()
```
##### Example 8: Field Aliases (Renaming API Fields)
When joined tables share the same field name (e.g., both `hospitals` and `doctors` have `name`), you can use aliases to expose them under different API names. Use the `{:schema_field, operators}` tuple syntax:
```elixir
defmodule MyApp.HospitalFilter do
use FatEcto.Query.Dynamics.Buildable,
filterable: [
name: ["$ILIKE", "$EQUAL"], # hospital's own name
doctors: [
doctor_name: {:name, ["$ILIKE"]}, # "doctor_name" in API -> :name in DB
rating: ["$GTE", "$LTE"]
]
],
default_dynamic: :return_true
def override_buildable(_field, _operator, _value), do: nil
end
# Now the API can filter both names without conflict
params = %{
"name" => %{"$EQUAL" => "City Hospital"},
"doctor_name" => %{"$ILIKE" => "%Smith%"}
}
dynamics = MyApp.HospitalFilter.build(params)
Hospital
|> join(:left, [h], d in assoc(h, :doctors), as: :doctors)
|> where(^dynamics)
|> Repo.all()
# Generates: WHERE h.name = 'City Hospital' AND d.name ILIKE '%Smith%'
```
---
### πŸ”„ FatEcto.Sort.Sortable – Effortless Sorting
Sorting should be simpleβ€”and with `Sortable`, it is! Your frontend can send sorting parameters, and FatEcto will seamlessly generate the right sorting queries, allowing you to build powerful, customizable sorting logic without breaking a sweat. 😎
#### Usage of FatSortable
```elixir
defmodule Fat.SortQuery do
import Ecto.Query
use FatEcto.Sort.Sortable,
sortable: [id: "$ASC", email: "*", name: ["$ASC", "$DESC"]],
overrideable: ["custom_field"]
@impl true
def override_sortable("custom_field", "$DESC") do
{:desc, dynamic([u], fragment("?->>'custom_field'", u.metadata))}
end
def override_sortable(_field, _operator), do: nil
end
```
##### Sort Aliases
Just like Buildable, you can alias sort field names using the `{:schema_field, directions}` tuple:
```elixir
defmodule MyApp.HospitalSort do
use FatEcto.Sort.Sortable,
sortable: [
id: "*",
hospital_name: {:name, ["$ASC", "$DESC"]} # "hospital_name" in API -> :name in DB
]
end
order = MyApp.HospitalSort.build(%{"hospital_name" => "$ASC"})
# Generates: ORDER BY h.name ASC
```
##### Sorting on Joined Tables
Use nested keyword lists to sort on fields from joined associations β€” same syntax as Buildable join filters:
```elixir
defmodule MyApp.HospitalSort do
use FatEcto.Sort.Sortable,
sortable: [
name: "*",
doctors: [
doctor_name: {:name, "*"}, # alias on joined table
rating: ["$ASC", "$DESC"] # non-aliased join field
]
]
end
order = MyApp.HospitalSort.build(%{"doctor_name" => "$DESC", "rating" => "$ASC"})
Hospital
|> join(:left, [h], d in assoc(h, :doctors), as: :doctors)
|> order_by(^order)
|> Repo.all()
# Generates: ORDER BY d.name DESC, d.rating ASC
```
---
### πŸ“Œ FatEcto.Pagination.Paginator – Paginate Like a Pro
No more hassle with pagination! FatPaginator helps you paginate Ecto queries efficiently, keeping your APIs snappy and responsive.
#### Usage of FatPaginator
```elixir
defmodule Fat.MyPaginator do
use FatEcto.Pagination.V2Paginator,
default_limit: 10,
repo: FatEcto.Repo,
max_limit: 100
end
```
---
## πŸš€ Contributing
We love contributions! If you’d like to improve FatEcto, submit an issue or pull request. Let’s build something amazing together! πŸ”₯
---
## πŸ“œ License
FatEcto is released under the MIT License.
πŸ“– See the full documentation at [HexDocs](https://hexdocs.pm/fat_ecto/) for more details.