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.