Create an Org on public schema and initial User on Org schema
LogicMan:
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):
- Generate a random string to be used as the organization schema name.
- Create a schema with the random string as the schema name and apply migrations.
- Create the organization on the public schema.
- Create the user on the organization schema.
- 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" => "my-org@mail.com"}
user_attrs = %{"email" => "user@mail.com", "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.
ZachDaniel:
That question covers a bit of ground 😄
ZachDaniel:
There are a few things you’ll want. 1.
ash_postgres
has options on a resource called
manage_tenant
ZachDaniel:
manage_tenant do
template ["org_", :schema_name]
create? true
update? true
end
ZachDaniel:
Or even just
template [:schema_name]
ZachDaniel:
If you put that in your organization resource, then anytime an organization is created or updated, the schema will be managed
ZachDaniel:
(only if
schema_name
changed)
ZachDaniel:
Then in your create action, you’d set
schema_name
to your random string, and
ash_postgres
would do the rest
ZachDaniel:
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
ZachDaniel:
and you could create your lookup table as well there
ZachDaniel:
So combining hooks on a custom create action +
manage_tenant
should do what you want 🙂
ZachDaniel:
And then you’ll want to follow the above guide for migrations and the like
LogicMan:
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: "u@mail.com", password: "password12345", password_confirmation: "password12345"}
User
|> Ash.Changeset.for_create(:register_with_password, attrs)
|> Accounts.create()
although the above code is raising an error:
LogicMan:
** (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:
You likely just need to update ash_postgres
ZachDaniel:
and ash
ZachDaniel:
that bug was fixed in the last few weeks
ZachDaniel:
And yeah, it would look very similar to that 😄
LogicMan:
Alright 😁 , let me do that, will report back
LogicMan:
Updated the deps, am now able to create a User using
:register_with_password
action
ZachDaniel:
🥳