Current section
Files
Jump to
Current section
Files
lib/bind.ex
## File: bind/lib/bind.ex
defmodule Bind do
import Ecto.Query
@doc """
Merges additional filters with existing query params.
"""
def filter(query_string, filters) when is_binary(query_string) do
query_string
|> Bind.QueryString.to_map()
|> filter(filters)
end
def filter(params, filters) when is_map(params) do
Map.merge(params, filters)
end
@doc """
Builds an Ecto query for the given schema based on the provided parameters.
"""
def query(params, schema, opts \\ [])
def query(params, schema, opts) when is_map(params) do
allowed_joins = Keyword.get(opts, :joins, [])
case Bind.QueryBuilder.build_where_query(params, allowed_joins) do
{:error, reason} ->
{:error, reason}
where_query ->
sort_query = Bind.QueryBuilder.build_sort_query(params)
schema
|> where(^where_query)
|> Bind.QueryBuilder.apply_joins(params, allowed_joins)
|> order_by(^Enum.into(sort_query, []))
|> Bind.QueryBuilder.add_limit_query(params)
|> Bind.QueryBuilder.add_offset_query(params)
end
end
def query(query_string, schema, opts) when is_binary(query_string) do
query_string
|> Bind.QueryString.to_map()
|> query(schema, opts)
end
@doc """
Maps over query parameters, letting you transform values by pattern matching field names.
"""
def map(query_string, field_mappers) when is_binary(query_string) do
query_string
|> URI.decode_query()
|> map(field_mappers)
end
def map(params, field_mappers) when is_map(params) do
Enum.reduce(params, %{}, fn {key, value}, acc ->
case Bind.Parse.where_field(key) do
# Handle regular fields [field_name, constraint]
[field_name, _] ->
field = to_string(field_name)
new_value = find_mapper(field_mappers, field).(value)
Map.put(acc, key, new_value)
# Handle JSONB fields [json_field, json_key, constraint]
[json_field, json_key, _constraint] when is_binary(json_key) ->
field = to_string(json_field)
new_value = find_mapper(field_mappers, field).(value)
Map.put(acc, key, new_value)
# Handle join fields [assoc, field, constraint, :join]
[_assoc, field_name, _constraint, :join] ->
field = to_string(field_name)
new_value = find_mapper(field_mappers, field).(value)
Map.put(acc, key, new_value)
# Handle join JSONB fields [assoc, field, json_key, constraint, :join_jsonb]
[_assoc, field_name, _json_key, _constraint, :join_jsonb] ->
field = to_string(field_name)
new_value = find_mapper(field_mappers, field).(value)
Map.put(acc, key, new_value)
# Handle non-where fields (like start, limit)
nil ->
{field, is_negated} =
case String.starts_with?(key, "-") do
true -> {String.trim_leading(key, "-"), true}
false -> {key, false}
end
new_value = find_mapper(field_mappers, field).(value)
final_key = if is_negated, do: "-#{field}", else: field
Map.put(acc, final_key, new_value)
end
end)
end
defp find_mapper(mappers, field) do
case Map.get(mappers, String.to_atom(field)) do
nil ->
case Enum.find(mappers, fn
{%Regex{} = re, _} -> Regex.match?(re, field)
_ -> false
end) do
{_, mapper} -> mapper
nil -> & &1
end
mapper ->
mapper
end
end
@doc """
Maps over query parameters with error handling.
Returns {:ok, mapped_params} on success or {:error, reason} on failure.
"""
def map_safe(query_string, field_mappers) when is_binary(query_string) do
query_string
|> URI.decode_query()
|> map_safe(field_mappers)
end
def map_safe(params, field_mappers) when is_map(params) do
try do
result =
Enum.reduce_while(params, {:ok, %{}}, fn {key, value}, {:ok, acc} ->
case Bind.Parse.where_field(key) do
[field_name, _] ->
field = to_string(field_name)
handle_map_safe_field(key, value, field, field_mappers, acc)
[json_field, json_key, _constraint] when is_binary(json_key) ->
field = to_string(json_field)
handle_map_safe_field(key, value, field, field_mappers, acc)
[_assoc, field_name, _constraint, :join] ->
field = to_string(field_name)
handle_map_safe_field(key, value, field, field_mappers, acc)
[_assoc, field_name, _json_key, _constraint, :join_jsonb] ->
field = to_string(field_name)
handle_map_safe_field(key, value, field, field_mappers, acc)
nil ->
{field, is_negated} =
case String.starts_with?(key, "-") do
true -> {String.trim_leading(key, "-"), true}
false -> {key, false}
end
mapper = find_mapper(field_mappers, field)
if should_skip_transformation?(value) && has_custom_mapper?(field_mappers, field) do
{:cont, {:ok, acc}}
else
case apply_mapper_safe(mapper, value) do
{:ok, new_value} ->
final_key = if is_negated, do: "-#{field}", else: field
{:cont, {:ok, Map.put(acc, final_key, new_value)}}
{:error, reason} ->
{:halt, {:error, reason}}
end
end
end
end)
case result do
{:ok, mapped} -> {:ok, mapped}
{:error, reason} -> {:error, {:transformation_failed, reason}}
end
rescue
e -> {:error, {:transformation_failed, Exception.message(e)}}
end
end
# Helper for map_safe field handling
defp handle_map_safe_field(key, value, field, field_mappers, acc) do
mapper = find_mapper(field_mappers, field)
if should_skip_transformation?(value) && has_custom_mapper?(field_mappers, field) do
{:cont, {:ok, acc}}
else
case apply_mapper_safe(mapper, value) do
{:ok, new_value} -> {:cont, {:ok, Map.put(acc, key, new_value)}}
{:error, reason} -> {:halt, {:error, reason}}
end
end
end
defp apply_mapper_safe(mapper, value) do
case mapper.(value) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
result -> {:ok, result}
end
end
defp should_skip_transformation?(value) do
value in [nil, ""]
end
defp has_custom_mapper?(mappers, field) do
case Map.get(mappers, String.to_atom(field)) do
nil ->
Enum.any?(mappers, fn
{%Regex{} = re, _} -> Regex.match?(re, field)
_ -> false
end)
_ ->
true
end
end
end