defmodule DynamoMigration do @moduledoc """ Version management module for migration file of DynamoDB. Dependes on ExAws and ExAws.Dynamo. See also https://github.com/ex-aws/ex_aws_dynamo. Usage: 1. $ mix dynamo.setup # Creates migrations table. 2. $ mix dynamo.gen.migration create_tests_table --table Tests # Generates migration file. ```elixir defmodule Dynamo.Migrations.CreateTestsTable do @table_name "Tests" def table_name, do: @table_name def change do # Rewrite any migration code. ExAws.Dynamo.create_table( "Tests", [id: :hash], %{id: :number}, 1, 1, :provisioned ) |> ExAws.request!() end end ``` 3. $ mix dynamo.migrate # Migrates `priv/dynamo/migrations/*`. """ require Logger alias ExAws.Dynamo @table "migrations" @migration_file_path "priv/dynamo/migrations" @doc false @spec migration_file_path :: String.t() def migration_file_path, do: @migration_file_path @doc """ Called from `mix dynamo.migrate` Executes migration files if there had not migrated. """ @spec migrate :: :ok def migrate do case File.ls(migration_file_path()) do {:ok, files} -> files |> compile_migrations() |> Enum.filter(fn {_, version} -> migration_required?(version) end) |> Enum.each(fn {mod, version} -> mod.change() insert_migration_version(mod.table_name(), version) Logger.debug("Migrate #{mod} was succeed") end) error -> error end end @doc """ Creates migrations table for version management. Called from `mix dynamo.setup` """ @spec setup :: :ok def setup do if @table in table_names() do Logger.debug("Migrations table was already created.") else Dynamo.create_table( @table, [version: :hash], [version: :number], 1, 1, :provisioned ) |> ExAws.request!() Logger.debug("Migrations table was created.") end end @doc """ Reset table migrations. """ def reset(only_prefix \\ nil) do IO.inspect(only_prefix) current_versions = versions() table_names() |> Enum.reject(&(&1 == @table)) |> Enum.filter(fn table_name -> unless only_prefix do true else table_name =~ Regex.compile!("\A#{table_name}") end end) |> Enum.each(&delete_table(&1, current_versions)) migrate() end @doc false @spec migration_required?(binary()) :: boolean def migration_required?(version) do Dynamo.get_item(@table, %{version: version}) |> ExAws.request!() |> (fn result -> result == %{} end).() end defp versions() do Dynamo.scan(@table) |> ExAws.request!() |> Map.get("Items", []) |> Enum.map(fn version -> %{ version: version["version"]["N"] |> String.to_integer(), table_name: version["table_name"]["S"] } end) end defp insert_migration_version(table_name, version) do Dynamo.put_item( @table, %{table_name: table_name, version: version} ) |> ExAws.request!() end defp compile_migrations(files) do files |> Enum.map(fn file -> [mod | _] = (migration_file_path() <> "/" <> file) |> Code.compile_file() |> Enum.map(&elem(&1, 0)) version = Regex.scan(~r/[0-9]/, file) |> Enum.join() |> String.to_integer() {mod, version} end) end defp delete_table(table_name, versions) do table_name |> Dynamo.delete_table() |> ExAws.request!() versions |> Enum.filter(&(table_name == &1[:table_name])) |> Enum.each(fn version -> Dynamo.delete_item(@table, %{version: version[:version]}) |> ExAws.request!() end) end defp table_names do Dynamo.list_tables() |> ExAws.request!() |> Map.get("TableNames", []) end end