I have a mutli-tenant application where each tenant gets their own schema, with a bunch of tables underneath it that I create during signup. In addition, I insert some records into tables underneath the public schema, so that I have a way of “mapping” unauthenticated/pre-authenticated requests from the web to the appropriate schema’s tables. One such public table is the “workspaces” table, which contains a subdomain column and a schema_name column. If a request is made to democompany.myapp.com, then I do a lookup and see that “democompany” subdomain’s schema name is “tenant_demo_company”, and use that as the prefix for subsequent lookups.
This works well, but I’m having issues with signup. Specifically, I want to allow the user to enter both their Organization info (e.g. company name) as well as their desired subdomain, on the same form, like the simplified one below:
controller:
def new(conn, _) do
changeset = Organizations.Organization.create_organization_changeset(%Organization{workspace: %Workspace{}})
render(conn, "new.html", changeset: changeset)
end
new.html.eex:
<%= form_for @changeset, signup_path(@conn, :create), fn f -> %>
<%= text_input f, :name, class: "form-control", required: true, autofocus: true %>
<%= label f, :organization_name %>
<%= error_tag f, :name %>
<%= inputs_for f, :workspace, fn w -> %>
<%= text_input w, :subdomain, required: true %>
<%= error_tag w, :subdomain %>
<% end %>
<% end %>
Here’s where it gets hairy: I haven’t figured out a way to insert this data in a single transaction (to take advantage of automatic rollbacks) in a way that structures invalid changesets properly.
I first tried this:
def create_organization(attrs) do
Ecto.Mutli.new()
|> Ecto.Multi.run(:organization_with_workspace, fn(_repo, _result) ->
%Organization{}
|> Organization.create_organization_changeset(attrs)
|> cast_assoc(:workspace, with: &Workspace.changeset/2)
|> Repo.insert(prefix: TenantActions.build_prefix(tenant))
end)
...
|> Repo.transaction()
end
This failed completely, because Organization and Workspace tables exist under different db schemas, and Ecto tries to insert the Workspace into the tenant’s schema. This is despite the fact that I have @schema_prefix module attribute set for Workspace:
defmodule MyApp.Workspaces.Workspace do
use Ecto.Schema
@schema_prefix "public"
schema "workspaces" do
field :subdomain, :string
field :name, :string
belongs_to :organization, Organizations.Organization
timestamps(type: :utc_datetime_usec)
end
end
I then tried doing this:
Ecto.Multi.new()
|> Ecto.Multi.run(:organization, fn(_repo, _result) ->
changeset = Organization.create_organization_changeset(%Organization{}, attrs)
tenant = Ecto.Changeset.get_field(changeset, :slug)
Repo.insert(changeset, prefix: TenantActions.build_prefix(tenant))
end)
|> Ecto.Multi.run(:workspace, fn(_repo, %{organization: organization}) ->
attrs = %{subdomain: attrs["workspace"]["subdomain"], name: organization.name, tenant: organization.slug}
%Workspace{}
|> Workspace.changeset(attrs)
|> put_assoc(:organization, organization)
|> Repo.insert()
end)
|> Repo.transaction()
This worked for the “happy path”, but failed for invalid changesets, because the resulting changeset had the workplace as the parent and organization as the child:
#Ecto.Changeset<
action: :insert,
changes: %{
name: "Demo Company",
organization: #Ecto.Changeset<action: :update, changes: %{}, errors: [],
data: #MyApp.Organizations.Organization<>, valid?: true>,
organization_id: 1,
subdomain: "democompany",
tenant: "demo_company_0"
},
errors: [
unique_subdomains: {"That workspace URL is not available.",
[constraint: :unique, constraint_name: "unique_subdomains"]}
],
data: #MyApp.Workspaces.Workspace<>,
valid?: false
>,
How can I get these “inverted”, so that the following signup form renders the invalid changeset’s errors properly?