Problem loading organization relationship in multi-tenant setup

I am having difficulty loading a relationship in Ash. I have two tables, departments and organizations. The organizations table has a UUID primary key and a unique identity field domain. The departments table has a foreign key organization_id references the organizations table. This is a multi-tenancy setup where the organization is the tenant holder and the strategy is by :context. Multi-tenancy is set to the domain field as the tenant prefix rather than the UUID.

Is there a way to load the relationship using the unique domain field?

get_village = MyAshPhoenixApp.Client.Department.get_by_id!("0715bc32-0b15-4e44-aa23-5df65e8c35eb", tenant: "default", load: :organization)

SELECT v0."id", v0."name", v0."inserted_at", v0."updated_at", v0."organization_id" FROM "default"."departments" AS v0 WHERE (v0."id"::uuid = $1::uuid) ["0715bc32-0b15-4e44-aa23-5df65e8c35eb"]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:704

SELECT o0."id", o0."name", o0."domain", o0."go_live_date", o0."is_live", o0."email_domains", o0."inserted_at", o0."updated_at" FROM "organizations" AS o0 WHERE (o0."id"::uuid IN ($1::uuid)) [nil]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:704

Note there are two queries executed. The first query fetches the village by id, and the second query tries to fetch the organization by id. The query is expecting an UUID and the value provided is nil. The unique identity “domain” field of the “organizations” table for the prefixes in my multi-tenancy setup.

#MyAshPhoenixApp.Client.Department<
  organization: nil, #<<<<<< NOT LOADED
  __meta__: #Ecto.Schema.Metadata<:loaded, "default", "departments">,
  id: "0715bc32-0b15-4e44-aa23-5df65e8c35eb",
  ...
>

Is there a way to switch relationship lookup from get_by_id to get_by_domain? Or is there another way to load the relationship using the domain field?

Here are two gist files for my Organization and Department:
organization.ex gist
department.ex gist

What you likely want is this:

  1. Implement the to tenant protocol for your organization so you can use that as the tenant: Multitenancy — ash v3.0.0-rc.34

This allows having a different value for attribute and context multitenancu (i.e domain for the schema, id for attribute)

  1. Make organizations multitenant using attribute multitenancy.
multitenancy do
  strategy :attribute
  attribute :id
end

This will prevent non tenant scoped queries against organizations, but you can (as of recently) allow them on an action-by-action basis using the multitenancy option of read actions: DSL: Ash.Resource.Dsl — ash v3.0.0-rc.34

  1. Make the organization relationship a has_one with no_attributes?: true
has_one :organization, Organization do
  no_attributes? true
end

Then, when you set a tenant while doing your query, set it to the %Organization{}. The twnant will constrain the relationship to only be the org for the tenant you’re requesting.

I wrote this on my phone so could be some missing bits, but these are at least some tools that might help :slight_smile:

Unfortunately, this did not get me across the multitenancy bridge. I tried to follow the advice given but have not gotten to a successful conclusion.

When I follow Multitenancy — ash v3.0.0-rc.34 I am getting a compiler error adding the following code to my organization module:

defimpl Ash.ToTenant do
  def to_tenant(resource, %MyAshPhoenixApp.Client.Organization{:domain => domain, :id => id}) do 
    # ----->>>>> The Compiler error generated from the line above down below
    if Ash.Resource.Info.data_layer(resource) == AshPostgres.DataLayer
&& Ash.Resource.Info.multitenancy_strategy(resource) == :context do
      domain
    else
      id
    end
  end
end

Compilation Error:

MyAshPhoenixApp.Client.Organization.__struct__/0 is undefined, cannot expand struct MyAshPhoenixApp.Client.Organization. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your codeElixir

If the following is changed:

From:
def to_tenant(resource, %MyAshPhoenixApp.Client.Organization{:domain => domain, :id => id}) do

To:
def to_tenant(resource, %{:domain => domain, :id => id}) do

The compilation error goes away but there is still a warning.

The inferred type for the 2nd argument is not a
supertype of the expected type for the to_tenant/2 callback
in the Ash.ToTenant behaviour.

Success type:
%{:domain => _, :id => _, _ => _}

Also when the query is run it no longer returns any data. Whereas before these changes it did return the Department but with a nil “organization”.

get_department = MyAshPhoenixApp.Client.Department.get_by_id!("0715bc32-0b15-4e44-aa23-5df65e8c35eb", tenant: "default", load: :organization)

defimpl Ash.ToTenant do
  def to_tenant(%{domain: domain, id: id}, resource) do 
    if Ash.Resource.Info.data_layer(resource) == AshPostgres.DataLayer
&& Ash.Resource.Info.multitenancy_strategy(resource) == :context do
      domain
    else
      id
    end
  end
end

The argument order takes the org first, and removing the struct from the matching can solve the dependency.

Hey thanks for the quick reply!!

This is what the documentation has at 3.0.0-rc.34

# in Organization resource

defimpl Ash.ToTenant do
  def to_tenant(resource, %MyApp.Accounts.Organization{id: id}) do
    if Ash.Resource.Info.data_layer(resource) == AshPostgres.DataLayer
      && Ash.Resource.Info.multitenancy_strategy(resource) == :context do
      "org_#{id}"
    else
      id
    end
  end
end

Thanks for pointing that out, I’ve pushed a fix to main and it will update in the next release.

1 Like