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
drumusician
drumusicianOPโ€ข2y ago
I'll paste the read here
defmodule McFun.Games.Manual.GameManual do
use Ash.Resource.ManualRead
require Ecto.Query
import Ecto.Query

def read(ash_query, ecto_query, _opts, _context) do
org_id = Ash.Query.get_argument(ash_query, :organisation_id)
query =
ecto_query
|> join_default_game_meta()
|> join_org_game_meta(org_id)
|> select_merge_data(org_id)
result = McFun.Repo.all(query)
{:ok, result}
end
defp join_default_game_meta(query) do
from(game in query,
join: default_game_meta in McFun.Games.GameMeta,
on: is_nil(default_game_meta.organisation_id) and default_game_meta.game_id == game.id,
as: :default_game_meta
)
end
defp join_org_game_meta(query, nil), do: query
defp join_org_game_meta(query, org_id) do
from(
game in query,
join: org_game_meta in assoc(game, :game_meta),
as: :org_game_meta,
on: org_game_meta.organisation_id == ^org_id
)
end
defp select_merge_data(query, nil) do
from(
[game, default_game_meta: default_game_meta] in query,
select_merge: %{
game
| display_title: default_game_meta.display_title,
game_meta: %{
display_title: default_game_meta.display_title,
description: default_game_meta.description,
icon_src: default_game_meta.icon_src
}
}
)
end
defp select_merge_data(query, _org_id) do
from(
[game, org_game_meta: org_game_meta, default_game_meta: default_game_meta] in query,
select_merge: %{
game
| display_title: coalesce(org_game_meta.display_title, default_game_meta.display_title),
game_meta: %{
display_title: coalesce(org_game_meta.display_title, default_game_meta.display_title),
description: coalesce(org_game_meta.description, default_game_meta.description),
icon_src: coalesce(org_game_meta.icon_src, default_game_meta.icon_src)
}
}
)
end
end
defmodule McFun.Games.Manual.GameManual do
use Ash.Resource.ManualRead
require Ecto.Query
import Ecto.Query

def read(ash_query, ecto_query, _opts, _context) do
org_id = Ash.Query.get_argument(ash_query, :organisation_id)
query =
ecto_query
|> join_default_game_meta()
|> join_org_game_meta(org_id)
|> select_merge_data(org_id)
result = McFun.Repo.all(query)
{:ok, result}
end
defp join_default_game_meta(query) do
from(game in query,
join: default_game_meta in McFun.Games.GameMeta,
on: is_nil(default_game_meta.organisation_id) and default_game_meta.game_id == game.id,
as: :default_game_meta
)
end
defp join_org_game_meta(query, nil), do: query
defp join_org_game_meta(query, org_id) do
from(
game in query,
join: org_game_meta in assoc(game, :game_meta),
as: :org_game_meta,
on: org_game_meta.organisation_id == ^org_id
)
end
defp select_merge_data(query, nil) do
from(
[game, default_game_meta: default_game_meta] in query,
select_merge: %{
game
| display_title: default_game_meta.display_title,
game_meta: %{
display_title: default_game_meta.display_title,
description: default_game_meta.description,
icon_src: default_game_meta.icon_src
}
}
)
end
defp select_merge_data(query, _org_id) do
from(
[game, org_game_meta: org_game_meta, default_game_meta: default_game_meta] in query,
select_merge: %{
game
| display_title: coalesce(org_game_meta.display_title, default_game_meta.display_title),
game_meta: %{
display_title: coalesce(org_game_meta.display_title, default_game_meta.display_title),
description: coalesce(org_game_meta.description, default_game_meta.description),
icon_src: coalesce(org_game_meta.icon_src, default_game_meta.icon_src)
}
}
)
end
end
excuse the missing spacing, otherwise the post was too long ๐Ÿ™ƒ
ZachDaniel
ZachDanielโ€ข2y ago
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?
drumusician
drumusicianOPโ€ข2y ago
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.
ZachDaniel
ZachDanielโ€ข2y ago
Could you describe the relationship you'd like to make? I might be able to help ๐Ÿ˜„
drumusician
drumusicianOPโ€ข2y ago
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!
ZachDaniel
ZachDanielโ€ข2y ago
Interesting, that does make sense.
drumusician
drumusicianOPโ€ข2y ago
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:
has_one :default_game_meta, McFun.Games.GameMeta do
filter(expr(is_nil(organisation_id)))
end

has_one(:game_meta, McFun.Games.GameMeta) do
from_many?(true)

filter(expr(organisation_id == ^arg(:organisation_id)))
end
has_one :default_game_meta, McFun.Games.GameMeta do
filter(expr(is_nil(organisation_id)))
end

has_one(:game_meta, McFun.Games.GameMeta) do
from_many?(true)

filter(expr(organisation_id == ^arg(:organisation_id)))
end
And then create a calculation that uses a fragment with the coalesce or something siimilar
ZachDaniel
ZachDanielโ€ข2y ago
Yes, you should be able to do something like that ๐Ÿ™‚
drumusician
drumusicianOPโ€ข2y ago
calculate(:display_title, :string, expr(game_meta.display_title || default_game_meta.display_title))
calculate(:display_title, :string, expr(game_meta.display_title || default_game_meta.display_title))
ZachDaniel
ZachDanielโ€ข2y ago
That is close, yes but you will get errors about references because calculations can't directly refer to related fields
drumusician
drumusicianOPโ€ข2y ago
yeah it's lovely psuedo code that doesn't work ๐Ÿ˜†
ZachDaniel
ZachDanielโ€ข2y ago
However, with the recent addition of "anonymous aggregates", you should be able to do this
calculate(:display_title, :string, expr(first(game_meta, field: :display_title) || first(default_game_meta, field: :display_title)))
calculate(:display_title, :string, expr(first(game_meta, field: :display_title) || first(default_game_meta, field: :display_title)))
drumusician
drumusicianOPโ€ข2y ago
ohh inline aggregates, wow!
ZachDaniel
ZachDanielโ€ข2y ago
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
drumusician
drumusicianOPโ€ข2y ago
๐Ÿ™‚
ZachDaniel
ZachDanielโ€ข2y ago
oh, also:
filter(expr(organisation_id == ^arg(:organisation_id)))
filter(expr(organisation_id == ^arg(:organisation_id)))
^arg doesn't work in a relationship
drumusician
drumusicianOPโ€ข2y ago
yeah I wanted to ask about that thought so
ZachDaniel
ZachDanielโ€ข2y ago
are you referring to the organization_id of the record you're talking about?
drumusician
drumusicianOPโ€ข2y ago
yeah that is the idea
ZachDaniel
ZachDanielโ€ข2y ago
has_one(:game_meta, McFun.Games.GameMeta) do
from_many?(true)

filter(expr(organisation_id == source(organisation_id)))
end
has_one(:game_meta, McFun.Games.GameMeta) do
from_many?(true)

filter(expr(organisation_id == source(organisation_id)))
end
drumusician
drumusicianOPโ€ข2y ago
oh no, it doesn't exist on the source
ZachDaniel
ZachDanielโ€ข2y ago
ah, okay interesting
drumusician
drumusicianOPโ€ข2y ago
it's an argument being passed in
ZachDaniel
ZachDanielโ€ข2y ago
understood ๐Ÿค” So you can do that, but it gets a bit interesting has_many :game_metas, ... assuming you have that
drumusician
drumusicianOPโ€ข2y ago
read :by_id do
argument(:id, :uuid)
argument(:organisation_id, :integer, default: nil)

filter(expr(id == ^arg(:id)))
prepare(build(load: [:default_game_meta, :game_meta, :display_title]))
end
read :by_id do
argument(:id, :uuid)
argument(:organisation_id, :integer, default: nil)

filter(expr(id == ^arg(:id)))
prepare(build(load: [:default_game_meta, :game_meta, :display_title]))
end
ZachDaniel
ZachDanielโ€ข2y ago
We're getting to some relatively uncharted territory here, but I am doing this kind of thing in another app
drumusician
drumusicianOPโ€ข2y ago
installing ash-functions... ๐Ÿ˜‰
ZachDaniel
ZachDanielโ€ข2y ago
calculate
:display_title,
:string,
expr(
first(game_meta, field: :display_title, query: [filter: is_nil(organization_id)])
|| first(default_game_meta, field: :display_title, query: [filter: organization_id == source(organization_id)])))
calculate
:display_title,
:string,
expr(
first(game_meta, field: :display_title, query: [filter: is_nil(organization_id)])
|| first(default_game_meta, field: :display_title, query: [filter: organization_id == source(organization_id)])))
I have honestly no idea of that source is going to work in this context oh, wait you don't need source
calculate
:display_title,
:string,
expr(
first(game_meta, field: :display_title, query: [filter: is_nil(organization_id)])
|| first(default_game_meta, field: :display_title, query: [filter: organization_id == ^arg(:organization_id)])))
calculate
:display_title,
:string,
expr(
first(game_meta, field: :display_title, query: [filter: is_nil(organization_id)])
|| first(default_game_meta, field: :display_title, query: [filter: organization_id == ^arg(:organization_id)])))
that will work ๐Ÿ™‚
drumusician
drumusicianOPโ€ข2y ago
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:
calculate(
:display_title,
:string,
expr(
first(game_meta,
field: :display_title,
query: [filter: organisation_id == ^arg(:organisation_id)]
) ||
first(default_game_meta,
field: :display_title
)
)
)
calculate(
:display_title,
:string,
expr(
first(game_meta,
field: :display_title,
query: [filter: organisation_id == ^arg(:organisation_id)]
) ||
first(default_game_meta,
field: :display_title
)
)
)
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
ZachDaniel
ZachDanielโ€ข2y ago
It should be expecting it at the game_meta level...
drumusician
drumusicianOPโ€ข2y ago
ok, then it should actually work
ZachDaniel
ZachDanielโ€ข2y ago
maybe try this:
calculate(
:display_title,
:string,
expr(
first(game_meta,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
)
calculate(
:display_title,
:string,
expr(
first(game_meta,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
)
try wrapping the filter on that with expr/1
drumusician
drumusicianOPโ€ข2y ago
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:
has_one(:org_game_meta, McFun.Games.GameMeta) do
filter(expr(organisation_id == 1))
end
has_one(:org_game_meta, McFun.Games.GameMeta) do
filter(expr(organisation_id == 1))
end
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:
first(game_meta,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
)
first(game_meta,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
)
ZachDaniel
ZachDanielโ€ข2y ago
So, the inline aggregate ought to do the thing i.e this:
calculate(
:display_title,
:string,
expr(
first(game_meta,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
)
calculate(
:display_title,
:string,
expr(
first(game_meta,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
)
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?
drumusician
drumusicianOPโ€ข2y ago
ok, so debugging this a bit. It does work if I hardcode the ID instead of the ^arg, so this does work:
calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(organisation_id == 1)]
) ||
first(default_game_meta,
field: :display_title
)
)
)
calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(organisation_id == 1)]
) ||
first(default_game_meta,
field: :display_title
)
)
)
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
ZachDaniel
ZachDanielโ€ข2y ago
Oh You need to add an argument to the calculation Do you have that
calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(^arg(:organization_id) == 1)]
) ||
first(default_game_meta,
field: :display_title
)
)
) do
argument :organization_id, :uuid, allow_nil?: false
end
calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(^arg(:organization_id) == 1)]
) ||
first(default_game_meta,
field: :display_title
)
)
) do
argument :organization_id, :uuid, allow_nil?: false
end
drumusician
drumusicianOPโ€ข2y ago
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
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.InvalidCalculationArgument{calculation: :display_title, field: :organisation_id, message: "is required", value: nil, changeset: nil, query: nil, error_context: [], vars: [], path: [:load], stacktrace: #Stacktrace<>, class: :invalid}], stacktraces?: true, changeset: nil, query: #Ash.Query<resource: McFun.Games.Game, arguments: %{id: "84f31793-53a5-4208-baae-fd25b3123e9c", organisation_id: 1}, filter: #Ash.Filter<id == "84f31793-53a5-4208-baae-fd25b3123e9c">, load: [default_game_meta: [], game_metas: []], errors: [%Ash.Error.Query.InvalidCalculationArgument{calculation: :display_title, field: :organisation_id, message: "is required", value: nil, changeset: nil, query: nil, error_context: [], vars: [], path: [:load], stacktrace: #Stacktrace<>, class: :invalid}]>, error_context: [nil], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}}
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.InvalidCalculationArgument{calculation: :display_title, field: :organisation_id, message: "is required", value: nil, changeset: nil, query: nil, error_context: [], vars: [], path: [:load], stacktrace: #Stacktrace<>, class: :invalid}], stacktraces?: true, changeset: nil, query: #Ash.Query<resource: McFun.Games.Game, arguments: %{id: "84f31793-53a5-4208-baae-fd25b3123e9c", organisation_id: 1}, filter: #Ash.Filter<id == "84f31793-53a5-4208-baae-fd25b3123e9c">, load: [default_game_meta: [], game_metas: []], errors: [%Ash.Error.Query.InvalidCalculationArgument{calculation: :display_title, field: :organisation_id, message: "is required", value: nil, changeset: nil, query: nil, error_context: [], vars: [], path: [:load], stacktrace: #Stacktrace<>, class: :invalid}]>, error_context: [nil], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}}
I'll try to do this with a calculation module implementation, let's see if I can get that in a working state
ZachDaniel
ZachDanielโ€ข2y ago
How are you passing the arguments to the calc? They have to be passed to the calc specifically, not just the action
drumusician
drumusicianOPโ€ข2y ago
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?
drumusician
drumusicianOPโ€ข2y ago
๐Ÿคฆโ€โ™‚๏ธ
No description
drumusician
drumusicianOPโ€ข2y ago
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:
read :by_id do
argument(:id, :uuid)
argument(:organisation_id, :integer, allow_nil?: false)

filter(expr(id == ^arg(:id)))

prepare(build(load: [:default_game_meta, :game_metas, display_title: [organisation_id: expr(^arg(:organisation_id))]]))
end
read :by_id do
argument(:id, :uuid)
argument(:organisation_id, :integer, allow_nil?: false)

filter(expr(id == ^arg(:id)))

prepare(build(load: [:default_game_meta, :game_metas, display_title: [organisation_id: expr(^arg(:organisation_id))]]))
end
The Calculation:
calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
) do
argument(:organisation_id, :integer, allow_nil?: false, default: 1)
end
calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(organisation_id == ^arg(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
) do
argument(:organisation_id, :integer, allow_nil?: false, default: 1)
end
The relationships:
has_many(:game_metas, McFun.Games.GameMeta)

has_one :default_game_meta, McFun.Games.GameMeta do
filter(expr(is_nil(organisation_id)))
end
has_many(:game_metas, McFun.Games.GameMeta)

has_one :default_game_meta, McFun.Games.GameMeta do
filter(expr(is_nil(organisation_id)))
end
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
query getGame {
getGame(id: "84f31793-53a5-4208-baae-fd25b3123e9c", organisationId: 1) {
id
title
displayTitle(organisationId: 1)
}
}
query getGame {
getGame(id: "84f31793-53a5-4208-baae-fd25b3123e9c", organisationId: 1) {
id
title
displayTitle(organisationId: 1)
}
}
Thanks for all the help again, really excited that we figured it out and hopefully it can provide useful in the future for others ๐Ÿ˜‰
ZachDaniel
ZachDanielโ€ข2y ago
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.
drumusician
drumusicianOPโ€ข2y ago
I think the default is only needed to get the right type of argument, because it fails in this manner:
"errors": [
{
"message": "In argument \"organisationId\": Expected type \"Int!\", found null.",
"locations": [
{
"line": 5,
"column": 5
}
]
}
]
"errors": [
{
"message": "In argument \"organisationId\": Expected type \"Int!\", found null.",
"locations": [
{
"line": 5,
"column": 5
}
]
}
]
but if I for instance do this:
query getGame {
getGame(id: "84f31793-53a5-4208-baae-fd25b3123e9c", organisationId: 8) {
id
title
displayTitle(organisationId: 1)
}
}
query getGame {
getGame(id: "84f31793-53a5-4208-baae-fd25b3123e9c", organisationId: 8) {
id
title
displayTitle(organisationId: 1)
}
}
the ID at the top always wins and the bottom one doesn't have any effect
ZachDaniel
ZachDanielโ€ข2y ago
Well, it wins because of the way you're loading it, yeah which is actually not necessarily the right behavior
drumusician
drumusicianOPโ€ข2y ago
do you mean the default GameMeta ?
ZachDaniel
ZachDanielโ€ข2y ago
Lets try something actually
read :by_id do
argument(:id, :uuid)
argument(:organisation_id, :integer, allow_nil?: false)

filter(expr(id == ^arg(:id)))

prepare set_context(:organisation_id, arg(:organisation_id))
# you shouldn't need to load things manually
# prepare(build(load: [:default_game_meta, :game_metas, display_title: [organisation_id: expr(^arg(:organisation_id))]]))
end

calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(organisation_id == ^context(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
)
read :by_id do
argument(:id, :uuid)
argument(:organisation_id, :integer, allow_nil?: false)

filter(expr(id == ^arg(:id)))

prepare set_context(:organisation_id, arg(:organisation_id))
# you shouldn't need to load things manually
# prepare(build(load: [:default_game_meta, :game_metas, display_title: [organisation_id: expr(^arg(:organisation_id))]]))
end

calculate(
:display_title,
:string,
expr(
first(game_metas,
field: :display_title,
query: [filter: expr(organisation_id == ^context(:organisation_id))]
) ||
first(default_game_meta,
field: :display_title
)
)
)
This uses the context to set the organisation_id, then the API never thinks it has to set it
drumusician
drumusicianOPโ€ข2y ago
ok, trying, just a sec
ZachDaniel
ZachDanielโ€ข2y ago
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
drumusician
drumusicianOPโ€ข2y ago
it doesn't know about set_context/2
ZachDaniel
ZachDanielโ€ข2y ago
hmm... oh yeah maybe thats not a builtin there? do this
prepare fn query, _ ->
Ash.Query.set_context(query, :organisation_id, query.arguments[:organisation_id])
end
prepare fn query, _ ->
Ash.Query.set_context(query, :organisation_id, query.arguments[:organisation_id])
end
drumusician
drumusicianOPโ€ข2y ago
ok, with a few changes to your example it worked:
prepare fn query, _ ->
Ash.Query.set_context(query, %{organisation_id: query.arguments[:organisation_id]})
end
prepare(build(load: [:default_game_meta, :game_metas, :display_title]))
prepare fn query, _ ->
Ash.Query.set_context(query, %{organisation_id: query.arguments[:organisation_id]})
end
prepare(build(load: [:default_game_meta, :game_metas, :display_title]))
- set_context needed a map
ZachDaniel
ZachDanielโ€ข2y ago
ah, yeah, right
drumusician
drumusicianOPโ€ข2y ago
and without the load it doesn't work
ZachDaniel
ZachDanielโ€ข2y ago
oh really??
drumusician
drumusicianOPโ€ข2y ago
yeah, code_interface as well as the graphql
ZachDaniel
ZachDanielโ€ข2y ago
okay, interesting
drumusician
drumusicianOPโ€ข2y ago
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
ZachDaniel
ZachDanielโ€ข2y ago
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
drumusician
drumusicianOPโ€ข2y ago
nope I think that is not needed
ZachDaniel
ZachDanielโ€ข2y ago
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
read(organisation_id) {
field(organisation_id)
}
read(organisation_id) {
field(organisation_id)
}
you could use read metadata Oh...so is the only thing you use organisation_id on the resource for to load this calculation?
drumusician
drumusicianOPโ€ข2y ago
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
ZachDaniel
ZachDanielโ€ข2y ago
If so, I think you ought to do something like this:
defmodule OrgGameMeta do
use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

graphql do
type :org_game_meta
end

attributes do
attribute :display_title, :string, allow_nil?: false
....
end
end

defmodule GetOrgGameMeta do
use Ash.Calculation

require Ash.Query

def load(_, _, %{organisation_id: organisation_id}) do
game_metas_query = Ash.Query.filter(GameMeta, organisation_id == organisation_id)
[:default_game_meta, game_metas: game_metas_query]
end

def calculate(records, _, _) do
Enum.map(records, fn record ->
specific_meta = record.game_meta |> Enum.at(0) || raise "not found"
%OrgGameMeta{
display_title: record.default_game_meta.display_title || specific_meta.display_title
}
end)
end
end

calculate :game_meta, OrgGameMeta, GetOrgGameMeta
defmodule OrgGameMeta do
use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

graphql do
type :org_game_meta
end

attributes do
attribute :display_title, :string, allow_nil?: false
....
end
end

defmodule GetOrgGameMeta do
use Ash.Calculation

require Ash.Query

def load(_, _, %{organisation_id: organisation_id}) do
game_metas_query = Ash.Query.filter(GameMeta, organisation_id == organisation_id)
[:default_game_meta, game_metas: game_metas_query]
end

def calculate(records, _, _) do
Enum.map(records, fn record ->
specific_meta = record.game_meta |> Enum.at(0) || raise "not found"
%OrgGameMeta{
display_title: record.default_game_meta.display_title || specific_meta.display_title
}
end)
end
end

calculate :game_meta, OrgGameMeta, GetOrgGameMeta
Okay, so if you use a module based calculation, you can produce a complex type that contains the specific metadata. and then
{
getGame(id: game_id) {
gameMeta(organisation_id: organisation_id) {
displayTitle
}
}
}
{
getGame(id: game_id) {
gameMeta(organisation_id: organisation_id) {
displayTitle
}
}
}
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
drumusician
drumusicianOPโ€ข2y ago
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.
ZachDaniel
ZachDanielโ€ข2y ago
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
drumusician
drumusicianOPโ€ข2y ago
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?
ZachDaniel
ZachDanielโ€ข2y ago
Yep ๐Ÿ™‚
drumusician
drumusicianOPโ€ข2y ago
nice!
ZachDaniel
ZachDanielโ€ข2y ago
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 that
drumusician
drumusicianOPโ€ข2y ago
ok, 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?
ZachDaniel
ZachDanielโ€ข2y ago
๐Ÿค” 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.
drumusician
drumusicianOPโ€ข2y ago
ah ok, what does the relationship_context do then?
ZachDaniel
ZachDanielโ€ข2y ago
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"
drumusician
drumusicianOPโ€ข2y ago
ah ok clear

Did you find this page helpful?