Get user's role in given organization

Users belong to multiple organizations. In some orgs they are regular users and in others they are admins. I need to add something to the User resource that takes an organization ID and returns the user's role in that organization. What would that thing be? A read action? A calculation? There's an existing calculation called organization_hierarchy that let's me get a list of organizations the user is a member of, e.g.
user = Ash.load!(changeset.data, organization_hierarchy: [id: changeset.data.id, format: :list])
user = Ash.load!(changeset.data, organization_hierarchy: [id: changeset.data.id, format: :list])
21 Replies
ZachDaniel
ZachDaniel5mo ago
It depends, what is a role? A string? A record? If its a string, you'd use a calculation
calculate :role_in_organization, :string do
argument :organization_id, :uuid, allow_nil?: false

calculation expr(
first(roles, query: [
filter: organization_id == ^arg(:organization_id)
]
)
end
calculate :role_in_organization, :string do
argument :organization_id, :uuid, allow_nil?: false

calculation expr(
first(roles, query: [
filter: organization_id == ^arg(:organization_id)
]
)
end
Ege
EgeOP5mo ago
it's an atom
ZachDaniel
ZachDaniel5mo ago
Then yeah the calculation route would make sense 👍
Ege
EgeOP5mo ago
organization_hierarchy returns a list of organization structs, which have the role on them So how would this calculation work? Would I not need to first load the organizations?
ZachDaniel
ZachDaniel5mo ago
Its an expression calculation, using an aggregate But it sounds like there may be a detail that matters here, you're saying there is no direct relationship between the given role and user and org? i.e it might be somewhere in the hierarchy of roles?
Ege
EgeOP5mo ago
Here's how I would write it in pure Elixir:
def get_user_role_in_organization(user, org_id) do
organizations = get_organization_hierarchy(user.id, format: :list)
Enum.find(organizations, & &1.id == org_id).role
end
def get_user_role_in_organization(user, org_id) do
organizations = get_organization_hierarchy(user.id, format: :list)
Enum.find(organizations, & &1.id == org_id).role
end
Or:
user = Repo.preload(user, [:organizations]
Enum.find(user.organizations, & &1.id == org_id).role
user = Repo.preload(user, [:organizations]
Enum.find(user.organizations, & &1.id == org_id).role
ZachDaniel
ZachDaniel5mo ago
Oh, okay, so you don't actually need the hierarchy? There is a role somewhere with user_id and organization_id on it?
Ege
EgeOP5mo ago
Well, the hierarchy is just poorly named, it returns a list of organizations the user is a member of, and by default it returns them in tree format, but it can also be a flat list which is the format option
ZachDaniel
ZachDaniel5mo ago
Enum.find(user.organizations, & &1.id == org_id).role
Enum.find(user.organizations, & &1.id == org_id).role
This is confusing to me organization has a role field?
ken-kost
ken-kost5mo ago
here's my idea: user has many user_orgs, user has many orgs through user_orgs. and user orgs resource has a role attribute. then you could load user orgs, take org id as argument and calculate the role.
Ege
EgeOP5mo ago
You're right to be confused because I made a mistake in my explanation 🙂
ZachDaniel
ZachDaniel5mo ago
Does user have a relationship to roles? And organization has a relationship to roles? i.e something liek this? role | user_id, organization_id, name?
Ege
EgeOP5mo ago
So the relationship is actually like this:
many_to_many :organizations, Organization do
through OrganizationMember
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :organization_id
end
many_to_many :organizations, Organization do
through OrganizationMember
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :organization_id
end
It is the OrganizationMember that has the role (along with the user ID and org ID)
ZachDaniel
ZachDaniel5mo ago
Perfect So my original answer should work I made a mistake there though, sorry. i left out the field
calculate :role_in_organization, :string do
argument :organization_id, :uuid, allow_nil?: false

calculation expr(
first(organization_members, :name, query: [
filter: organization_id == ^arg(:organization_id)
]
)
end
calculate :role_in_organization, :string do
argument :organization_id, :uuid, allow_nil?: false

calculation expr(
first(organization_members, :name, query: [
filter: organization_id == ^arg(:organization_id)
]
)
end
Ege
EgeOP5mo ago
:name should be :role?
ZachDaniel
ZachDaniel5mo ago
If that is what the attribute is called then yes role_in_organization equals "the role of the user's first organization_member where organization_id matches"
Ege
EgeOP5mo ago
OK. I think it's going to be a bit more complex but this gets me started Thank you!
ZachDaniel
ZachDaniel5mo ago
As a side note, you can do it w/ regular elixir like this
defmodule RoleInOrganization do
use Ash.Resource.Calculation

def load(_, _, _), do: [organization_members: [:role]]

def calculate(records, _, context) do
{:ok, Enum.map(records, fn record ->
Enum.find_value(record.organization_members, fn member ->
member.organization_id == context.arguments.organization_id
end)
end)
end
end

calculate :role_in_organization, :atom, RoleInOrganization
defmodule RoleInOrganization do
use Ash.Resource.Calculation

def load(_, _, _), do: [organization_members: [:role]]

def calculate(records, _, context) do
{:ok, Enum.map(records, fn record ->
Enum.find_value(record.organization_members, fn member ->
member.organization_id == context.arguments.organization_id
end)
end)
end
end

calculate :role_in_organization, :atom, RoleInOrganization
If you don't want to figure out the expression version https://hexdocs.pm/ash/calculations.html#module-calculations
forest
forest4mo ago
I have a very similar setup with User, Organization, and OrganizationMember. I tried this calculation (mostly to learn) and am getting the following error.
calculations do
calculate :role_in_org,
:atom,
expr(first(memberships, :role, query: [filter: id == ^arg(:id)])) do
argument :id, :uuid, allow_nil?: false
end
end
calculations do
calculate :role_in_org,
:atom,
expr(first(memberships, :role, query: [filter: id == ^arg(:id)])) do
argument :id, :uuid, allow_nil?: false
end
end
{:error,
%Ash.Error.Unknown{
bread_crumbs: ["Error returned from: MyApp.Accounts.User.read"],
query: "#Query<>",
errors: [
%Ash.Error.Unknown.UnknownError{
error: "Aggregate options must be keyword list. In: first(memberships, :role, [query: [filter: id == \"9216d25f-4834-4194-a20a-df0acb032c67\"]])",
field: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: ["Error returned from: MyApp.Accounts.User.read"],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
]
}}
{:error,
%Ash.Error.Unknown{
bread_crumbs: ["Error returned from: MyApp.Accounts.User.read"],
query: "#Query<>",
errors: [
%Ash.Error.Unknown.UnknownError{
error: "Aggregate options must be keyword list. In: first(memberships, :role, [query: [filter: id == \"9216d25f-4834-4194-a20a-df0acb032c67\"]])",
field: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: ["Error returned from: MyApp.Accounts.User.read"],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
]
}}
On User
has_many :memberships, MyApp.Organizations.OrganizationMember do
source_attribute :id
destination_attribute :user_id
end
has_many :memberships, MyApp.Organizations.OrganizationMember do
source_attribute :id
destination_attribute :user_id
end
ZachDaniel
ZachDaniel4mo ago
calculations do
calculate :role_in_org,
:atom,
expr(first(memberships, field: :role, query: [filter: id == ^arg(:id)])) do
argument :id, :uuid, allow_nil?: false
end
end
calculations do
calculate :role_in_org,
:atom,
expr(first(memberships, field: :role, query: [filter: id == ^arg(:id)])) do
argument :id, :uuid, allow_nil?: false
end
end
forest
forest4mo ago
thank you.

Did you find this page helpful?