Make params required in a rails api - ruby-on-rails

I have a controller that accepts three params, title, users and project_type. I want to make all the params required
I have seen people do things like
def project_params
params.require(:title,:project_type, :users)
.permit(:title, :project_type, :users)
end
And then do Project.new(project_params), but I need to work a little with the params first. How can I make this possible?
I make a post request in postman like this:
module Api
module V1
class ProjectsController < ApplicationController
def create
admins = params[:admins]
users = get_user_array()
project_type = ProjectCategory.find_by(name: params[:project_type])
project = Project.new(
title: params[:title],
project_category: project_type,
project_users: users)
if project.save
render json: {data:project}, status: :ok
else
render json: {data:project.errors}, status: :unprocessable_entity
end
end
...
end
end
end
{
"title": "Tennis",
"project_type": "Sports",
"users": [{"name": "john Dow", "email": "johnDoe#gmail.com"}],
}

I would say that you are using ActionController::Parameters#require wrong. Its not meant to validate that the all the required attributes are present - thats what model validations are for. Rather you should just use params.require to ensure that the general structure of the parameters is processable.
For example if you used the rails scaffold you would get the following whitelist:
params.require(:project)
.permit(:title, :project_type)
This is because there is no point in continuing execution if the project key is missing from the params hash since this would give you an empty hash or nil.
ActionController::Parameters#require will raise a ActionController::ParameterMissing error which will return a 400 - Bad Request response which is the wrong response code for what you are doing. You also should not use exceptions for normal application flow. A missing attribute is not an exceptional event.
Instead if you want to use a flat params hash you should whitelist it with:
def project_params
params.permit(:title, :project_type, users: [:name, :email])
end

I think that if you don't have to get anything from the frontend to run get_user_array(), you could only allow and require title and project_type.
def create
users = get_user_array()
project = Project.new(project_params)
project.users = users
if project.save
render json: {data:project}, status: :ok
else
render json: {data:project.errors}, status: :unprocessable_entity
end
end
private
def project_params
params.require(:project).permit(:title, :project_type).tap do |project_params|
project_params.require(:title, :project_type)
end
end
If you need to process something before creating the project, you can do this:
project_category = ProjectCategory.find_by(name: project.project_type)

Related

How to upload multiple images and update an entity at the same time. Rails API and JS frontend

I'm trying to make one API call using formData from a React frontend that has multiple images plus strings/booleans etc. to update an entity
I'm using active storage and I am able to attach multiple images to the entity but when I want to update the entity with strings etc. with one API call (ideally). It says this:
param is missing or the value is empty: property
I know that the reason is that my formData is structured like this:
data: {"title": "xxxxxx", "description": "xxxxxx" etc.}, images[]: File, images[]: File etc.
And that I am requiring property as a key and not data:
def property_params
params.require(:property).permit(:title, :description, :max_guests, :price_per_day, :address,:average_rating, :has_beach_nearby, :has_beds, :has_kitchen, :has_swimming_pool, :has_hdtv,:has_bathtub, :images [])
end
But when I don't do it this way, images is not recognised as it is not permitted so the API doesn't receive it
I am attaching images like this (and it works):
if params[:images].present?
params[:images].each do |image|
#property.images.attach(image)
end
end
Is it possible to upload multiple images and the data using one API call? If not, what do I need to do?
Here is the entire Property controller:
class Api::V1::PropertiesController < ApiController
before_action :authenticate_user!, only: [:update, :create, :destroy]
def index
#properties = Property.all
render json: #properties
end
def show
#property = Property.find(params[:id])
render json: #property
end
def update
#property = Property.find(params[:id])
if current_user === #property.user
if params[:images].present?
params[:images].each do |image|
#property.images.attach(image)
end
end
#property.update(property_params)
render json: {
success: "Successfully updated",property: #property},
status: 200
else
render json: {
error: "You are not authorized to update this property"},
status: 403
end
end
private
def property_params
params.require(:property).permit(:title, :description, :max_guests, :price_per_day, :address,:average_rating, :has_beach_nearby, :has_beds, :has_kitchen, :has_swimming_pool, :has_hdtv,:has_bathtub, :images [])
end
Been stuck for a while so would greatly appreciate any help (new to Rails)
Rack uses brackets to pass hashes in formdata:
irb(main):005:0> Rack::Utils.parse_nested_query("property[title]=foo&property[description]=bar")
=> {"property"=>{"title"=>"foo", "description"=>"bar"}}
Arrays can be passed by using empty brackets:
irb(main):008:0> Rack::Utils.parse_nested_query("property[foo][]=a&property[foo]&[]=property[foo][]=c")
=> {"property"=>{"foo"=>nil}}
For multipart form data you would usually use a form (either a visible form or a virtual form) and set the name attribute on the inputs:
<form action="/properties" method="post" multipart>
<input name="property[images][]" type="file" />
<input name="property[images][]" type="file" />
</form>
Rack will stitch the parts of the request body together and create a single parameters hash.
You have a syntax error as well:
# Use sensible line lengths and this won't happen as often...
params.require(:property)
.permit(
:title, :description, :max_guests,
:price_per_day, :address, :average_rating,
:has_beach_nearby, :has_beds, :has_kitchen,
:has_swimming_pool, :has_hdtv, :has_bathtub,
images: []
)
To whitelist arrays or nested parameters you need to use a hash as the last argument to permit. images: [] will permit an array of permitted scalar which includes ActionDispatch::UploadedFile.
If you want to whitelist non-conventional params which contains both nested and flat params you need to get a bit more creative. The key thing to remember is that ActionController::Parameters is a hash like object and you can manipulate it like a hash:
def property_params
params.require(:data)
.permit(
:title, :description, :max_guests,
:price_per_day, :address, :average_rating,
:has_beach_nearby, :has_beds, :has_kitchen,
:has_swimming_pool, :has_hdtv, :has_bathtub
).merge(
# fetch is like require but does not raise if the key is not present
images: params.fetch(:images, [])
)
end
With ActiveStorage you don't actually need to manually attach the files.
I seem to have solved it by doing this:
def update
#property = Property.find(params[:id])
if current_user === #property.user
if params[:images]
#property.images.attach(params[:images])
end
property = #property.update(property_params)
if property
render json: {
success: "Successfully updated",
property: #property,
images: #property.images
}, status: 200
else
render json: {
error: "Error when updating this property",
}, status: 500
end
else
render json: {
error: "You are not authorized to update this property"
}, status: 403
end
end
private
def property_params
json = JSON.parse(params[:data])
json = ActionController::Parameters.new(json)
json.permit(
:title, :description, :max_guests,
:price_per_day, :address, :average_rating,
:has_beach_nearby, :has_beds, :has_kitchen,
:has_swimming_pool, :has_hdtv, :has_bathtub, images: []
)
end
Not sure if this is correct but it seems to work

Update Multiple Records with a single command in Ruby

Basically I want to update an array of objects that my api recieves in a single command. I have done it when I was inserting but I couldn't find a way to do update it.
Here is m create method for multiple insertions:
def create_all
if Attendance.create attendance_params
render json: { message: "attendance added" }, status: :ok
else
render json: { message: "error in creation" }, status: :bad_request
end
end
Params:
def attendance_params
params.require(:attendance).map do |p|
p.permit(
:student_id,
:id,
:attendance
)
end
end
I tried to do similar thing with update but it generates this error:
Completed 500 Internal Server Error in 11ms (ActiveRecord: 2.7ms)
Argument Error (When assigning attributes, you must pass a hash as an argument.)
my update method is this:
def update_attendance
if Attendance.update attendance_params
render json: { message: "attendance updated" }, status: :ok
end
end
ActiveRecord Create can take an array of hashes and create multiple records simultaneously.
However, ActiveRecord Update cannot.
You could potentially create an "update_batch" method on your model that allows for an array of hashes. You would have to send an array of hashes and each hash would have to include the id of the record you are updating (and allow that in your strong parameters definition). Then in your update_batch method you would have to grab the id from each hash and update each:
class Attendance < ActiveRecord
def update_batch(attendance_records)
attendance_records.each do |record|
Attendance.find(record[:id]).update(record.except(:id))
end
end
end
Please check this example and try applying it:
Attendance.where(:student_id => [23,45,68,123]).update_all(:attendance => true)
Or if you're trying to update all Attendance records:
Attendance.update_all(:attendance => true)
Also, please check this link:
https://apidock.com/rails/ActiveRecord/Relation/update_all

Rails strong parameters - Request allowed without required key

I'm working on a Rails API and I'm using strong parameters in the controllers. I have a request spec that is failing for one model but works on all other models. The controllers for each model are pretty much all the same.
As you can see below in the spec, the request body SHOULD be { "tag": { "name": "a good name" }}. However, this spec is using { "name": "a good name" } which SHOULD be invalid because it's missing he "tag" key. This same spec for the same controller functionality works fine for plenty of other models.
Another interesting twist is that if I change the controller's strong parameter to params.require(:not_tag).permit(:name) it throws an error for not including the "not_tag" key.
Ruby: 2.6.5p114
Rails: 6.0.1
Expected response status: 422
Received response status: 201
Controller
class TagsController < ApplicationController
before_action :set_tag, only: [:show, :update, :destroy]
# Other methods...
# POST /tags
def create
#tag = Tag.new(tag_params)
if #tag.save
render "tags/show", status: :created
else
render json: #tag.errors, status: :unprocessable_entity
end
end
# Other methods...
private
# Use callbacks to share common setup or constraints between actions.
def set_tag
#tag = Tag.find_by(id: params[:id])
if !#tag
object_not_found
end
end
# Only allow a trusted parameter "white list" through.
def tag_params
params.require(:tag).permit(:name)
end
# render response for objects that aren't found
def object_not_found
render :json => {:error => "404 not found"}.to_json, status: :not_found
end
end
Request Spec
require 'rails_helper'
include AuthHelper
include Requests::JsonHelpers
RSpec.describe "Tags", type: :request do
before(:context) do
#user = create(:admin)
#headers = AuthHelper.authenticated_header(#user)
end
# A bunch of other specs...
describe "POST /api/tags" do
context "while authenticated" do
it "fails to create a tag from malformed body with 422 status" do
malformed_body = { "name": "malformed" }.to_json
post "/api/tags", params: malformed_body, headers: #headers
expect(response).to have_http_status(422)
expect(Tag.all.length).to eq 0
end
end
end
# A bunch of other specs...
after(:context) do
#user.destroy
#headers = nil
end
end
This behaviour is because of the ParamsWrapper functionality which is enabled by default in Rails 6. wrap_parameters wraps the parameters that are received, into a nested hash. Hence, this allows clients to send requests without nesting data in the root elements.
For example, in a model named Tag, it basically converts
{
name: "Some name",
age: "Some age"
}
to
{
tag:
{
name: "Some name",
age: "Some age"
}
}
However, as you see in your test, if you change the required key to not_tag, the wrapping breaks the API call, as expected.
This configuration can be changed using the config/initializers/wrap_parameters.rb file. In that file, you could set wrap_parameters format: [:json] to wrap_parameters format: [] to disallow such wrapping of parameters.

Service Object returning status

I am making a rails json api which uses service objects in controllers actions and basing on what happend in service I have to render proper json. The example looks like this.
star_service.rb
class Place::StarService
def initialize(params, user)
#place_id = params[:place_id]
#user = user
end
def call
if UserStaredPlace.find_by(user: user, place_id: place_id)
return #star was already given
end
begin
ActiveRecord::Base.transaction do
Place.increment_counter(:stars, place_id)
UserStaredPlace.create(user: user, place_id: place_id)
end
rescue
return #didn't work
end
return #gave a star
end
private
attr_reader :place_id, :user
end
places_controller.rb
def star
foo_bar = Place::Star.new(params, current_user).call
if foo_bar == #sth
render json: {status: 200, message: "sth"}
elsif foo_bar == #sth
render json: {status: 200, message: "sth"}
else
render json: {status: 400, message: "sth"}
end
And my question is, if I should return plain text from service object or there is some better approach?
It'll be opinionated of course but still...
Rendering views with data, returning data, redirecting etc are the responsibilities of controllers. So any data, plain text and other things you have to handle in your controller.
Service object have to provide one single public method for any huge complex operation performing. And obviously that method has to return simple value which tells controller if operation was completed successfully or not. So it must be true or false. Maybe some recognizable result (object, simple value) or errors hash. It's the ideal use case of course but it's the point.
As for your use case your service may return the message or false. And then controller will render that message as json.
And your star method must live in your controller, be private probably and looks like that:
def star
foo_bar = Place::Star.new(params, current_user).call
if foo_bar
render json: {status: 200, message: foobar}
else
render json: {status: 400, message: "Failed"}
end
end
Your Service:
class Place::StarService
def initialize(params, user)
#place_id = params[:place_id]
#user = user
end
def call
if UserStaredPlace.find_by(user: user, place_id: place_id)
return "Message when star is already given"
end
begin
ActiveRecord::Base.transaction do
Place.increment_counter(:stars, place_id)
UserStaredPlace.create(user: user, place_id: place_id)
end
rescue
return false
end
return "Message if gave a star"
end
private
attr_reader :place_id, :user
end

POST/Create multiple items

I'm currently using the below to add one product that has a name and a brand via API call. I would like to be able to submit an array of 'products' and then add then to my DB.
Could anyone suggest:
1) How would I do this in the controller?
2) How would I structure the API POST body?
Current call looks like:
http://localhost:3000/api/v1/products?brand=brand&name=name
My Controller:
def create
#newProduct = Product.create(product_params)
if #newProduct.save
render json: {message: "Product created"}
else
render json: {error: "Failed to create product"}
end
end
private
def product_params
params.permit(:name, :brand)
end
Thanks
Add a new route in routes file with line below
get 'create_multiple_products'
Send data in an array
{"products":[
{"name":"playstation"},
{"name":"xbox"},
{"name":"blueray"}
]}
then add a new method in controller and call the create in a loop
def create_multiple_products
response["products"].each do |p|
Product.create( p )
end
end
The above is pseudocode, you might want to try a test driven approach setting up expected api and matching with returned data with rspec. http://matthewlehner.net/rails-api-testing-guidelines/

Resources