lib/query_builder/schema.ex

defmodule EctoShorts.QueryBuilder.Schema do
  @moduledoc """
  This module contains query building parts for schemas themselves,
  when passed a query it can pull the schema from it and attempt
  to filter on any natural field
  """

  require Logger
  require Ecto.Query

  alias EctoShorts.QueryBuilder
  alias EctoShorts.QueryBuilder.Schema.ComparisonFilter

  @behaviour QueryBuilder

  @impl QueryBuilder
  def create_schema_filter({filter_field, val}, query) do
    create_schema_filter(
      {filter_field, val},
      QueryBuilder.query_schema(query),
      query
    )
  end

  def create_schema_filter({filter_field, val}, schema, query) do
    cond do
      filter_field in schema.__schema__(:query_fields) ->
        case schema.__schema__(:type, filter_field) do
          {:array, _} ->
            ComparisonFilter.build_array(query, schema.__schema__(:field_source, filter_field), val)
          _ ->
            ComparisonFilter.build(query, schema.__schema__(:field_source, filter_field), val)
        end

      filter_field in schema.__schema__(:associations) ->
        binding_alias = :"ecto_shorts_#{filter_field}"

        query = Ecto.Query.join(
          query,
          :inner,
          [scm],
          assoc in assoc(scm, ^filter_field)
        )

        query = %{query |
          aliases: add_relational_alias(query, binding_alias),
          joins: add_join_alias(query, filter_field, binding_alias)
        }

        ComparisonFilter.build_relational(query, binding_alias, val)

      true ->
        Logger.debug("[EctoShorts] #{Atom.to_string(filter_field)} is not a field for #{schema.__schema__(:source)} where filter")

        query
    end
  end

  defp add_relational_alias(query, new_alias) do
    if query.aliases[new_alias] do
      raise ArgumentError, message: "already defined #{new_alias} as an alias within query #{inspect query}"
    else
      # Here we need to put the size of current aliases plus one to indicate the next value
      Map.put(query.aliases, new_alias, map_size(query.aliases) + 1)
    end
  end

  defp add_join_alias(query, filter_field, binding_alias) do
    Enum.map(query.joins, fn join ->
      if elem(join.assoc, 1) === filter_field do
        %{join | as: binding_alias}
      else
        join
      end
    end)
  end
end