Packages

Builds dynamic Ecto queries based on given parameters, allowing developers to retrieve data flexibly without writing custom queries for each use case.

Current section

Files

Jump to
bind lib query_builder.ex
Raw

lib/query_builder.ex

defmodule Bind.QueryBuilder do
import Ecto.Query
def add_limit_query(query, params) do
case Map.get(params, "limit") do
nil ->
Ecto.Query.limit(query, [r], 10)
limit_param when is_integer(limit_param) ->
Ecto.Query.limit(query, [r], ^limit_param)
limit_param when is_binary(limit_param) ->
Ecto.Query.limit(query, [r], ^String.to_integer(limit_param))
end
end
def add_offset_query(query, params) do
cond do
start_id = Map.get(params, "start") ->
Ecto.Query.where(query, [r], field(r, :id) > ^start_id)
start_id = Map.get(params, "-start") ->
Ecto.Query.where(query, [r], field(r, :id) < ^start_id)
true ->
query
end
end
def build_where_query(params, allowed_joins \\ []) do
case validate_where_query(params, allowed_joins) do
{:error, reason} ->
{:error, reason}
nil ->
Enum.reduce(params, Ecto.Query.dynamic(true), fn {param, param_value}, dynamic ->
case Bind.Parse.where_field(param) do
nil ->
dynamic
# Join fields - skip in where, handled separately
[_assoc, _field, _constraint, :join] ->
dynamic
[_assoc, _field, _json_key, _constraint, :join_jsonb] ->
dynamic
parsed_field ->
case build_constraint(parsed_field, param_value) do
{:error, reason} ->
{:error, reason}
constraint ->
dynamic([r], ^dynamic and ^constraint)
end
end
end)
end
end
def validate_where_query(params, allowed_joins \\ []) do
Enum.find_value(params, fn {param, param_value} ->
case Bind.Parse.where_field(param) do
nil ->
nil
# Validate join is allowed
[assoc, _field, _constraint, :join] ->
if assoc in allowed_joins do
nil
else
{:error, "Join not allowed: #{assoc}"}
end
# Validate join_jsonb is allowed
[assoc, _field, _json_key, _constraint, :join_jsonb] ->
if assoc in allowed_joins do
nil
else
{:error, "Join not allowed: #{assoc}"}
end
parsed_field ->
case build_constraint(parsed_field, param_value) do
{:error, reason} -> {:error, reason}
_ -> nil
end
end
end)
end
def build_sort_query(params) do
case params["sort"] do
nil -> [asc: :id]
"" -> [asc: :id]
sort_param -> Bind.Parse.sort_field(sort_param)
end
end
@doc """
Extracts join fields from params and groups by association.
Returns map of %{assoc_name => [{:field, field, constraint, value} | {:jsonb, field, json_key, constraint, value}, ...]}
"""
def extract_joins(params) do
Enum.reduce(params, %{}, fn {param, value}, acc ->
case Bind.Parse.where_field(param) do
[assoc, field, constraint, :join] ->
joins = Map.get(acc, assoc, [])
Map.put(acc, assoc, [{:field, field, constraint, value} | joins])
[assoc, field, json_key, constraint, :join_jsonb] ->
joins = Map.get(acc, assoc, [])
Map.put(acc, assoc, [{:jsonb, field, json_key, constraint, value} | joins])
_ ->
acc
end
end)
end
@doc """
Builds join clauses and where conditions for joined associations.
"""
def apply_joins(query, params, allowed_joins) do
joins = extract_joins(params)
Enum.reduce(joins, query, fn {assoc, constraints}, q ->
if assoc in allowed_joins do
# Add join
q = join(q, :inner, [r], j in assoc(r, ^assoc), as: ^assoc)
# Add where conditions for this join
Enum.reduce(constraints, q, fn
{:field, field, constraint, value}, q2 ->
dynamic = join_constraint(assoc, field, constraint, value)
where(q2, ^dynamic)
{:jsonb, field, json_key, constraint, value}, q2 ->
dynamic = join_jsonb_constraint(assoc, field, json_key, constraint, value)
where(q2, ^dynamic)
end)
else
q
end
end)
end
def constraint(field, "search", value) do
tsquery =
value
|> String.split(~r/\s+/, trim: true)
|> Enum.map(&"#{&1}:*")
|> Enum.join(" & ")
dynamic([r], fragment("? @@ to_tsquery('simple', ?)", field(r, ^field), ^tsquery))
end
defp join_constraint(assoc, field, "search", value) do
tsquery =
value
|> String.split(~r/\s+/, trim: true)
|> Enum.map(&"#{&1}:*")
|> Enum.join(" & ")
dynamic([{^assoc, j}], fragment("? @@ to_tsquery('simple', ?)", field(j, ^field), ^tsquery))
end
# Build dynamic for join constraint
defp join_constraint(assoc, field, "eq", value) do
dynamic([{^assoc, j}], field(j, ^field) == ^value)
end
defp join_constraint(assoc, field, "neq", value) do
dynamic([{^assoc, j}], field(j, ^field) != ^value)
end
defp join_constraint(assoc, field, "gt", value) do
dynamic([{^assoc, j}], field(j, ^field) > ^value)
end
defp join_constraint(assoc, field, "gte", value) do
dynamic([{^assoc, j}], field(j, ^field) >= ^value)
end
defp join_constraint(assoc, field, "lt", value) do
dynamic([{^assoc, j}], field(j, ^field) < ^value)
end
defp join_constraint(assoc, field, "lte", value) do
dynamic([{^assoc, j}], field(j, ^field) <= ^value)
end
defp join_constraint(assoc, field, "contains", value) do
dynamic([{^assoc, j}], ilike(field(j, ^field), ^"%#{value}%"))
end
defp join_constraint(assoc, field, "starts_with", value) do
dynamic([{^assoc, j}], ilike(field(j, ^field), ^"#{value}%"))
end
defp join_constraint(assoc, field, "ends_with", value) do
dynamic([{^assoc, j}], ilike(field(j, ^field), ^"%#{value}"))
end
defp join_constraint(assoc, field, "true", _value) do
dynamic([{^assoc, j}], field(j, ^field) == true)
end
defp join_constraint(assoc, field, "false", _value) do
dynamic([{^assoc, j}], field(j, ^field) == false)
end
defp join_constraint(assoc, field, "nil", value) when value in ["true", true] do
dynamic([{^assoc, j}], is_nil(field(j, ^field)))
end
defp join_constraint(assoc, field, "nil", value) when value in ["false", false] do
dynamic([{^assoc, j}], not is_nil(field(j, ^field)))
end
defp join_constraint(assoc, field, "in", value) do
values = String.split(value, ",")
dynamic([{^assoc, j}], field(j, ^field) in ^values)
end
# Build dynamic for join JSONB constraint
defp join_jsonb_constraint(assoc, field, json_key, "eq", value) do
dynamic([{^assoc, j}], fragment("? ->> ? = ?", field(j, ^field), ^json_key, ^value))
end
defp join_jsonb_constraint(assoc, field, json_key, "contains", value) do
dynamic(
[{^assoc, j}],
fragment("? ->> ? ILIKE ?", field(j, ^field), ^json_key, ^"%#{value}%")
)
end
defp join_jsonb_constraint(assoc, field, json_key, "starts_with", value) do
dynamic([{^assoc, j}], fragment("? ->> ? ILIKE ?", field(j, ^field), ^json_key, ^"#{value}%"))
end
defp join_jsonb_constraint(assoc, field, json_key, "ends_with", value) do
dynamic([{^assoc, j}], fragment("? ->> ? ILIKE ?", field(j, ^field), ^json_key, ^"%#{value}"))
end
# Handle both regular fields and JSONB fields
defp build_constraint([field, constraint], value) do
constraint(field, constraint, value)
end
defp build_constraint([json_field, json_key, constraint], value) do
jsonb_constraint(json_field, json_key, constraint, value)
end
@doc """
Parses a constraint and returns a dynamic query fragment.
"""
def constraint(field, "eq", value) do
dynamic([r], field(r, ^field) == ^value)
end
def constraint(field, "neq", value) do
dynamic([r], field(r, ^field) != ^value)
end
def constraint(field_name, "gt", value) do
dynamic([r], field(r, ^field_name) > ^value)
end
def constraint(field_name, "gte", value) do
dynamic([r], field(r, ^field_name) >= ^value)
end
def constraint(field, "lt", value) do
dynamic([r], field(r, ^field) < ^value)
end
def constraint(field, "lte", value) do
dynamic([r], field(r, ^field) <= ^value)
end
def constraint(field, "true", _value) do
dynamic([r], field(r, ^field) == true)
end
def constraint(field, "false", _value) do
dynamic([r], field(r, ^field) == false)
end
def constraint(field, "starts_with", value) do
dynamic([r], ilike(field(r, ^field), ^"#{value}%"))
end
def constraint(field, "ends_with", value) do
dynamic([r], ilike(field(r, ^field), ^"%#{value}"))
end
def constraint(field, "in", value) do
values = String.split(value, ",")
dynamic([r], field(r, ^field) in ^values)
end
def constraint(field, "contains", value) do
dynamic([r], ilike(field(r, ^field), ^"%#{value}%"))
end
def constraint(field, "nil", value) when value in ["true", true] do
dynamic([r], is_nil(field(r, ^field)))
end
def constraint(field, "nil", value) when value in ["false", false] do
dynamic([r], not is_nil(field(r, ^field)))
end
def constraint(field, "empty", value) when value in ["true", true] do
dynamic([r], fragment("jsonb_array_length(?) = 0", field(r, ^field)))
end
def constraint(field, "empty", value) when value in ["false", false] do
dynamic([r], fragment("jsonb_array_length(?) > 0", field(r, ^field)))
end
def constraint(field, constraint, _value) do
{:error, "Invalid constraint: #{field}[#{constraint}]"}
end
# JSONB constraint functions
def jsonb_constraint(json_field, json_key, "eq", value) do
dynamic([r], fragment("? ->> ? = ?", field(r, ^json_field), ^json_key, ^value))
end
def jsonb_constraint(json_field, json_key, "contains", value) do
dynamic([r], fragment("? ->> ? ILIKE ?", field(r, ^json_field), ^json_key, ^"%#{value}%"))
end
def jsonb_constraint(json_field, json_key, "starts_with", value) do
dynamic([r], fragment("? ->> ? ILIKE ?", field(r, ^json_field), ^json_key, ^"#{value}%"))
end
def jsonb_constraint(json_field, json_key, "ends_with", value) do
dynamic([r], fragment("? ->> ? ILIKE ?", field(r, ^json_field), ^json_key, ^"%#{value}"))
end
def jsonb_constraint(json_field, json_key, "empty", value) when value in ["true", true] do
dynamic(
[r],
fragment("jsonb_array_length(? -> ?) = 0", field(r, ^json_field), ^json_key)
)
end
def jsonb_constraint(json_field, json_key, "empty", value) when value in ["false", false] do
dynamic(
[r],
fragment("jsonb_array_length(? -> ?) > 0", field(r, ^json_field), ^json_key)
)
end
def jsonb_constraint(json_field, json_key, constraint, _value) do
{:error, "Invalid JSONB constraint: #{json_field}.#{json_key}[#{constraint}]"}
end
end