Skip to main content

lib/mix/tasks/keksdose.install.ex

defmodule Mix.Tasks.Keksdose.Install do
  @moduledoc """
  Generates the migration for the `consent_records` table.

      mix keksdose.install
      mix keksdose.install --adapter mysql
      mix keksdose.install --adapter sqlite

  Detects the configured repo's adapter and emits the right column type for
  `categories` (jsonb / json / text). Override with `--adapter`.
  """

  use Mix.Task

  @shortdoc "Generates the keksdose migration"

  @template_path Path.expand("../../../priv/templates/add_consent_records_table.exs.eex", __DIR__)

  @adapter_columns %{
    postgres: ":jsonb",
    mysql: ":json",
    sqlite: ":text",
    other: ":text"
  }

  @adapter_module_map %{
    Ecto.Adapters.Postgres => :postgres,
    Ecto.Adapters.MyXQL => :mysql,
    Ecto.Adapters.SQLite3 => :sqlite,
    Ecto.Adapters.Tds => :other
  }

  @impl Mix.Task
  def run(args) do
    opts = opts(args)

    template_path = Keyword.get(opts, :template_path, @template_path)
    migrations_dir = Keyword.get(opts, :migrations_dir, Path.join(["priv", "repo", "migrations"]))
    module_prefix = Keyword.get(opts, :module_prefix, infer_module_prefix())
    adapter = resolve_adapter(opts)

    categories_column = Map.fetch!(@adapter_columns, adapter)

    File.mkdir_p!(migrations_dir)

    filename = "#{timestamp()}_add_consent_records_table.exs"
    target = Path.join(migrations_dir, filename)

    body =
      EEx.eval_file(template_path,
        module_prefix: module_prefix,
        categories_column: categories_column
      )

    File.write!(target, body)

    Mix.shell().info("""
    Created #{target}
      adapter:           #{adapter}
      categories column type: #{categories_column}

    Next steps:

      1. Add to config/config.exs:

           config :keksdose, repo: #{module_prefix}.Repo

      2. Run the migration:

           mix ecto.migrate

      3. Mount the plug in your router (pick any path):

           forward "/api/cookie-consents", Keksdose.PlugHandler

      4. Inject the endpoint URL into your layout:

           <%= raw Keksdose.FrontendConfig.inject_script("/api/cookie-consents") %>
    """)

    target
  end

  defp opts(args) do
    {opts, _, _} =
      OptionParser.parse(args,
        strict: [
          template_path: :string,
          migrations_dir: :string,
          module_prefix: :string,
          adapter: :string
        ]
      )

    opts
  end

  defp resolve_adapter(opts) do
    case Keyword.get(opts, :adapter) do
      flag when is_binary(flag) -> parse_adapter!(flag)
      nil -> adapter_from_configured_repo()
    end
  end

  defp adapter_from_configured_repo do
    with repo when is_atom(repo) and not is_nil(repo) <-
           Application.get_env(:keksdose, :repo),
         mod when not is_nil(mod) <- safe_repo_adapter(repo) do
      Map.get(@adapter_module_map, mod, :other)
    else
      _ -> :postgres
    end
  end

  defp safe_repo_adapter(repo) do
    repo.__adapter__()
  rescue
    _ -> nil
  end

  defp parse_adapter!(flag) do
    case String.downcase(flag) do
      "postgres" -> :postgres
      "postgresql" -> :postgres
      "mysql" -> :mysql
      "myxql" -> :mysql
      "sqlite" -> :sqlite
      "sqlite3" -> :sqlite
      "other" -> :other
      _ -> Mix.raise("Unknown --adapter: #{flag}. Expected postgres | mysql | sqlite.")
    end
  end

  defp infer_module_prefix do
    app = Mix.Project.config()[:app] || :my_app
    app |> Atom.to_string() |> Macro.camelize()
  end

  defp timestamp do
    Calendar.strftime(DateTime.utc_now(), "%Y%m%d%H%M%S")
  end
end