Current section
Files
Jump to
Current section
Files
lib/extension.ex
defmodule QueryBuilder.Extension do
@moduledoc ~S"""
Use this module to create an extension module to `QueryBuilder` for app specific query utilities.
Use your query builder extension module wherever you would normally use `QueryBuilder`
Example:
```
defmodule MyApp.QueryBuilder do
use QueryBuilder.Extension, from_opts_full_ops: [:where_initcap]
import Ecto.Query
defmacro __using__(opts) do
quote do
require QueryBuilder
QueryBuilder.__using__(unquote(opts))
end
end
# Add app specific query functions
#---------------------------------
def where_initcap(query, field, value) do
text_equals_condition = fn field, value, get_binding_fun ->
{field, binding} = get_binding_fun.(field)
dynamic([{^binding, x}], fragment("initcap(?)", ^value) == field(x, ^field))
end
query
|> where(&text_equals_condition.(field, value, &1))
end
end
defmodule MyApp.Accounts.User do
use MyApp.QueryBuilder
schema "users" do
field :name, :string
field :active, :boolean
end
end
defmodule MyApp.Accounts do
alias MyApp.QueryBuilder, as: QB
def list_users(opts \\ []) do
# Query list can include custom query functions as well:
# [where_initcap: QB.args(:name, "john"), where: [active: true]]
MyApp.Accounts.User
|> QB.from_opts(opts, mode: :full)
|> Repo.all()
end
end
```
"""
defmacro __using__(opts) do
opts = validate_extension_opts!(opts)
from_opts_full_ops = Keyword.get(opts, :from_opts_full_ops, [])
boundary_ops_user_asserted = Keyword.get(opts, :boundary_ops_user_asserted, [])
Code.ensure_compiled!(QueryBuilder)
delegates =
QueryBuilder.__info__(:functions)
|> Enum.reject(fn {fun, _arity} ->
fun in [:module_info, :from_opts] or String.starts_with?(Atom.to_string(fun), "__")
end)
|> Enum.map(fn {fun, arity} ->
args = Macro.generate_arguments(arity, __MODULE__)
quote do
defdelegate unquote(fun)(unquote_splicing(args)), to: QueryBuilder
end
end)
quote do
# Expose all QueryBuilder functions: QueryBuilder.__info__(:functions)
@doc false
def __query_builder_extension_from_opts_config__() do
%{
from_opts_full_ops: unquote(from_opts_full_ops),
boundary_ops_user_asserted: unquote(boundary_ops_user_asserted)
}
end
unquote_splicing(delegates)
@doc ~S"""
Applies a keyword list of operations to a query.
Like `QueryBuilder.from_opts/*`, this defaults to boundary mode.
To use the full `from_opts` surface, pass `mode: :full`. Custom extension
operations are rejected by default in full mode; allowlist them via
`use QueryBuilder.Extension, from_opts_full_ops: [...]`.
Note: operations allowlisted via `boundary_ops_user_asserted: [...]` are also
accepted in full mode (full is a superset).
"""
def from_opts(query, opts) do
from_opts(query, opts, mode: :boundary)
end
def from_opts(query, opts, from_opts_opts) do
QueryBuilder.__from_opts__(query, opts, __MODULE__, from_opts_opts)
end
end
end
defp validate_extension_opts!(opts) when is_list(opts) do
unless Keyword.keyword?(opts) do
raise ArgumentError,
"use QueryBuilder.Extension expects options to be a keyword list, got: #{inspect(opts)}"
end
allowed_keys = [:from_opts_full_ops, :boundary_ops_user_asserted]
case Keyword.keys(opts) -- allowed_keys do
[] ->
:ok
unknown ->
raise ArgumentError,
"use QueryBuilder.Extension got unknown options #{inspect(unknown)}; " <>
"supported options: #{inspect(allowed_keys)}"
end
validate_extension_ops_list!(opts, :from_opts_full_ops)
validate_extension_ops_list!(opts, :boundary_ops_user_asserted)
opts
end
defp validate_extension_opts!(opts) do
raise ArgumentError,
"use QueryBuilder.Extension expects options to be a keyword list, got: #{inspect(opts)}"
end
defp validate_extension_ops_list!(opts, key) do
case Keyword.get(opts, key, []) do
list when is_list(list) ->
Enum.each(list, fn
op when is_atom(op) ->
:ok
other ->
raise ArgumentError,
"use QueryBuilder.Extension expects #{inspect(key)} to be a list of atoms, got: #{inspect(other)} in #{inspect(list)}"
end)
other ->
raise ArgumentError,
"use QueryBuilder.Extension expects #{inspect(key)} to be a list of atoms, got: #{inspect(other)}"
end
end
end