defmodule Mix.ArangoXEcto do
@moduledoc """
Migration task helper functions
"""
@typedoc """
Specifies a migration version type
"""
@type version :: String.t() | Integer.t()
@doc """
Creates the database specified in the config
"""
@spec create_base_database() :: {:ok, String.t()} | {:error, Integer.t()}
def create_base_database do
get_database_name!()
|> create_database()
end
@doc """
Creates a new database of `name`
"""
@spec create_database(String.t()) :: {:ok, String.t()} | {:error, Integer.t()}
def create_database(name) when is_binary(name) do
{:ok, conn} = system_db()
Arangox.post(conn, "/_api/database", %{name: name})
|> format_response(name)
end
@doc """
Creates the migrations collection
Creates an empty system collection to store migrations.
"""
@spec create_migrations :: :ok | {:error, Integer.t()}
def create_migrations do
{:ok, conn} = system_db()
Arangox.post(conn, "/_api/collection", %{
type: 2,
isSystem: true,
name: "_migrations"
})
|> format_response()
end
@doc """
Creates a document to store migrations
Creates a document with key of the database name. Seperating the migrations
into seperate documents allow for more organisation and better debugging.
"""
@spec create_migration_document() :: :ok | {:error, Integer.t()}
def create_migration_document do
{:ok, conn} = system_db()
key =
config()
|> Keyword.fetch!(:database)
Arangox.post(conn, "/_api/document/_migrations", %{_key: key, migrations: []})
|> format_response()
end
@doc """
Gets the path to the priv repo folder
Will return a full file path to the priv/repo folder.
"""
@spec path_to_priv_repo(Ecto.Repo.t()) :: String.t()
def path_to_priv_repo(repo) do
app = Keyword.fetch!(repo.config(), :otp_app)
repo_dir =
repo.get_dynamic_repo()
|> to_string()
|> String.split(".")
|> List.last()
|> Macro.underscore()
Path.join([Mix.Project.deps_paths()[app] || File.cwd!(), "priv", repo_dir])
end
@doc """
Creates a timestamp for the migration file
"""
@spec timestamp() :: String.t()
def timestamp do
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
end
@doc """
Gets the default repo's database name
"""
@spec get_database_name!() :: String.t()
def get_database_name! do
config()
|> Keyword.fetch!(:database)
end
@doc """
Gets all the migrated versions
"""
@spec migrated_versions(String.t()) :: [String.t() | Integer.t()]
def migrated_versions(db_name) do
{:ok, conn} = system_db()
{:ok, versions} =
query(conn, """
RETURN DOCUMENT("_migrations/#{db_name}").migrations
""")
versions
end
@doc """
Updates the migrated versions in the migration collection
"""
@spec update_versions(version(), String.t()) :: [version()]
def update_versions(version, db_name) when is_binary(version),
do: update_versions(String.to_integer(version), db_name)
def update_versions(version, db_name) do
{:ok, conn} = system_db()
migrated = migrated_versions(db_name)
if Enum.member?(migrated, version) do
migrated
else
new_versions = [version | migrated]
{:ok, _} =
Arangox.patch(conn, "/_api/document/_migrations/#{db_name}", %{migrations: new_versions})
new_versions
end
end
@doc """
Removes a version from the migrations collection
"""
@spec remove_version(version(), String.t()) :: [version()]
def remove_version(version, db_name) when is_binary(version),
do: remove_version(String.to_integer(version), db_name)
def remove_version(version, db_name) do
{:ok, conn} = system_db()
new_versions =
migrated_versions(db_name)
|> List.delete(version)
{:ok, _} =
Arangox.patch(conn, "/_api/document/_migrations/#{db_name}", %{migrations: new_versions})
new_versions
end
@doc """
Gets the default repo
The first in the list of running repos is used.
"""
@spec get_default_repo!() :: Ecto.Repo.t()
def get_default_repo! do
Mix.Ecto.parse_repo([])
|> List.first()
|> case do
nil -> Mix.raise("No Default Repo Found")
repo -> repo
end
end
## Helpers
defp query(conn, query_str) do
Arangox.transaction(conn, fn cursor ->
cursor
|> Arangox.cursor(query_str)
|> Enum.reduce([], fn resp, acc ->
acc ++ resp.body["result"]
end)
|> List.flatten()
end)
end
defp config(opts \\ []) do
get_default_repo!().config()
|> Keyword.merge(opts)
|> ensure_endpoint_value()
end
defp ensure_endpoint_value(config) do
if Keyword.has_key?(config, :endpoints) do
config
else
Keyword.put(config, :endpoints, "http://localhost:8529")
end
end
defp system_db do
options =
config(
pool_size: 1,
database: "_system"
)
Arangox.start_link(options)
end
defp format_response(response, pass_arg \\ nil)
defp format_response({:ok, _}, nil), do: :ok
defp format_response({:ok, _}, pass_arg) when not is_nil(pass_arg), do: {:ok, pass_arg}
defp format_response({:error, %{status: status}}, _), do: {:error, status}
defp format_response({:error, _reason}, _), do: {:error, 0}
defp pad(i) when i < 10, do: <<?0, ?0 + i>>
defp pad(i), do: to_string(i)
end