Resource

Ash resources are used to model data and define actions which are used to manipulate that data. In the Ash world we often compare the resources with nouns and the actions with verbs.

In Ash 3.0, resources are grouped into domains - context boundaries where related resources are defined together. This helps organize your application and makes it easier to understand the relationships between resources.

To-Do-List Example

To dive into resources we use a simple to-do-list application as an example. As a preparation for this, use Igniter to create a new application:

$ mix archive.install hex igniter_new
$ mix igniter.new app --install ash
$ cd app

Alternatively, you can follow the Ash Setup Guide for other setup options.

We want to create a task resource which has a content attribute and an id attribute as a primary key. We also want to include the actions create, read, update and delete. Ash provides those actions for free but we have to include them into the resource.

In Ash 3.0, resources need to be registered to a domain. The domain acts as a boundary for related resources and provides a place to define shared functionality. Each domain is registered with your application’s OTP app.

Configure the Domain

First, we need to configure our OTP app to recognize our domain:

config/config.exs
import Config

config :app, :ash_domains, [App.ToDoList]

Now we create the ToDoList domain module which contains the resource Task.

lib/app/to_do_list.ex
defmodule App.ToDoList do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.ToDoList.Task
  end
end

For the resource(s) we create a new directory:

$ mkdir -p lib/app/to_do_list

Configure the Resource

The resource defines attributes which are the fields of the resource. In our case we have two attributes: id and content. The id attribute is a special attribute because it is the primary key of the resource. We use the uuid_primary_key macro to define the id attribute as a primary key. The content attribute is a simple string.

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id
    attribute :content, :string
  end
end

In this example we use the Ash.DataLayer.Ets as a database layer. ETS (Erlang Term Storage) is an in-memory data store which is built into your Erlang system. For our training purpose this is ideal because we don’t have to install and configure a database.

ETS does not save any data to disk! With every restart of iex you have to re-create the example data. For production applications, you should use AshPostgres or another persistent data layer.

The resulting directory structure should look like this:

$ tree lib
lib
├── app
│   ├── application.ex
│   ├── to_do_list
│   │   └── task.ex
│   └── to_do_list.ex
└── app.ex

3 directories, 4 files

We now have a resource but because we haven’t defined any actions we can’t do anything with it yet. Let’s change that.

Create

To create a resource, we need to add the create action to the resource. In Ash 3.0, we also add code interface definitions to our domain to create functions that make it easier to work with the resource.

First, let’s add the create action to our resource:

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id
    attribute :content, :string
  end

  actions do
    defaults [:create]
  end
end

Then, we add a code interface definition to our domain:

lib/app/to_do_list.ex
defmodule App.ToDoList do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.ToDoList.Task do
      define :create_task, action: :create
    end
  end
end

This creates a App.ToDoList.create_task/1-2 function that we can use to create tasks.

Fire up the IEx (Elixir’s Interactive Shell) to create your first task:

$ iex -S mix
Compiling 2 files (.ex)
Erlang/OTP 26 [erts-14.0.2] [...]

Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.create_task!(%{content: "Mow the lawn"})
#App.ToDoList.Task<
  __meta__: #Ecto.Schema.Metadata<:built, "">,
  id: "8e868c09-c0d0-4362-8270-09272acab769",
  content: "Mow the lawn",
  aggregates: %{},
  calculations: %{},
  ...
>
iex(2)>

The function App.ToDoList.create_task!/1-2 raises an error if something goes wrong (e.g. a validation error). Alternatively you can use App.ToDoList.create_task/1-2 which returns a tuple with the status and the resource.

iex(2)> App.ToDoList.create_task(%{content: "Mow the lawn"})
{:ok,
 #App.ToDoList.Task<
   __meta__: #Ecto.Schema.Metadata<:built, "">,
   id: "a8430505-ef7e-4f64-bc2c-2a6db216d8ea",
   content: "Mow the lawn",
   aggregates: %{},
   calculations: %{},
   ...
 >}
iex(3)>

You can still create a task the long way with the following code:

App.ToDoList.Task
|> Ash.Changeset.for_create(:create, %{content: "Mow the lawn"})
|> Ash.create!()

The create_task/1-2 code interface function is just a lot more convenient.

Read

Writing is one thing but it only makes sense if you can read the written data too. To make our life a bit easier we add a read action and a code_interface define for read:

lib/app/to_do_list/resources/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :content, :string
  end

  actions do
    # add :read here
    defaults [:create, :read]
  end

  code_interface do
    define_for App.ToDoList
    define :create
    # add this line
    define :read
  end
end

Index

To fetch a list of all tasks in the database we can use the App.ToDoList.Task.read!/1 (results in a list) or App.ToDoList.Task.read/1 (results in a tuple with a status and a list) functions. Those are automatically generated by Ash by the code_interface part of the task resource.

$ iex -S mix
Compiling 2 files (.ex)
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.create!(%{content: "Mow the lawn"})
#App.ToDoList.Task<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "881c6c08-223c-41b1-9d61-2d3a40e478bd",
  content: "Mow the lawn",
  ...
>
iex(2)> App.ToDoList.Task.create!(%{content: "Buy milk"})
#App.ToDoList.Task<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "22b11587-20fe-40d2-830e-50f8930c13c9",
  content: "Buy milk",
  ...
>
iex(3)> App.ToDoList.Task.read! |> Enum.map(& &1.content)
["Buy milk", "Mow the lawn"]
iex(4)> App.ToDoList.Task.read
{:ok,
 [
   #App.ToDoList.Task<
     __meta__: #Ecto.Schema.Metadata<:loaded>,
     id: "22b11587-20fe-40d2-830e-50f8930c13c9",
     content: "Buy milk",
     ...
   >,
   #App.ToDoList.Task<
     __meta__: #Ecto.Schema.Metadata<:loaded>,
     id: "881c6c08-223c-41b1-9d61-2d3a40e478bd",
     content: "Mow the lawn",
     ...
   >
 ]}
iex(5)>

If you have an empty database this is your result for both functions:

$ iex -S mix
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.read!
[]
iex(2)> App.ToDoList.Task.read
{:ok, []}
iex(3)>

Show

Often one wants to fetch a specific set of data by an id. The Ash code_interface has an easy solution for this common scenario.

lib/app/to_do_list/resources/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :content, :string
  end

  actions do
    defaults [:create, :read]
  end

  code_interface do
    define_for App.ToDoList
    define :create
    define :read
    # add this line
    define :by_id, get_by: [:id], action: :read (1)
  end
end
1This generates the functions App.ToDoList.Task.by_id/1 and App.ToDoList.Task.by_id!/1

Let’s try it out:

$ iex -S mix
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> alias App.ToDoList.Task
App.ToDoList.Task
iex(2)> Task.read (1)
{:ok, []}
iex(3)> {:ok, task} = Task.create(%{content: "Mow the lawn"})
{:ok,
 #App.ToDoList.Task<
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
   content: "Mow the lawn",
   ...
 >}
iex(4)> task.id
"a5648b48-4eb3-443d-aba7-fafbbfedc564"
iex(5)> Task.by_id("a5648b48-4eb3-443d-aba7-fafbbfedc564")
{:ok,
 #App.ToDoList.Task<
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
   content: "Mow the lawn",
   ...
 >}
iex(6)> Task.by_id!("a5648b48-4eb3-443d-aba7-fafbbfedc564")
#App.ToDoList.Task<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
  content: "Mow the lawn",
  ...
>
1Just to establish that there are no tasks in the database.

And here an example when there is no task in the database for the given id:

$ iex -S mix
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.by_id("not-in-the-db")
{:error,
 %Ash.Error.Query.NotFound{
   primary_key: nil,
   resource: App.ToDoList.Task,
   changeset: nil,
   query: nil,
   error_context: [],
   vars: [],
   path: [],
   stacktrace: #Stacktrace<>,
   class: :invalid
 }}
 iex(2)> App.ToDoList.Task.by_id!("not-in-the-db")
 ** (Ash.Error.Query.NotFound) record not found
     [...]

Update

Ash provides a simple way to update a resource and by now you can probably guess how it works:

lib/app/to_do_list/resources/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :content, :string
  end

  actions do
    # add :update to the list
    defaults [:create, :read, :update]
  end

  code_interface do
    define_for App.ToDoList
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    # add this line
    define :update
  end
end

Let’s try it out:

$ iex -S mix
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> alias App.ToDoList.Task
App.ToDoList.Task
iex(2)> {:ok, task} = Task.create(%{content: "Mow the lawn"})
{:ok,
 #App.ToDoList.Task<
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
   content: "Mow the lawn",
   ...
 >}
iex(3)> Task.update(task, %{content: "Play golf"})
{:ok,
 #App.ToDoList.Task<
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
   content: "Play golf",
   ...
 >}
iex(4)> Task.update!(task, %{content: "Buy milk"})
#App.ToDoList.Task<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
  content: "Buy milk",
  ...
>

Destroy (delete)

And finally we can destroy a resource. Again, this is very similar to the other actions:

lib/app/to_do_list/resources/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :content, :string
  end

  actions do
    # add :delete to list
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.ToDoList
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :update
    # Add this line
    define :destroy
  end
end

Let’s try it out:

iex -S mix
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> {:ok, task} = App.ToDoList.Task.create(%{content: "Mow the lawn"})
{:ok,
 #App.ToDoList.Task<
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "5bd2b15e-fd29-4d3f-9356-cbfe06ea7eee",
   content: "Mow the lawn",
   ...
 >}
iex(2)> App.ToDoList.Task.destroy(task)
:ok
iex(3)> App.ToDoList.Task.by_id(task.id) (1)
{:error,
 %Ash.Error.Query.NotFound{
   primary_key: nil,
   resource: App.ToDoList.Task,
   changeset: nil,
   query: nil,
   error_context: [],
   vars: [],
   path: [],
   stacktrace: #Stacktrace<>,
   class: :invalid
 }}
iex(4)>
1Because the task is destroyed we can’t find it anymore.

Defaults

Attributes can have default values. Let’s add a is_done boolean attribute with a default of false and a validation that doesn’t allow nil for this attribute:

lib/app/to_do_list/resources/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id

    attribute :content, :string do
      allow_nil? false
      constraints min_length: 1, max_length: 255
    end

    attribute :priority, :integer do
      allow_nil? true
      constraints min: 1, max: 3
    end

    attribute :is_done, :boolean do
      allow_nil? false
      default false
    end
  end

  actions do
    defaults [:create]
  end

  code_interface do
    define_for App.ToDoList
    define :create
  end
end

Now we can create a new task without providing a value for is_done:

iex> App.ToDoList.Task.create(%{content: "Mown the lawn"})
{:ok,
 #App.ToDoList.Task<
   __meta__: #Ecto.Schema.Metadata<:built, "">,
   id: "07d5b3f1-b960-4390-8980-5e731251d7af",
   content: "Mown the lawn",
   priority: nil,
   is_done: false,
   aggregates: %{},
   calculations: %{},
   ...
 >}

default_accept

Sometimes a resource as an attribute which we don’t want to have writeble for the user. Ash provides a functionality for this. Within the actions we can use default_accept to define a whitelist of accepted attributes.

In our example application we want to allow the user to create and update the content and priority attributes but not the is_done attribute.

lib/app/to_do_list/resources/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  # ...

  actions do
    default_accept [:content, :priority] # add this line
    defaults [:create]
  end

  # ...
end

Should a user try to change the id_done attribute in a create or update the system will not accept it. See the "cannot be changed" message:

$ iex -S mix
Compiling 2 files (.ex)
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> App.ToDoList.Task.create(%{content: "Mow the lawn", is_done: true})
{:error,
 %Ash.Error.Invalid{
   errors: [
     %Ash.Error.Changes.InvalidAttribute{
       field: :is_done,
       message: "cannot be changed",
       private_vars: nil,
       value: true,
       changeset: nil,
       query: nil,
       error_context: [],
       vars: [],
       path: [],
       stacktrace: #Stacktrace<>,
       class: :invalid
     }
   ],
   stacktraces?: true,
   changeset: #Ash.Changeset<
     api: App.ToDoList,
     action_type: :create,
     action: :create,
     attributes: %{content: "Mow the lawn", is_done: true},
     relationships: %{},
     errors: [
       %Ash.Error.Changes.InvalidAttribute{
         field: :is_done,
         message: "cannot be changed",
         private_vars: nil,
         value: true,
         changeset: nil,
         query: nil,
         error_context: [],
         vars: [],
         path: [],
         stacktrace: #Stacktrace<>,
         class: :invalid
       }
     ],
     data: #App.ToDoList.Task<
       __meta__: #Ecto.Schema.Metadata<:built, "">,
       id: nil,
       content: nil,
       priority: nil,
       is_done: nil,
       aggregates: %{},
       calculations: %{},
       ...
     >,
     valid?: false
   >,
   query: nil,
   error_context: [nil],
   vars: [],
   path: [],
   stacktrace: #Stacktrace<>,
   class: :invalid
 }}
iex(2)>