lib/context.ex

defmodule Uploadex.Context do
  @moduledoc """
  Context Helper functions for handling files.

  Note that all functions in this module require the Uploader as an argument. You are free to call them like that:

      iex> Uploadex.Context.create_with_file(changeset, Repo, MyUploader)
      {:ok, %User{}}

  However, by doing `use Uploadex, repo: Repo` in your uploader, you can call these functions directly through the uploader to avoid having to pass these
  extra arguments around:

      iex> MyUploader.create_with_file(changeset)
      {:ok, %User{}}
  """

  alias Ecto.{
    Changeset,
    Multi,
  }
  alias Uploadex.Uploader

  @doc """
  Inserts the changeset and store the record files in a database transaction,
  so if the files fail to be stored the record will not be created.
  """
  @spec create_with_file(Changeset.t, module, Uploader.t, keyword) :: {:ok, any} | {:error, any()}
  def create_with_file(changeset, repo, uploader, opts \\ []) do
    Multi.new()
    |> Multi.run(:insert, fn _repo, _ -> repo.insert(changeset, opts) end)
    |> Multi.run(:store_files, fn _repo, %{insert: record} -> uploader.store_files(record) end)
    |> repo.transaction()
    |> convert_result()
  end

  @doc """
  Updates the record and its files in a database transaction,
  so if the files fail to be stored the record will not be created.

  This function also deletes files that are no longer referenced.
  """
  @spec update_with_file(Changeset.t, any, module, Uploader.t, keyword) :: {:ok, any} | {:error, any()}
  def update_with_file(changeset, previous_record, repo, uploader, opts \\ []) do
    Multi.new()
    |> Multi.run(:update, fn _repo, _ -> repo.update(changeset, opts) end)
    |> Multi.run(:store_files, fn _repo, %{update: record} -> uploader.store_files(record, previous_record) end)
    |> Multi.run(:delete_file, fn _repo, %{update: record} -> uploader.delete_previous_files(record, previous_record) end)
    |> repo.transaction()
    |> convert_result()
  end

  @doc """
  Similar to `update_with_file/3`, but does not delete previous files.
  """
  @spec update_with_file_keep_previous(Changeset.t, module, Uploader.t, keyword) :: {:ok, any} | {:error, any()}
  def update_with_file_keep_previous(changeset, repo, uploader, opts \\ []) do
    Multi.new()
    |> Multi.run(:update, fn _repo, _ -> repo.update(changeset, opts) end)
    |> Multi.run(:store_files, fn _repo, %{update: record} -> uploader.store_files(record) end)
    |> repo.transaction()
    |> convert_result()
  end

  @doc """
  Deletes the record and all of its files.
  This is not in a database transaction, since the delete operation never returns errors.
  """
  @spec delete_with_file(any, module, Uploader.t, keyword) :: {:ok, any} | {:error, any()}
  def delete_with_file(record_or_changeset, repo, uploader, opts \\ []) do
    case repo.delete(record_or_changeset, opts) do
      {:ok, record} -> uploader.delete_files(record)
      {:error, error} -> {:error, error}
    end
  end

  defp convert_result({:error, _, msg, _}), do: {:error, msg}
  defp convert_result({:ok, %{insert: record}}), do: {:ok, record}
  defp convert_result({:ok, %{update: record}}), do: {:ok, record}
end