Skip to main content

lib/mix/tasks/attached.gen.migration.ex

defmodule Mix.Tasks.Attached.Gen.Migration do
  @shortdoc "Generates a migration for an attachment field"
  @moduledoc """
  Generates an Ecto migration for an `attached` field on a schema.

      $ mix attached.gen.migration MyApp.Accounts.User avatar

  Adds an `avatar_attached_original_id` column to the schema's table
  (respects `config :attached, default_foreign_key_suffix: "_original_id"`).
  """

  use Mix.Task

  import Mix.Generator

  @impl true
  def run(args) do
    case args do
      [schema_module, field] ->
        generate(schema_module, field)

      _ ->
        Mix.shell().error("""
        Usage: mix attached.gen.migration Schema field

        Example:
          mix attached.gen.migration MyApp.Accounts.User avatar
        """)
    end
  end

  defp generate(schema_module, field) do
    migrations_path = Path.join(["priv", "repo", "migrations"])
    File.mkdir_p!(migrations_path)

    module_parts = String.split(schema_module, ".")
    schema_name = List.last(module_parts) |> Macro.underscore()
    table_name = schema_name <> "s"

    timestamp = timestamp()
    name = "add_#{field}_attached_original_to_#{table_name}"
    filename = "#{timestamp}_#{name}.exs"
    content = one_migration(schema_module, table_name, field, name)

    path = Path.join(migrations_path, filename)
    create_file(path, content)
    Mix.shell().info("Run `mix ecto.migrate` to apply.")
  end

  defp one_migration(schema_module, table_name, field, name) do
    app = Mix.Project.config()[:app]

    migration_module =
      "#{app |> to_string() |> Macro.camelize()}.Repo.Migrations.#{Macro.camelize(name)}"

    fk_suffix = Application.get_env(:attached, :default_foreign_key_suffix, "_attached_original_id")
    fk_col = "#{field}#{fk_suffix}"

    """
    defmodule #{migration_module} do
      use Ecto.Migration

      def change do
        # #{schema_module} — attached :#{field}
        alter table(:#{table_name}) do
          add :#{fk_col}, references(:attached_originals, type: :binary_id, on_delete: :restrict)
        end

        create index(:#{table_name}, [:#{fk_col}])
      end
    end
    """
  end

  defp timestamp do
    {{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
    "#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
  end

  defp pad(i) when i < 10, do: "0#{i}"
  defp pad(i), do: "#{i}"
end