defmodule OuterfacesEctoApi.QueryEngine.QueryJoiner do @moduledoc """ Provides functions for dynamically constructing Ecto queries with filters and nested joins. """ import Ecto.Query @spec ensure_joins(Ecto.Query.t(), list({atom(), module(), atom(), atom()})) :: Ecto.Query.t() def ensure_joins(query, chain) when is_list(chain) do chain |> Enum.reduce({query, nil}, fn {assoc_name, related_schema, owner_key, related_key}, {acc_query, parent_alias} -> expected_alias = if parent_alias, do: String.to_atom("#{parent_alias}_#{assoc_name}"), else: assoc_name if Enum.any?(acc_query.joins, fn j -> j.as == expected_alias end) do {acc_query, expected_alias} else new_query = do_join(acc_query, assoc_name, related_schema, owner_key, related_key, parent_alias) {new_query, expected_alias} end end) |> elem(0) end @spec do_join(Ecto.Query.t(), atom(), module(), atom(), atom(), atom() | nil) :: Ecto.Query.t() def do_join(query, assoc_name, related_schema, owner_key, related_key, parent_alias \\ nil) do new_alias = if parent_alias do String.to_atom("#{parent_alias}_#{assoc_name}") else assoc_name end if parent_alias do from([{^parent_alias, parent}] in query, left_join: assoc in ^related_schema, as: ^new_alias, on: field(parent, ^owner_key) == field(assoc, ^related_key) ) else from(e0 in query, left_join: assoc in ^related_schema, as: ^new_alias, on: field(e0, ^owner_key) == field(assoc, ^related_key) ) end end defmodule NamedBinding do @moduledoc """ Helper functions for working with named bindings in Ecto queries. """ @spec find_all(Ecto.Query.t(), list(atom())) :: list({atom(), integer(), Ecto.Query.Join.t()}) def find_all(query, binding_names) when is_list(binding_names) do binding_names |> Enum.reduce({[], nil}, fn binding_name, {acc, parent_alias} -> alias_to_find = if parent_alias do String.to_atom("#{parent_alias}_#{binding_name}") else binding_name end case find(query, alias_to_find) do nil -> {acc, alias_to_find} {idx, join} -> {[{alias_to_find, idx, join} | acc], alias_to_find} end end) |> elem(0) |> Enum.reverse() end @spec find(Ecto.Query.t(), atom()) :: {integer(), Ecto.Query.Join.t()} | nil def find(query, binding_name) do query.joins |> Enum.with_index(1) |> Enum.find_value(fn {join, idx} -> if join.as == binding_name, do: {idx, join}, else: nil end) end end end