Update nested resource through API/Json - ruby-on-rails

I'm building an API endpoint to update a model. i can update every column except nested resources, I've tried different approachs but nothing seems to work
This is the JSON i'm trying to send to the server
{
"reservation": {
"reservation_dates": [
{
"is_desirable": true,
"date": "5-10-2019"
}
]
}
}
I'm getting a unpermitted_param from reservation_date although i've added it to my
def permitted_attributes_for_update
params.require(:reservation).permit(:date, :time, :comment, :budget, :currency, :status,
:general_text, :idea_text, :artist_text, :desired_city,
:desired_country, :desired_googleid, :studio_id, :artist_id,
:tattoos, reservation_dates: [], general_url_array: [],idea_url_array: [],
artist_url_array: [])
end
I want to either be able to update directly from the JSON or at least permit the array so I can use later on my UpdateService
Thanks for any help
edit: this is the error I'm getting

You need to specify what is permitted in reservation_dates:
params.require(:reservation).permit(reservation_dates_attributes: [:is_desirable, : date], ...)
However, be careful that if your reservation has_many reservation_dates, then this will cause conflicts: reservation_dates are supposed to be instances of ReservationDate but you are providing hashes. The rails' way is to use reservation_dates_attributes instead of reservation_dates.

Related

Rails declare multiple required parameters

In my Rails 7 app I'm receiving request from the external API. I want to check if the incoming request is valid. To do so I've to use strong parameters, here is the sample JSON request which hit my endpoint:
"sdd_request": {
"return_url": "https://example.com/return",
"data": {
"debit_method": "CORE",
"debtor": {
"name": "John Doe",
"email": "john.doe#example.com",
}
},
"extension": {
"signees": [{email: 'test#test.com', name: 'joe', last_name: 'smith' }],
"creditor": {
"id": "12345",
"name": "Acme Inc.",
"address": {
"city": "New York",
}
}
}
}
}
So the required are:
- return_url
- data
- signees
- creditor
How to require above parameters?
What I did was:
def sdd_setup_request_params
params.require(:sdd_request).permit(
:return_url,
data: [
:debit_method,
debtor: [
:name,
:email,
]
],
extension: [
signees: [],
creditor: [
:id,
:name,
address: [
:city,
]
]
]
]
)
end
But from what I understand that required whole object of sdd_request not individual components because if e.g. signees are missing it will nor raise an error of missing parameters am I right?
The role of strong parameters is not to validate the data. It's just to whitelist the parameters to avoid mass assignment vulnerabilities.
require is used to bail early if the general structure of the parameters makes it pointless to continue processing the request. Like for example in your typical Rails controller:
params.require(:person)
.permit(:name, :age, :city)
There is no point in continuing to process the request if the key :person is missing so a ActionController::ParameterMissing exception is raised which Rails rescues and returns a 400 - Bad Request response.
This prevents the potential nil errors that could occur when you expect a hash and get nil instead.
Validating the presence of the actual attributes is the job of the model in Rails.
class Person
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name
attribute :age
attribute :city
validates :name, :age, :city, presence: true
end
You might be thinking now: But waaaah I don't have a model because I'm gettting it from an API and I'm not saving it. Models can still be really useful even without persistence since the represent the entities in your application in a normalized form.
If you still really want to avoid a model for whatever reason there are gems that provide model-less validation in the controller with a bit more grace then a web of if statements.
While you can use require on nested hash structures like JSONAPI.org this will just return a single key and will raise on the first missing key so it cannot be used to give meaningful feedback about whats wrong with the input.
params.require(:data)
.require(:attributes)
.permit(:name, :age, :city)

Rails Strong Params how to Permit a nested Array

I have the following params:
params={"data"=>
{"type"=>"book",
"id"=>14,
"attributes"=>
{"id"=>14,
"created_at"=>"2022-06-27 21:15:39",
"title"=>"sdfdsf",
"targeting"=> { "release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]],
"days"=>["Monday", "Tuesday", "Wednesday"],
"gender"=>["male", "female"]
}
}
}
When I use this, I can get every value but release_times is always null:
When I use this:
safe_params = params.require(:data).permit( attributes: [:id, :created_at, :title, { targeting: {} }])
How can I extract the release times value?
I tried doing this
safe_params = params.require(:data).permit( attributes: [:id, :created_at, :title, { targeting: [:days, :gender, release_times:[]]}])
But I get the error:
Validation failed: Targeting gender should be a list of values, Targeting days should be a list of values
How can I extract all the values from targeting including the release_times?
As Ruby on Rails API states, when using ActionController::Parameters you want to declare that a parameter should be an array (list) by mapping it to a empty array. Like you did with release_times.
You should permit targeting params with [days: [], gender: []] instead of [:days, :gender]. This should solve the error.
But even them, release_times is an array of arrays, which I believe is not supported at the moment (there is an old issue for it).
One way you could bypass this would be by changing the way you're communicating release_times. Using an arrays of hashes instead of nested arrays.
From this:
"release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]]
To this (or something similar):
"release_times"=>[{"start" => "4:00", "end"=>"5:00"}, {"start" =>"5:00", "end" => "6:00"}]
That way, you could do this:
safe_params = params.require(:data).permit(attributes: [:id, :created_at, :title, { targeting: [days: [], gender: [], release_times: [:start, :end]] }])
Exactly how you would implement that is up to you, but I hope it helps.
**Also, there was a typo with release_times.
You can do some testing yourself. Open rails c and do something like this:
param = ActionController::Parameters.new("targeting"=> { "release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]]})
param.require(:targeting).permit(release_times: []) # > Doesn't return times.
new_param = ActionController::Parameters.new("targeting"=> { "release_times"=>[{"start" => "4:00", "end"=>"5:00"}, {"start" =>"5:00", "end" => "6:00"}] })
new_param.require(:targeting).permit(release_times: [:start, :end]) # > Return times.
Just an observation, using permit! would work. But as strong params doc says:
Extreme care should be taken when using permit! as it will allow all
current and future model attributes to be mass-assigned.
So you could try to slice arguments yourself and them permit! - but I can't tell you that's the way to go.
Learn more about Mass Assignment Vulnerability here.

Undefined method `with_indifferent_access' for #<Array: for nested model in rails

I have a nested form with three models vehicle, vehicle_key_feature and vehicle_detail where vehicle_key_feature and vehicle_detail has one to one relation with vehicle. It is working fine when I use strong params following way -
params.require(:vehicle).permit(:title, vehicle_key_feature_attributes: [:android_auto], vehicle_detail_attributes: [:tech_specs])
since I have lots of strong params for all three models, I would like to keep nested attributes params in a separate method and merge them with vehicle_params. But it's showing me following error
undefined method with_indifferent_access' for #Array
I have written the following codebase, I checked console and params.inspect which is in expected form.
def vehicle_params
params.require(:vehicle).permit(
:title, :category_id, :make, :model, :model_number, :mileage, :exterior, :interior, :transmission, :engine_type, :drivetrain, :fuel_efficiency, :engine, :condition, :description, :dealer_id
)
.merge(vehicle_key_feature_attributes)
.merge(vehicle_detail_attributes)
end
def vehicle_key_feature_attributes
{
vehicle_key_feature_attributes: [
:android_auto, :apple_carplay, :backup_camera, :blind_spot_monitor, :bluetooth,
:forward_collision_warning, :interior_accents, :keyless_entry, :side_impact_air_bags
]
}
end
def vehicle_detail_attributes
{
vehicle_detail_attributes: [
:exterior, :interior, :entertainment, :mechanical, :safety, :tech_specs, :details
]
}
end
What is the best solution to extract these two nested attributes in two separate methods?
Your second snippet is doing something else. To replicate what the first one does, add your hashes to permit's argument list.
params.require(:vehicle).permit(:title, ..., vehicle_key_feature_attributes.merge(vehicle_detail_attributes))

How to get nested JSON / hash elements using postgres_ext-serializers?

I'm trying to get the postgres_ext-serializers gem working, and I built a test project very similar to https://github.com/dockyard/postgres_ext-serializers/blob/master/test/test_helper.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :mobile
embed :ids, include: true
has_one :address, serializer: AddressSerializer
def include_mobile?
false
end
alias_method :include_address?, :include_mobile?
end
class AddressSerializer < ActiveModel::Serializer
attributes :id, :district_name
embed :ids, include: true
end
When I try to run the serializers the output doesn't seem to have nested elements. For example my serializer to_json output is:
"{\"users\":[{\"id\":1,\"name\":\"Aaron\",\"mobile\":null}, \n {\"id\":2,\"name\":\"Bob\",\"mobile\":null}],\"addresses\":[{\"id\":1,\"district_name\":\"Rob's Address\"}]}"
Notice how users and address are two separate elements of a hash, intead of being nested. If I remove the postgres_ext-serializers gem, then the output is as expected:
"[{\"id\":1,\"name\":\"Rob\",\"mobile\":null,\"address\":{\"id\":1,\"district_name\":\"Rob's Address\"}},{\"id\":2,\"name\":\"Bob\",\"mobile\":null,\"address\":null}]"
The address is embedded in the user hash exactly how I'm expecting it.
What am I missing, do I need to change anything to make the elements nested when using postgres_ext-serializers?
Thanks!
It seems, that the JSON you receive after serialization is what postgres_ext-serializers expects. Take a look into this test. expected_json in the first test case case is:
{
"users": [
{
"id": <UserID>,
"name": "John",
"mobile": "51111111",
"offer_ids": [],
"reviewed_offer_ids": []
}
],
"offers": [],
"addresses": [
{
"id": <AddressID>,
"district_name": "mumbai"
}
]
}
It looks very similar to the JSON you have received. But, to be honest, as include_address? method in your example returns false, I expect you must have not "addresses" field included into resulting JSON at all.

Rails - Strong Parameters - Nested Objects

I've got a pretty simple question. But haven't found a solution so far.
So here's the JSON string I send to the server:
{
"name" : "abc",
"groundtruth" : {
"type" : "Point",
"coordinates" : [ 2.4, 6 ]
}
}
Using the new permit method, I've got:
params.require(:measurement).permit(:name, :groundtruth)
This throws no errors, but the created database entry contains null instead of the groundtruth value.
If I just set:
params.require(:measurement).permit!
Everything get's saved as expected, but of course, this kills the security provided by strong parameters.
I've found solutions, how to permit arrays, but not a single example using nested objects. This must be possible somehow, since it should be a pretty common use case. So, how does it work?
As odd as it sound when you want to permit nested attributes you do specify the attributes of nested object within an array. In your case it would be
Update as suggested by #RafaelOliveira
params.require(:measurement)
.permit(:name, :groundtruth => [:type, :coordinates => []])
On the other hand if you want nested of multiple objects then you wrap it inside a hash… like this
params.require(:foo).permit(:bar, {:baz => [:x, :y]})
Rails actually have pretty good documentation on this: http://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-permit
For further clarification, you could look at the implementation of permit and strong_parameters itself: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/strong_parameters.rb#L246-L247
I found this suggestion useful in my case:
def product_params
params.require(:product).permit(:name).tap do |whitelisted|
whitelisted[:data] = params[:product][:data]
end
end
Check this link of Xavier's comment on github.
This approach whitelists the entire params[:measurement][:groundtruth] object.
Using the original questions attributes:
def product_params
params.require(:measurement).permit(:name, :groundtruth).tap do |whitelisted|
whitelisted[:groundtruth] = params[:measurement][:groundtruth]
end
end
Permitting a nested object :
params.permit( {:school => [:id , :name]},
{:student => [:id,
:name,
:address,
:city]},
{:records => [:marks, :subject]})
If it is Rails 5, because of new hash notation:
params.permit(:name, groundtruth: [:type, coordinates:[]]) will work fine.

Resources