get selected fields in create/update action

Hi, I have this policy to check if a user is only selecting fields that they are allowed to see. This worked well for reads because it was possible to get the selected/loaded fields from the query. Is it possible to do something similar for mutations
defmodule Demo.Policies.SelectsAllowedFields do
@moduledoc """
Checks if an action selects only fields that are allowed

Takes a mapping of roles to allowed fields as well as a bypass option
to circumvent the check for a given role

e.g:

user: [:id, :name, :last_name],
super_user: [:id, :address],
bypass: admin
"""
use Ash.Policy.SimpleCheck

require Logger

@impl true
def describe(_) do
"Checks if only allowed fields are selected"
end

@impl true
def match?(actor, context, options) do
bypass_role = options[:bypass]

matched =
case get_role(actor) do
role when is_atom(bypass_role) and role == bypass_role ->
true

role ->
match(
options[role],
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))
)
end

if not matched do
dbg()
allowed_fields = options[get_role(actor)] || []

selected_fields =
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))

Logger.debug("""
Actor (#{Map.get(actor, :id, "unknown")}) with role #{get_role(actor)} tried to access resource #{context.resource}"

selecting: #{inspect(selected_fields)}
allowed: #{inspect(allowed_fields)}
diff: #{inspect(selected_fields -- allowed_fields)}
""")
end

matched
end

def get_role(%{roles: roles}) when is_list(roles), do: List.first(roles)
def get_role(_), do: nil

# Get all attributes from the resource struct if no fields are selected
#
# @see https://www.ash-hq.org/docs/module/ash/2.4.10/ash-query#function-select-3
#
# ignore meta fields starting with `__`
# and field with structs as values as those point to
# calculations/aggregates/relationships
defp get_selects(%{query: %{select: nil}, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: nil, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: %{select: select}}), do: select
defp get_selects(e), do: raise(e)

defp get_resource_fields(resource) do
struct = resource.__struct__

struct
|> Map.keys()
|> Enum.filter(fn key ->
!String.starts_with?(to_string(key), "__") and
!Enum.any?([:aggregates, :calculations], fn special_field -> special_field == key end) and
!is_struct(
Map.get(
struct,
key
)
)
end)
end

defp get_loads(%{query: %{load: load}}), do: Keyword.keys(load)
defp get_loads(_), do: []
defp get_calculations(%{query: %{calculations: calculations}}), do: Map.keys(calculations)
defp get_calculations(_), do: []
defp get_aggregates(%{query: %{aggregates: aggregates}}), do: Map.keys(aggregates)
defp get_aggregates(_), do: []

defp match(allowed_fields, selected_fields)
when is_list(allowed_fields) and is_list(selected_fields) do
case selected_fields -- allowed_fields do
[] ->
true

_ ->
false
end
end

defp match(_, _), do: false
end
defmodule Demo.Policies.SelectsAllowedFields do
@moduledoc """
Checks if an action selects only fields that are allowed

Takes a mapping of roles to allowed fields as well as a bypass option
to circumvent the check for a given role

e.g:

user: [:id, :name, :last_name],
super_user: [:id, :address],
bypass: admin
"""
use Ash.Policy.SimpleCheck

require Logger

@impl true
def describe(_) do
"Checks if only allowed fields are selected"
end

@impl true
def match?(actor, context, options) do
bypass_role = options[:bypass]

matched =
case get_role(actor) do
role when is_atom(bypass_role) and role == bypass_role ->
true

role ->
match(
options[role],
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))
)
end

if not matched do
dbg()
allowed_fields = options[get_role(actor)] || []

selected_fields =
[]
|> Enum.concat(get_selects(context))
|> Enum.concat(get_loads(context))
|> Enum.concat(get_calculations(context))
|> Enum.concat(get_aggregates(context))

Logger.debug("""
Actor (#{Map.get(actor, :id, "unknown")}) with role #{get_role(actor)} tried to access resource #{context.resource}"

selecting: #{inspect(selected_fields)}
allowed: #{inspect(allowed_fields)}
diff: #{inspect(selected_fields -- allowed_fields)}
""")
end

matched
end

def get_role(%{roles: roles}) when is_list(roles), do: List.first(roles)
def get_role(_), do: nil

# Get all attributes from the resource struct if no fields are selected
#
# @see https://www.ash-hq.org/docs/module/ash/2.4.10/ash-query#function-select-3
#
# ignore meta fields starting with `__`
# and field with structs as values as those point to
# calculations/aggregates/relationships
defp get_selects(%{query: %{select: nil}, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: nil, resource: resource}),
do: get_resource_fields(resource)

defp get_selects(%{query: %{select: select}}), do: select
defp get_selects(e), do: raise(e)

defp get_resource_fields(resource) do
struct = resource.__struct__

struct
|> Map.keys()
|> Enum.filter(fn key ->
!String.starts_with?(to_string(key), "__") and
!Enum.any?([:aggregates, :calculations], fn special_field -> special_field == key end) and
!is_struct(
Map.get(
struct,
key
)
)
end)
end

defp get_loads(%{query: %{load: load}}), do: Keyword.keys(load)
defp get_loads(_), do: []
defp get_calculations(%{query: %{calculations: calculations}}), do: Map.keys(calculations)
defp get_calculations(_), do: []
defp get_aggregates(%{query: %{aggregates: aggregates}}), do: Map.keys(aggregates)
defp get_aggregates(_), do: []

defp match(allowed_fields, selected_fields)
when is_list(allowed_fields) and is_list(selected_fields) do
case selected_fields -- allowed_fields do
[] ->
true

_ ->
false
end
end

defp match(_, _), do: false
end
38 Replies
ZachDaniel
ZachDaniel•2y ago
🤔 currently, not really. You can get the selected fields because there is a select option on changeset but we would need to add load to changeset as well which would make sense to do 🙂 Let me take a look
barnabasj
barnabasjOP•2y ago
I thought I read somewhere in the docs that it's not possible to load stuff with mutations. But its been a while so not sure
ZachDaniel
ZachDaniel•2y ago
Yeah, there is a way to load them but its done with api.load after the initial create I'll give you the first sneak preview of what I'm working on this week to solve for this need
field_policy :field do
authorize_if expr(id == ^actor(:id))
authorize_if SomeRuntimeCheck # forbidden if you're filtering on this
end
field_policy :field do
authorize_if expr(id == ^actor(:id))
authorize_if SomeRuntimeCheck # forbidden if you're filtering on this
end
ZachDaniel
ZachDaniel•2y ago
It will come w/ a similar thing to %Ash.NotLoaded{} called %Ash.ForbiddenField{} and it will resolve that field to a forbidden error in gql (the entire request won't be forbidden, but the fields you can't see will have a forbidden error in their resolution The other aspect of this though is that we will actually deprecate loading? checks in policies You can still use selecting, but everything related to can you load X should be done as a field policy
barnabasj
barnabasjOP•2y ago
Fortunately I never used loading?, I did it all with this extension so far. But until now it was almost exlcusivly reads.
ZachDaniel
ZachDaniel•2y ago
Yeah, there is an issue w/ that though specifically we don't rerun a read action on api.load all of our authorization is done as "row level" so if you can see a row, it lets you api.load anything on it. AshGraphql first gets the record and uses api.load meaning your policies might not be doing what you think they are doing 😢
barnabasj
barnabasjOP•2y ago
But the query for the first resource would still be getting the load list in the query right? If so I would already check for this on the first resource, if not I do have policies on all the related resources as well so I should be good for now. But definitly something to consider
ZachDaniel
ZachDaniel•2y ago
the current implementation it will not get the load, no the load happens later
barnabasj
barnabasjOP•2y ago
this will trigger the policy on the loaded resource though right?
ZachDaniel
ZachDaniel•2y ago
yeah, all resource policies are still run but aggregates/calculations will always appear to not be being loaded
barnabasj
barnabasjOP•2y ago
Great, in that case I will be able to sleep tonight 🤣
ZachDaniel
ZachDaniel•2y ago
So like if you have a calculation you're locking down using that check you showed me to see if its being loaded it will always appear that its not being loaded selected attributes will always be correct though
barnabasj
barnabasjOP•2y ago
ok, that probably means all my calculations are not secured, because there are no policies attached to calculation directly, same with aggregates. But relationships should be ok
ZachDaniel
ZachDaniel•2y ago
Well, if you're locking down calcs/aggs that way, I'm sorry Its definitely not clear that this is how it works in the docs, hopefully will have field policies done this week
barnabasj
barnabasjOP•2y ago
Just to make sure, policies for calculation would only work correctly with the loading? policy right now?
ZachDaniel
ZachDaniel•2y ago
policies for aggregates and calculations won't work with ash_graphql essentially no matter how it works Actually, just calculations, yeah because it does use Ash.Query.load for aggregates actually...I can make this better sooner one sec I added something to core recently that should let me make AshGraphql use Ash.Query.load for calculations So then the only problem would be if you tried to use loading? with a relationship i.e in your case you have get_loads that will always appear empty
frankdugan3
frankdugan3•2y ago
Sounds like these kinds of policy expectations would a good thing to add to my app's integration tests for the GraphQL API. 😅
ZachDaniel
ZachDaniel•2y ago
ah, okay but there is an implementation detail you need to know about for your get_calculations using this new stuff:
defp get_calculations(%{query: %{calculations: calculations}}), do: calculations |> Map.values() |> Enum.map(&(&1.calc_name)) |> Enum.reject(&is_nil/1)
defp get_calculations(_), do: []

defp get_calculations(%{query: %{calculations: calculations}}), do: calculations |> Map.values() |> Enum.map(&(&1.calc_name)) |> Enum.reject(&is_nil/1)
defp get_calculations(_), do: []

So there may actually be a way to solve this without the need for field_policies (although field policies would be nice to have). The primary reason that ash_graphql can't use Ash.Query.load is because the caller might do this:
things{
relationship(limit: 10) {
}
relationship(limit: 2) {

}
}
things{
relationship(limit: 10) {
}
relationship(limit: 2) {

}
}
Right now, there is not a way to load the same relationship two different ways so AshGraphql uses api.load after the fact to account for that. But if I add Ash.Query.load_relationship_as in the same way we have Ash.Query.load_calculation_as, and add a relationships key to the resource similar to calculations and aggregates for "anonymous" calcs/aggs, then we can create a single Ash.Query.load statement, and then your policies will work 100% as you have them (as long as you add that get_calculations/1 change I mentioned above) I could potentially do that to start, and then add field policies after. That would prevent any unexpected behavior for current implementations. But once field_policy is added, I'd still want people to use that because it will play nice with api.load if someone uses it elsewhere Okay @barnabasj if you try main of ash_graphql with the latest release of ash (2.9.19), and make that get_calculations/1 change, that check should now honor loaded calcs in ash_graphql
barnabasj
barnabasjOP•2y ago
thanks i will have a look at it shortly
ZachDaniel
ZachDaniel•2y ago
As for the original question: I'm going to be adding Ash.Changeset.load and your policy will then be usable on changesets as well in almost exactly the same way. You should do it with aggregates too, actually:
defp get_aggregates(%{query: %{aggregates: aggregates}}), do: aggregates |> Map.values() |> Enum.map(&(&1.agg_name)) |> Enum.reject(&is_nil/1)
defp get_aggregates(_), do: []
defp get_aggregates(%{query: %{aggregates: aggregates}}), do: aggregates |> Map.values() |> Enum.map(&(&1.agg_name)) |> Enum.reject(&is_nil/1)
defp get_aggregates(_), do: []
barnabasj
barnabasjOP•2y ago
@Blibs might be interesting for you A quick test looked promising. The policy seems to work, but I'm getting an error down the line. Not 100% sure if that is just coincidental or related. Will take a closer look tomorrow
[error] Task #PID<0.1700.0> started from #PID<0.1695.0> terminating
** (KeyError) key :source_attribute not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
(ash 2.9.19) lib/ash/actions/read.ex:1472: anonymous fn/3 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(ash 2.9.19) lib/ash/actions/read.ex:1461: anonymous fn/9 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.19) lib/ash/actions/read.ex:820: anonymous fn/4 in Ash.Actions.Read.data_field/3
(ash 2.9.19) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.19) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.19) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.19) lib/ash/engine/engine.ex:702: Ash.Engine.start_pending_tasks/1
(ash 2.9.19) lib/ash/engine/engine.ex:323: Ash.Engine.run_to_completion/1
(ash 2.9.19) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.19) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.19) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.19) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.19) lib/ash/actions/load.ex:1544: Ash.Actions.Load.artificial_limit_and_offset/9
Function: &:erlang.apply/2
Args: [#Function<15.63837685/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>, ...
[error] Task #PID<0.1700.0> started from #PID<0.1695.0> terminating
** (KeyError) key :source_attribute not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
(ash 2.9.19) lib/ash/actions/read.ex:1472: anonymous fn/3 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(ash 2.9.19) lib/ash/actions/read.ex:1461: anonymous fn/9 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.19) lib/ash/actions/read.ex:820: anonymous fn/4 in Ash.Actions.Read.data_field/3
(ash 2.9.19) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.19) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.19) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.19) lib/ash/engine/engine.ex:702: Ash.Engine.start_pending_tasks/1
(ash 2.9.19) lib/ash/engine/engine.ex:323: Ash.Engine.run_to_completion/1
(ash 2.9.19) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.19) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.19) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.19) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.19) lib/ash/actions/load.ex:1544: Ash.Actions.Load.artificial_limit_and_offset/9
Function: &:erlang.apply/2
Args: [#Function<15.63837685/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>, ...
ZachDaniel
ZachDaniel•2y ago
😢 Hmm...yeah something is up there I'll have it raise a more informative (but still raise) error in that case that could potentially help track the issue down. okay, I've added Ash.Changeset.loadto ash core, and used it in AshGraphql The only remaining "issue" is that AshGraphql loads related resources after-the-fact, not in the main query Alright, so the last thing to cover this stuff/wrap it up is the ability to load the same relationship multiple times in different ways. I'm not entirely sure how difficult this is going to end up being 🙂 But once I do that, I can add utilities like Ash.Changeset.loading(changeset) => [%Attribute{}, %Calculation{}, %Aggregate{}] and Ash.Query.loading that will get you the list of things being loaded, to abstract away some of that complexity.
barnabasj
barnabasjOP•2y ago
So far everything seems to work, thank you. After some further tests, two problems emerged. I found a place where we use the aliases already. There we get this error now:
[error] Task #PID<0.1457.0> started from #PID<0.1450.0> terminating
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:__ash_graphql_calculation__, "image16x9"} of type Tuple
(elixir 1.14.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir 1.14.3) lib/string/chars.ex:22: String.Chars.to_string/1
(ash 2.9.21) lib/ash/filter/filter.ex:2357: anonymous fn/1 in Ash.Filter.add_expression_part/3
(elixir 1.14.3) lib/enum.ex:1251: anonymous fn/3 in Enum.flat_map/2
(stdlib 4.2) maps.erl:411: :maps.fold_1/3
(elixir 1.14.3) lib/enum.ex:2480: Enum.flat_map/2
(ash 2.9.21) lib/ash/filter/filter.ex:2356: Ash.Filter.add_expression_part/3
(ash 2.9.21) lib/ash/filter/filter.ex:2078: anonymous fn/3 in Ash.Filter.parse_expression/2
(elixir 1.14.3) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.3) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.9.21) lib/ash/filter/filter.ex:314: Ash.Filter.parse/5
(ash 2.9.21) lib/ash/query/query.ex:2032: Ash.Query.do_filter/2
(ash 2.9.21) lib/ash/actions/load.ex:1750: Ash.Actions.Load.get_query/4
(ash 2.9.21) lib/ash/actions/load.ex:969: anonymous fn/10 in Ash.Actions.Load.data/10
(ash 2.9.21) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.21) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.21) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
Function: &:erlang.apply/2
Args: [#Function<15.63837685/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>,
[error] Task #PID<0.1457.0> started from #PID<0.1450.0> terminating
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:__ash_graphql_calculation__, "image16x9"} of type Tuple
(elixir 1.14.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir 1.14.3) lib/string/chars.ex:22: String.Chars.to_string/1
(ash 2.9.21) lib/ash/filter/filter.ex:2357: anonymous fn/1 in Ash.Filter.add_expression_part/3
(elixir 1.14.3) lib/enum.ex:1251: anonymous fn/3 in Enum.flat_map/2
(stdlib 4.2) maps.erl:411: :maps.fold_1/3
(elixir 1.14.3) lib/enum.ex:2480: Enum.flat_map/2
(ash 2.9.21) lib/ash/filter/filter.ex:2356: Ash.Filter.add_expression_part/3
(ash 2.9.21) lib/ash/filter/filter.ex:2078: anonymous fn/3 in Ash.Filter.parse_expression/2
(elixir 1.14.3) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.3) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.9.21) lib/ash/filter/filter.ex:314: Ash.Filter.parse/5
(ash 2.9.21) lib/ash/query/query.ex:2032: Ash.Query.do_filter/2
(ash 2.9.21) lib/ash/actions/load.ex:1750: Ash.Actions.Load.get_query/4
(ash 2.9.21) lib/ash/actions/load.ex:969: anonymous fn/10 in Ash.Actions.Load.data/10
(ash 2.9.21) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.21) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.21) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
Function: &:erlang.apply/2
Args: [#Function<15.63837685/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>,
And I had a calculation that worked before now triggers this error:
[error] Task #PID<0.1708.0> started from #PID<0.1698.0> terminating
** (RuntimeError) Internal Error:

Calculation depends on relationship but we could not determine the relationship

Resource: JdlEngine.Inventory.Resources.Hotel

Relationship Dependency:
%{path: [hotel_itinerary_items: #Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem>, hotel: #Ash.Query<resource: Demo.Inventory.Resources.Hotel>], query: #Ash.Query<resource: Demo.Geo.Resources.Country>, relationship: :country, type: :relationship}

(ash 2.9.21) lib/ash/actions/read.ex:1500: anonymous fn/15 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:1479: anonymous fn/10 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:838: anonymous fn/4 in Ash.Actions.Read.data_field/3
(ash 2.9.21) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.21) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.21) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/engine/engine.ex:702: Ash.Engine.start_pending_tasks/1
(ash 2.9.21) lib/ash/engine/engine.ex:323: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
Function: &:erlang.apply/2
Args: [#Function<15.63837685/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>,
[error] Task #PID<0.1708.0> started from #PID<0.1698.0> terminating
** (RuntimeError) Internal Error:

Calculation depends on relationship but we could not determine the relationship

Resource: JdlEngine.Inventory.Resources.Hotel

Relationship Dependency:
%{path: [hotel_itinerary_items: #Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem>, hotel: #Ash.Query<resource: Demo.Inventory.Resources.Hotel>], query: #Ash.Query<resource: Demo.Geo.Resources.Country>, relationship: :country, type: :relationship}

(ash 2.9.21) lib/ash/actions/read.ex:1500: anonymous fn/15 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:1479: anonymous fn/10 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:838: anonymous fn/4 in Ash.Actions.Read.data_field/3
(ash 2.9.21) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.21) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.21) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/engine/engine.ex:702: Ash.Engine.start_pending_tasks/1
(ash 2.9.21) lib/ash/engine/engine.ex:323: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
Function: &:erlang.apply/2
Args: [#Function<15.63837685/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>,
I tried to debug the second error a bit, and it seems it is looking for a hotel relationship on the hotel instead of the items, but I'm not sure why
ZachDaniel
ZachDaniel•2y ago
Will look into these both today.
barnabasj
barnabasjOP•2y ago
ash: 8fdd319697ff39262fc4162f38fd6c55e2b9b189 is the last commit that I know worked for us in relation to the query/loads. Policy problems aside This is the load statement in the calc that now fails:
@impl true
def load(_, _, _) do
[
hotel_itinerary_items:
Demo.Offer.Resources.ItineraryItem.HotelItineraryItem
|> Ash.Query.load(
hotel:
Demo.Inventory.Resources.Hotel
|> Ash.Query.select(:id)
|> Ash.Query.load(:country)
)
|> Ash.Query.load(:sort_order),
villa_itinerary_items:
Demo.Offer.Resources.ItineraryItem.VillaItineraryItem
|> Ash.Query.load(
villa:
Demo.Inventory.Resources.Villa
|> Ash.Query.select(:id)
|> Ash.Query.load(:country)
)
|> Ash.Query.load(:sort_order)
]
end
@impl true
def load(_, _, _) do
[
hotel_itinerary_items:
Demo.Offer.Resources.ItineraryItem.HotelItineraryItem
|> Ash.Query.load(
hotel:
Demo.Inventory.Resources.Hotel
|> Ash.Query.select(:id)
|> Ash.Query.load(:country)
)
|> Ash.Query.load(:sort_order),
villa_itinerary_items:
Demo.Offer.Resources.ItineraryItem.VillaItineraryItem
|> Ash.Query.load(
villa:
Demo.Inventory.Resources.Villa
|> Ash.Query.select(:id)
|> Ash.Query.load(:country)
)
|> Ash.Query.load(:sort_order)
]
end
ZachDaniel
ZachDaniel•2y ago
Okay, so I just pushed up a fix for the first issue the second one might be more complicated If you're still around, the error message I was outputting there is actually wrong a bit
barnabasj
barnabasjOP•2y ago
i can try it shortly
ZachDaniel
ZachDaniel•2y ago
awesome, thanks actually we need both okay, just pushed a better message up to main, hopefully will help me track the error down
barnabasj
barnabasjOP•2y ago
Getting the same error here: https://discordapp.com/channels/711271361523351632/1115269100860616735/1115929808187179033 The calc load now givs me this error:
[error] Task #PID<0.6739.0> started from #PID<0.6731.0> terminating
** (RuntimeError) Internal Error:

Calculation depends on relationship but we could not determine the relationship

Resource: Demo.Inventory.Resources.Hotel

Dependency:
%{path: [hotel_itinerary_items: #Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem>], query: #Ash.Query<resource: Demo.Inventory.Resources.Hotel, select: [:id]>, relationship: :hotel, type: :relationship}

Relationship Dependency:
%{path: [hotel_itinerary_items: #Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem>, hotel: #Ash.Query<resource: Demo.Inventory.Resources.Hotel, select: [:id]>], query: #Ash.Query<resource: Demo.Geo.Resources.Country>, relationship: :country, type: :relationship}

(ash 2.9.21) lib/ash/actions/read.ex:1493: anonymous fn/3 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:1479: anonymous fn/9 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:838: anonymous fn/4 in Ash.Actions.Read.data_field/3
(ash 2.9.21) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.21) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.21) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/engine/engine.ex:702: Ash.Engine.start_pending_tasks/1
(ash 2.9.21) lib/ash/engine/engine.ex:323: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.21) lib/ash/actions/load.ex:1549: Ash.Actions.Load.artificial_limit_and_offset/9
Function: &:erlang.apply/2
[error] Task #PID<0.6739.0> started from #PID<0.6731.0> terminating
** (RuntimeError) Internal Error:

Calculation depends on relationship but we could not determine the relationship

Resource: Demo.Inventory.Resources.Hotel

Dependency:
%{path: [hotel_itinerary_items: #Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem>], query: #Ash.Query<resource: Demo.Inventory.Resources.Hotel, select: [:id]>, relationship: :hotel, type: :relationship}

Relationship Dependency:
%{path: [hotel_itinerary_items: #Ash.Query<resource: Demo.Offer.Resources.ItineraryItem.HotelItineraryItem>, hotel: #Ash.Query<resource: Demo.Inventory.Resources.Hotel, select: [:id]>], query: #Ash.Query<resource: Demo.Geo.Resources.Country>, relationship: :country, type: :relationship}

(ash 2.9.21) lib/ash/actions/read.ex:1493: anonymous fn/3 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:1479: anonymous fn/9 in Ash.Actions.Read.calculation_dependency_requests/9
(elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
(elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2
(ash 2.9.21) lib/ash/actions/read.ex:838: anonymous fn/4 in Ash.Actions.Read.data_field/3
(ash 2.9.21) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
(ash 2.9.21) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
(ash 2.9.21) lib/ash/engine/engine.ex:552: Ash.Engine.async/2
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/engine/engine.ex:702: Ash.Engine.start_pending_tasks/1
(ash 2.9.21) lib/ash/engine/engine.ex:323: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.21) lib/ash/actions/load.ex:1549: Ash.Actions.Load.artificial_limit_and_offset/9
Function: &:erlang.apply/2
what I do not understand about this error message is why is the Hotel the resource, because the calculation is on the collection which is not in the error message so the load would be collection -> hotel_itinerary_items -> hotel -> country
ZachDaniel
ZachDaniel•2y ago
are you sure its the same error? could there be a different stacktrace?
barnabasj
barnabasjOP•2y ago
let me check,
[error] Task #PID<0.7072.0> started from #PID<0.7061.0> terminating
** (Ash.Error.Unknown) Unknown Error

Context: resolving data on process Demo.Offer.Resources.ItineraryCollection.Image.read
* Context: resolving data on process Demo.Offer.Resources.ItineraryCollection.Image.read

** (Protocol.UndefinedError) protocol String.Chars not implemented for {:__ash_graphql_calculation__, "image16x9"} of type Tuple
(elixir 1.14.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir 1.14.3) lib/string/chars.ex:22: String.Chars.to_string/1
(ash 2.9.21) lib/ash/actions/read.ex:2525: Ash.Actions.Read.calculation_request/14
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/actions/read.ex:2661: Ash.Actions.Read.add_calculation_values/14
(ash 2.9.21) lib/ash/actions/read.ex:411: anonymous fn/8 in Ash.Actions.Read.as_requests/5
(ash 2.9.21) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.9.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.9.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.9.21) lib/ash/engine/engine.ex:712: Ash.Engine.advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:637: Ash.Engine.fully_advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:578: Ash.Engine.do_run_iteration/2
(elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.9.21) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.21) lib/ash/actions/load.ex:1549: Ash.Actions.Load.artificial_limit_and_offset/9
(ash 2.9.21) lib/ash/actions/load.ex:977: anonymous fn/10 in Ash.Actions.Load.data/10
(elixir 1.14.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir 1.14.3) lib/string/chars.ex:22: String.Chars.to_string/1
(ash 2.9.21) lib/ash/actions/read.ex:2525: Ash.Actions.Read.calculation_request/14
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/actions/read.ex:2661: Ash.Actions.Read.add_calculation_values/14
(ash 2.9.21) lib/ash/actions/read.ex:411: anonymous fn/8 in Ash.Actions.Read.as_requests/5
(ash 2.9.21) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.9.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.9.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.9.21) lib/ash/engine/engine.ex:712: Ash.Engine.advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:637: Ash.Engine.fully_advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:578: Ash.Engine.do_run_iteration/2
(elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.9.21) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.21) lib/ash/actions/load.ex:1549: Ash.Actions.Load.artificial_limit_and_offset/9
(ash 2.9.21) lib/ash/actions/load.ex:977: anonymous fn/10 in Ash.Actions.Load.data/10
Function: &:erlang.apply/2
[error] Task #PID<0.7072.0> started from #PID<0.7061.0> terminating
** (Ash.Error.Unknown) Unknown Error

Context: resolving data on process Demo.Offer.Resources.ItineraryCollection.Image.read
* Context: resolving data on process Demo.Offer.Resources.ItineraryCollection.Image.read

** (Protocol.UndefinedError) protocol String.Chars not implemented for {:__ash_graphql_calculation__, "image16x9"} of type Tuple
(elixir 1.14.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir 1.14.3) lib/string/chars.ex:22: String.Chars.to_string/1
(ash 2.9.21) lib/ash/actions/read.ex:2525: Ash.Actions.Read.calculation_request/14
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/actions/read.ex:2661: Ash.Actions.Read.add_calculation_values/14
(ash 2.9.21) lib/ash/actions/read.ex:411: anonymous fn/8 in Ash.Actions.Read.as_requests/5
(ash 2.9.21) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.9.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.9.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.9.21) lib/ash/engine/engine.ex:712: Ash.Engine.advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:637: Ash.Engine.fully_advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:578: Ash.Engine.do_run_iteration/2
(elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.9.21) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.21) lib/ash/actions/load.ex:1549: Ash.Actions.Load.artificial_limit_and_offset/9
(ash 2.9.21) lib/ash/actions/load.ex:977: anonymous fn/10 in Ash.Actions.Load.data/10
(elixir 1.14.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir 1.14.3) lib/string/chars.ex:22: String.Chars.to_string/1
(ash 2.9.21) lib/ash/actions/read.ex:2525: Ash.Actions.Read.calculation_request/14
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash 2.9.21) lib/ash/actions/read.ex:2661: Ash.Actions.Read.add_calculation_values/14
(ash 2.9.21) lib/ash/actions/read.ex:411: anonymous fn/8 in Ash.Actions.Read.as_requests/5
(ash 2.9.21) lib/ash/engine/request.ex:1048: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.9.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.9.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.9.21) lib/ash/engine/engine.ex:712: Ash.Engine.advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:637: Ash.Engine.fully_advance_request/2
(ash 2.9.21) lib/ash/engine/engine.ex:578: Ash.Engine.do_run_iteration/2
(elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.9.21) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
(ash 2.9.21) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
(ash 2.9.21) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
(ash 2.9.21) lib/ash/actions/read.ex:173: Ash.Actions.Read.do_run/3
(ash 2.9.21) lib/ash/actions/read.ex:96: Ash.Actions.Read.run/3
(ash 2.9.21) lib/ash/actions/load.ex:1549: Ash.Actions.Load.artificial_limit_and_offset/9
(ash 2.9.21) lib/ash/actions/load.ex:977: anonymous fn/10 in Ash.Actions.Load.data/10
Function: &:erlang.apply/2
ok, yeah stack trace is bigger :facepalm:
ZachDaniel
ZachDaniel•2y ago
okay, pushed up another fix for that just more places we're thinking we can do to_string(calc.name) but we can't always could potentially be more, its hard to find them all the time 😆 I'm surprised tests in ash_graphql didn't find this. I guess its because it didn't test dynamic calcs with calcs with dependencies okay, and I think I found the issue for the dependency issue also so main should have fixes for both hopefully
barnabasj
barnabasjOP•2y ago
%{
{:__ash_graphql_calculation__, "image16x9"} => #Demo.Calculations.ImageUrlCaculation<[]> - %{named_transformations: ["crop_16:9_centered"]}
}
calculation: %{
calc_name: {:__ash_graphql_calculation__, "image16x9"},
constraints: [allow_empty?: false, trim?: true],
context: %{
%{
{:__ash_graphql_calculation__, "image16x9"} => #Demo.Calculations.ImageUrlCaculation<[]> - %{named_transformations: ["crop_16:9_centered"]}
}
calculation: %{
calc_name: {:__ash_graphql_calculation__, "image16x9"},
constraints: [allow_empty?: false, trim?: true],
context: %{
I logged the calculations in the query, I was just wondering why the calc_name was not the original calc name. Is there a way to get the original calc name from the alias?
ZachDaniel
ZachDaniel•2y ago
oh yeah that is what that is supposed to be thanks for pointing that out calc_name should be nil for anonymous calculations (which you can't do through the api) and :the_resource_calc_name otherwise okay, pushed a fix for that as well
barnabasj
barnabasjOP•2y ago
great, thanks. now i can get the correct calc name and check if the user is allowed to get it too 🚀
ZachDaniel
ZachDaniel•2y ago
🥳 🥳 🥳
barnabasj
barnabasjOP•2y ago
Just clicked through all the pages, looks like everything is working now :party_parrot:

Did you find this page helpful?