Create an Org on public schema and initial User on Org schema

I want to register an organization and an initial user at the same time. Creating an organizations automatically creates and apply migrations to the organization schema. The organization resource is global, so it's is saved on the public schema. The user should be saved on the organization schema. Steps(implementation in Ecto): 1. Generate a random string to be used as the organization schema name. 2. Create a schema with the random string as the schema name and apply migrations. 3. Create the organization on the public schema. 4. Create the user on the organization schema. 5. Save a user email and organization id to the "Lookup" table on the public schema. How can I implement this in Ash? Below is a similar implementation in Ecto: """
org_attrs = %{"name" => "My Org", "email" => "[email protected]"}

user_attrs = %{"email" => "[email protected]", "password" => "password123"}

schema_name = generate_random_string() # adb8469e0544

Ecto.Multi.new()
|> Ecto.Multi.run(:create_org, fn _repo, %{} ->
new_attrs =
org_attrs
|> Map.put("schema_name", schema_name)

MyApp.Accounts.create_organization(new_attrs)
end)
|> Ecto.Multi.run(:create_user, fn repo, %{create_org: org} ->
new_attrs =
user_attrs
|> Map.put("organization_id", org.id)

changeset = MyApp.Accounts.change_user(%MyApp.Accounts.User{}, new_attrs)
# Insert the user into the organization schema
repo.insert(changeset, prefix: org.schema_name)
end)
|> Ecto.Multi.run(:create_lookup, fn _repo, %{create_user: user, create_org: org} ->
new_attrs =
%{"email" => user.email, "organization_id" => org.id, "schema_name"=> org.schema_name}

MyApp.Accounts.create_lookup(new_attrs)
end)
org_attrs = %{"name" => "My Org", "email" => "[email protected]"}

user_attrs = %{"email" => "[email protected]", "password" => "password123"}

schema_name = generate_random_string() # adb8469e0544

Ecto.Multi.new()
|> Ecto.Multi.run(:create_org, fn _repo, %{} ->
new_attrs =
org_attrs
|> Map.put("schema_name", schema_name)

MyApp.Accounts.create_organization(new_attrs)
end)
|> Ecto.Multi.run(:create_user, fn repo, %{create_org: org} ->
new_attrs =
user_attrs
|> Map.put("organization_id", org.id)

changeset = MyApp.Accounts.change_user(%MyApp.Accounts.User{}, new_attrs)
# Insert the user into the organization schema
repo.insert(changeset, prefix: org.schema_name)
end)
|> Ecto.Multi.run(:create_lookup, fn _repo, %{create_user: user, create_org: org} ->
new_attrs =
%{"email" => user.email, "organization_id" => org.id, "schema_name"=> org.schema_name}

MyApp.Accounts.create_lookup(new_attrs)
end)
How can I implement this in Ash when using AshAuthentication? I find it easy to reason about implementation generated by phx.gen.auth but I'm not sure where I can plug in a custom implementation as shown above. Hope this makes sense. Thanks in advance.
7 Replies
ZachDaniel
ZachDaniel3y ago
That question covers a bit of ground 😄 There are a few things you'll want. 1. ash_postgres has options on a resource called manage_tenant
manage_tenant do
template ["org_", :schema_name]
create? true
update? true
end
manage_tenant do
template ["org_", :schema_name]
create? true
update? true
end
Or even just template [:schema_name] If you put that in your organization resource, then anytime an organization is created or updated, the schema will be managed (only if schema_name changed) Then in your create action, you'd set schema_name to your random string, and ash_postgres would do the rest Then you'd have an action on organization like this:
create :create do
argument :email, :string
argument :password, :string

change fn changeset, _ ->
changeset
|> Ash.Changeset.force_change_attribute(:schema_name, random_string()
|> Ash.Changeset.after_action(fn changeset, org ->
register_your_user_here(..., tenant: org.schema_name)
end)
end
end
create :create do
argument :email, :string
argument :password, :string

change fn changeset, _ ->
changeset
|> Ash.Changeset.force_change_attribute(:schema_name, random_string()
|> Ash.Changeset.after_action(fn changeset, org ->
register_your_user_here(..., tenant: org.schema_name)
end)
end
end
and you could create your lookup table as well there So combining hooks on a custom create action + manage_tenant should do what you want 🙂
ZachDaniel
ZachDaniel3y ago
And then you'll want to follow the above guide for migrations and the like
Jmanda
JmandaOP3y ago
So what can an implementation in register_your_user_here(..., tenant: org.schema_name) look like? Is it right to do it this way:
attrs = %{email: "[email protected]", password: "password12345", password_confirmation: "password12345"}

User
|> Ash.Changeset.for_create(:register_with_password, attrs)
|> Accounts.create()
attrs = %{email: "[email protected]", password: "password12345", password_confirmation: "password12345"}

User
|> Ash.Changeset.for_create(:register_with_password, attrs)
|> Accounts.create()
although the above code is raising an error:
** (UndefinedFunctionError) function Ash.NotLoaded.__changeset__/0 is undefined or private
(ash 2.5.10) Ash.NotLoaded.__changeset__()
(ecto 3.9.4) lib/ecto/changeset.ex:409: Ecto.Changeset.change/2
(ecto 3.9.4) lib/ecto/changeset/relation.ex:173: Ecto.Changeset.Relation.do_change/4
(ecto 3.9.4) lib/ecto/changeset/relation.ex:335: Ecto.Changeset.Relation.single_change/5
(ecto 3.9.4) lib/ecto/changeset/relation.ex:165: Ecto.Changeset.Relation.change/3
...
** (UndefinedFunctionError) function Ash.NotLoaded.__changeset__/0 is undefined or private
(ash 2.5.10) Ash.NotLoaded.__changeset__()
(ecto 3.9.4) lib/ecto/changeset.ex:409: Ecto.Changeset.change/2
(ecto 3.9.4) lib/ecto/changeset/relation.ex:173: Ecto.Changeset.Relation.do_change/4
(ecto 3.9.4) lib/ecto/changeset/relation.ex:335: Ecto.Changeset.Relation.single_change/5
(ecto 3.9.4) lib/ecto/changeset/relation.ex:165: Ecto.Changeset.Relation.change/3
...
ZachDaniel
ZachDaniel3y ago
You likely just need to update ash_postgres and ash that bug was fixed in the last few weeks And yeah, it would look very similar to that 😄
Jmanda
JmandaOP3y ago
Alright 😁 , let me do that, will report back Updated the deps, am now able to create a User using :register_with_password action
ZachDaniel
ZachDaniel3y ago
🥳

Did you find this page helpful?