Current section

Files

Jump to
igniter lib igniter libs ecto.ex
Raw

lib/igniter/libs/ecto.ex

defmodule Igniter.Libs.Ecto do
@moduledoc "Codemods & utilities for working with Ecto"
import Macro, only: [camelize: 1, underscore: 1]
@known_repos [
Ecto.Repo,
AshPostgres.Repo,
AshSqlite.Repo,
AshMysql.Repo
]
@doc """
Generates a new migration file for the given repo.
## Options
- `:body` - the body of the migration
- `:timestamp` - the timestamp to use for the migration.
Primarily useful for testing so you know what the filename will be.
- `:on_exists` - what to do if the migration *module* already exists. Options are:
- `:increment` - Calls this function again, but with an increasing number at the end, until it finds a free name. (default)
- `:skip` - do nothing
- `:overwrite` - overwrites the file
- `{:error, error}` - adds an issue to the igniter that prevents writing and displays to the user
- `{:warning, warning}` - adds a warning to the igniter that allows writing but displays to the user
"""
@spec gen_migration(Igniter.t(), repo :: module(), name :: String.t(), opts :: Keyword.t()) ::
Igniter.t()
def gen_migration(igniter, repo, name, opts \\ []) do
# getting the repos configuration will be a bit harder here
# we'd need to look in config files or in the repos init or in the repo function?
# not worth it for now
path =
Path.join(
"priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}",
"migrations"
)
base_name = "#{underscore(name)}.exs"
file = Path.join(path, "#{opts[:timestamp] || timestamp()}_#{base_name}")
igniter = Igniter.include_glob(igniter, Path.join(path, "**/*.exs"))
body =
opts[:body] ||
"""
def change do
# your migration here
end
"""
module = Module.concat([repo, Migrations, camelize(name)])
case Igniter.Project.Module.module_exists(igniter, module) do
{true, igniter} ->
case Keyword.get(opts, :on_exists, :increment) do
:skip ->
igniter
:increment ->
name
|> String.split("_", trim: true)
|> List.last()
|> Integer.parse()
|> case do
{integer, ""} when is_integer(integer) ->
gen_migration(igniter, repo, name <> "_#{integer + 1}", opts)
_ ->
gen_migration(igniter, repo, name <> "_1", opts)
end
:overwrite ->
Igniter.create_new_file(
igniter,
file,
"""
defmodule #{inspect(module)} do
use Ecto.Migration
#{body}
end
""",
on_exists: :overwrite
)
{:error, error} ->
Igniter.add_issue(igniter, error)
{:warning, error} ->
Igniter.add_warning(igniter, error)
end
{false, igniter} ->
Igniter.create_new_file(igniter, file, """
defmodule #{inspect(module)} do
use Ecto.Migration
#{body}
end
""")
end
end
@doc """
Selects a repo module from the list of available repos.
## Options
* `:label` - The label to display to the user when selecting the repo
"""
@spec select_repo(Igniter.t(), Keyword.t()) :: {Igniter.t(), nil | module()}
def select_repo(igniter, opts \\ []) do
label = Keyword.get(opts, :label, "Which repo should be used?")
case list_repos(igniter) do
{igniter, []} ->
{igniter, nil}
{igniter, [repo]} ->
{igniter, repo}
{igniter, repos} ->
{igniter, Igniter.Util.IO.select(label, repos, display: &inspect/1)}
end
end
@doc "Lists all the ecto repos in the project"
@spec list_repos(Igniter.t()) :: {Igniter.t(), [module()]}
def list_repos(igniter) do
Igniter.Project.Module.find_all_matching_modules(igniter, fn _mod, zipper ->
move_to_repo_use(zipper) != :error
end)
end
defp move_to_repo_use(zipper) do
Igniter.Code.Function.move_to_function_call(zipper, :use, [1, 2], fn zipper ->
Enum.any?(@known_repos, fn repo ->
Igniter.Code.Function.argument_equals?(
zipper,
0,
repo
)
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, ?0 + i>>
defp pad(i), do: to_string(i)
end