Set tenant within read action in resource dsl?

Hi, is it possible to set the tenant within a read action in the Resource DSL? I tried to call set_tenant/1 in there but it doesn't seem to work...
20 Replies
ZachDaniel
ZachDaniel3y ago
It should be 🙂 What exactly are you doing? Where does the tenant come from? Or is it a static value?
Adam Harris
Adam HarrisOP3y ago
I'm trying to pass the tenant in via an argument to the action (it's a bit of a temporary hack to debug something else actually)
ZachDaniel
ZachDaniel3y ago
Does something like this not work?
read :read do
prepare fn query, _ ->
Ash.Query.set_tenant(query, query.arguments[:tenant])
end
end
read :read do
prepare fn query, _ ->
Ash.Query.set_tenant(query, query.arguments[:tenant])
end
end
Adam Harris
Adam HarrisOP3y ago
sorry I forgot you could use anonymous fns like that within the DSL. Yep that worked - thank you! 👍🏼 @Zach Daniel Actually, I'm not sure whether this is working or not. Basically I have two related resources - one (TenantedResource) has context tenancy and the other resource (UntenantedResource) doesn't have tenancy. I'm trying to make a query action on UntenantedResource to load some related records of TenantedResource. The query works when I call the action from UntenantedResource's api, specifying the tenant. However when I try and call it via a graphql query and pass the tenant in as an argument, I'm getting errors like:
Queries against the TenantedResource resource require a tenant to be specified
Queries against the TenantedResource resource require a tenant to be specified
ZachDaniel
ZachDaniel3y ago
🤔 interesting. The use case should be supported. What is the stacktrace of your error?
Adam Harris
Adam HarrisOP3y ago
actually I think it's an ash_graphql error when I remove the tenant: tenant from the call to UntenantedResource's api, but keep the tenant argument, it still works
ZachDaniel
ZachDaniel3y ago
do you have other multitenant stuff working in AshGraphql currently? Or is this your first time setting it up
Adam Harris
Adam HarrisOP3y ago
actually I just have resources without multitenancy working on the graphql api at the moment - so yes it might be the first multitenant stuff I've done with ash graphql
ZachDaniel
ZachDaniel3y ago
Gotcha, okay. Not quite sure what is going wrong yet, but lets start at the basics. Do you want to be setting multitenancy as an argument? Typically this would be set either in a subdomain, or a path, that kind of thing. Its okay for a tenant to be set when referring to a non-multitenant resource generally
Adam Harris
Adam HarrisOP3y ago
ultimately, I don't want to be setting multitenancy as an argument. But for now it's just convenient for testing things out quickly without making decisions about all that stuff
ZachDaniel
ZachDaniel3y ago
gotcha. Well, it should be convenient 😆 So what is the stacktrace of the error?
Adam Harris
Adam HarrisOP3y ago
it's actually just a stop-gap temporary API that we need to spin up fairly quickly
** (Ash.Error.Invalid) Input Invalid

* Queries against the TenantedResource resource require a tenant to be specified
(ash 2.6.21) lib/ash/actions/read.ex:603: Ash.Actions.Read.validate_multitenancy/2
(ash 2.6.21) lib/ash/actions/read.ex:303: anonymous fn/13 in Ash.Actions.Read.as_requests/5
(ash 2.6.21) lib/ash/engine/request.ex:1051: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.21) lib/ash/engine/request.ex:932: Ash.Engine.Request.do_try_resolve/5
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.6.21) lib/ash/engine/request.ex:998: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.21) lib/ash/engine/engine.ex:661: Ash.Engine.advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:567: Ash.Engine.fully_advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:508: Ash.Engine.do_run_iteration/2
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.21) lib/ash/engine/engine.ex:268: Ash.Engine.run_to_completion/1
(ash 2.6.21) lib/ash/engine/engine.ex:213: Ash.Engine.do_run/2
(ash 2.6.21) lib/ash/engine/engine.ex:116: Ash.Engine.run/2
(ash 2.6.21) lib/ash/actions/read.ex:170: Ash.Actions.Read.do_run/3
(ash 2.6.21) lib/ash/actions/read.ex:90: Ash.Actions.Read.run/3
(ash 2.6.21) lib/ash/error/error.ex:463: Ash.Error.choose_error/2
(ash 2.6.21) lib/ash/error/error.ex:218: Ash.Error.to_error_class/2
(ash 2.6.21) lib/ash/actions/read.ex:181: Ash.Actions.Read.do_run/3
(ash 2.6.21) lib/ash/actions/read.ex:90: Ash.Actions.Read.run/3
(ash 2.6.21) lib/ash/actions/load.ex:674: anonymous fn/9 in Ash.Actions.Load.data/9
(ash 2.6.21) lib/ash/engine/request.ex:1047: Ash.Engine.Request.do_try_resolve_local/4
** (Ash.Error.Invalid) Input Invalid

* Queries against the TenantedResource resource require a tenant to be specified
(ash 2.6.21) lib/ash/actions/read.ex:603: Ash.Actions.Read.validate_multitenancy/2
(ash 2.6.21) lib/ash/actions/read.ex:303: anonymous fn/13 in Ash.Actions.Read.as_requests/5
(ash 2.6.21) lib/ash/engine/request.ex:1051: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.21) lib/ash/engine/request.ex:932: Ash.Engine.Request.do_try_resolve/5
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.6.21) lib/ash/engine/request.ex:998: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.21) lib/ash/engine/engine.ex:661: Ash.Engine.advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:567: Ash.Engine.fully_advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:508: Ash.Engine.do_run_iteration/2
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.21) lib/ash/engine/engine.ex:268: Ash.Engine.run_to_completion/1
(ash 2.6.21) lib/ash/engine/engine.ex:213: Ash.Engine.do_run/2
(ash 2.6.21) lib/ash/engine/engine.ex:116: Ash.Engine.run/2
(ash 2.6.21) lib/ash/actions/read.ex:170: Ash.Actions.Read.do_run/3
(ash 2.6.21) lib/ash/actions/read.ex:90: Ash.Actions.Read.run/3
(ash 2.6.21) lib/ash/error/error.ex:463: Ash.Error.choose_error/2
(ash 2.6.21) lib/ash/error/error.ex:218: Ash.Error.to_error_class/2
(ash 2.6.21) lib/ash/actions/read.ex:181: Ash.Actions.Read.do_run/3
(ash 2.6.21) lib/ash/actions/read.ex:90: Ash.Actions.Read.run/3
(ash 2.6.21) lib/ash/actions/load.ex:674: anonymous fn/9 in Ash.Actions.Load.data/9
(ash 2.6.21) lib/ash/engine/request.ex:1047: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.21) lib/ash/engine/engine.ex:661: Ash.Engine.advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:567: Ash.Engine.fully_advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:508: Ash.Engine.do_run_iteration/2
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.21) lib/ash/engine/engine.ex:268:
Ash.Engine.run_to_completion/1
(ash 2.6.21) lib/ash/engine/engine.ex:213: Ash.Engine.do_run/2
(ash 2.6.21) lib/ash/engine/engine.ex:116: Ash.Engine.run/2
(ash 2.6.21) lib/ash/actions/read.ex:170: Ash.Actions.Read.do_run/3
(ash 2.6.21) lib/ash/actions/read.ex:90: Ash.Actions.Read.run/3
(ash 2.6.21) lib/ash/api/api.ex:1310: Ash.Api.load!/4
Function: &:erlang.apply/2
Args: [#"fun"ction<13.10028942/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>,
...
(ash 2.6.21) lib/ash/engine/request.ex:282: Ash.Engine.Request.do_next/1
(ash 2.6.21) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.21) lib/ash/engine/engine.ex:661: Ash.Engine.advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:567: Ash.Engine.fully_advance_request/2
(ash 2.6.21) lib/ash/engine/engine.ex:508: Ash.Engine.do_run_iteration/2
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.21) lib/ash/engine/engine.ex:268:
Ash.Engine.run_to_completion/1
(ash 2.6.21) lib/ash/engine/engine.ex:213: Ash.Engine.do_run/2
(ash 2.6.21) lib/ash/engine/engine.ex:116: Ash.Engine.run/2
(ash 2.6.21) lib/ash/actions/read.ex:170: Ash.Actions.Read.do_run/3
(ash 2.6.21) lib/ash/actions/read.ex:90: Ash.Actions.Read.run/3
(ash 2.6.21) lib/ash/api/api.ex:1310: Ash.Api.load!/4
Function: &:erlang.apply/2
Args: [#"fun"ction<13.10028942/1 in Dataloader.Source.AshGraphql.Dataloader.run_batches/1>,
...
the related resource is loaded through a many_to_many relationship
ZachDaniel
ZachDaniel3y ago
🤔 very interesting. Is the join resource multitenant? Okay, first things first though. Can you confirm that:
UntenantedResource
|> Api.get!("id")
|> Api.load!(:tenanted)
UntenantedResource
|> Api.get!("id")
|> Api.load!(:tenanted)
doesn't raise the same error? okay, I think I found the issue actually
Adam Harris
Adam HarrisOP3y ago
Yes - The join resource is multi tenant too
ZachDaniel
ZachDaniel3y ago
Well, I've definitely found at least one/a main issue
Adam Harris
Adam HarrisOP3y ago
I’ve just left my computer, but I’m pretty sure that
UntenantedResource
|> Api.get!("id")
|> Api.load!(:tenanted)
UntenantedResource
|> Api.get!("id")
|> Api.load!(:tenanted)
Works (probably after adding a tenant option) because when I call the action directly from the untenanted resource api it works
ZachDaniel
ZachDaniel3y ago
Yeah, I should have a fix for this in main shortly Although the join resource having a tenant may make your life difficult, as we don't currently provide a way to set arguments on the read action of a join resource so doing tenancy that way may be problematic 😦 So either we need to find a way to allow you to specify arguments that belong on the join resource action, and then specify a way to ferry those through in Ash, or you'll want to use a header or something like that for tenancy to sidestep this current issue. Or you'll need a workaround using something like calculations.
calculate :relationship_name_with_tenant,
{:array, RelatedItems},
{RelationshipWithTenant, relationship: :relationship_name} do
argument :tenant, :string, allow_nil?: false
end

defmodule RelationshipWithTenant do
use Ash.Calculation

def calculate(records, opts, %{tenant: tenant}) do
loaded_records = YourApi.load!(records, opts[:relationship], tenant: "tenant")
{:ok, Enum.map(loaded_records, &Map.get(&1, opts[:relationship]))}
end
end
calculate :relationship_name_with_tenant,
{:array, RelatedItems},
{RelationshipWithTenant, relationship: :relationship_name} do
argument :tenant, :string, allow_nil?: false
end

defmodule RelationshipWithTenant do
use Ash.Calculation

def calculate(records, opts, %{tenant: tenant}) do
loaded_records = YourApi.load!(records, opts[:relationship], tenant: "tenant")
{:ok, Enum.map(loaded_records, &Map.get(&1, opts[:relationship]))}
end
end
I've pushed my "fix", but I'm pretty sure the very next error you're going to get is "TenantedJoinResource requires a tenant"
Adam Harris
Adam HarrisOP3y ago
Ah ok, thanks! Using a header for tenancy probably makes more sense in our context anyway, so I’ll try that if I come across an error
ZachDaniel
ZachDaniel3y ago
Definitely open to ideas on improving "mixed tenancy" like this, as you've highlighted some inconveniences around join tables (and of course the bug you found).
Adam Harris
Adam HarrisOP3y ago
I expect this was a probably pretty weird edge case though - trying to access a tenanted resource through an untenanted one... However it's working perfectly now, setting the tenant via a header - thank you!

Did you find this page helpful?