Aggregate first the full resource instead of some field

Eduardo B. Alexandre
2023-02-23

Eduardo B. Alexandre:

I have a property resource that can contain offers.

When I’m fetching the property, I want to also fetch the current offer from the actor if there is one.

Right now, I have this:

  preparations do
    prepare build(load: :offeror_current_offer)
  end

  aggregates do
    first :offeror_current_offer, :offers do
      filter expr(offeror_id == ^actor(:id) and status in [:open, :accepted, :evaluating])

      sort updated_at: :desc
    end
  end

But this will not work since the first aggregation requires me to select a field from :offers , but I want it to actually select the whole offer.

Is there some way for me to do this with aggregates?

ZachDaniel:

has_one :offeror_current_offer, Offer do
  sort updated_at: :desc
end

ZachDaniel:

no need for an aggregate there

Eduardo B. Alexandre:

Ah, that’s cool!

Eduardo B. Alexandre:

So, I added this:

    has_one :offeror_current_offer, Markets.Property.Offer do
      filter expr(offeror_id == ^actor(:id) and status in [:open, :accepted, :evaluating])

      sort updated_at: :desc
    end

Eduardo B. Alexandre:

But the whole thing exploded during query compilation 😅

Seems to be related to the ^actor(:id) bit

ZachDaniel:

oh

ZachDaniel:

yeah sorry

ZachDaniel:

you can’t use the actor in either filter

ZachDaniel:

You’re going to want a calculation for this

ZachDaniel:

(sorry about that)

ZachDaniel:

calculate :offeror_current_offer, GetCurrentOfferor

defmodule ...GetCurrentOfferror do
  use Ash.Resource.Calculation

  def calculate(records, _, %{actor: actor}) do
    Enum.map(records, fn record -> 
      ...get the current offer
    end)
  end
end

Eduardo B. Alexandre:

I think calculate has arity 3 right? Seems like it expects the return type as the second argument, I tried adding the Offer object to it but seems like that is not correct, I will take a look at the docs

Eduardo B. Alexandre:

Yeah.. I don’t get it.

I’m adding the calculation this way:

calculate :offeror_current_offer, Markets.Property.Offer, GetCurrentOfferor

But I get this error

== Compilation error in file lib/marketplace/markets/property.ex ==
** (ArgumentError) value nil is invalid for type {:parameterized, Marketplace.Markets.Property.Offer.EctoType, []}, can't set default
    (ecto 3.9.4) lib/ecto/schema.ex:2221: Ecto.Schema.validate_default!/3
    (ecto 3.9.4) lib/ecto/schema.ex:1928: Ecto.Schema.__field__/4
    (stdlib 4.1.1) erl_eval.erl:744: :erl_eval.do_apply/7
    (stdlib 4.1.1) erl_eval.erl:136: :erl_eval.exprs/6
    (elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (stdlib 4.1.1) erl_eval.erl:744: :erl_eval.do_apply/7
    (stdlib 4.1.1) erl_eval.erl:136: :erl_eval.exprs/6
    (stdlib 4.1.1) erl_eval.erl:987: :erl_eval.try_clauses/10

From what I saw from other examples that should be the correct way to add the return type as the resource

ZachDaniel:

Yeah, that looks right to me, not sure what is going on there…

ZachDaniel:

oh

ZachDaniel:

actually one sec

ZachDaniel:

ash main should work for you now

Eduardo B. Alexandre:

Hm, I got the same error

ZachDaniel:

hm…

ZachDaniel:

you sure you updated?

ZachDaniel:

I just pushed it a second ago

ZachDaniel:

are you sure its the same exact error?

Eduardo B. Alexandre:

let me try again

Eduardo B. Alexandre:

== Compilation error in file lib/marketplace/markets/property.ex ==
** (ArgumentError) value nil is invalid for type {:parameterized, Marketplace.Markets.Property.Offer.EctoType, []}, can't set default
    (ecto 3.9.4) lib/ecto/schema.ex:2221: Ecto.Schema.validate_default!/3
    (ecto 3.9.4) lib/ecto/schema.ex:1928: Ecto.Schema.__field__/4
    (stdlib 4.1.1) erl_eval.erl:744: :erl_eval.do_apply/7
    (stdlib 4.1.1) erl_eval.erl:136: :erl_eval.exprs/6
    (elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (stdlib 4.1.1) erl_eval.erl:744: :erl_eval.do_apply/7
    (stdlib 4.1.1) erl_eval.erl:136: :erl_eval.exprs/6
    (stdlib 4.1.1) erl_eval.erl:987: :erl_eval.try_clauses/10

ZachDaniel:

how did you update to main?

ZachDaniel:

you set it to github: "ash-project/ash" ? did mix deps.update ash ?

Eduardo B. Alexandre:

Yep

ZachDaniel:

🤔

ZachDaniel:

Can you comment out your calculation

ZachDaniel:

and in iex do this

ZachDaniel:

actually nvm

ZachDaniel:

I see whats going on

ZachDaniel:

okay try main again

Eduardo B. Alexandre:

Yep! Now it is compiling 😁

Eduardo B. Alexandre:

So, should I expect the calculation to have the actor inside the context (last field of calculate function) by default if the action itself was called with the actor set?

Because I’m not getting it

ZachDaniel:

😢 I keep leading you astray with this.

ZachDaniel:

The actor is not passed to calculations

ZachDaniel:

There is an open ticket for this to be fixed

Eduardo B. Alexandre:

So I guess for now there is no good way to calculate extra field in the resource that requires an actor?

ZachDaniel:

lemme try to fix it, one sec

ZachDaniel:

alright, try main

ZachDaniel:

the actor should be in the context now

Eduardo B. Alexandre:

Yep, now I can see the actor! Amazing work as always!

Eduardo B. Alexandre:

So, last question (hopefully) about this.

I see that Ash is smart when doing queries that need to fetch data from a lot of resources, it sends the resource’s ids as a list and fetches data to all of them in a single query.

I don’t want to do one query per resource inside the calculate function, so can you direct me to some code, or where Ash does that single query so I can use as reference and do the same with my own query for this calculation?

ZachDaniel:

The calculation function takes a list of records and returns a list of results

ZachDaniel:

So that you can do exactly that same kind of thing

ZachDaniel:

i.e get the ids of all the records being loaded, and then fetch only the relevant records, and then return them properly

ZachDaniel:

i.e

def calculate(records, _, %{actor: actor}) do
  record_ids = Enum.map(records, &(&1.id))

  stuff = get_stuff(record_ids, actor)

  Enum.map(records, fn record -> 
    stuff[record.id]
  end)
end

Eduardo B. Alexandre:

All right! So here is my solution in case someone wants to use it as a reference:

In my Offer resource, I created an action to retrieve offers from a list of properties_ids:

    read :list_own_valids_from_properties do
      argument :properties_ids, {:array, :uuid} do
        allow_nil? false
      end

      prepare build(sort: [updated_at: :desc])

      filter expr(
               property_id in ^arg(:properties_ids) and offeror_id == ^actor(:id) and
                 status != :old
             )
    end

On my Property resource, I added this calculation:

  calculations do
    calculate :offeror_current_offer, Markets.Property.Offer, GetCurrentOfferor
  end

And here is the calculation implementation:

defmodule GetCurrentOfferor do
  alias Marketplace.Markets.Property.Offer

  use Ash.Calculation

  @impl true
  def calculate(properties, _, %{actor: actor}) do
    properties_ids = Enum.map(properties, & &1.id)

    offers =
      %{properties_ids: properties_ids}
      |> Offer.list_own_valids_from_properties!(actor: actor)
      |> Enum.map(fn %{property_id: property_id} = offer -> {property_id, offer} end)
      |> Enum.into(%{})

    Enum.map(properties, fn %{id: property_id} ->
      offers[property_id]
    end)
  end
end