View Source Composite (composite v0.4.3)
A utility for writing dynamic queries.
It allows getting rid of some boilerplate when building a query based on input parameters.
params = %{search_query: "John Doe"}
User
|> where(active: true)
|> Composite.new(params)
|> Composite.param(:org_id, &filter_by_org_id/2)
|> Composite.param(:search_query, &search_by_full_name/2)
|> Composite.param(:org_name, &filter_by_org_name/2, requires: :org)
|> Composite.param(:org_type, &filter_by_org_type/2, requires: :org)
|> Composite.dependency(:org, &join_orgs/1)
|> Repo.all()
Even though most of the examples in this doc use Ecto
, Composite itself is not limited only to it.
Ecto
is an optional dependency and it is present only for having an implementation of Ecto.Queryable
OOTB.
You're able to use Composite with any Elixir term, as it is just an advanced wrapper around Enum.reduce/3
.
Summary
Types
@type apply_fun(query) :: (query, value :: any() -> query) | (query -> query)
@type dependencies() :: nil | dependency_name() | [dependency_name()]
@type dependency_name() :: atom()
@type dependency_option() :: {:requires, dependencies()}
@type load_dependency(query) :: (query -> query) | (query, params() -> query)
@type param_option(query) :: {:requires, dependencies() | (value :: any() -> dependencies())} | {:ignore?, (any() -> boolean())} | {:on_ignore, (query -> query)} | {:ignore_requires, dependencies()}
@type param_path_item() :: any()
@type params() :: Access.t()
@type t(query) :: %Composite{ dep_definitions: %{ optional(dependency_name()) => [ {load_dependency(query), [dependency_option()]} ] }, input_query: query, param_definitions: [ {[param_path_item()], apply_fun(query), [param_option(query)]} ], params: params() | nil, required_deps: [dependency_name()] }
Functions
Applies handlers to query.
Used when composite is defined with new/2
.
If used with Ecto
, then calling this function is not necessary,
as Composite
implements Ecto.Queryable
protocol, so applying will be done automatically when it is needed.
Applies handlers to query.
Used when composite is defined with new/0
@spec dependency(t(query), dependency_name(), load_dependency(query), [ dependency_option() ]) :: t(query) when query: any()
Defines a dependency loader.
Dependency is an instruction which is being applied lazily to a query. The same dependency can be required by many parameters, but it will be invoked only once. Dependency can depend on other dependency.
Useful for joining tables.
params = %{org_type: :nonprofit, is_org_closed: false}
User
|> Composite.new(params)
|> Composite.param(:is_org_closed, &where(&1, [orgs: orgs], orgs.closed == ^&2), requires: :orgs)
|> Composite.param(:org_type, &where(&1, [orgs: orgs], orgs.type == ^&2), requires: :orgs)
|> Composite.dependency(:orgs, &join(&1, :inner, [users], orgs in assoc(users, :org), as: :orgs))
It is also possible to require a dependency only if specific value is set. In example below dependency :phone
will be
loaded only if value of :search
param starts from +
sign
composite
|> Composite.param(
:search,
fn
query, "+" <> _ = phone_number -> where(query, [phones: phones], phones.number == ^phone_number)
query, query_string -> where(query, [records], ilike(records.text, ^query_string))
end,
requires: fn
"+" <> _ -> :phone
_ -> nil
end
)
|> Composite.dependency(:phone, &join(&1, :inner, [records], phones in assoc(records, :phone), as: :phones))
When loader
function has arity 2, then all parameters are passed in the second argument.
Options
:requires
- allows to set dependencies for current dependency.
@spec force_require(t(query), dependency_name() | [dependency_name()]) :: t(query) when query: any()
Forces loading dependency even if it is not required by params
.
Initializes a Composite
struct with delayed application of query
and params
.
Must be used with apply/3
.
composite =
Composite.new()
|> Composite.param(:organization_id, &where(&1, organization_id: ^&2))
|> Composite.param(:age_more_than, &where(&1, [users], users.age > ^&2))
params = %{organization_id: 1}
User
|> where(active: true)
|> Composite.apply(composite, params)
|> Repo.all()
Initializes a Composite
struct with query
and params
.
Must be used with apply/1
.
This strategy is useful when working with Ecto.Query
in pipe-based queries.
params = %{organization_id: 1}
User
|> where(active: true)
|> Composite.new(params)
|> Composite.param(:organization_id, &where(&1, organization_id: ^&2))
|> Composite.param(:age_more_than, &where(&1, [users], users.age > ^&2))
|> Repo.all()
Please note, that there is no explicit Composite.apply/1
call before Repo.all/1
, because Composite
implements Ecto.Queryable
protocol.
@spec param(t(query), param_path_item() | [param_path_item()], apply_fun(query), [ param_option(query) ]) :: t(query) when query: any()
Defines a parameter handler.
Handler is applied to a query when apply/1
or apply/3
is invoked.
All handlers are invoke in the same order as they are defined.
If the parameter requires dependencies, then they will be loaded before the parameters' handler and only if
parameter wasn't ignored. Examples with dependencies usage can be found in doc for dependency/4
params = %{location: "Arctic", order: :age_desc}
User
|> Composite.new(params)
|> Composite.param(:location, &where(&1, location: ^&2),
ignore?: &(&1 in [nil, "WORLDWIDE", ""])
)
|> Composite.param(
:order,
fn
query, :name_asc -> query |> order_by(name: :asc)
query, :age_desc -> query |> order_by(age: :desc)
end,
on_ignore: &order_by(&1, inserted_at: :desc)
)
If input parameters have nested maps (or any other key-based data structure):
params = %{filter: %{name: "John"}}
User
|> Composite.new(params)
|> Composite.param([:filter, :name], &where(&1, name: ^&2))
Options
:ignore?
- if function returnstrue
, then handlerapply_fun/1
won't be applied. Default value is&(&1 in [nil, "", [], %{}])
.:on_ignore
- a function that will be applied instead ofapply_fun/1
if value is ignored. Defaults toFunction.identity/1
.:requires
- points to the dependencies which has to be loaded before callingapply_fun/1
. It is, also, possible to specify dependencies dynamically based on a value of the parameter by passing a function. The latter function will always receive not ignored values. Defaults tonil
(which is equivalent to[]
).:ignore_requires
- points to the dependencies which has to be loaded when value is ignored. May be needed for custom:on_ignore
implementation.