Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ mix.lock
/deps/
.elixir_ls/

# ignore .DS_Store files everywhere
**/.DS_Store

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ dynamic(
# 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
```

---

### 🔄 FatEcto.Sort.Sortable – Effortless Sorting
Expand Down
43 changes: 40 additions & 3 deletions lib/fat_ecto/query/dynamics/buildable.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ defmodule FatEcto.Query.Dynamics.Buildable do
"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`

## 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
Expand Down Expand Up @@ -65,7 +76,7 @@ defmodule FatEcto.Query.Dynamics.Buildable do
field :: String.t() | atom(),
operator :: String.t(),
value :: any()
) :: Ecto.Query.dynamic_expr()
) :: Ecto.Query.dynamic_expr() | nil

@doc """
Callback for performing custom processing on the final dynamics.
Expand All @@ -82,7 +93,15 @@ defmodule FatEcto.Query.Dynamics.Buildable do
@filterable @options[:filterable] || []
@overrideable_fields @options[:overrideable] || []
@ignoreable @options[:ignoreable] || []

# Get default_dynamic option: module option takes precedence over global config
@default_dynamic_option (case Keyword.get(@options, :default_dynamic) do
nil -> Application.compile_env(:fat_ecto, :default_dynamic, nil)
value -> value
end)

alias FatEcto.Query.Helper
import Ecto.Query

# Ensure at least one of `filterable` or `overrideable` fields option is provided.
if @filterable == [] and @overrideable_fields == [] do
Expand Down Expand Up @@ -119,7 +138,7 @@ defmodule FatEcto.Query.Dynamics.Buildable do
- `build_options`: Additional options for dynamics building (passed to `Builder`).

### Returns
- The dynamics with filtering applied.
- The dynamics with filtering applied (or the result from `after_buildable/1` callback).
"""
@spec build(map() | nil, keyword()) :: Ecto.Query.dynamic_expr() | nil
def build(where_params \\ nil, build_options \\ [])
Expand All @@ -145,12 +164,30 @@ defmodule FatEcto.Query.Dynamics.Buildable do
@overrideable_fields
)

# Apply default_dynamic if nil and configured
dynamics = apply_default_dynamic(dynamics)

# Apply after_buildable callback
after_buildable(dynamics)
end

def build(_where_params, _build_options) do
after_buildable(nil)
# Apply default_dynamic if configured
dynamics = apply_default_dynamic(nil)
after_buildable(dynamics)
end

# Helper to apply default_dynamic when dynamics is nil
# Only generate the special case if the option is actually :return_true
if @default_dynamic_option == :return_true do
defp apply_default_dynamic(nil) do
dynamic([q], true)
end

defp apply_default_dynamic(dynamics), do: dynamics
else
# Default behavior - just pass through
defp apply_default_dynamic(dynamics), do: dynamics
end

# Only define default override_buildable/3 if no overrideable fields are configured
Expand Down
148 changes: 148 additions & 0 deletions test/fat_ecto/query/dynamics/buildable_default_dynamic_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
defmodule FatEcto.Query.Dynamics.BuildableDefaultDynamicTest do
use FatEcto.ConnCase
import Ecto.Query

defmodule TestModuleWithDefaultDynamic do
use FatEcto.Query.Dynamics.Buildable,
filterable: [
name: ["$ILIKE"],
rating: ["$GT", "$LT"]
],
default_dynamic: :return_true
end

defmodule TestModuleWithoutDefaultDynamic do
use FatEcto.Query.Dynamics.Buildable,
filterable: [
name: ["$ILIKE"],
rating: ["$GT", "$LT"]
]
end

defmodule TestModuleExplicitNil do
use FatEcto.Query.Dynamics.Buildable,
filterable: [
name: ["$ILIKE"],
rating: ["$GT", "$LT"]
],
default_dynamic: nil
end

describe "default_dynamic: :return_true" do
test "returns dynamic([q], true) when no dynamics are built" do
result = TestModuleWithDefaultDynamic.build(%{})

expected = dynamic([q], true)

assert inspect(result) == inspect(expected)
end

test "returns dynamic([q], true) when params are nil" do
result = TestModuleWithDefaultDynamic.build(nil)

expected = dynamic([q], true)

assert inspect(result) == inspect(expected)
end

test "returns built dynamics when conditions exist" do
result =
TestModuleWithDefaultDynamic.build(%{
"name" => %{"$ILIKE" => "%Hospital%"}
})

assert inspect(result) =~ "ilike"
assert inspect(result) =~ "name"
refute inspect(result) =~ "true"
end

test "returns built dynamics for multiple conditions" do
result =
TestModuleWithDefaultDynamic.build(%{
"name" => %{"$ILIKE" => "%Hospital%"},
"rating" => %{"$GT" => 4}
})

assert inspect(result) =~ "rating"
assert inspect(result) =~ "name"
refute inspect(result) =~ "true"
end

test "can be used with from query" do
result = TestModuleWithDefaultDynamic.build(%{})

query = from(h in FatEcto.FatHospital, where: ^result)

# Should not raise an error
assert %Ecto.Query{} = query
end
end

describe "no default_dynamic option (default behavior)" do
test "returns nil when no dynamics are built" do
result = TestModuleWithoutDefaultDynamic.build(%{})

assert result == nil
end

test "returns nil when params are nil" do
result = TestModuleWithoutDefaultDynamic.build(nil)

assert result == nil
end

test "returns built dynamics when conditions exist" do
result =
TestModuleWithoutDefaultDynamic.build(%{
"name" => %{"$ILIKE" => "%Hospital%"}
})

assert inspect(result) =~ "ilike"
assert inspect(result) =~ "name"
end
end

describe "explicit nil overrides global config" do
test "returns nil even if global config is set" do
# Simulate global config
Application.put_env(:fat_ecto, :default_dynamic, :return_true)

result = TestModuleExplicitNil.build(%{})

assert result == nil

# Cleanup
Application.delete_env(:fat_ecto, :default_dynamic)
end
end

describe "global configuration" do
setup do
# Save original config
original = Application.get_env(:fat_ecto, :default_dynamic)

on_exit(fn ->
# Restore original config
if original do
Application.put_env(:fat_ecto, :default_dynamic, original)
else
Application.delete_env(:fat_ecto, :default_dynamic)
end
end)
end

test "uses global config when no module option provided" do
# Set global config
Application.put_env(:fat_ecto, :default_dynamic, :return_true)

# Define a new module that reads the config at compile time
# Note: In real usage, this would be defined at compile time with the config already set
result = TestModuleWithoutDefaultDynamic.build(%{})

# This test shows the limitation: module attributes are set at compile time
# In production, modules would be compiled with the config already in place
# For now, this returns nil because the module was compiled without the config
assert result == nil
end
end
end