lib/mix/tasks/pgflow.helpers.ex

defmodule Mix.Tasks.Pgflow.Helpers do
  @moduledoc false

  @doc false
  def resolve_repo(nil) do
    app = Mix.Project.config()[:app]

    case Application.get_env(app, :ecto_repos, []) do
      [repo] ->
        repo

      [repo | _] = repos ->
        Mix.shell().info(
          "Multiple repos configured (#{inspect(repos)}); using #{inspect(repo)}. " <>
            "Pass `--repo` to choose a specific one."
        )

        repo

      [] ->
        Mix.raise("No Ecto repos configured. Add `:ecto_repos` to your app config.")
    end
  end

  def resolve_repo(repo_string) do
    Module.concat([repo_string])
  end

  @doc false
  def get_app_module do
    case Mix.Project.config()[:app] do
      nil ->
        Mix.raise("Could not determine app name from Mix.Project")

      app ->
        app |> to_string() |> Macro.camelize()
    end
  end

  @doc false
  def generate_timestamp do
    {{year, month, day}, {hour, minute, second}} = :calendar.universal_time()

    :io_lib.format("~4..0B~2..0B~2..0B~2..0B~2..0B~2..0B", [
      year,
      month,
      day,
      hour,
      minute,
      second
    ])
    |> IO.iodata_to_binary()
  end

  @doc false
  def camelize(string) do
    string
    |> String.split("_")
    |> Enum.map_join(&String.capitalize/1)
  end

  @doc """
  Shared driver for the migration-generator mix tasks.

  Parses the standard `--migrations-path/-p` switch, ensures the directory
  exists, builds a timestamped filename like `<ts>_<suffix>.exs`, writes the
  migration body produced by `content_fn.(app_module)`, and prints the message
  produced by `message_fn.(filepath)`.

  Each generator task can stay a thin shim around this — keep the per-task
  content and message functions local for grep-ability and easier testing.
  """
  @spec write_migration([String.t()], String.t(), (String.t() -> String.t()), (String.t() ->
                                                                                 String.t())) ::
          :ok
  def write_migration(args, suffix, content_fn, message_fn \\ &default_migration_message/1)
      when is_binary(suffix) and is_function(content_fn, 1) and is_function(message_fn, 1) do
    {opts, _, _} =
      OptionParser.parse(args,
        switches: [migrations_path: :string],
        aliases: [p: :migrations_path]
      )

    migrations_path = Keyword.get(opts, :migrations_path, "priv/repo/migrations")
    File.mkdir_p!(migrations_path)

    timestamp = generate_timestamp()
    app_module = get_app_module()
    filepath = Path.join(migrations_path, "#{timestamp}_#{suffix}.exs")

    File.write!(filepath, content_fn.(app_module))
    Mix.shell().info(message_fn.(filepath))
    :ok
  end

  defp default_migration_message(filepath) do
    """
    Generated migration: #{filepath}

    Run the migration with:
        mix ecto.migrate
    """
  end

  @doc false
  def priv_path(repo) do
    case repo.config()[:priv] do
      nil -> "priv/repo"
      priv when is_binary(priv) -> priv
    end
  rescue
    _ -> "priv/repo"
  end

  @doc false
  def build_down_statements(flow_slug, has_cron, unschedule_sql)

  def build_down_statements(flow_slug, true, unschedule_sql) do
    """
        execute "#{unschedule_sql}"
    #{base_down_statements(flow_slug)}
    """
    |> String.trim_trailing()
  end

  def build_down_statements(flow_slug, false, _unschedule_sql) do
    base_down_statements(flow_slug)
  end

  defp base_down_statements(flow_slug) do
    """
        execute "DELETE FROM pgflow.deps WHERE flow_slug = '#{flow_slug}'"
        execute "DELETE FROM pgflow.steps WHERE flow_slug = '#{flow_slug}'"
        execute "DELETE FROM pgflow.flows WHERE flow_slug = '#{flow_slug}'"
        execute "SELECT pgmq.drop_queue('#{flow_slug}')"
    """
    |> String.trim_trailing()
  end
end