lib/ash/resource/transformers/create_join_relationship.ex

defmodule Ash.Resource.Transformers.CreateJoinRelationship do
  @moduledoc """
  Creates an automatically named `has_many` relationship for each many_to_many.
  """
  use Spark.Dsl.Transformer

  alias Spark.{Dsl.Transformer, Error.DslError}

  @extension Ash.Resource.Dsl

  def transform(dsl_state) do
    dsl_state
    |> Transformer.get_entities([:relationships])
    |> Enum.filter(&(&1.type == :many_to_many))
    |> Enum.reduce_while({:ok, dsl_state}, fn relationship, {:ok, dsl_state} ->
      dsl_state
      |> Transformer.get_entities([:relationships])
      |> Enum.find(&(&1.name == relationship.join_relationship))
      |> case do
        nil when relationship.through == nil ->
          error =
            DslError.exception(
              path: [:relationships, relationship.name],
              message:
                "Either `through` or `join_relationship` with an existing relationship is required."
            )

          {:halt, {:error, error}}

        nil ->
          {:ok, join_relationship} =
            Transformer.build_entity(
              @extension,
              [:relationships],
              :has_many,
              [
                name: relationship.join_relationship,
                destination: relationship.through,
                destination_attribute: relationship.source_attribute_on_join_resource,
                api: relationship.api,
                source_attribute: relationship.source_attribute,
                private?: true
              ]
              |> add_messages(relationship)
            )

          join_relationship =
            Map.put(join_relationship, :autogenerated_join_relationship_of, relationship.name)

          dsl_state = Transformer.add_entity(dsl_state, [:relationships], join_relationship)

          {:cont, {:ok, dsl_state}}

        join_relationship ->
          relationship =
            %{
              relationship
              | through: join_relationship.destination,
                source_attribute: join_relationship.source_attribute,
                source_attribute_on_join_resource: join_relationship.destination_attribute
            }

          dsl_state =
            Transformer.replace_entity(
              dsl_state,
              [:relationships],
              relationship,
              &(&1.name == relationship.name)
            )

          {:cont, {:ok, dsl_state}}
      end
    end)
  end

  defp add_messages(opts, relationship) do
    new_opts =
      [
        not_found_message: relationship.not_found_message,
        violation_message: relationship.violation_message
      ]
      |> Enum.reject(fn {_, v} ->
        is_nil(v)
      end)

    Keyword.merge(opts, new_opts)
  end

  def before?(Ash.Resource.Transformers.SetRelationshipSource), do: true
  def before?(_), do: false
end