How to handle a JSON Array POST request with Rails? - ruby-on-rails

I am trying to create multiple database entries using only one JSON request.
Each entry consists only of two values, a type (of action) and a time (when the action happened). To get multiple of those into one request, I am using a JSON Array.
This is what my create action in the controller looks like:
def create
respond_to do |format|
#actions = []
save_succeeded = true
params[:action].each do |action|
new_action = Action.new(type: action.type, time: action.time)
save_succeeded = false unless new_action.save
#actions << new_action
end
if save_succeeded
format.json { render json: #actions, status: :created }
else
format.json { render json: #actions.errors, status: 501 }
end
end
end
When I send a post request to the controller (/actions.json) like this:
[{ "type": 0, "time": 1234567890 },{ "type": 0, "time": 1234567891 }]
I get back an empty array [] and a status code of 201 Created.
This means, the save_succeeded variable is still true, but the actions did not get added to the array. Furthermore, the actions are not in my database.
What am I doing wrong? What am I overlooking?

I would refactor the code a bit:
def create
actions = params[:action].inject([]) do |memo, action|
memo << Action.create!(type: action[:type], time: action[:time])
end
render json: #actions, status: :created
rescue ActiveRecord::RecordInvalid => e
render json: e.message, status: 501
end
end
Couple of notable changes:
use create! and rescue ActiveRecord::RecordInvalid - create! will raise a ActiveRecord::RecordInvalid if the save fails. Then, the rescue block will rescue the exception and you can render a nice error message.
you cannot use action.time, because params is a Hash, not an object.
if you want to build an array to render later, you can use inject.
if you would like to have some atomicity to this (either everything is created or nothing!), you can wrap the whole thing in a transaction block.
It's worth mentioning that I haven't tested the code above, but it should give you a direction and (maybe) it will be a drop-in replacement.
Hope that helps!

Related

Active record callbacks running before creation of associated records in create and update methods

In my update and create method for my game controller, I am creating some game_events. There are then after_save callbacks on my game model that should run. These callbacks generate data that I need, and are saved in their respective columns. However, the issue I'm having is that the game_events are being created it seems, after the callbacks are being called. So that my "set_events_user" callback doesn't seem to acknowledge that there are any game_events. However, if from the console I save the game a second time, the callbacks acknowledge these game_events, and the events_user data is generated fine.
Essentially the reason my controller methods for update and create look so weird is because I've tried different ways of rearrangthe order of events so that when the callbacks run, the game_events are acknowledged. I've tried extracting the bit of code where it iterates over the events_user array from the params into a seperate method, then calling save a second time in one method to get it to acknowledge the events.
def create
#game = Game.create(game_params)
authorize #game
#game.organization_id = current_user.organization_id
params[:events_user].each do |e|
#game.game_events.create({
event_type: e["event_type"],
event_id: e["event_id"],
})
end
#game.save
render json: #game, status: :ok
rescue StandardError => e
render json: #game.errors, status: :unprocessable_entity
end
def update
authorize #game
#game.game_events.destroy_all
params[:events_user].each do |e|
#game.game_events.create({
event_type: e["event_type"],
event_id: e["event_id"],
})
end
#game.update(game_params)
render json: #game, status: :ok
rescue StandardError => e
render json: #game.errors, status: :unprocessable_entity
end
after_save :set_events_user
def set_events_user
#sequence = []
self.game_events.by_sequence.each do |e|
#e = {}
#e[:id] = e.id
#e[:event_id] = e.event_id
#e[:event_type] = e.event_type
#e[:question_text] = e.event.question_text
#e[:a1] = {es: e.event.a1, en: e.event.a1 }
#e[:a2] = {es: e.event.a2, en: e.event.a2 }
#e[:a3] = {es: e.event.a3, en: e.event.a3 }
#e[:correct_answer] = e.event.correct_answer if e.event_type == 'Question'
#sequence << #e
end
#sequence.to_json
self.events_user = #sequence
end
No error message, but I expect that the gme_events are created, the callback runs upon being saved, and the game is saved with the generated data. However, unless I save it manually from the console a second time, the first time the data is empty because it won't acknowledge game events.

How to render json message before returning from the function

How can I render a json message just before return?
With the following code, it returns after the last function is completed:
def create_company
begin
company = current_user.company
result = company.create_users
render(json: {message: result[0]}, status: :ok)
company.send_email(result[1])
rescue => e
render(json: { error: e.message }, status: :unprocessable_entity)
end
end
I expected to get the rendering result and then send an email, not waiting until the email to be sent in order to render the result.
If this is a controller method, you'll want render to be the last thing you call, not company.send
def create_company
begin
company = current_user.company
result = company.create_users
company.send_email(result[1])
render(json: {message: result[0]}, status: :ok)
rescue => e
render(json: { error: e.message }, status: :unprocessable_entity)
end
end
edit
Based on your comments, I see that you want the email job to go out after the render happens. To do this, you'll need to setup an async service. Depending if you're using rails and what version, you can use ActiveJob. That in it's own right is a process to setup if you've never done it before, so you'll want to read up on it.
Your controller method would look like the following
def create_company
begin
company = current_user.company
result = company.create_users
company.send_email(result[1]).deliver_later
render(json: {message: result[0]}, status: :ok)
rescue => e
render(json: { error: e.message }, status: :unprocessable_entity)
end
end
deliver_later is an active job method that you can call when sending an email if you have active job configured.

Rails rescue standard error doesn't get sent

I have a controller method as follows
def create
#game = Game.create_new_game(game_params)
render 'show', status: 200
rescue StandardError => e
render json: {
status: 500,
error: e.to_s
}
end
I added a binding.pry and I can clearly see the following error in my console:
#<ActiveRecord::RecordInvalid: Validation failed: Name can't be blank, Duration can't be blank>
But it still sends the status:200 to the client side. Is there a different way the errors are supposed to be handled?
EDIT:
create_new_game method in Game
def self.create_new_game(prms)
#player = Player.find(prms[:player].to_i)
#game = #player.games.create!(
name: prms[:name],
duration: prms[:duration]
)
#game.save!
end
ActiveRecord does not normally raise an exception when a record is invalid. Its only when you use the "bang" methods like .create! that an exception is raised. Its also impossible for us to know what is going on inside Game.create_new_game. These are used for example in seed files wherethe developer should be aware that the validation failed or in nested transactions where it should trigger a rollback.
But what you're doing is not a good practice since exceptions should be used for exceptional events - not normal control flow.
Instead you should check the return value of saving/updating the record and determine the response.
def create
#game = Game.create(game_params)
if #game.save
status: :created, location: #game
# or
render json: #game
else
render json: {
status: 500,
error: #game.errors.full_messages
}
end
end

Refactoring multiple render in controller

In my rails controller, I have to check after getting #group with before_action that this group is not system.
But I have lot's of repetition in my controller. I've tried to turn into a separate method but I get the classic :
Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".
Here is a part of my code without the separate method who give me the error.
def destroy
if #group.is_system?
render json: { errors: 'You can\'t delete a group system' }, status: 403
return
end
...
end
def update
if params[:group] && !params[:group].empty?
if #group.is_system?
render json: { errors: 'You can\'t edit a group system' }, status: 403
return
end
...
else
render json: { errors: 'Missing correct parameters' }, status: :unprocessable_entity
end
end
.....
You could have in a parent controller:
def render_errors(errors, status)
render json: { errors: Array(errors) }, status: status
end
def render_403(errors)
render_errors(errors, 403)
end
def render_422(errors)
render_errors(errors, 422)
end
then in your action:
before_action :check_system
def check_system
# I assume you already defined #group
render_403('You can\'t delete a group system') if #group.is_system?
end
Notice I changed a bit of your code: having errors key which is only a string is very misleading, should be an array.

Creating multiple objects with one http request

I have an rails app with json api. So far I can create single objects via POST request.
It's fairly simple:
def create
customer = Customer.new(customer_params)
if customer.save
render json: customer, status: 201
else
render json: customer.errors, status: 422
end
end
and:
private
def customer_params
params.require(:customer).permit(:name, :city)
end
Now I want to create multiple customers by passing an array in my http request. Like this:
{
"customer": [
{
"name": "foo",
"city": "New York"
},
{
"name": "bar",
"city": "Chicago"
}
]
}
However, I don't know how to approach this. The first issue is that my strong parameters function doesn't accept arrays.
Is there a way to use strong parameters and let me loop through the array?
I would see it as a new controller method
something like:
def multi_create
render json: customer.errors, status: 422 and return unless params[:customers]
all_created = true
customers = []
params[:customers].each do |customer_params|
customer = Customer.create(name: customer_params[:name], city: customer_params[:city])
customers << customer
all_created &&= customer.valid?
end
if all_created
render json: customers, status: 201
else
render json: customers.map(&:errors), status: 422
end
end
You also need to add the route. then you could post your json to that route with the change that the outermost key should be customers.
I would not run this code without any changes but you get the general idea. And you can refactor it to your liking.

Resources