mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-20 21:43:12 +12:00
104 lines
2.7 KiB
Elixir
104 lines
2.7 KiB
Elixir
|
defmodule AshPostgres.MultiTenancy do
|
||
|
@moduledoc "Helpers used to manage multitenancy"
|
||
|
|
||
|
@dialyzer {:nowarn_function, load_migration!: 1}
|
||
|
|
||
|
@tenant_name_regex ~r/^[a-zA-Z0-9_-]+$/
|
||
|
def create_tenant(tenant_name, repo) do
|
||
|
# This is done in a task, and manually cleaned up, because the
|
||
|
# ecto migrator runs its migrations in async tasks, so we can't
|
||
|
# be in a transaction while we do it
|
||
|
Ecto.Adapters.SQL.query!(repo, "CREATE SCHEMA IF NOT EXISTS \"#{tenant_name}\"", [])
|
||
|
|
||
|
migrate_tenant(tenant_name, repo)
|
||
|
|
||
|
:ok
|
||
|
rescue
|
||
|
exception ->
|
||
|
{:error, error_message(exception)}
|
||
|
end
|
||
|
|
||
|
def migrate_tenant(tenant_name, repo) do
|
||
|
tenant_migrations_path =
|
||
|
repo.config()[:tenant_migrations_path] || default_tenant_migration_path(repo)
|
||
|
|
||
|
Code.compiler_options(ignore_module_conflict: true)
|
||
|
|
||
|
[tenant_migrations_path, "**", "*.exs"]
|
||
|
|> Path.join()
|
||
|
|> Path.wildcard()
|
||
|
|> Enum.map(&extract_migration_info/1)
|
||
|
|> Enum.filter(& &1)
|
||
|
|> Enum.map(&load_migration!/1)
|
||
|
|> Enum.each(fn {version, mod} ->
|
||
|
Ecto.Migration.Runner.run(
|
||
|
repo,
|
||
|
[],
|
||
|
version,
|
||
|
mod,
|
||
|
:forward,
|
||
|
:up,
|
||
|
:up,
|
||
|
all: true,
|
||
|
prefix: tenant_name
|
||
|
)
|
||
|
end)
|
||
|
after
|
||
|
Code.compiler_options(ignore_module_conflict: false)
|
||
|
end
|
||
|
|
||
|
# sobelow_skip ["SQL"]
|
||
|
def rename_tenant(repo, old_name, new_name) do
|
||
|
validate_tenant_name!(old_name)
|
||
|
validate_tenant_name!(new_name)
|
||
|
Ecto.Adapters.SQL.query(repo, "ALTER SCHEMA \"#{old_name}\" RENAME TO \"#{new_name}\"")
|
||
|
:ok
|
||
|
end
|
||
|
|
||
|
defp load_migration!({version, _, file}) when is_binary(file) do
|
||
|
loaded_modules = file |> Code.compile_file() |> Enum.map(&elem(&1, 0))
|
||
|
|
||
|
if mod = Enum.find(loaded_modules, &migration?/1) do
|
||
|
{version, mod}
|
||
|
else
|
||
|
raise Ecto.MigrationError,
|
||
|
"file #{Path.relative_to_cwd(file)} does not define an Ecto.Migration"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp migration?(mod) do
|
||
|
function_exported?(mod, :__migration__, 0)
|
||
|
end
|
||
|
|
||
|
defp extract_migration_info(file) do
|
||
|
base = Path.basename(file)
|
||
|
|
||
|
case Integer.parse(Path.rootname(base)) do
|
||
|
{integer, "_" <> name} -> {integer, name, file}
|
||
|
_ -> nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp validate_tenant_name!(tenant_name) do
|
||
|
unless Regex.match?(@tenant_name_regex, tenant_name) do
|
||
|
raise "Tenant name must match #{inspect(@tenant_name_regex)}, got: #{tenant_name}"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp default_tenant_migration_path(repo) do
|
||
|
repo_name = repo |> Module.split() |> List.last() |> Macro.underscore()
|
||
|
|
||
|
"priv/"
|
||
|
|> Path.join(repo_name)
|
||
|
|> Path.join("tenant_migrations")
|
||
|
end
|
||
|
|
||
|
defp error_message(msg) do
|
||
|
if Exception.exception?(msg) do
|
||
|
Exception.message(msg)
|
||
|
else
|
||
|
msg
|
||
|
end
|
||
|
end
|
||
|
end
|