Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Ash: 11 - Aggregates

ash_tutorial/aggregates.livemd

Ash: 11 - Aggregates

Application.put_env(:ash, :validate_domain_resource_inclusion?, false)
Application.put_env(:ash, :validate_domain_config_inclusion?, false)
Mix.install([{:ash, "~> 3.0"}], consolidate_protocols: false)

Aggregates

Code Interfaces HomeCalculations

In this tutorial you will add an Aggregate on a Resource

Aggregates in Ash allow for retrieving summary information over groups of related data. Aggregates can be either count, first, sum or list.

Implement a count aggregate on the ‘open tickets’ a representative is assigned to.

First explore the comments in the code below, to make this work properly you have to:

  • Set the representative belongs_to relationship to writable
  • Add the representative_id to the open action
  • Add the representative_id to the code interface
  • Add the inverse relationship of belongs_to inside the Representative resource

Only the latter is really required for aggregates to work, the first 3 allow you to be a bit more concise when creating new tickets.

To add an aggregate define an aggregates do .. end block inside the representative.

Inside the aggregates block, define a count :count_of_open_tickets, :tickets do .. end block.

Then inside this block, define a filter like so: filter expr(status == :open)

Also, notice what happens when you don’t define a filter.

Show Solution

  aggregates do
    count :count_of_open_tickets, :tickets do
      filter expr(status == :open)
    end
  end

Enter your solution

defmodule Tutorial.Support.Ticket do
  use Ash.Resource,
    domain: Tutorial.Support,
    data_layer: Ash.DataLayer.Ets

  actions do
    defaults [:read]

    # On creation set the representative by providing the id
    create :open do
      accept [:subject, :description, :representative_id]
    end

    update :close do
      accept []
      change set_attribute(:status, :closed)
    end

    update :assign do
      accept [:representative_id]
    end
  end

  attributes do
    uuid_primary_key(:id)
    attribute :subject, :string, allow_nil?: false
    attribute :description, :string, allow_nil?: true

    attribute :status, :atom do
      constraints one_of: [:open, :closed]
      default :open
      allow_nil? false
    end

    create_timestamp :created_at
    update_timestamp :updated_at
  end

  relationships do
    belongs_to :representative, Tutorial.Support.Representative
  end

  code_interface do
    define :assign, args: [:representative_id]
    # <- added representative_id
    define :open, args: [:subject, :description, :representative_id]
    define :close, args: []
  end
end

defmodule Tutorial.Support.Representative do
  use Ash.Resource,
    domain: Tutorial.Support,
    data_layer: Ash.DataLayer.Ets

  actions do
    defaults [:read]

    create :create do
      accept [:name]
    end
  end

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

  # Added the inverse relationship of belongs_to in the ticket.
  # This way we can reference :tickets inside the aggregates.
  relationships do
    has_many :tickets, Tutorial.Support.Ticket
  end

  code_interface do
    define :create, args: [:name]
  end

  # <- Add the aggregates here
  aggregates do
    count :count_of_open_tickets, :tickets do
      filter expr(status == :open)
    end
  end
end
defmodule Tutorial.Support do
  use Ash.Domain

  resources do
    resource Tutorial.Support.Ticket
    resource Tutorial.Support.Representative
  end
end

Query the Aggregate

Use a Bulk Action to create 4 tickets, 3 of which are assigned to Joe.

# Create a representative
joe = Tutorial.Support.Representative.create!("Joe Armstrong")

# Bulk create 4 tickets, 3 of which are assigned to Joe
[
  %{subject: "I can't see my eyes!"},
  %{subject: "I can't find my hand!", representative_id: joe.id},
  %{subject: "My fridge is flying away!", representative_id: joe.id},
  %{subject: "My bed is invisible!", representative_id: joe.id}
]
|> Ash.bulk_create(Tutorial.Support.Ticket, :open, return_records?: true)

Close the last ticket assigned to Joe.

require Ash.Query

# Retrieve the last ticket
[last_ticket] =
  Tutorial.Support.Ticket
  |> Ash.Query.filter(representative_id == ^joe.id)
  |> Ash.Query.sort(created_at: :desc)
  |> Ash.Query.limit(1)
  |> Ash.read!()

# Close the last ticket
Tutorial.Support.Ticket.close!(last_ticket)
joe = Ash.load!(joe, [:count_of_open_tickets])
joe.count_of_open_tickets

The result should be 2, as you opened 4 tickets, 3 were assigned to Joe, but the last assigned ticket was closed.

Code Interfaces HomeCalculations