ManualRead not resolving in graphQL
Hi All,
I have created a ManualRead module to implement a tricky relationship which has defaults that can be overridden. Before going too much into details, this ManualRead is working fine when I use the code interface, but when I call the same action through graphQL the values I expect are all nil.
It is a bit too long to paste here, but was wondering if there is something general I need to think of when doing this?
73 Replies
I'll paste the read here
excuse the missing spacing, otherwise the post was too long ๐
Will need to look into this a bit more, but the fundamental difference is that ash_graphql uses select statements to limit the results returned
FWIW from what Iโm seeing this looks like the purpose of this manual read is to attach some computed data that I think could be calculations, which might solve your problem?
ok, yeah I had the same issue a while back, but with a simpler relation. Maybe I'll try it again with a calculation indeed. The complexity lies in that I need to do multiple joins and a select merge to mimic a has_one relationship. I looked into using AshPostgres.ManualRelationship, but couldn't really make that fit, but that is also maybe just due to the fact that I just don't understand it well enough ๐
I'll give the calculations another shot and let you know. Thanks again for the help! Hope you had a good time at ElixirConf! I need to watch some of the videos still, had an online ticket only, so your talk I only partly watched.
Could you describe the relationship you'd like to make? I might be able to help ๐
of course, I'd be glad to! Let me see if I can make it clear in one go ๐
We are building a system that contains Games that can differ in metadata, so title, icon, description for different organizations in our system, but doesn't have to.
So a Game does not belong to an organization directly.
For every game we have an entry in the game_meta table, which has organization_id set to nil, which has the default metadata. Then for every organization that wants to include the game in their portfolio we add an entry in the game_meta table tied to the organization, ie. has the organization_id set for that org and any values they want to override. The nice thing with Postgres 15, which we will upgrade to soon, is that we can set
distinct_nulls: false
so that I can create an unique_index where there can only be 1 default ๐
In the end we join both the default game_meta and on top of that (using coalesce) layer the data that needs to be overridden. And the ideal goal would be to add this metadata as top level values so to the outside world it looks like it's just part of the Game itself.
Hope that it makes sense? I was just reading up on the expression docs and it seems that I might have jumoed the gun on calculations too quickly. They indeed seem very powerful!Interesting, that does make sense.
So I guess I could do this. Or at least I'm hoping I can do this.
load both the default and org specific meta as a relationship:
And then create a calculation that uses a fragment with the coalesce or something siimilar
Yes, you should be able to do something like that ๐
That is close, yes
but you will get errors about references because calculations can't directly refer to related fields
yeah it's lovely psuedo code that doesn't work ๐
However, with the recent addition of "anonymous aggregates", you should be able to do this
ohh inline aggregates, wow!
oh, thats right, I called them "inline aggregates" not "anonymous aggregates" ๐
inline is better
Also, I think you won't need the
from_many? true
option unless its possible for the filter you're providing to give you more than one result๐
oh, also:
^arg
doesn't work in a relationshipyeah I wanted to ask about that
thought so
are you referring to the
organization_id
of the record you're talking about?yeah that is the idea
oh no, it doesn't exist on the source
ah, okay interesting
it's an argument being passed in
understood
๐ค
So you can do that, but it gets a bit interesting
has_many :game_metas, ...
assuming you have thatWe're getting to some relatively uncharted territory here, but I am doing this kind of thing in another app
installing ash-functions... ๐
I have honestly no idea of that
source
is going to work in this context
oh, wait
you don't need source
that will work ๐trying that now, when I find that syntax error that is blocking my compilation... ๐คฆโโ๏ธ
ok, almost there. It was the other way around, so I think this shoud be it:
but now it is failing because of organisation_id
which is a field on the game meta and here it seems that it expects it at the game level, right?
the default_game_meta I removed the filter, because I am filtering that in the relationship
It should be expecting it at the
game_meta
level...ok, then it should actually work
maybe try this:
try wrapping the filter on that with
expr/1
ok, it is compiling now, but the default is now returned...
ok, gotta go. Getting late here in Amsterdam ๐ let's sleep on this one. Thanks for the awesome help so far!! You are really going the extra mile here! Much appreciated.
so for this the only thing that I need still is to have a relationship to game_meta for an organisation which can filter based on an argument passed into the action.
ie. this works with a hardcoded id:
I understand that passing the argument into the relationship filter is not an option, so what would the best approach for this? A calculation or a manual relationship? I'll try to see for myself as well, but any pointers would be great.
or if this is not working due to a bug, than this could be the way to go for me, but not sure:
So, the inline aggregate ought to do the thing
i.e this:
You won't be able to have a parameterized relationship anytime soon, so parameterizing the aggregate is the best way, if its doable
But you're saying this doesn't work?
ok, so debugging this a bit. It does work if I hardcode the ID instead of the ^arg, so this does work:
so it seems it can't access the argument here as well
is there a way to output the full query to debug this?
I'll check if I can debug it in the test suite
Oh
You need to add an argument to the calculation
Do you have that
at first not, but I added this and had no results, but when I add the
allow_nil?: false
it now returns an error and apparently the argument evaluates to nil
I'll try to do this with a calculation module implementation, let's see if I can get that in a working stateHow are you passing the arguments to the calc?
They have to be passed to the calc specifically, not just the action
ok, that might the missing piece. I now do have a working version with a calculation module, but I don't like the way it feels currently... ๐
So I guess I'm not passing it into the calc... ๐ฌ how is that done?
๐คฆโโ๏ธ

I guess that was the missing piece then?
let me try
๐ฅณ ๐ฅณ ๐ฅณ working!! Thanks @Zach Daniel !!
So here is the implementation that now works as expected:
the read action:
The Calculation:
The relationships:
So for the graphQL to work I had to add a default organisation_id, otherwise I would need to pass it into the specific field like below
Thanks for all the help again, really excited that we figured it out and hopefully it can provide useful in the future for others ๐
How did you do the default organization is out of curiosity?
Thatโs actually pretty confusing
Is it falling back to the argument on the query? If so thatโs not what itโs supposed to do ๐ฅถ
Oh, yeah okay so this is pretty interestingโฆTBH itโs kind of luck that it works that way and that ash_graphql doesnโt clobber your load statement.
I think the default is only needed to get the right type of argument, because it fails in this manner:
but if I for instance do this:
the ID at the top always wins and the bottom one doesn't have any effect
Well, it wins because of the way you're loading it, yeah
which is actually not necessarily the right behavior
do you mean the default GameMeta ?
Lets try something actually
This uses the context to set the organisation_id, then the API never thinks it has to set it
ok, trying, just a sec
if that doesn't work, then I'd like to fix it, as its the sensible way to connect these things in this case
i.e something from the action decides how the calculations ought to be rendered
it doesn't know about set_context/2
hmm...
oh yeah
maybe thats not a builtin there?
do this
ok, with a few changes to your example it worked:
- set_context needed a map
ah, yeah, right
and without the load it doesn't work
oh really??
yeah, code_interface as well as the graphql
okay, interesting
I thought that behaved like Ecto Associations that you explicitly needed to load, so wasn't surprising to me actually, but I understand it should behave differently then
btw as a sidenote...Now that I have this one field working and I need to add more of these I am actually thinking that I can probably model this whole thing better in a calculation module and pass in the keys I need to override, right? But that is something for post MVP, we need to launch this sucker in a month ๐ฌ , so for the time being I really like this concise and clear way
So, there is an interesting way to go about this that might make your life a lot easier in that case
assuming you don't need to filter or sort on these values
nope I think that is not needed
well....okay, actually there is still the main issue we're dealing with here, which is that you don't want them to have to provide the organisation_id twice.
i.e
you could use read metadata
Oh...so is the only thing you use
organisation_id
on the resource for to load this calculation?yes to load organisation specific overrides, but in the end it should probably also return a 404 if there is no entry for an organisation_id, but that is not an issue yet
If so, I think you ought to do something like this:
Okay, so if you use a module based calculation, you can produce a complex type that contains the specific metadata.
and then
So with that strategy:
1. don't have to get fancy with inline aggregates/expressions
2. group up the fields that are merged into one type
3. resolve that type with regular elixir code
ahh, that is actually very nice!! I think this make a lot of sense to model it this way.
the map in the 3rd argument in the load function, is that the context?
Alright, signing off here in Europe. Thanks again for your help!! Once we get more people involved in Ash at my company (which is going to happen), I hope I can get you in for some training. Would be really helpful I think.
Definitely open to it ๐weโve got training materials that weโre actively refining, just did a whole day training in ElixirConf.
Context merged with arguments
Ok, implementing the above now. This is actually very nice! One question, can a calculation depend on values from another calculation?
And a bit related to that question, can calculations be nested? ie. If have a calculation the calculates a certain (typed) object, can that object contain calculations of it's own?
Yep ๐
nice!
The way that you need to load that is interesting
if you want to load it in code
load: [calculation: {%{}, further: :loads}]
You pass it a tuple like thatok, the direct request for a Game with the above setup works really nice. The thing I am running into now is that if I load a game through:
playlist
-> playlist_entry
-> game
The module doesn't seem to get the organisation_id context which I'm setting now in a playlist... Is that context something that should be explicitly passed on through the relationship configuration?๐ค you can add the argument to a primary read
and then in graphql it would be something like
playlist{ playlistEntry {game(organisation_id: organisation_id)}}
we don't copy the parent query context to all of the loaded query contexts
There is an interesting question of adding context that gets set for all loaded queries, or some mechanism for this to happen.ah ok, what does the
relationship_context
do then?well, you can set that manually
but you need something functional
like "given the parent context, and the child context, give me a new context"
ah ok clear