Aggregate first the full resource instead of some field

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
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?
21 Replies
ZachDaniel
ZachDaniel3y ago
has_one :offeror_current_offer, Offer do
sort updated_at: :desc
end
has_one :offeror_current_offer, Offer do
sort updated_at: :desc
end
no need for an aggregate there
Blibs
BlibsOP3y ago
Ah, that's cool! 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
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
Blibs
BlibsOP3y ago
But the whole thing exploded during query compilation 😅 Seems to be related to the ^actor(:id) bit
ZachDaniel
ZachDaniel3y ago
oh yeah sorry you can't use the actor in either filter You're going to want a calculation for this (sorry about that)
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
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
Blibs
BlibsOP3y ago
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 Yeah.. I don't get it. I'm adding the calculation this way:
calculate :offeror_current_offer, Markets.Property.Offer, GetCurrentOfferor
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
== 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
ZachDaniel3y ago
Yeah, that looks right to me, not sure what is going on there... oh actually one sec ash main should work for you now
Blibs
BlibsOP3y ago
Hm, I got the same error
ZachDaniel
ZachDaniel3y ago
hm... you sure you updated? I just pushed it a second ago
Blibs
BlibsOP3y ago
let me try again
ZachDaniel
ZachDaniel3y ago
are you sure its the same exact error?
Blibs
BlibsOP3y ago
== 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
== 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
ZachDaniel3y ago
how did you update to main? you set it to github: "ash-project/ash" ? did mix deps.update ash?
Blibs
BlibsOP3y ago
Yep
ZachDaniel
ZachDaniel3y ago
🤔 Can you comment out your calculation and in iex do this actually nvm I see whats going on okay try main again
Blibs
BlibsOP3y ago
Yep! Now it is compiling 😁 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
ZachDaniel3y ago
😢 I keep leading you astray with this. The actor is not passed to calculations There is an open ticket for this to be fixed
Blibs
BlibsOP3y ago
So I guess for now there is no good way to calculate extra field in the resource that requires an actor?
ZachDaniel
ZachDaniel3y ago
lemme try to fix it, one sec alright, try main the actor should be in the context now
Blibs
BlibsOP3y ago
Yep, now I can see the actor! Amazing work as always! 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
ZachDaniel3y ago
The calculation function takes a list of records and returns a list of results So that you can do exactly that same kind of thing i.e get the ids of all the records being loaded, and then fetch only the relevant records, and then return them properly 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
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
Blibs
BlibsOP3y ago
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
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
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
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

Did you find this page helpful?