Skip to content

tanweerdev/fat_ecto

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

351 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

FatEcto: Supercharge Your Ecto Queries with Ease! πŸš€

Build Status Coverage Status hex.pm version hex.pm downloads hex.pm license Last Updated


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:

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

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
# 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
# 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
# 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
# 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
# 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:

# 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.

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:

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

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:

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:

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

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 for more details.

Packages

 
 
 

Contributors

Languages