defmodule Mix.Tasks.Relyra.Install do
@moduledoc false
use Mix.Task
@shortdoc "Scaffold the minimal Relyra integration surface"
@impl true
def run(args) do
{opts, _argv, _invalid} =
OptionParser.parse(args,
switches: [
module: :string,
router: :string,
repo: :string,
live_admin: :boolean,
admin_path: :string,
no_config: :boolean,
force: :boolean
],
aliases: [m: :module]
)
module_name = Keyword.get(opts, :module, "MyApp")
force = Keyword.get(opts, :force, false)
no_config = Keyword.get(opts, :no_config, false)
root_module_path = module_path(module_name)
ensure_generated_file!(
Path.join(["lib", root_module_path, "relyra", "connections.ex"]),
connection_template(module_name),
force
)
ensure_generated_file!(
Path.join(["lib", root_module_path, "relyra", "user_mapper.ex"]),
user_mapper_template(module_name),
force
)
if Keyword.get(opts, :live_admin, false) do
ensure_generated_file!(
Path.join(["lib", root_module_path, "relyra", "admin_scope.ex"]),
admin_scope_template(module_name),
force
)
end
unless no_config do
ensure_config!()
end
maybe_update_router(Keyword.get(opts, :router), module_name, force, opts)
repo_label = Keyword.get(opts, :repo, "the host app")
Mix.shell().info("Relyra install scaffolded for #{module_name} in #{repo_label}.")
end
defp ensure_generated_file!(path, contents, force) do
File.mkdir_p!(Path.dirname(path))
cond do
force or not File.exists?(path) ->
File.write!(path, contents)
true ->
Mix.shell().info("Skipped existing #{path} (pass --force to overwrite)")
end
end
defp ensure_config! do
File.mkdir_p!("config")
path = "config/config.exs"
sentinel_start = "# --- Relyra START ---"
sentinel_end = "# --- Relyra END ---"
snippet = """
#{sentinel_start}
config :relyra,
connection_resolver: Relyra.ConnectionResolver.Default,
request_store: Relyra.RequestStore.ETS,
replay_store: Relyra.ReplayStore.ETS
#{sentinel_end}
"""
existing = if File.exists?(path), do: File.read!(path), else: "import Config\n"
if String.contains?(existing, sentinel_start) do
:ok
else
File.write!(path, existing <> snippet)
end
end
defp maybe_update_router(nil, module_name, _force, opts) do
maybe_print_live_admin_instructions(module_name, opts)
end
defp maybe_update_router(router_path, module_name, force, opts) do
if File.exists?(router_path) do
contents = File.read!(router_path)
if String.contains?(contents, "saml_routes()") do
maybe_print_live_admin_instructions(module_name, opts)
else
Mix.shell().info(
"Router found at #{router_path}, but route injection is ambiguous. Add saml_routes() manually."
)
Mix.shell().info(
"Generated for #{module_name}; use the router macro in the appropriate scope."
)
maybe_print_live_admin_instructions(module_name, opts)
end
else
if force do
Mix.shell().info("Router file #{router_path} missing; skipped due to --force.")
else
Mix.shell().info(
"Router file #{router_path} missing; add saml_routes() manually if needed."
)
end
maybe_print_live_admin_instructions(module_name, opts)
end
end
defp maybe_print_live_admin_instructions(module_name, opts) do
if Keyword.get(opts, :live_admin, false) do
admin_path = Keyword.get(opts, :admin_path, "/relyra/admin")
Mix.shell().info("""
Live admin scaffolded. Mount it in a LiveView-enabled router scope:
import Relyra.LiveAdmin.Router
relyra_admin_routes("#{admin_path}",
repo: #{module_name}.Repo,
scope_provider: #{module_name}.Relyra.AdminScope
)
The generated `#{module_name}.Relyra.AdminScope` reads:
- `session[\"admin_actor\"]`
- `session[\"admin_actor_label\"]`
- `session[\"admin_organization_id\"]`
Update that module to match your host app's authenticated session shape.
""")
end
end
defp module_path(module_name) do
module_name
|> Macro.underscore()
end
defp connection_template(module_name) do
"""
defmodule #{module_name}.Relyra.Connections do
@moduledoc false
@behaviour Relyra.ConnectionResolver
@impl true
def resolve_connection(_request_context, _opts) do
{:error, Relyra.Error.new(:adapter_not_configured, "Configure #{module_name}.Relyra.Connections", %{})}
end
end
"""
end
defp user_mapper_template(module_name) do
"""
defmodule #{module_name}.Relyra.UserMapper do
@moduledoc false
@behaviour Relyra.UserMapper
@impl true
def map_attributes(_assertion, _connection, _opts) do
{:error, Relyra.Error.new(:adapter_not_configured, "Configure #{module_name}.Relyra.UserMapper", %{})}
end
end
"""
end
defp admin_scope_template(module_name) do
"""
defmodule #{module_name}.Relyra.AdminScope do
@moduledoc false
@behaviour Relyra.LiveAdmin.ScopeProvider
alias Relyra.LiveAdmin.Scope
@impl true
def resolve_admin_scope(session, _params, _opts) when is_map(session) do
case Map.get(session, "admin_actor") do
actor when is_binary(actor) and actor != "" ->
{:ok,
%Scope{
actor: actor,
actor_label: Map.get(session, "admin_actor_label"),
organization_id: Map.get(session, "admin_organization_id")
}}
_other ->
{:error, :unauthenticated}
end
end
def resolve_admin_scope(_session, _params, _opts), do: {:error, :unauthenticated}
end
"""
end
end