Passing the Organization id to the Link schema

I’m trying create a form to insert new Links that belongs to an Organization.

On my application, every Organization means a Tenant, as I’m learning how to create multitenancy application using foreing keys. Here is my tables’ diagram:

At the end, what I’m trying to create is something related to the Flyio’s dashboard where we are able to switch between different Organizations (tenants) and from each Organization, insert new items (in my case, the Links).

flyio_dashboard

Here is my Organization index page:

path /organizations

organizations

The Organization show page where I can create a New Link:

path /organizations/01

And the New Link page.

path /organizations/01/links/new

The router for this page is set this way
get "/organizations/:organization_id/links", LinkController, :create

At this point, I would like to insert the organization_id on the links schema and fill the proper column.

I’ve being trying to achieve it reading the documentation and some tutorials but at this point haven’t success.

Any help would be greatly appreciated.

The question is not clear to me, where exactly are you having problems? Is it just about inserting the proper org id on the link table? If yes, what have you tried so far?

Yes. The problem is just insert the organization_id to the link table.

At this point, I’m trying to pass the organization_id as a property to the create function on LinkController, but it doesn’t work as I don’t know how to collect this property and pass it to the function. And not sure whether this the best approach to achieve it.

def create(conn, %{"link" => link_params, "organization_id" => organization_id}) do
    organization = Repo.get(Organization, organization_id)

    case Links.create_link(organization, link_params) do
      {:ok, link} ->
        conn
        |> put_flash(:info, "Link created successfully.")
        |> redirect(to: ~p"/links/#{link}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

And the Links.create_link function.

def create_link(organization, link_params) do
    %Link{}
    |> Link.changeset(Map.merge(link_params, %{"organization_id" => organization.id}))
    |> Repo.insert()
  end

Are you casting the organization_id on the Link changeset? This should be pretty straightforward. What does your schema look like? Do you have an association there?

PS.: I believe this should be tagged as Ecto as it’s not related to Phoenix itself :+1:

I’m checking everything now.

Here is the links schema with the changeset casting the organization_id.

schema "links" do
    field :url, :string
    field :visits, :integer
    field :organization_id, :binary_id

    timestamps()
  end

  @doc false
  def changeset(link, attrs) do
    link
    |> cast(attrs, [:url, :visits, :organization_id])
    |> validate_required([:url, :visits, :organization_id])
  end

And the organizations schema:

schema "organizations" do
    field :name, :string

    ...

    has_many :links, Cacau.Links.Link

    timestamps()
  end

Ah yes. I’m tagging it to Ecto :slightly_smiling_face:

Can you check if the changeset is valid before you try to insert it? It has happened to me before that another problem on the changeset goes unnoticed because I’m looking just at the errors on the form. If I was supposed to guess, I’d say you could be having problems with casting UUIDs (I sure had my fair share of those in the past).

I see. Checking everything.

iex(3)> params = %{url: "http://google.com", visits: 1}
%{url: "http://google.com", visits: 1}
iex(6)> link = %Link{}
%Cacau.Links.Link{
  __meta__: #Ecto.Schema.Metadata<:built, "links">,
  id: nil,
  url: nil,
  visits: nil,
  organization_id: nil,
  inserted_at: nil,
  updated_at: nil
}

It is false when not passing the organization_id param:

iex(7)> Link.changeset(link, params)
#Ecto.Changeset<
  action: nil,
  changes: %{url: "http://google.com", visits: 1},
  errors: [organization_id: {"can't be blank", [validation: :required]}],
  data: #Cacau.Links.Link<>,
  valid?: false
>

And true when passing the organization_id:

iex(8)> params = %{url: "http://google.com", visits: 1, organization_id: "ca82c067-7037-4208-915c-d1cd7333422a"}
%{
  url: "http://google.com",
  visits: 1,
  organization_id: "ca82c067-7037-4208-915c-d1cd7333422a"
}
iex(9)> Link.changeset(link, params)
#Ecto.Changeset<
  action: nil,
  changes: %{
    url: "http://google.com",
    visits: 1,
    organization_id: "ca82c067-7037-4208-915c-d1cd7333422a"
  },
  errors: [],
  data: #Cacau.Links.Link<>,
  valid?: true
>

There’s definitely something weird going on… I made a small repro and it works as expected:

defmodule Repo.Migrations.Test do
  use Ecto.Migration

  defmodule Link do
    use Ecto.Schema

    schema "links" do
      field(:name, :string)
      field(:organization_id, :binary_id)
    end
  end

  def change do
    create table(:organizations, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :name, :string
    end

    create table(:links) do
      add :name, :string
      add :organization_id, references(:organizations, type: :uuid)
    end

    flush()

    Repo.insert_all("organizations", [
      %{id: Ecto.UUID.dump!("8e8ab846-32dc-43c7-9378-3a6e37fc5dae"), name: "Org1"}
    ])

    params =
      %{
        name: "Link",
        organization_id: "8e8ab846-32dc-43c7-9378-3a6e37fc5dae"
      }

    %Link{}
    |> Ecto.Changeset.cast(params, [:name, :organization_id])
    |> Repo.insert!()
  end
end

Not addressing your problem, apologies, but what do you use to create your tables diagram? It looks lovely.

Hi. Thanks for asking.

I’m learning it now.

2 Likes

My apologies for not explain it better earlier. It works when we hard code the organization_id param on this way.

My problem is that I’m trying to pass it (the organization_id) to the Add Link form when trying to insert it.

This is the form that I trying to pass the org_id.

Sure, I’m not sure I understand where is your problem exactly then. Can you elaborate? If you are receiving the organization_id via a post request on your controller, this should pretty much work as it does for the “hardcoded” version.

Once on a Organization item show page, I open the New Link form. I’m trying to associate the Link to the Organization.

It opens the New Link page throught this router:

Therefore the organization_id is in the URL.

The first possible solution that came to my mind to grab the organization_id from the URL was to destruct it (is it the correct word) on the create function params. This is what I’m trying, but it is not working.

I’m not sure if this is the correct way or if there’s a best practice to perform it, which is collect the organization_id from the URL, pass it to the create function and update the assign with it so that it could be inserted on the links table.

OK, I see what is your problem now…

You don’t have a post route. It should be something along the lines of:

post "/organizations/:organization_id/links", LinkController, :create

I think you are mixing some concepts here, one thing is the GET request for opening the page and another completely different thing is the POST request for submitting the information to the controller (where you are going to retrieve the organization_id and pass it on to your context to be persisted).

2 Likes

Wow :open_mouth:
Now I see. It was wrong. Totally missed the post router.

Now, even though using the post method on router (as below), I still getting an error message.

post "/organizations/:organization_id/links", LinkController, :create

The error message says:

no function clause matching in AppWeb.LinkController.create/2

The following arguments were given to AppWeb.LinkController.create/2:
%Plug.Conn
%{"_csrf_token"

I’m missing something here that should be straightforward :slightly_smiling_face:

Yeah, it seems you have a couple of things to fix on your side first.

One thing that might help you here is using the Phoenix generators and taking a look at how the code is structured there, they are a good way of learning Phoenix basics.

Also, there are some great resources (besides the docs) that you might want to take a look at, they cover pretty much everything we just talked about and more. Cheers! :blush:

@thiagomajesk thanks for all your support. Learned a lot.

I’ll continue here learning more on my side :wave:

1 Like

Unless You cast organization_id, this will not work… and it would not be the cleanest solution

It’s better to use build_assoc

  def create_link(organization, link_params) do
    organization
    |> Ecto.build_assoc(:links)
    |> Link.changeset(link_params)
    |> Repo.insert()
  end

or…

  def create_link(organization, link_params) do
    %Link{organization_id: organization.id}
    |> Link.changeset(link_params)
    |> Repo.insert()
  end

It’s possible to set directly attributes on the struct, before the changeset, this way, no need to cast

1 Like

That was discussed previously, please read the thread :sweat_smile:

Also, I’m not sure “better” is the correct qualifier here. I think build_assoc can be great for some use cases, but the current pattern is completely valid as well (tbh you could even put the value organization_id just before inserting since this is a business-specific logic that you control).

Learning more, I’ve noticed that I’m not able to collect the organization via the path_params when on the Link create function.

It is possible to see it on the new function. Here is the code:

def new(conn, _params) do
    organization =
      Repo.get_by(
        Cacau.Accounts.Organization,
        id: conn.path_params["organization_id"]
      )

    IO.inspect(organization,
      label: "It is possible to see the organization here on Links new function"
    )

    changeset = Links.change_link(%Link{})
    render(conn, :new, changeset: changeset)
  end

I’m able to see the organization_id on the path_params.

from_links_new_function

But once on the create function, the path_params is empty.

def create(conn, %{"link" => link_params}) do
    IO.inspect(conn, label: "passando no Links create")

    organization =
      Repo.get_by(
        Cacau.Accounts.Organization,
        id: conn.path_params["organization_id"] # <- The path_param is empty
      )

    IO.inspect(organization,
      label: "The path_param is not being passed here on Links create function"
    )

    case Links.create_link(link_params) do
      {:ok, link} ->
        conn
        |> put_flash(:info, "Link created successfully.")
        |> redirect(to: ~p"/links/#{link}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

from_links_create_function

Here is the point where I’m focused on, trying to fix it, or be able to collect this param and pass it to Links.create_link.

1 Like