Note: This library is under active development and the API may change.

AshPhoenixGenApi

CI Coverage Hex Version

An Ash Framework extension for generating PhoenixGenApi function configurations from Ash resources and domains.

What is PhoenixGenApi? PhoenixGenApi is a Elixir framework for building distributed, service-oriented APIs. It uses a "supporter" module pattern where service nodes expose function configurations that gateway nodes can pull or subscribe to. This extension bridges Ash Framework and PhoenixGenApi by automatically generating those function configurations from your Ash resource definitions.

AshPhoenixGenApi bridges the Ash Framework and PhoenixGenApi by allowing you to define PhoenixGenApi endpoints directly in your Ash resource and domain DSLs. It automatically generates PhoenixGenApi.Structs.FunConfig structs from your Ash actions, including type mappings, argument ordering, and configuration defaults.

Features

  • DSL-driven API configuration — Define PhoenixGenApi endpoints alongside your Ash resource definitions
  • Automatic type mapping — Ash types are automatically converted to PhoenixGenApi argument types
  • Auto-derived arguments — Action arguments and accepted attributes are automatically extracted from Ash actions
  • Auto-generated code interface — Elixir functions are generated on the resource module for each gen_api action (create, read, update, destroy, generic)
  • Domain-level aggregation — Auto-generates a "supporter" module that aggregates FunConfigs from all resources
  • Active push configuration — Push API configs to gateway nodes on startup, with MFA-based runtime node resolution
  • Permission callback — Custom MFA callback for permission checking, receives (request_type, args) and returns true/false
  • Compile-time verification — Validates action existence, request type uniqueness, and argument consistency
  • Resolution hierarchy — Configuration values cascade from action → resource → domain → built-in defaults
  • PhoenixGenApi client interface — Generated supporter modules implement get_config/1, get_config_version/1, fun_configs/0, push_to_gateway/2, etc.

Installation

Add ash_phoenix_gen_api to your list of dependencies in mix.exs:

def deps do
  [
    {:ash_phoenix_gen_api, "~> 1.0.0"},
    {:ash, "~> 3.5"},
    {:phoenix_gen_api, "~> 2.14"}
  ]
end

Then fetch dependencies:

mix deps.get

Quick Start

1. Add the Resource extension

Add AshPhoenixGenApi.Resource to your Ash resources:

defmodule MyApp.Chat.DirectMessage do
  use Ash.Resource,
    domain: MyApp.Chat,
    extensions: [AshPhoenixGenApi.Resource]

  attributes do
    uuid_primary_key :id
    attribute :from_user_id, :uuid do
      public? true
    end
    attribute :to_user_id, :uuid do
      public? true
    end
    attribute :content, :string do
      public? true
      allow_nil? true
    end
    attribute :reply_to_id, :uuid do
      public? true
      allow_nil? true
    end
    attribute :file_id, :uuid do
      public? true
      allow_nil? true
    end
  end

  actions do
    create :create do
      accept [:from_user_id, :to_user_id, :content, :reply_to_id, :file_id]
    end

    read :read do
      primary? true
    end

    update :update_content do
      accept [:content]
    end

    destroy :destroy
  end

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    timeout 5_000
    response_type :async
    request_info true
    version "0.0.1"

    action :create do
      request_type "send_direct_message"
      timeout 10_000
      check_permission {:arg, "from_user_id"}
    end

    action :read do
      request_type "get_conversation"
      timeout 5_000
    end

    action :update_content do
      request_type "update_content"
      response_type :sync
    end
  end
end

2. Add the Domain extension

Add AshPhoenixGenApi.Domain to your Ash domain:

defmodule MyApp.Chat do
  use Ash.Domain,
    extensions: [AshPhoenixGenApi.Domain]

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    version "0.0.1"
    supporter_module MyApp.Chat.GenApiSupporter
  end

  resources do
    resource MyApp.Chat.DirectMessage
    resource MyApp.Chat.GroupMessage
  end
end

3. Use the generated supporter module

After compilation, MyApp.Chat.GenApiSupporter is auto-generated:

# Get all FunConfigs (for PhoenixGenApi pull)
MyApp.Chat.GenApiSupporter.fun_configs()
#=> [%PhoenixGenApi.Structs.FunConfig{request_type: "send_direct_message", ...}, ...]

# Get config for remote pull
MyApp.Chat.GenApiSupporter.get_config(:gateway_1)
#=> {:ok, [%PhoenixGenApi.Structs.FunConfig{...}, ...]}

# Get config version
MyApp.Chat.GenApiSupporter.get_config_version(:gateway_1)
#=> {:ok, "0.0.1"}

# Find a specific FunConfig by request_type
MyApp.Chat.GenApiSupporter.get_fun_config("send_direct_message")
#=> %PhoenixGenApi.Structs.FunConfig{request_type: "send_direct_message", ...}

# List all request types
MyApp.Chat.GenApiSupporter.list_request_types()
#=> ["send_direct_message", "get_conversation", ...]

4. Configure the gateway node

On the Phoenix gateway node, configure phoenix_gen_api in config.exs:

config :phoenix_gen_api, :gen_api,
  service_configs: [
    %{
      service: "chat",
      nodes: {ClusterHelper, :get_nodes, [:chat]},
      module: MyApp.Chat.GenApiSupporter,
      function: :get_config,
      args: [:gateway_1]
    }
  ]

DSL Reference

Resource DSL (gen_api)

The gen_api section is added to Ash resources when using AshPhoenixGenApi.Resource.

Section Options

OptionTypeDefaultDescription
service:atom | :stringrequiredService name for routing
nodes:local | {:list, :atom} | {:tuple, [:atom, :atom, {:list, :any}]}:localDefault target nodes
choose_node_mode:random | :hash | :round_robin | {:hash, string}:randomDefault node selection strategy
timeoutpos_integer | :infinity5000Default timeout in milliseconds
response_type:sync | :async | :stream | :none:asyncDefault response mode
request_info:booleantrueDefault for passing request info
check_permissionfalse | :any_authenticated | {:arg, string} | {:role, [string]}falseDefault permission check mode
permission_callback{module, atom, list} | nilnilDefault permission callback MFA. Callback receives (request_type, args) and returns true/false. Takes precedence over check_permission
version:string"0.0.1"Default version string
retrypos_integer | {:same_node, pos_integer} | {:all_nodes, pos_integer}nilDefault retry configuration
code_interface?:booleantrueWhether to auto-generate code interface functions for gen_api actions

Action Entity Options

OptionTypeDefaultDescription
name:atomrequiredAsh action name to expose
request_type:stringaction name as stringPhoenixGenApi request type string
timeoutpos_integer | :infinitysection defaultTimeout in milliseconds
response_type:sync | :async | :stream | :nonesection defaultResponse mode
request_info:booleansection defaultWhether to pass request info
check_permissionsee abovesection defaultPermission check mode
permission_callback{module, atom, list} | nilsection defaultPermission callback MFA. Overrides check_permission when set
choose_node_modesee abovesection defaultNode selection strategy
nodessee abovesection defaultTarget nodes
retrysee abovesection defaultRetry configuration
version:stringsection defaultAPI version string
mfa{module, atom, list}auto-generatedExplicit MFA tuple
arg_typesmap | nilauto-derivedExplicit argument types
arg_orders[string] | nilauto-derivedExplicit argument order
disabled:booleanfalseDisable this endpoint
code_interface?:boolean | nilnilWhether to generate code interface for this action. nil inherits from section-level

MFA Entity Options

The mfa entity defines a standalone PhoenixGenApi endpoint that calls an arbitrary MFA function directly — with no Ash action involved. This is useful for utility endpoints, batch operations, or service-to-service calls that don't map to standard Ash CRUD actions.

OptionTypeDefaultDescription
name:atomrequiredUnique identifier for this MFA endpoint
request_type:stringrequiredPhoenixGenApi request type string
mfa{module, atom, list}requiredMFA tuple to call. Function receives predefined_args ++ converted_args ++ info_args
arg_typesmaprequiredArgument types map. Use %{} for no arguments. No auto-derivation
arg_orders[string] | :map:mapArgument order. :map passes args as a map; a list passes positional args
timeoutpos_integer | :infinitysection defaultTimeout in milliseconds
response_type:sync | :async | :stream | :nonesection defaultResponse mode
request_info:booleansection defaultWhether to pass request info
check_permissionsee abovesection defaultPermission check mode
permission_callback{module, atom, list} | nilsection defaultPermission callback MFA. Overrides check_permission when set
choose_node_modesee abovesection defaultNode selection strategy
nodessee abovesection defaultTarget nodes
retrysee abovesection defaultRetry configuration
version:stringsection defaultAPI version string
disabled:booleanfalseDisable this endpoint

Example:

gen_api do
  service "chat"

  mfa :ping do
    request_type "ping"
    mfa {MyApp.Chat.Api, :ping, []}
    arg_types %{}
    timeout 1_000
  end

  mfa :search do
    request_type "search"
    mfa {MyApp.SearchHandler, :search, []}
    arg_types %{"query" => :string, "limit" => :num}
    arg_orders ["query", "limit"]
    response_type :sync
  end
end

Domain DSL (gen_api)

The gen_api section is added to Ash domains when using AshPhoenixGenApi.Domain.

Section Options

OptionTypeDefaultDescription
service:atom | :stringService name (used as default for resources)
nodessee above:localDefault target nodes
choose_node_modesee above:randomDefault node selection strategy
timeoutsee above5000Default timeout
response_typesee above:asyncDefault response mode
request_info:booleantrueDefault for passing request info
check_permissionsee abovefalseDefault permission check mode
permission_callback{module, atom, list} | nilnilDefault permission callback MFA. Callback receives (request_type, args) and returns true/false. Takes precedence over check_permission
version:string"0.0.1"Default version string
retrysee abovenilDefault retry configuration
supporter_module:atomrequiredModule name for auto-generated supporter
define_supporter?:booleantrueWhether to auto-generate the supporter module
push_nodes[atom] | {module, atom, list} | :local | nilnilTarget gateway nodes to push config to
push_on_startup:booleanfalseWhether to push config on application startup

Type Mapping

Ash types are automatically mapped to PhoenixGenApi argument types:

Ash TypePhoenixGenApi Type
:string, Ash.Type.String:string
:string (with max_length constraint){:string, max_bytes}
:ci_string, Ash.Type.CiString:string
:integer, Ash.Type.Integer:num
:float, Ash.Type.Float:num
:decimal, Ash.Type.Decimal:num
:uuid, Ash.Type.UUID:uuid
:uuid_v7, Ash.Type.UUIDv7:uuid
:boolean, Ash.Type.Boolean:boolean
:date, Ash.Type.Date:string
:time, Ash.Type.Time:string
:datetime, Ash.Type.DateTime:datetime
:utc_datetime, Ash.Type.UtcDateTime:datetime
:utc_datetime_usec, Ash.Type.UtcDateTimeUsec:datetime
:naive_datetime, Ash.Type.NaiveDateTime:naive_datetime
:naive_datetime_usec, Ash.Type.NaiveDateTimeUsec:naive_datetime
:atom, Ash.Type.Atom:string
:map, Ash.Type.Map:map
Ash.Type.Json:map
:struct, Ash.Type.Struct:map
:keyword, Ash.Type.Keyword:map
:binary, Ash.Type.Binary:string
:term, Ash.Type.Term:string
{:array, :string}{:list_string, 1000, 50}
{:array, :integer}{:list_num, 1000}
{:array, :uuid}{:list_string, 1000, 50}
{:array, :float}{:list_num, 1000}
{:array, :map}{:list, 1000}
{:array, :boolean}{:list, 1000}

Nil Attribute Support

When an Ash attribute or argument has allow_nil? true, the generated arg_types uses an extended format that includes the :allow_nil? option:

# For attributes with allow_nil? false (default)
"content" => :string

# For attributes with allow_nil? true
"content" => [type: :string, allow_nil?: true]

# With constraints and allow_nil? true
"description" => [type: {:string, 255}, max_bytes: 255, allow_nil?: true]
"tags" => [type: :list_string, max_items: 1000, max_item_bytes: 50, allow_nil?: true]

The extended format is a keyword list with:

  • :type - The PhoenixGenApi type (atom or tuple)
  • :allow_nil? - Always true when present
  • Type-specific constraints (e.g., :max_bytes, :max_items, :max_item_bytes)
  • :default_value - Present when the Ash attribute has a default value

This allows PhoenixGenApi clients to properly handle optional fields and validate input accordingly.

See AshPhoenixGenApi.TypeMapper for the complete mapping and customization options.

Resolution Order

Configuration values are resolved in this order (highest priority first):

  1. Action-level explicit config — e.g., action :foo do timeout 10_000 end
  2. Resource section-level defaults — e.g., gen_api do timeout 5_000 end
  3. Domain section-level defaults — e.g., gen_api do timeout 5_000 end
  4. Built-in defaults — e.g., timeout defaults to 5000

For arg_types and arg_orders:

  1. Explicit arg_types/arg_orders on the action entity
  2. Auto-derived from the Ash action's accepted attributes and arguments

For mfa:

  1. Explicit mfa on the action entity
  2. Auto-generated as {ResourceModule, :action_name, []}

Auto-Derived Arguments

When arg_types and arg_orders are not explicitly set, the extension automatically derives them from the Ash action:

  • For :create and :update actions: Includes accepted attributes and action arguments
  • For :read, :destroy, and :action types: Includes only action arguments

Basic Example

Given this Ash action:

actions do
  create :create do
    accept [:from_user_id, :to_user_id, :content, :sent_at, :metadata, :reply_to_id, :file_id]
  end
end

The auto-derived arg_types and arg_orders would be:

arg_types: %{
  "from_user_id" => :string,       # UUID → :string
  "to_user_id" => :string,         # UUID → :string
  "content" => :string,            # String → :string
  "sent_at" => :datetime,          # DateTime → :datetime
  "metadata" => :map,              # Map → :map
  "reply_to_id" => :string,        # UUID → :string
  "file_id" => :string             # UUID → :string
},
arg_orders: ["from_user_id", "to_user_id", "content", "sent_at", "metadata", "reply_to_id", "file_id"]

Nil Attribute Example

When attributes have allow_nil? true, the extended format is used:

attributes do
  attribute :content, :string do
    allow_nil? true
  end
  attribute :reply_to_id, :uuid do
    allow_nil? true
  end
  attribute :tags, {:array, :string} do
    allow_nil? true
    constraints [max_length: 10]
  end
end

actions do
  create :create do
    accept [:content, :reply_to_id, :tags]
  end
end

The auto-derived arg_types would be:

arg_types: %{
  "content" => [type: :string, allow_nil?: true],
  "reply_to_id" => [type: :uuid, allow_nil?: true],
  "tags" => [type: :list_string, max_items: 1000, max_item_bytes: 50, allow_nil?: true]
},
arg_orders: ["content", "reply_to_id", "tags"]

Default Values with Nil Attributes

When an attribute has both allow_nil? true and a default value:

attributes do
  attribute :priority, :integer do
    allow_nil? true
    default 0
  end
end

The auto-derived type includes the default value:

arg_types: %{
  "priority" => [type: :num, allow_nil?: true, default_value: 0]
}

Generated Supporter Module

The domain extension auto-generates a supporter module that implements the PhoenixGenApi client config interface. This module:

  1. Aggregates FunConfigs from all resources in the domain that have AshPhoenixGenApi.Resource
  2. Implements get_config/1 — Returns {:ok, fun_configs()} for PhoenixGenApi pull
  3. Implements get_config_version/1 — Returns {:ok, version} for version checking
  4. Implements fun_configs/0 — Returns the aggregated list of FunConfig structs
  5. Implements list_request_types/0 — Returns all available request type strings
  6. Implements get_fun_config/1 — Returns a specific FunConfig by request_type

The generated module matches the interface described in the PhoenixGenApi documentation for remote config pulling.

Introspection

Resource Introspection

# Check if a resource has gen_api configured
AshPhoenixGenApi.Resource.Info.has_gen_api?(MyApp.Chat.DirectMessage)
#=> true

# Get the service name
AshPhoenixGenApi.Resource.Info.gen_api_service(MyApp.Chat.DirectMessage)
#=> "chat"

# Get all action configs
AshPhoenixGenApi.Resource.Info.gen_api_actions(MyApp.Chat.DirectMessage)
#=> [%ActionConfig{name: :create, ...}, ...]

# Get a specific action config
AshPhoenixGenApi.Resource.Info.action(MyApp.Chat.DirectMessage, :create)
#=> %ActionConfig{name: :create, request_type: "send_direct_message", ...}

# Get only enabled actions
AshPhoenixGenApi.Resource.Info.enabled_actions(MyApp.Chat.DirectMessage)
#=> [%ActionConfig{disabled: false, ...}, ...]

# Get the generated FunConfig structs
AshPhoenixGenApi.Resource.Info.fun_configs(MyApp.Chat.DirectMessage)
#=> [%PhoenixGenApi.Structs.FunConfig{...}, ...]

# Get a specific FunConfig by request_type
AshPhoenixGenApi.Resource.Info.fun_config(MyApp.Chat.DirectMessage, "send_direct_message")
#=> %PhoenixGenApi.Structs.FunConfig{request_type: "send_direct_message", ...}

# Get all request types (includes both action and mfa endpoints)
AshPhoenixGenApi.Resource.Info.request_types(MyApp.Chat.DirectMessage)
#=> ["send_direct_message", "get_conversation", ..., "ping"]

# Get all MFA configs
AshPhoenixGenApi.Resource.Info.mfas(MyApp.Chat.DirectMessage)
#=> [%MfaConfig{name: :ping, ...}, ...]

# Get a specific MFA config by name
AshPhoenixGenApi.Resource.Info.mfa(MyApp.Chat.DirectMessage, :ping)
#=> %MfaConfig{name: :ping, request_type: "ping", mfa: {MyApp.Chat.Api, :ping, []}, ...}

# Get only enabled MFA configs
AshPhoenixGenApi.Resource.Info.enabled_mfas(MyApp.Chat.DirectMessage)
#=> [%MfaConfig{name: :ping, disabled: false, ...}, ...]

# Get effective values with fallback resolution
AshPhoenixGenApi.Resource.Info.effective_timeout(MyApp.Chat.DirectMessage, :create)
#=> 10_000
AshPhoenixGenApi.Resource.Info.effective_mfa(MyApp.Chat.DirectMessage, :create)
#=> {MyApp.Chat.DirectMessage, :create, []}

Domain Introspection

# Check if a domain has gen_api configured
AshPhoenixGenApi.Domain.Info.has_gen_api?(MyApp.Chat)
#=> true

# Get the supporter module name
AshPhoenixGenApi.Domain.Info.supporter_module(MyApp.Chat)
#=> MyApp.Chat.GenApiSupporter

# Get all resources with gen_api configured
AshPhoenixGenApi.Domain.Info.resources_with_gen_api(MyApp.Chat)
#=> [MyApp.Chat.DirectMessage, MyApp.Chat.GroupMessage]

# Get aggregated FunConfigs from all resources
AshPhoenixGenApi.Domain.Info.fun_configs(MyApp.Chat)
#=> [%PhoenixGenApi.Structs.FunConfig{...}, ...]

# Get all request types across all resources
AshPhoenixGenApi.Domain.Info.all_request_types(MyApp.Chat)
#=> ["send_direct_message", "get_conversation", ...]

# Get a configuration summary
AshPhoenixGenApi.Domain.Info.summary(MyApp.Chat)
#=> %{
#=>   service: "chat",
#=>   version: "0.0.1",
#=>   supporter_module: MyApp.Chat.GenApiSupporter,
#=>   total_fun_configs: 5,
#=>   resources: [
#=>     %{resource: MyApp.Chat.DirectMessage, request_types: ["send_direct_message", ...]},
#=>     %{resource: MyApp.Chat.GroupMessage, request_types: ["send_group_message", ...]}
#=>   ]
#=> }

Compile-Time Verification

The extension performs compile-time verification to catch configuration errors early:

Resource Verification

  • Action existence — Every action entity must reference an existing Ash action
  • MFA required fields — Every mfa entity must have request_type, mfa, and arg_types set
  • MFA tuple validity — The mfa field must be a valid {module, function, args_list} tuple
  • Request type uniqueness — No two endpoints (actions or mfas) in the same resource may share a request_type
  • Arg consistency — When both arg_types and arg_orders are provided, their keys must match
  • Permission arg existence — When check_permission is {:arg, "name"}, the argument must exist in arg_types (for mfa entities) or the Ash action (for action entities)
  • Action MFA validity — When an explicit mfa is provided on an action entity, it must be a valid {module, function, args} tuple

Domain Verification

  • Supporter module name — Must be a valid Elixir module name
  • Service configuration — Resources with gen_api must have a service configured (either on the resource or the domain)
  • Request type uniqueness across resources — No two resources in the domain may expose the same request_type

Code Interface

When code_interface? is true (the default), the extension auto-generates Elixir functions on the resource module for each gen_api action. This allows you to call actions directly without building queries or changesets manually.

# Create action — auto-generates create/2 and create!/2
{:ok, message} = MyApp.Chat.DirectMessage.create(%{content: "Hello"})
message = MyApp.Chat.DirectMessage.create!(%{content: "Hello"})

# Read action — auto-generates read/2 and read!/2
{:ok, messages} = MyApp.Chat.DirectMessage.read()
messages = MyApp.Chat.DirectMessage.read!()

# Update action — auto-generates update/3 and update!/3 (requires record)
{:ok, updated} = MyApp.Chat.DirectMessage.update(message, %{content: "Updated"})
updated = MyApp.Chat.DirectMessage.update!(message, %{content: "Updated"})

# Destroy action — auto-generates destroy/3 and destroy!/3 (requires record)
:ok = MyApp.Chat.DirectMessage.destroy(message)
:ok = MyApp.Chat.DirectMessage.destroy!(message)

# Generic action — auto-generates action_name/2 and action_name!/2
{:ok, result} = MyApp.Chat.DirectMessage.greet(%{name: "World"})

You can disable code interface generation at the section level or per-action:

gen_api do
  service "chat"
  code_interface? false  # Disable for all actions

  action :create do
    code_interface? true  # Re-enable for this action only
  end

  action :read  # Inherits section-level false
end

Permission Callback

In addition to the built-in permission modes (false, :any_authenticated, {:arg, "arg_name"}, {:role, ["admin"]}), you can specify a custom callback function for permission checking using permission_callback.

The callback receives request_type (string) and args (map) as arguments and returns true (continue) or false (permission denied).

defmodule MyApp.Permissions do
  def check_permission(request_type, args) do
    case request_type do
      "delete_user" -> args["role"] == "admin"
      "update_profile" -> args["user_id"] == args["target_user_id"]
      _ -> true
    end
  end
end

# In your resource:
gen_api do
  service "chat"
  permission_callback {MyApp.Permissions, :check_permission, []}

  action :delete_user do
    # Uses the section-level permission_callback
  end

  action :admin_action do
    # Override with a different callback
    permission_callback {MyApp.Permissions, :check_admin, []}
  end
end

When permission_callback is set, it takes precedence over check_permission and is stored as {:callback, {Module, :function, []}} in the FunConfig's check_permission field.

Active Push Configuration

In addition to the pull-based model (where the gateway pulls config from service nodes), you can configure the supporter module to actively push its configuration to gateway nodes.

gen_api do
  service "chat"
  supporter_module MyApp.Chat.GenApiSupporter
  version "0.0.1"
  push_nodes [:"gateway1@host", :"gateway2@host"]
  # Or use an MFA tuple for runtime resolution:
  # push_nodes {ClusterHelper, :get_gateway_nodes, []}
end

Then push config during application startup:

def start(_type, _args) do
  # ... start supervision tree, then:
  MyApp.Chat.GenApiSupporter.push_to_configured_nodes()
  # Or push to a specific node:
  MyApp.Chat.GenApiSupporter.push_on_startup(:"gateway1@host")
end

The generated supporter module includes these push functions:

FunctionDescription
build_push_config/0Builds a PushConfig struct from the domain config
push_to_gateway/2Pushes config to a specific gateway node
push_on_startup/2Pushes config on application startup
verify_on_gateway/2Verifies config version on a gateway node
resolve_push_nodes/0Resolves push_nodes at runtime (handles MFA tuples)
push_to_configured_nodes/1Pushes to all configured push_nodes

Example: Chat Service

Here's a complete example matching the ChatService pattern from PhoenixGenApi:

defmodule MyApp.Chat.DirectMessage do
  use Ash.Resource,
    domain: MyApp.Chat,
    extensions: [AshPhoenixGenApi.Resource]

  attributes do
    uuid_primary_key :id
    attribute :from_user_id, :uuid, public?: true
    attribute :to_user_id, :uuid, public?: true
    attribute :content, :string, public?: true, allow_nil?: true
    attribute :reply_to_id, :uuid, public?: true, allow_nil?: true
    attribute :file_id, :uuid, public?: true, allow_nil?: true
    attribute :order, :integer, public?: true
    attribute :read, :boolean, public?: true, allow_nil?: true, default: false
    attribute :deleted, :boolean, public?: true, allow_nil?: true, default: false
  end

  actions do
    create :create do
      accept [:from_user_id, :to_user_id, :content, :reply_to_id, :file_id]
    end

    read :read do
      primary? true
    end

    update :mark_read do
      accept [:read]
    end

    destroy :destroy
  end

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    timeout 5_000
    response_type :async
    request_info true
    version "0.0.1"

    action :create do
      request_type "send_direct_message"
      timeout 10_000
      check_permission {:arg, "from_user_id"}
    end

    action :read do
      request_type "get_conversation"
    end

    action :mark_read do
      request_type "mark_direct_messages_as_read"
      arg_types %{"read" => :boolean}
    end

    # Standalone MFA endpoint — no Ash action needed
    mfa :typing_indicator do
      request_type "typing_indicator"
      mfa {MyApp.Chat.Api, :broadcast_typing, []}
      arg_types %{"conversation_id" => :string}
      response_type :none
    end
  end
end

defmodule MyApp.Chat do
  use Ash.Domain,
    extensions: [AshPhoenixGenApi.Domain]

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    version "0.0.1"
    supporter_module MyApp.Chat.GenApiSupporter
  end

  resources do
    resource MyApp.Chat.DirectMessage
  end
end

This generates the same FunConfig structures that were previously hand-written in ChatService.Interface.GenApi.Supporter, but now derived automatically from your Ash resource definitions.

Modules

ModuleDescription
AshPhoenixGenApiTop-level module with documentation and helpers
AshPhoenixGenApi.ResourceResource-level DSL extension
AshPhoenixGenApi.Resource.InfoResource introspection helpers
AshPhoenixGenApi.Resource.ActionConfigAction configuration struct
AshPhoenixGenApi.Resource.SharedTypesShared type definitions for config structs
AshPhoenixGenApi.Resource.EffectiveFieldMacro for effective_* field resolution
AshPhoenixGenApi.DomainDomain-level DSL extension
AshPhoenixGenApi.Domain.InfoDomain introspection helpers
AshPhoenixGenApi.TypeMapperAsh type to PhoenixGenApi type mapping
AshPhoenixGenApi.Transformers.DefineFunConfigsResource transformer
AshPhoenixGenApi.Transformers.DefineDomainSupporterDomain transformer
AshPhoenixGenApi.Verifiers.VerifyActionConfigsResource verifier
AshPhoenixGenApi.Verifiers.VerifyDomainConfigDomain verifier

License

MPL 2.0