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):

  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" => "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:

🥳