I am trying to set up context multitenancy in a project. I created an organization resource, and this is the Tenant. Then, I created a post resource. Both modules are shown below. When I create an organization, it creates the DB with the prefix org_<id>
. I can create a post with no issue using:
HgaSiteSuite.Blog.Post
|> Ash.Changeset.for_create(:create, %{title: "tite", content: "content here", organization_id: "ae1ca97a-c13f-496a-802e-eb86edc6356b"})
|> Ash.Changeset.set_tenant("org_ae1ca97a-c13f-496a-802e-eb86edc6356b")
|> Ash.create!()
When I try to fetch a post for a tenant though. I get undefined_table
error with this:
HgaSiteSuite.Blog.Post
> Ash.Query.for_read(:feed)
|> Ash.Query.set_tenant("ae1ca97a-c13f-496a-802e-eb86edc6356b")
|> Ash.read!()
and I get an Invalid filter value when adding the prefix to the tenant
HgaSiteSuite.Blog.Post
> Ash.Query.for_read(:feed)
|> Ash.Query.set_tenant("org_ae1ca97a-c13f-496a-802e-eb86edc6356b")
|> Ash.read!()
Any help would be awesome; I am a noob with ash, so I’m sure I’m missing something obvious.
Organization Resource
defmodule HgaSiteSuite.Organizations.Organization do
use Ash.Resource,
domain: HgaSiteSuite.Organizations,
data_layer: AshPostgres.DataLayer,
extensions: [AshAdmin.Resource]
defimpl Ash.ToTenant do
def to_tenant(%{id: id}, resource) 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
postgres do
table("organizations")
repo(HgaSiteSuite.Repo)
manage_tenant do
template(["org_", :id])
end
end
actions do
defaults([:read, :destroy])
create :create do
accept([:root_domain, :subdomains])
end
update :update do
accept([:root_domain, :subdomains])
end
read :list do
prepare(build(sort: [inserted_at: :desc]))
end
read :by_root_domain do
argument(:root_domain, :string, allow_nil?: false)
filter(expr(root_domain == ^arg(:root_domain)))
end
read :by_id do
argument(:id, :uuid, allow_nil?: false)
get?(true)
filter(expr(id == ^arg(:id)))
end
end
attributes do
uuid_primary_key(:id)
attribute :root_domain, :string do
allow_nil?(false)
end
attribute(:subdomains, {:array, :string})
attribute :inserted_at, :utc_datetime_usec do
writable?(false)
default(&DateTime.utc_now/0)
match_other_defaults?(true)
allow_nil?(false)
end
attribute :updated_at, :utc_datetime_usec do
writable?(false)
default(&DateTime.utc_now/0)
update_default(&DateTime.utc_now/0)
match_other_defaults?(true)
allow_nil?(false)
end
end
end
Post Resource
defmodule HgaSiteSuite.Blog.Post do
use Ash.Resource,
domain: HgaSiteSuite.Blog,
data_layer: AshPostgres.DataLayer,
extensions: [AshAdmin.Resource]
admin do
form do
field :content, type: :markdown
end
end
postgres do
table("posts")
repo(HgaSiteSuite.Repo)
end
multitenancy do
strategy :context
attribute :organization_id
end
relationships do
belongs_to :organization, HgaSiteSuite.Organizations.Organization
end
actions do
defaults([:read, :destroy])
create :create do
accept([
:title,
:content,
:tags,
:meta_title,
:meta_description,
:image_url,
:organization_id
])
change(fn changeset, _ctx ->
title = Ash.Changeset.get_attribute(changeset, :title)
slug = generate_slug(title)
Ash.Changeset.change_attribute(changeset, :slug, slug)
end)
end
update :update do
accept([:title, :content, :tags, :slug, :meta_title, :meta_description, :image_url])
end
read :feed do
prepare(build(sort: [inserted_at: :desc]))
end
read :by_slug do
argument(:slug, :string, allow_nil?: false)
filter(expr(slug == ^arg(:slug)))
end
read :by_id do
argument(:id, :uuid, allow_nil?: false)
get?(true)
filter(expr(id == ^arg(:id)))
end
end
attributes do
uuid_primary_key(:id)
attribute :organization_id, :uuid do
allow_nil? false
end
attribute :title, :string do
allow_nil?(false)
end
attribute(:meta_title, :string)
attribute(:meta_description, :string)
attribute(:image_url, :string)
attribute(:tags, {:array, :string})
attribute(:content, :string)
attribute(:slug, :string)
attribute :inserted_at, :utc_datetime_usec do
writable?(false)
default(&DateTime.utc_now/0)
match_other_defaults?(true)
allow_nil?(false)
end
attribute :updated_at, :utc_datetime_usec do
writable?(false)
default(&DateTime.utc_now/0)
update_default(&DateTime.utc_now/0)
match_other_defaults?(true)
allow_nil?(false)
end
end
defp generate_slug(title) when not is_nil(title) do
title
|> String.downcase()
|> String.replace(~r/[^a-z0-9\s-]/, "")
|> String.split()
|> Enum.uniq()
|> Enum.take(4)
|> Enum.join("-")
end
defp generate_slug(_title), do: nil
end