View Source Composite (composite v0.4.1)

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.

Link to this section Summary

Functions

Applies handlers to query.

Applies handlers to query.

Forces loading dependency even if it is not required by params.

Initializes a Composite struct with delayed application of query and params.

Initializes a Composite struct with query and params.

Defines a parameter handler.

Link to this section 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()}
Link to this type

load_dependency(query)

View Source
@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()]
}

Link to this section Functions

@spec apply(t(query)) :: query when query: any()

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.

Link to this function

apply(input_query, composite, params)

View Source
@spec apply(query, t(query), params()) :: query when query: any()

Applies handlers to query.

Used when composite is defined with new/0

Link to this function

dependency(composite, dependency, func, opts \\ [])

View Source
@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

Options

  • :requires - allows to set dependencies for current dependency.
Link to this function

force_require(composite, dependency_or_dependencies)

View Source
@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.

@spec new() :: t(any())

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()
Link to this function

new(input_query, params)

View Source
@spec new(query, params()) :: t(query) when query: any()

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.

Link to this function

param(composite, path, func, opts \\ [])

View Source
@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

Options

  • :ignore? - if function returns true, then handler apply_fun/1 won't be applied. Default value is &(&1 in [nil, "", [], %{}]).
  • :on_ignore - a function that will be applied instead of apply_fun/1 if value is ignored. Defaults to Function.identity/1.
  • :requires - points to the dependencies which has to be loaded before calling apply_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 to nil (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.