2020-10-29 15:26:45 +13:00
|
|
|
defmodule AshPostgres.MultiTenancy do
|
|
|
|
@moduledoc "Helpers used to manage multitenancy"
|
|
|
|
|
|
|
|
@dialyzer {:nowarn_function, load_migration!: 1}
|
|
|
|
|
|
|
|
@tenant_name_regex ~r/^[a-zA-Z0-9_-]+$/
|
2021-01-27 09:07:26 +13:00
|
|
|
def create_tenant!(tenant_name, repo) do
|
2020-10-29 15:26:45 +13:00
|
|
|
# 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)
|
|
|
|
end
|
|
|
|
|
2021-01-27 13:16:29 +13:00
|
|
|
def migrate_tenant(tenant_name, repo, migrations_path \\ nil) do
|
2020-10-29 15:26:45 +13:00
|
|
|
tenant_migrations_path =
|
2021-01-27 13:16:29 +13:00
|
|
|
migrations_path ||
|
|
|
|
repo.config()[:tenant_migrations_path] || default_tenant_migration_path(repo)
|
2020-10-29 15:26:45 +13:00
|
|
|
|
|
|
|
Code.compiler_options(ignore_module_conflict: true)
|
|
|
|
|
2021-01-27 13:16:29 +13:00
|
|
|
Ecto.Migration.SchemaMigration.ensure_schema_migrations_table!(
|
|
|
|
repo,
|
|
|
|
repo.config(),
|
|
|
|
prefix: tenant_name
|
|
|
|
)
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
[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
|
|
|
|
)
|
2021-01-27 13:37:32 +13:00
|
|
|
|
2022-01-25 11:59:31 +13:00
|
|
|
Ecto.Migration.SchemaMigration.up(repo, repo.config(), version, prefix: tenant_name)
|
2020-10-29 15:26:45 +13:00
|
|
|
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)
|
2022-05-03 11:30:01 +12:00
|
|
|
|
|
|
|
if to_string(old_name) != to_string(new_name) do
|
|
|
|
Ecto.Adapters.SQL.query(repo, "ALTER SCHEMA \"#{old_name}\" RENAME TO \"#{new_name}\"")
|
|
|
|
end
|
|
|
|
|
2020-10-29 15:26:45 +13:00
|
|
|
: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()
|
2021-07-31 04:22:39 +12:00
|
|
|
otp_app = repo.config()[:otp_app]
|
2020-10-29 15:26:45 +13:00
|
|
|
|
2021-07-31 04:22:39 +12:00
|
|
|
:code.priv_dir(otp_app)
|
2020-10-29 15:26:45 +13:00
|
|
|
|> Path.join(repo_name)
|
|
|
|
|> Path.join("tenant_migrations")
|
|
|
|
end
|
|
|
|
end
|