Skip to content

Instantly share code, notes, and snippets.

@yuchunc
Last active February 19, 2025 01:34
Show Gist options
  • Save yuchunc/dee7324a2bd4e8360793731289bc237f to your computer and use it in GitHub Desktop.
Save yuchunc/dee7324a2bd4e8360793731289bc237f to your computer and use it in GitHub Desktop.
defmodule EctoQueryMaker do
@moduledoc """
Query builder to abstract away boilerplate queries.
"""
import Ecto.Query
alias Ecto.{Query, Queryable}
@callback apply_filter(filter :: atom | {atom, any}, query :: Query.t()) :: Query.t()
@callback apply_option(opt :: atom | {atom, any}, query :: Query.t()) :: Query.t()
@callback apply_join(join :: atom | {atom, any}, query :: Query.t()) :: Query.t()
@callback apply_preload(preload :: atom | {atom, any}) :: atom | {atom, any}
@optional_callbacks apply_filter: 2, apply_option: 2, apply_join: 2, apply_preload: 1
@doc """
## Example
iex> # simple case
def get_resources(filters, opts) do
Resource
|> EctoQueryMaker.appy(filters, opts)
|> Repo.all
end
iex> # complex query in same module
def get_resources(filters, opts) do
Resource
|> EctoQueryMaker.appy(__MODULE__, filters, opts)
|> Repo.all
end
iex> # complex query module
defmodule ResourceQuery do
# custom filters
def apply_filter({key, value}, query)
def apply_filter(:complex_filter_key, query)
# custom joins
def apply_join({key, value}, query)
def apply_join(:complex_join_key, query)
# custom preloads
def apply_preload({key, value}, query)
def apply_preload(:complex_preload_key, query)
# custom options, use when doe not fit neatly in to previous queries
def apply_option({key, value}, query)
def apply_option(:complex_option_key, query)
end
iex> def get_resources(filters, opts) do
Resource
|> EctoQueryMaker.appy(ResourceQuery, filters, opts)
|> Repo.all
end
"""
@spec apply(Queryable.t(), list(), list()) :: Query.t()
def apply(query, filters, opts) do
apply(query, __MODULE__, filters, opts)
end
@spec apply(Queryable.t(), module(), list(), list()) :: Query.t()
def apply(query, module, filters, opts) do
Code.ensure_loaded(module)
query
|> apply_filters(filters, {function_exported?(module, :apply_filter, 2), module})
|> apply_options(opts, {function_exported?(module, :apply_option, 2), module})
end
defp apply_filters(query, [], _module_info), do: query
defp apply_filters(query, [filter | tail], {true, module} = module_info) do
case module.apply_filter(filter, query) do
^query -> apply_filter(filter, query)
query -> query
end
|> apply_filters(tail, module_info)
end
defp apply_filters(query, [filter | tail], module_info) do
apply_filter(filter, query)
|> apply_filters(tail, module_info)
end
defp apply_filter({attr, value}, query) when is_list(value) do
where(query, [r], field(r, ^attr) in ^value)
end
defp apply_filter({attr, value}, query) do
where(query, [r], field(r, ^attr) == ^value)
end
defp apply_options(query, [], _module_info), do: query
defp apply_options(query, [{:select, []} | tail], module_info) do
apply_options(query, tail, module_info)
end
defp apply_options(query, [{:select, select_by} | tail], module_info) do
query
|> select([q], map(q, ^select_by))
|> apply_options(tail, module_info)
end
defp apply_options(query, [{:preload, preloads} | tail], {_, module} = module_info) do
preloads = apply_preloads([], preloads, {function_exported?(module, :apply_preload, 1), module})
query
|> preload(^preloads)
|> apply_options(tail, module_info)
end
defp apply_options(query, [{:join, joins} | tail], {_, module} = module_info) do
query
|> apply_joins(joins, {function_exported?(module, :apply_join, 2), module})
|> apply_options(tail, module_info)
end
defp apply_options(query, [opt | tail], {true, module} = module_info) do
module.apply_option(opt, query)
|> apply_options(tail, module_info)
end
defp apply_options(query, [_ | tail], module_info) do
apply_options(query, tail, module_info)
end
defp apply_joins(query, [], _module_info), do: query
defp apply_joins(query, [join | tail], {true, module} = module_info) do
module.apply_join(join, query)
|> apply_joins(tail, module_info)
end
defp apply_joins(query, [_ | tail], module_info) do
apply_joins(query, tail, module_info)
end
defp apply_preloads(preloads, [], _module_info), do: preloads
defp apply_preloads(preloads, [preload | tail], {true, module} = module_info) do
[module.apply_preload(preload) | preloads]
|> apply_preloads(tail, module_info)
end
defp apply_preloads(preloads, [preload | tail], module_info) do
apply_preloads([preload | preloads], tail, module_info)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment