View Source QueryElf behaviour (query_elf v0.4.0)

Defines an Ecto query builder.

It accepts the following options:

  • :schema - the Ecto.Schema for which the queries will be built (required)
  • :searchable_fields - a list of fields to build default filters for. This option is simply a shorthand syntax for using the QueryElf.Plugins.AutomaticFilters plugin with the given list as the fields option. You should check the plugin's documentation for more details. (default: [])
  • :sortable_fields - a list of fields to build default sorters for. This option is simply a shorthand syntax for using the QueryElf.Plugins.AutomaticSorters plugin with the given list as the fields option. You should check the plugin's documentation for more details. (default: [])
  • :plugins - a list of plugins that can be used to increment the query builder's functionality. See QueryElf.Plugin for more details. (default: [])

Example

defmodule MyQueryBuilder do
  use QueryElf,
    schema: MySchema,
    searchable_fields: [:id, :name],
    plugins: [
      {QueryElf.Plugins.OffsetPagination, default_per_page: 10},
      MyCustomPlugin
    ]

  def filter(:my_filter, value, _query) do
    dynamic([s], s.some_field - ^value == 0)
  end
end

MyQueryBuilder.build_query(id__in: [1, 2, 3], my_filter: 10)

Sharing plugins and configuration accross query builders

Sometimes you have certain plugins that you wish to always use, while allowing some degree of fexibility to each individual query builder definition. For those scenarios, you can use something like the following:

defmodule MyQueryElf do
  defmacro __using__(opts) do
    # Always define filters for the `id` field
    searchable_fields = (opts[:searchable_fields] || []) ++ [:id]

    # Always define a sorter for the `id` field
    sortable_fields = (opts[:sortable_fields] || []) ++ [:id]

    # Use a default per page of `20`, but allow the user to change this value
    default_per_page = opts[:default_per_page] || 20

    # Allow the user to include extra plugins
    extra_plugins = opts[:plugins] || []

    quote do
      use QueryElf,
        schema: unquote(opts[:schema]),
        plugins: [
          {QueryElf.Plugins.AutomaticFilters, fields: unquote(searchable_fields)},
          {QueryElf.Plugins.AutomaticSorters, fields: unquote(sortable_fields)},
          {QueryElf.Plugins.OffsetPagination, default_per_page: unquote(default_per_page)},
          # put any other plugins here
        ] ++ unquote(extra_plugins)
    end
  end
end

defmodule MyQueryBuilder do
  use MyQueryElf,
    schema: MySchema,
    searchable_fields: ~w[id name age is_active roles]a
end

Using this strategy you can create a re-usable set of default plugins (and plugin configurations) that best suits your application needs, while allowing you to use QueryElf without these defaults if you ever need to.

Summary

Callbacks

Should return the query builder metadata requested.

Should return the base query in this query builder. This base query will be used when defining the empty_query/0 and build_query/1 callbacks. If not defined, defaults to the supplied schema.

Should receive a a keyword list or a map containing parameters and use it to build a query.

Same thing as build_query/1, but also receives some options for things like pagination and ordering.

The same thing as build_query/2, but instead of building a new query it receives and extends an existing one.

Should return a query that when applied, returns nothing.

Receives an atom representing a filter, the parameter for the aforementioned filter and an Ecto query. Should return an Ecto dynamic or a tuple containing an Ecto query and an Ecto dynamic.

Receives an atom representing a field to order by, the order direction, an extra argument to perform the ordering, and an Ecto query. Should return the Ecto query with the appropriate sorting options applied.

Functions

Similar to Ecto.Query.join/{4,5}, but can be called multiple times with the same alias.

Types

@type filter() :: keyword() | map()
@type metadata_type() :: :filters
@type options() :: keyword()
@type sort_direction() :: :asc | :desc

Callbacks

Link to this callback

__query_builder__(metadata_type)

View Source
@callback __query_builder__(metadata_type()) :: term()

Should return the query builder metadata requested.

@callback base_query() :: Ecto.Queryable.t()

Should return the base query in this query builder. This base query will be used when defining the empty_query/0 and build_query/1 callbacks. If not defined, defaults to the supplied schema.

This is useful when dealing with logical deletion or other business rules that need to be followed every time the query builder is used. Example:

defmodule UsersQueryBuilder do
  use QueryElf,
    schema: User,
    searchable_fields: [:id, :name]

  def base_query do
    from u in User, where: is_nil(u.deleted_at)
  end
end
@callback build_query(filter()) :: Ecto.Query.t()

Should receive a a keyword list or a map containing parameters and use it to build a query.

Link to this callback

build_query(filter, options)

View Source
@callback build_query(filter(), options()) :: Ecto.Query.t()

Same thing as build_query/1, but also receives some options for things like pagination and ordering.

Link to this callback

build_query(t, filter, options)

View Source
@callback build_query(Ecto.Query.t(), filter(), options()) :: Ecto.Query.t()

The same thing as build_query/2, but instead of building a new query it receives and extends an existing one.

@callback empty_query() :: Ecto.Query.t()

Should return a query that when applied, returns nothing.

Link to this callback

filter(atom, term, t)

View Source (optional)
@callback filter(atom(), term(), Ecto.Query.t()) ::
  Ecto.Query.dynamic() | {Ecto.Query.t(), Ecto.Query.dynamic()}

Receives an atom representing a filter, the parameter for the aforementioned filter and an Ecto query. Should return an Ecto dynamic or a tuple containing an Ecto query and an Ecto dynamic.

Most of the filtering should happen in the returned dynamic, and returning a query should only be used when the query needs to be extended for the dynamic to make sense. Example:

# this is a simple comparison, so just returning a dynamic will suffice
def filter(:my_filter, value, _query) do
  dynamic([s], s.some_field == ^value)
end

# this relies on a join, so the query must be extended accordingly
def filter(:my_other_filter, value, query) do
  {
    join(query, :left, [s], assoc(s, :some_relationship), as: :related),
    dynamic([related: r], r.some_field == ^value)
  }
end
Link to this callback

sort(atom, sort_direction, term, t)

View Source (optional)
@callback sort(atom(), sort_direction(), term(), Ecto.Query.t()) :: Ecto.Query.t()

Receives an atom representing a field to order by, the order direction, an extra argument to perform the ordering, and an Ecto query. Should return the Ecto query with the appropriate sorting options applied.

Example:

  def sort(:name, direction, _arg, query) do
    sort(query, [s], [{^direction, s.name}])
  end

Functions

Link to this macro

reusable_join(query, qual, bindings, expr, opts)

View Source (macro)

Similar to Ecto.Query.join/{4,5}, but can be called multiple times with the same alias.

Note that only the first join operation is performed, the subsequent ones that use the same alias are just ignored. Also note that because of this behaviour, its mandatory to specify an alias when using this function.

This is helpful when you need to perform a join while building queries one filter at a time, because the same filter could be used multiple times or you could have multiple filters that require the same join.

This scenario poses a problem with how the filter/3 callback work, as you need to return a dynamic with the filtering, which means that the join must have an alias, and by default Ecto raises an error when you add multiple joins with the same alias.

To solve this, it is recommended to use this macro instead of the default Ecto.Query.join/{4,5}. As an added bonus, there will be only one join in the query that can be reused by multiple filters.