defmodule Mix.Tasks.Incident.Postgres.Init do
@moduledoc """
This task will generate some basic setup when using `PostgresAdapter`.
When using this adapter you will need to have a table to store the events and aggregate locks.
This task will generate an `Ecto` migration to create the `events` and `aggregate_locks` table
with the needed columns and indexes. The task will respect your `Ecto` configuration for your
`EventStoreRepo`.
# Usage
```
mix incident.postgres.init -r AppName.EventStoreRepo
```
"""
use Mix.Task
import Ecto.Migrator
import Macro, only: [camelize: 1, underscore: 1]
import Mix.{Ecto, Generator}
@shortdoc "Generates the initial setup for Incident with Postgres Adapter"
@impl true
def run(["-r", repo]) do
no_umbrella!("incident.postgres.init")
event_store_repo =
[repo]
|> Module.concat()
|> ensure_repo([])
# Generates the events table migration
name = "create_events_table"
path = Path.relative_to(migrations_path(event_store_repo), Mix.Project.app_path())
file = Path.join(path, "#{timestamp()}_#{underscore(name)}.exs")
create_directory(path)
content =
[module_name: Module.concat([event_store_repo, Migrations, camelize(name)])]
|> events_migration_template()
|> Code.format_string!()
create_file(file, content)
# Generates the aggregate_locks table migration
name = "create_aggregate_locks_table"
path = Path.relative_to(migrations_path(event_store_repo), Mix.Project.app_path())
file = Path.join(path, "#{timestamp(1)}_#{underscore(name)}.exs")
content =
[module_name: Module.concat([event_store_repo, Migrations, camelize(name)])]
|> aggregate_locks_migration_template()
|> Code.format_string!()
create_file(file, content)
end
@impl true
def run(_) do
Mix.shell().error("""
Error: you need to pass the Ecto Event Store Repo using the -r flag.
Please notice that this task should run after you have your Ecto repos
configuration all set in your application config files.
# Usage
```
mix incident.postgres.init -r AppName.EventStoreRepo
```
""")
end
@spec timestamp(non_neg_integer()) :: String.t()
defp timestamp(offset \\ 0) when offset >= 0 do
%DateTime{year: y, month: m, day: d, hour: hh, minute: mm, second: ss} =
DateTime.add(DateTime.utc_now(), offset, :second)
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
end
@spec pad(integer) :: String.t()
defp pad(i) when i < 10, do: <<?0, ?0 + i>>
defp pad(i), do: to_string(i)
embed_template(:events_migration, """
defmodule <%= inspect @module_name %> do
use Ecto.Migration
def change do
create table(:events, primary_key: false) do
add(:id, :bigserial, primary_key: true)
add(:event_id, :binary_id, null: false)
add(:aggregate_id, :string, null: false)
add(:event_type, :string, null: false)
add(:version, :integer, null: false)
add(:event_date, :utc_datetime_usec, null: false)
add(:event_data, :map, null: false)
timestamps(type: :utc_datetime_usec, updated_at: false)
end
create(index(:events, [:aggregate_id]))
create(index(:events, [:event_type]))
create(index(:events, [:event_date]))
create(index(:events, [:version]))
create constraint(:events, :version_must_be_positive, check: "version > 0")
end
end
""")
embed_template(:aggregate_locks_migration, """
defmodule <%= inspect @module_name %> do
use Ecto.Migration
def change do
create table(:aggregate_locks, primary_key: false) do
add(:id, :bigserial, primary_key: true)
add(:aggregate_id, :string, null: false)
add(:owner_id, :integer, null: false)
add(:valid_until, :utc_datetime_usec, null: false)
end
create(index(:aggregate_locks, [:aggregate_id]))
create(index(:aggregate_locks, [:aggregate_id, :owner_id]))
create(index(:aggregate_locks, [:valid_until]))
end
end
""")
end