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

Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&query=perfume&page=2, I would like to get: ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
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
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
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
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

Other popular topics Top

WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
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
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 forese...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43657 311
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement