Packages

⚠️ EXPERIMENTAL: Expose Ash Resource actions over tRPC with Plug-compatible router/controller and tooling. Breaking changes may occur frequently.

Current section

Files

Jump to
ash_rpc lib mix tasks ash_rpc.install.ex
Raw

lib/mix/tasks/ash_rpc.install.ex

if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.AshRpc.Install do
@shortdoc "Installs AshRpc into a Phoenix project"
@moduledoc """
Installs AshRpc into the current Phoenix project:
- Generates `YourAppWeb.TrpcRouter`
- Adds `forward "/trpc", YourAppWeb.TrpcRouter` to your Phoenix router
This task will modify your router.ex and create the tRPC router file.
"""
use Igniter.Mix.Task
@impl Igniter.Mix.Task
def info(_argv, _source) do
%Igniter.Mix.Task.Info{
group: :ash
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
if Mix.Project.umbrella?() do
Mix.raise("mix ash_rpc.install can only be run inside an application directory")
end
app_name = Atom.to_string(Mix.Project.config()[:app])
web_module_name = Module.concat([Macro.camelize(app_name <> "_web")])
trpc_router_module = Module.concat([web_module_name, TrpcRouter])
# Check if AshAuthentication is available
auth_enabled? = has_auth_dependency?()
# Initialize status tracking
status = %{created: [], skipped: [], updated: []}
# Create the tRPC router module using Igniter with status tracking
{igniter, status} = create_trpc_router_smart(igniter, trpc_router_module, status)
# Update the router using a simple patch approach with status tracking
{igniter, status} = update_router_with_patch_smart(igniter, web_module_name, trpc_router_module, auth_enabled?, app_name, status)
# Report status to user
report_status(status, trpc_router_module)
igniter
end
defp create_trpc_router_smart(igniter, trpc_router_module, status) do
app_name = Atom.to_string(Mix.Project.config()[:app])
file_path = Path.join(["lib", "#{app_name}_web", "trpc_router.ex"])
if File.exists?(file_path) do
# File exists, check if it has the correct content
case File.read(file_path) do
{:ok, content} ->
expected_content = """
defmodule #{inspect(trpc_router_module)} do
use AshRpc.Web.Router, domains: []
end
"""
if String.trim(content) == String.trim(expected_content) do
{igniter, Map.update!(status, :skipped, &["#{inspect(trpc_router_module)} module (already exists with correct content)" | &1])}
else
{igniter, Map.update!(status, :skipped, &["#{inspect(trpc_router_module)} module (exists with different content, keeping existing)" | &1])}
end
{:error, _} ->
{igniter, status}
end
else
# File doesn't exist, create it
igniter = Igniter.Project.Module.create_module(igniter, trpc_router_module, """
defmodule #{inspect(trpc_router_module)} do
use AshRpc.Web.Router, domains: []
end
""")
{igniter, Map.update!(status, :created, &["#{inspect(trpc_router_module)} module" | &1])}
end
end
defp update_router_with_patch_smart(igniter, _web_module_name, trpc_router_module, auth_enabled?, app_name, status) do
# Construct router path manually to avoid Igniter API issues
router_path = Path.join(["lib", "#{app_name}_web", "router.ex"])
# Create the pipeline code
pipeline_code = create_pipeline_code(auth_enabled?, app_name)
# Create the scope code
scope_code = create_scope_code(trpc_router_module)
# Read and analyze the router file
case File.read(router_path) do
{:ok, content} ->
{updated_content, pipeline_added?, scope_added?} = update_router_content_smart(content, pipeline_code, scope_code)
if pipeline_added? or scope_added? do
File.write!(router_path, updated_content)
status = if pipeline_added?, do: Map.update!(status, :created, &["tRPC pipeline in router" | &1]), else: status
status = if scope_added?, do: Map.update!(status, :created, &["tRPC route forwarding in router" | &1]), else: status
{igniter, status}
else
{igniter, Map.update!(status, :skipped, &["Router configuration (already properly configured)" | &1])}
end
{:error, reason} ->
Mix.shell().error("Failed to read router file: #{:file.format_error(reason)}")
{igniter, status}
end
end
defp create_pipeline_code(auth_enabled?, app_name) do
if auth_enabled? do
"""
# Ash RPC pipeline - JSON only + Bearer auth via AshAuthentication
pipeline :ash_rpc do
plug :accepts, ["json"]
# Enables bearer token auth; requires AshAuthentication (or ash_authenticator) configured
plug :retrieve_from_bearer, :#{app_name}
plug :set_actor, :user
end
"""
else
"""
# Ash RPC pipeline - JSON only
# If using AshAuthentication, you can enable bearer token auth:
# plug :retrieve_from_bearer, :#{app_name}
# plug :set_actor, :user
pipeline :ash_rpc do
plug :accepts, ["json"]
end
"""
end
end
defp create_scope_code(trpc_router_module) do
"""
# Mount Ash RPC under the :ash_rpc pipeline
scope "/trpc" do
pipe_through :ash_rpc
forward "/", #{inspect(trpc_router_module)}
end
"""
end
defp update_router_content_smart(content, pipeline_code, scope_code) do
{content, pipeline_added} = add_pipeline_if_missing_smart(content, pipeline_code)
{content, scope_added} = add_scope_if_missing_smart(content, scope_code)
{content, pipeline_added, scope_added}
end
defp add_pipeline_if_missing_smart(content, pipeline_code) do
if String.contains?(content, "pipeline :ash_rpc do") do
{content, false}
else
new_content = if String.contains?(content, "pipeline :api do") do
String.replace(
content,
~r/( pipeline :api do\n.*?\n end\n)/s,
"\\1\n#{pipeline_code}"
)
else
# Fallback: insert before the first scope
String.replace(content, ~r/( scope "\/")/, "#{pipeline_code}\n\n \\1")
end
{new_content, true}
end
end
defp add_scope_if_missing_smart(content, scope_code) do
if String.contains?(content, "scope \"/trpc\"") do
{content, false}
else
new_content = if String.contains?(content, "pipeline :ash_rpc do") do
String.replace(
content,
~r/(pipeline :ash_rpc do\n.*?\n end\n)/s,
"\\1\n#{scope_code}"
)
else
# Fallback: append to end
content <> "\n#{scope_code}"
end
{new_content, true}
end
end
defp report_status(status, trpc_router_module) do
Mix.shell().info("\nAshRpc installation complete!")
Mix.shell().info("=============================")
unless Enum.empty?(status.created) do
Mix.shell().info("\n✅ Created:")
Enum.each(Enum.reverse(status.created), fn item ->
Mix.shell().info(" • #{item}")
end)
end
unless Enum.empty?(status.updated) do
Mix.shell().info("\n🔄 Updated:")
Enum.each(Enum.reverse(status.updated), fn item ->
Mix.shell().info(" • #{item}")
end)
end
unless Enum.empty?(status.skipped) do
Mix.shell().info("\n⏭️ Skipped:")
Enum.each(Enum.reverse(status.skipped), fn item ->
Mix.shell().info(" • #{item}")
end)
end
Mix.shell().info("\n📝 Next steps:")
Mix.shell().info(" • Add your Ash domains to the #{inspect(trpc_router_module)} domains list")
Mix.shell().info(" • Configure your frontend to connect to /trpc")
if Enum.empty?(status.created) and Enum.empty?(status.updated) do
Mix.shell().info("\n✨ AshRpc is already fully configured!")
end
end
defp has_auth_dependency? do
deps = Mix.Project.config()[:deps] || []
Enum.any?(deps, fn
dep ->
case dep do
{name, _opts} when is_atom(name) -> name in [:ash_authentication, :ash_authenticator]
name when is_atom(name) -> name in [:ash_authentication, :ash_authenticator]
_ -> false
end
end)
end
end
else
defmodule Mix.Tasks.AshRpc.Install do
@shortdoc "Installs AshRpc into a Phoenix project"
@moduledoc """
Installs AshRpc into the current Phoenix project:
- Generates `YourAppWeb.TrpcRouter`
- Adds `forward "/trpc", YourAppWeb.TrpcRouter` to your Phoenix router
This task will modify your router.ex and create the tRPC router file.
"""
use Mix.Task
@impl true
def run(_args) do
Mix.shell().error("""
The task 'ash_rpc.install' requires igniter to be run.
Please install igniter and try again.
For more information, see: https://hexdocs.pm/igniter
""")
exit({:shutdown, 1})
end
end
end