FatEcto.Query.Dynamics.Buildable behaviour (FatEcto v1.4.0)

View Source

Builds queries after filtering fields based on user-provided filterable and overrideable fields.

This module provides functionality to filter Ecto queries using predefined filterable fields (handled by Builder) and overrideable fields (handled by a fallback function).

Options

  • filterable: A keyword list of fields and their allowed operators. Supports nested structures for filtering on joined/associated tables. Example: [
    id: ["$EQUAL", "$NOT_EQUAL"],
    name: ["$ILIKE"],
    # Nested = join filter (association name must match `as:` binding)
    classifications: [
      subject_id: ["$EQUAL"],
      grade_id: ["$EQUAL", "$IN"]
    ]
    ]
  • overrideable: A list of fields that can be overridden. Example: ["name", "phone"]
  • ignoreable: A map of fields and their ignoreable values. Example: [
    "name" => ["%%", "", [], nil],
    "phone" => ["%%", "", [], nil]
    ]
  • default_dynamic: When set to :return_true, returns dynamic([q], true) instead of nil when no dynamics are built. Example: default_dynamic: :return_true

Join Filters (Nested Filterable)

When a filterable entry has a keyword list as its value (instead of a list of operator strings), it's treated as a join filter. The key becomes the expected named binding (via as: option in join).

use FatEcto.Query.Dynamics.Buildable,
  filterable: [
    title: ["$ILIKE"],                    # Direct filter on main table
    classifications: [                     # Join filter on :classifications binding
      subject_id: ["$EQUAL"],
      grade_id: ["$EQUAL", "$IN"]
    ]
  ]

The caller must provide the join with matching as: binding:

Video
|> join(:inner, [v], c in assoc(v, :classifications), as: :classifications)
|> where(^dynamics)

Join filters support all standard operators: $EQUAL, $NOT_EQUAL, $IN, $NOT_IN, $LIKE, $ILIKE, $GT, $GTE, $LT, $LTE, $NULL, $NOT_NULL.

Join filters also work seamlessly with $OR and $AND operators:

%{
  "$OR" => [
    %{"subject_id" => %{"$EQUAL" => "math-uuid"}},
    %{"grade_id" => %{"$EQUAL" => "grade-10-uuid"}}
  ]
}

Global Configuration

You can configure the default behavior for all Buildable modules in your config:

# config/config.exs
config :fat_ecto, :default_dynamic, :return_true

This will make all Buildable modules return dynamic([q], true) when no filters are applied, unless explicitly overridden at the module level with default_dynamic: nil.

Example 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

  def override_buildable("name", "$ILIKE", value) do
    dynamic([q], ilike(fragment("(?)::TEXT", q.name), ^value))
  end

  def override_buildable("phone", "$ILIKE", value) do
    dynamic([q], ilike(fragment("(?)::TEXT", q.phone), ^value))
  end

  def override_buildable(_field, _operator, _value) do
    nil
  end

  # Optional: Override after_buildable to perform custom processing on the final dynamics
  def after_buildable(dynamics) do
    IO.puts("Do something on final Dynamics")
    dynamics
  end
end

Summary

Callbacks

Callback for performing custom processing on the final dynamics.

Callback for handling custom filtering logic for overrideable fields.

Callbacks

after_buildable(dynamics)

@callback after_buildable(dynamics :: Ecto.Query.dynamic_expr() | nil) :: any()

Callback for performing custom processing on the final dynamics.

This function is called at the end of the build/2 function. The default behavior is to return the dynamics, but it can be overridden by the using module.

override_buildable(field, operator, value)

@callback override_buildable(
  field :: String.t() | atom(),
  operator :: String.t(),
  value :: any()
) :: Ecto.Query.dynamic_expr() | nil

Callback for handling custom filtering logic for overrideable fields.

This function acts as a fallback for overrideable fields. The default behavior is to return nil, but it can be overridden by the using module.