Aggregate first the full resource instead of some field
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