diff --git a/.gitignore b/.gitignore index e45aacc..d2673f1 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index dd47508..b2e33ac 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/fat_ecto/query/dynamics/buildable.ex b/lib/fat_ecto/query/dynamics/buildable.ex index 5c0b1b1..30acaf7 100644 --- a/lib/fat_ecto/query/dynamics/buildable.ex +++ b/lib/fat_ecto/query/dynamics/buildable.ex @@ -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 @@ -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. @@ -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 @@ -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 \\ []) @@ -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 diff --git a/test/fat_ecto/query/dynamics/buildable_default_dynamic_test.exs b/test/fat_ecto/query/dynamics/buildable_default_dynamic_test.exs new file mode 100644 index 0000000..5b4485d --- /dev/null +++ b/test/fat_ecto/query/dynamics/buildable_default_dynamic_test.exs @@ -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