Current section
Files
Jump to
Current section
Files
lib/mix/tasks/durable.gen.migration.ex
defmodule Mix.Tasks.Durable.Gen.Migration do
@shortdoc "Generates a new Durable migration"
@moduledoc """
Generates a new Durable internal migration.
This is for Durable library developers only, not end users.
Host applications should use `mix durable.gen.upgrade` instead.
$ mix durable.gen.migration add_compensation_tracking
The generated migration will be created at:
lib/durable/migration/migrations/v{timestamp}_{name}.ex
## Examples
$ mix durable.gen.migration add_workflow_metadata
$ mix durable.gen.migration add_retry_count
After generating a migration, you must:
1. Implement the `up/1` and `down/1` callbacks
2. Add the migration module to `@migrations` in `lib/durable/migration/migrator.ex`
"""
use Mix.Task
import Mix.Generator
@impl true
def run(args) do
case args do
[name | _] ->
generate_migration(name)
[] ->
Mix.raise("Expected migration name, e.g. mix durable.gen.migration add_new_field")
end
end
defp generate_migration(name) do
# Validate name
unless valid_name?(name) do
Mix.raise(
"Migration name must be lowercase with underscores, e.g. add_new_field, not #{name}"
)
end
# Generate timestamp
timestamp = generate_timestamp()
version = String.to_integer(timestamp)
# Build paths and names
module_name = camelize(name)
filename = "v#{timestamp}_#{name}.ex"
migrations_dir = Path.join(["lib", "durable", "migration", "migrations"])
filepath = Path.join(migrations_dir, filename)
# Ensure directory exists
File.mkdir_p!(migrations_dir)
# Generate the migration file
assigns = [
version: version,
module_name: module_name,
timestamp: timestamp
]
create_file(filepath, migration_template(assigns))
Mix.shell().info("""
Remember to add this migration to the @migrations list in:
lib/durable/migration/migrator.ex
Add:
Durable.Migration.Migrations.V#{timestamp}#{module_name}
""")
end
defp valid_name?(name) do
name =~ ~r/^[a-z][a-z0-9_]*$/
end
defp generate_timestamp do
{{year, month, day}, {hour, minute, second}} = :calendar.universal_time()
"#{year}" <>
String.pad_leading("#{month}", 2, "0") <>
String.pad_leading("#{day}", 2, "0") <>
String.pad_leading("#{hour}", 2, "0") <>
String.pad_leading("#{minute}", 2, "0") <>
String.pad_leading("#{second}", 2, "0")
end
defp camelize(name) do
name
|> String.split("_")
|> Enum.map_join(&String.capitalize/1)
end
defp migration_template(assigns) do
version = Keyword.fetch!(assigns, :version)
module_name = Keyword.fetch!(assigns, :module_name)
timestamp = Keyword.fetch!(assigns, :timestamp)
"""
defmodule Durable.Migration.Migrations.V#{timestamp}#{module_name} do
@moduledoc false
use Durable.Migration.Base
@impl true
def version, do: #{format_version(version)}
@impl true
def up(prefix) do
# Add your migration here
# Example:
# alter table(:workflow_executions, prefix: prefix) do
# add(:new_field, :string)
# end
:ok
end
@impl true
def down(prefix) do
# Add your rollback here
# Example:
# alter table(:workflow_executions, prefix: prefix) do
# remove(:new_field)
# end
:ok
end
end
"""
end
defp format_version(version) when is_integer(version) do
# Format with underscores for readability: 20_260_103_000_000
version
|> Integer.to_string()
|> String.graphemes()
|> Enum.chunk_every(2)
|> Enum.map_join("_", &Enum.join/1)
end
end