woohaaha

woohaaha

Why are @valid_attrs @update_attrs and @invalid_attrs using atom/declared-types keys while controllers will pass along stringified params?

When a controller takes incoming params they are stringified like:

%{"count" => "5", "active" => "true"}

however when we generate a context phoenix it will use the types to be specific on the @valid_attrs, @update_attrs, and @invalid_attrs it pre-populates in the context tests:

@valid_attrs %{count: 5, ative: true}

However, a controller code typically passes the incoming params right into the context. Here is an example from the Programming Phoenix book:

​def​ create(conn, %{​"​​user"​ => user_params}) ​do​
​ 	  ​case​ Accounts.register_user(user_params) ​do​
​ 	    {​:ok​, user} ->
​ 	      conn
​ 	      |> put_flash(​:info​, ​"​​#{​user.name​}​​ created!"​)
​ 	      |> redirect(​to:​ Routes.user_path(conn, ​:index​))
​ 	
​ 	    {​:error​, %Ecto.Changeset{} = changeset} ->
​ 	      render(conn, ​"​​new.html"​, ​changeset:​ changeset)
​ 	  ​end​
​ 	​end​

As we can see, the user_params are immediately passed along into the context. Had we used the generators the @valid_attrs would be typed and not strigified.

Can anyone tell me why it’s like this and how do you ensure that the context test code mimics real behavior?

Do you sanitize params? Or do you change the test code? Something else?

Thank you

Most Liked

LostKobrakai

LostKobrakai

Imo params should never be touched before they’ve gone through a changeset. It doesn’t need to be a schemas changeset and it doesn’t need persistance, but it should go through a changeset. Personally I even feel this can belong in the controller as opposed to in a context.

But actually doing validation at the incoming edge of the system AND doing it at the outgoing edge for data consistency in persistence is a lot of work and might seem unnecessary for as long as both validations essentially do the same, which they would for scaffold-ed code. As soon as you’re no longer passing params along “as is” to the changeset of your schema it’s no longer one and the same on both edges so it can be a solid solution to validate params directly in the controller already and let the schema’s changeset only deal with the data consistency as opposed to also deal with casting values.

Also I suggest treating the scaffolding of generators much more as a stepping stone then a holy grail. It’s meant go get people up to speed with phoenix quickly and show them high level concepts like not putting business logic into controllers, but it’s not an architecture you shouldn’t modify because it’s already perfect. There are tradeoffs being done just for the fact that code needs to be generated, there are tradeoffs between showing as few code as possible to not overwhelm people vs. showing a full picture. What even is a full picture to people of widely different degrees of experience generally and with phoenix specifically.

thiagomajesk

thiagomajesk

Hi @LostKobrakai! I don’t mean to highjack the thread, but now you got me curious…
I have to be honest and say that is quite “natural” given the way that the project is scaffolded to just patch the params since they are the common denominator in both controllers, contexts, and schemas. So I wonder what are you doing in cases where you want to use part of the data from the params and compute the rest of the fields in the program before they get cast.

I have another thread dedicated to this subject, would you mind sharing your experience there?

thiagomajesk

thiagomajesk

I’m gonna give you a practical example that came to my mind while reading your answer, picture this:

You have to insert a new user in the database and you must fill a required “role” field which is an enum in the database (standard, moderator, and administrator).

Although the names of the roles can be customized by each organization, they always will be represented by one of the valid values of the underlying enum type, so: padawan (standard), knight (moderator), and master (administrator). Because of this requirement, you have to fetch the values from the database using another module.

Now you have the following business rule to apply:

Two users can’t have the same role, the first user to request the role gets the role, the next one will be demoted automatically if the role is taken. This happens until every role is distributed to all users.

I see at least three things that must be done in order for this operation to succeed:

  • Validate if the user input is valid (username, email, etc)
  • Validate if the “role” field is a valid enum type (standard, moderator, and administrator)
  • Validate if the “role” field does not break any constraints (unique role by organization)

Considering the default scaffolded code, I imagine that this means that in the contexts either you:


1. Fetch the valid roles and patch the params map, calling the changeset function with all the required values that will be validated against the schema:
def create_user(params, organization_id) do
  role = Roles.valid_organization_role(organization_id)

  %User{}
  |> User.changeset(Map.put(params, :role, role))
  |> Repo.insert!
end

2. Call the changeset function that will cast only the input values. After that, you'd have to fetch and validate the computed values with the changeset in hands. This segments the validation logic between the context and the schema:
def create_user(params, organization_id) do
  %User{}
  |> User.changeset(params)
  |> fetch_and_validate_organization_role(organization_id)
  |> Repo.insert!
end

def fetch_and_validate_organization_role(changeset, organization_id) do
  role = Roles.valid_organization_role(organization_id)
  # using Ecto to validate the changeset and putting the changes in the changeset after?
end

3. Pass the params directly to the schema, cast the input values, and validate after you have cast the values. This IMHO breaks what I'd consider the boundary of the schema since you would have to call another context from the schema itself to validate the values (I'm not completely sure about this opinion):
def create_user(params, organization_id) do
  %User{}
  |> User.changeset(params, organization_id)
  |> Repo.insert!
end
defmodule User do
  
  def changeset(user, attrs, organization_id) do
   user
    |> cast(attrs, [:username, :email, :role])
    |> validate_required([:username, :email, :role])
    |> fetch_and_validate_organization_role(organization_id) 
  end
  
  def fetch_and_validate_organization_role(changeset, organization_id) do
    role = Roles.valid_organization_role(organization_id)
    # using Ecto to validate the changeset and putting the changes in the changeset after?
  end
end

4. Just don't use the schema to generate changeset and do everything in the context:
def create_user(params, organization_id) do
  %User{}
  |> user_changeset(params, organization_id)
  |> Repo.insert!
end

def user_changeset(user, attrs, organization_id) do
  user
  |> cast(attrs, [:username, :email, :role])
  |> validate_required([:username, :email, :role])
  |> fetch_and_validate_organization_role(organization_id) 
end
  
def fetch_and_validate_organization_role(changeset, organization_id) do
  role = Roles.valid_organization_role(organization_id)
  # using Ecto to validate the changeset and putting the changes in the changeset after?
end

IMHO, I guess that this architectural “flexibility” is both the strong and weak suit of Phoenix as a framework. If I’m being honest I have questioned myself a lot about this before and I don’t think I’ve got a clear answer to that yet. In fact, if you take a look at the thread I linked in my previous post, you’ll see that there are some suggestions about not using changesets in the schema but directly in the contexts, which would allow you to cast fetch, compute and sanitize incoming data without breaking the “boundaries” of each “layer”.

Where Next?

Popular in Questions Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
chokchit
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2733ms. You can configure how long re...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I fore...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "eq...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 47849 226
New

We're in Beta

About us Mission Statement