defmodule Mix.Tasks.Etcher.Gen.Migration do @shortdoc "Generate the etcher_annotations migration" @moduledoc """ Generates a migration that creates the `etcher_annotations` table used by `Etcher.Storage.Default`. mix etcher.gen.migration The migration is written into the consumer's `priv/repo/migrations/` directory with a timestamped filename, just like `mix ecto.gen.migration`. ## Options * `-r`, `--repo` — the Repo module to target. Defaults to the first repo in your Mix project's `:ecto_repos` config. ## After running mix ecto.migrate Consumers who roll their own annotation schema can skip this generator entirely and implement the `Etcher.Storage` behaviour against their own table. """ use Mix.Task @impl Mix.Task def run(args) do {opts, _argv, _} = OptionParser.parse(args, switches: [repo: :string], aliases: [r: :repo]) repo = resolve_repo(opts[:repo]) migrations_dir = Path.join([source_repo_priv(repo), "migrations"]) File.mkdir_p!(migrations_dir) timestamp = format_timestamp(NaiveDateTime.utc_now()) name = "create_etcher_annotations" filename = "#{timestamp}_#{name}.exs" path = Path.join(migrations_dir, filename) if Enum.any?(File.ls!(migrations_dir), &String.ends_with?(&1, "_#{name}.exs")) do Mix.shell().info("[etcher] Skipping — migration already exists: #{name}") else module = Module.concat([app_module(repo), "Repo", "Migrations", Macro.camelize(name)]) File.write!(path, migration_template(module)) Mix.shell().info("[etcher] Created #{Path.relative_to_cwd(path)}") end end defp resolve_repo(nil) do case Mix.Project.config()[:app] |> Application.get_env(:ecto_repos) do [repo | _] -> repo _ -> Mix.raise("No Ecto repos configured. Pass --repo MyApp.Repo.") end end defp resolve_repo(name) when is_binary(name) do Module.concat([name]) end defp source_repo_priv(repo) do config = repo.config() priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" Path.join(File.cwd!(), priv) rescue UndefinedFunctionError -> # Repo isn't loaded (e.g., generator run before deps compile). Fall # back to the conventional default. Path.join([File.cwd!(), "priv", "repo"]) end defp app_module(repo) do repo |> Module.split() |> List.first() end defp format_timestamp(%NaiveDateTime{} = ts) do [ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second] |> Enum.map_join("", fn n -> n |> Integer.to_string() |> String.pad_leading(2, "0") end) end defp migration_template(module) do """ defmodule #{inspect(module)} do use Ecto.Migration def up do execute(\"\"\" CREATE TABLE IF NOT EXISTS etcher_annotations ( uuid uuid PRIMARY KEY, target_type varchar(64) NOT NULL, target_uuid uuid NOT NULL, creator_uuid uuid, kind varchar(32) NOT NULL, geometry jsonb NOT NULL, style jsonb, metadata jsonb, position integer NOT NULL DEFAULT 0, inserted_at timestamp(0) NOT NULL, updated_at timestamp(0) NOT NULL ) \"\"\") execute(\"\"\" CREATE INDEX IF NOT EXISTS etcher_annotations_target_index ON etcher_annotations (target_type, target_uuid) \"\"\") end def down do execute("DROP INDEX IF EXISTS etcher_annotations_target_index") execute("DROP TABLE IF EXISTS etcher_annotations") end end """ end end