Building an aggregates-only Resource
I'm building an admin dashboard that will display several aggregate data points sourced from different database tables. These data points should be available from a single AshGraphql query. How would you do this?
I created a resource called
Dashboard
. My first approach was to create a calculation
to calculate a data point. That wasn't going anywhere, so I defined an primary read
action that loaded the data point from inside the prepare
block. That's where I'm at so far. The problem is that calling read()
on the API returns nil
.
Here is an example of the kind of data I want to expose:
- Member counts
- Signup counts
- Event counts
- Attendee counts54 Replies
The test for this looks like this:
The big challenge is trying to figure out what goes inside the
MemberCountCalculation
module. I'm not finding what I need in the documentation.
And here's the action:
Is this the right approach? What exactly goes in the prepare
block to make this work?Hmm...not sure why its returning
nil
for the result, as that pattern looks generally correct
The way you're doing it is a reasonable way to do what you're looking to do. Although likely what I'd do is add calculations and just return struct!(__MODULE__, %{})
from the read. I'd populate a random UUID also, as resources expect that. Even in cases like this it can be useful and is cheap enough to do I think. i.e struct!(__MODULE__, %{id: Ash.UUID.generate()})
Have you confirmed that your hook there is being called?
you said its returning nil
, but is your call to set_data
definitely happening for example?
If you call read_one
does it return a list?
**read instead of read_one
?Okay, that helps to know if I'm on the right track.
The hook you mention, is that the
prepare
block? Yes, that's getting called. In fact, it's called twice. The first time, it has an actor, and the second time, it doesn't. I suspect the second call is the one that's being used for the return value.
I added id: Ash.UUID.generate()
too.hm...its getting called twice ??
Yeah, I don't know why.
try putting it in a
before_action
block actually
since you don't technically want it to happen on query validation
it could mean that somewhere we are rerunning query preparations, which isn't necessarily that big of a bug since anything intensive should be behind a before_action
block
there is an Ash.Query.before_action
just like Ash.Changeset
Does the
Ash.Query.before_action()
go inside the prepare
block?
After some debugging, it shows that the prepare
block gets called both for Ash.Query.for_read(()
, and API.read_one!()
.
So here's the action def:
The read_one!()
is still returning nil
, and read!()
returns an empty list.
The "before_action"
and "actor present"
debug statements are displayed in the test output.
The "No actor present"
is no longer displayed.something very strange is happening then
can you inspect the value of
query.__validated_for_action__
at the beginning of your preparation?query.__validated_for_action__ : nil
It's nilboth times its called?
The reproduction here seems pretty simple, I'll take a look at it this evening or tomorrow morning. For now, what you actually want to do is pass the actor when building the query
Okay, I tried something. I commented out the multitenancy definition, and now it's returning the struct.
🤔
huh
huh
that doesn't make a lot of sense
what was the multitenancy definition?
Oh wait. It's not that.
I commented out the
Ash.Query.set_tenant(ctx.org.id)
in the testso strange
That's where the issue stems
so if a tenant is set, for some reason, its calling your query preparations twice
So do
for_read(...., actor: actor)
Using the code interface, for example, solves that problem, but that is actually the best way to pass things in when building queries
i.e Query.for_read(..., actor: actor, authorize?: authorize?, tenant: tenant)
same for change sets, and we carry it over
it just doesn't technically matter if you don't use the actor in preparations/changesets. But...for 3.0 maybe I'll actually make that error, and require passing it when building the query, to avoid this confusionOr, another way to get it to pass is to leave in the
Ash.Query.set_tenant(ctx.org.id)
, but comment out the multitenancy
section.good to know
so that means its not in the tenant setting code
its in the "doing multitenant stuff" code
About the
API.read!()
call, I realized that about the redundant actor specification, and removed it, so that it's only in the for_read()
call. I did that earlier.wow, okay, so this is actually pretty interesting
there is an interaction here I didn't consider
So here's my test now:
And based on what you said, the
set_tenant()
call is unecessary, if I pass the tenant to the for_read()
call...Yep
okay nvm I thought I had found it but I hadn't
but also I'm investigating in safari on an ipad 😆
So now I'm at this in the test:
I believe this will be easy for me to debug.
because it should be easy to reproduce
actually, lets try something real quick
can you raise an error if
actor
is nil
and show me the stacktrace?Raise it if the actor is nil? It's not nil though. Do you mean in the before_action() fn?
maybe I need to regroup 😆 is it still getting called twice?
but now both times the actor is not nil?
I'ts not getting called twice now, because I added the before_action() call. The prepare is getting called twice, but the before_action() anon fn is getting called once:
When I comment out the
multitenancy
block, the prepare
block only gets called once.But it still doesn't work right?
It does work for that case
Found it!
I needed to add the tenant attribute value to the struct:
I should have thought of that.
The
prepare
block is still called twice.Okay, yeah that makes sense
Sounds like we probably need some helpers for this potentially
like helpers for constructing manually returned records, where it sets some metadata that the simple data layer can yell at you about
Cool.
Sorry I didn't catch that sooner
So now I'm on to testing the GQL query...
🤞
I'm getting this in the logs:
Is this in a new api>?
My
graphql
section looks like this:
No, it's an existing apiis it in the registry?
Yep
hmm....from a gql perspective this shouldn't be any different than any other thing
maybe restart/recompile?
I'll try force-compile
does it show up when you look at the schema?
Looks like force-compile resolved that.
I'm onto another issue...
strange, that shouldn't be necessary
what's up?
I think it's just a bad test setup...
I think what you'll also want to do is make those fields into calculations, now that you've got the action returning.
That way when a subset of fields are requested over gql it won't calculate all of them
Okay, I see. Yeah, try that out.
👍 LMK if you run into anything else/if the issue you mentioned isn't due to your tests 😆
The GraphQL query is working.
Thanks for your help! I might need help with the calculations, but let's see how far I can get...
I can give you a quick template to get you started, unless you'd like to figure it out on your own
That would be helpful!
Then in your admin dashboard resource:
I see. Nice!
I converted it to a calculation, and it's working.
One small question: In the
read
prepare
block, I'm getting the tenant value from the actor
, because I couldn't find it another way. Is ther a way to get the tenant without having to get it from the actor?It should be in
query.tenant