lib/ex_teal/action.ex

defmodule ExTeal.Action do
  @moduledoc """
  ExTeal Actions allow you to perform one off tasks on your ExTeal Index resources with custom conditions.
  For example you might want to batch update a group of articles to a published state.

  Each ExTeal action should contain a `commit/3` function.

  **Fields have not been implemented yet**

  The commit function is responsible for modifying the state of the application to achieve the desired actions.
  Let's look at the `PublishAction` action:

  ```
  defmodule PortfolioWeb.ExTeal.PublishAction do
    use ExTeal.Action
    alias ExTeal.ActionResponse

    def title, do: "Publish" // defaults to Publish Action
    def key, do: "publish" // default to publish-action

    def commit(conn, fields, query) do
      resources = Repo.all(query) //having the raw query gives you the option to batch large requests.

      with {:ok, _results} <- Portfolio.Content.publish(resources) do //ideally the updates here are part of a Repo.transaction.
        ActionResponse.success("Successfully published")
       _ ->
        ActionResponse.error("Error publishing")
      end
    end
  end
  ```

  The commit should return an ActionResponse struct. The ActionResponse types that are available are 'success', 'error', 'redirect', 'download' and 'push'.
  """

  alias ExTeal.Resource.{Index, Records}
  alias Plug.Conn

  import Ecto.Query, only: [where: 3]

  @type action_responses :: :ok | {:error, String.t()} | ExTeal.ActionResponse.t()

  @callback options(Plug.Conn.t()) :: map()

  @callback commit(Plug.Conn.t(), [ExTeal.Field.t()], Ecto.Query.t()) :: action_responses()

  @doc """
  Override the default title for the action
  """
  @callback title() :: String.t()

  @doc """
  Override the default key for the action
  """
  @callback key() :: String.t()

  defmacro __using__(_) do
    quote do
      @behaviour ExTeal.Action
      alias ExTeal.Action

      @implied_title Phoenix.Naming.resource_name(__MODULE__)
      @implied_key Phoenix.Naming.underscore(@implied_title)

      def title, do: @implied_title
      def key, do: @implied_key

      def build_for(conn), do: Action.build_for(__MODULE__, conn)

      def options(_conn),
        do: %{
          available_for_entire_resource: true
        }

      defoverridable options: 1, title: 0, key: 0
    end
  end

  def build_for(action_module, conn) do
    %{
      key: action_module.key(),
      title: action_module.title(),
      options: action_module.options(conn),
      destructive: false,
      fields: []
    }
  end

  def action_for_key(actions, key) do
    case Enum.find(actions, &(key == &1.key())) do
      nil -> {:error, :not_found}
      action -> {:ok, action}
    end
  end

  def apply_action(resource, %Conn{params: params} = conn) do
    with {:ok, action} <- action_for_key(resource.actions(conn), params["action"]),
         {:ok, query} <- actionable_query(resource, conn) do
      fields = Map.get(params, "fields", %{})

      {:ok, action.commit(conn, fields, query)}
    else
      {:error, reason} -> {:error, reason}
    end
  end

  def actionable_query(resource, %Conn{params: %{"resources" => "all"} = params} = conn) do
    query =
      conn
      |> resource.handle_index(params)
      |> Records.preload(resource)
      |> Index.filter_via_relationships(params)
      |> Index.field_filters(conn, resource)
      |> Index.search(params, resource)

    {:ok, query}
  end

  def actionable_query(resource, %Conn{params: %{"resources" => ids} = params} = conn) do
    ids = ids |> String.split(",") |> Enum.map(&String.to_integer/1)

    query =
      conn
      |> resource.handle_index(params)
      |> Records.preload(resource)
      |> Index.filter_via_relationships(params)
      |> Index.field_filters(conn, resource)
      |> Index.search(params, resource)
      |> where([r], r.id in ^ids)

    {:ok, query}
  end

  def message(message), do: %{message: message}

  def error(message), do: %{error: message}
end