How to render two json objects as response on a rails controller - ruby-on-rails

I need some help I have a controller with an action that queries two models.
Now I need to send both of them as json in order to be used on my angular views.
In the example bellow how should I send the "complex" and its "fields" in one json response?
Ex.
def complexes_and_fields
complex = Complex.find(params[:id])
search_params = {complex_id: complex._id}
fields = Field.where(search_params)
if !complex.nil?
render json: ???.to_json, status: :ok
else
render json: { error_description: 'no complex found' },status: :bad_request
end

An easy way to do this is to build a hash with your objects
complex = Complex.find(params[:id])
search_params = {complex_id: complex._id}
fields = Field.where(search_params)
render json: { complex: complex, fields: fields, search_params: search_params }, status: :ok
Another way would be to user a view such as some_view.json.erb where you render the objects as you are expecting it in your angular view. Also you can use can use ActiveModelSerializers, read on https://github.com/rails-api/active_model_serializers
Ideally what you will want to do is encapsulate this response into its object and make a single call in your controller that returns you the results
Without going into too much details something like this
results = MyComplexFieldsObj.response(params[:id])
render son: results, status: :ok

This is an extremely common requirement in Rails applications. This need is rarely restricted to a single model, or a single location. As a result, a variety of gems exist to provide this kind of functionality (in many cases, without altering the signature of your render lines substantially).
This post offers a good listing. Personally, I've had a good experience with active_model_serializers and an acceptable experience with grape-entity. It's reasonable to review their documentation and decide which is best for you.

Related

Rendering ActiveRecord validation errors with attributes AND full error messages

Here's a simple controller update action:
def update
note = Note.find(params[:id])
if note.update(note_params)
render json: note.to_json
else
render json: {errors: note.errors}, status: :unprocessable_entity
end
end
This renders errors in the form
{"errors":{"title":["can't be blank"]}}
but I want it in the form of
{"errors":{"title":["Title can't be blank"]}}
Simply using {errors: note.errors.full_messages}
gives
{:errors=>["Title can't be blank"]} and misses the attribute keys.
The only way I can get it into the desired form seems to be a bit more involved:
full_messages_with_keys = note.errors.keys.inject({}) do |hash, key|
hash[key] = note.errors.full_messages_for(key)
hash
end
render json: {errors: full_messages_with_keys}, status: :unprocessable_entity
This works, but it seems odd that I have to do this since it seems to be a pretty common use case for doing validations on a SPA front-end. Is there a built-in method/more canonical way?
You can use ActiveModel::Errors#group_by_attribute to get a hash of errors per key:
person.errors.group_by_attribute
# => {:name=>[<#ActiveModel::Error>, <#ActiveModel::Error>]}
From there is simply a matter of generating the full message from each ActiveModel::Error instance:
note.errors
.group_by_attribute
.transform_values { |errors| errors.map(&:full_message) }
Is there a built-in method/more canonical way?
Not really. A framework can't cover every possible need. It provides the tools needed to format the errors however you want.
However instead of repeating this all across your controllers this functionality can be pushed into the model layer, a serializer or even monkeypatched onto ActiveModel::Errors if you want to get crazy.
class ApplicationRecord < ActiveRecord::Base
def grouped_errors
errors.group_by_attribute
.transform_values { |errors| errors.map(&:full_message) }
end
end

Rails only allowing certain strings as a param

I have a Rails 5.2.3 API that is being consumed by a Vue application I maintain.
In the API, I have a Questions table, and each Question has a specific type. I'm creating a feature where my users can create their own custom questions out of a select few options of question types, for the sake of simplicity let's say they only have 2 options to choose from, an OpenEnded question and a StarRating question.
While I realize the risk of bad user input here is impossible (since on the client side of things I'm just giving them a checklist and supplying that checklist to the API), I'd still like to have some checks in place to ensure that the ONLY type permitted in the params are OpenEnded and StarRating, especially since I'm planning on expanding this API to be public-facing at some point in the future, where the risk of bad input is indeed possible.
What's the proper way of handling this so that my API returns a 400 or 422 code if the question type isn't one of the whitelisted options?
The current create method and question_params method are about as bog-standard as they can possibly get
def create
#question = Question.new(question_params)
if #question.save
render json: { question: #question }, status: :created
else
render json: { errors: #question.errors }, status: :bad_request
end
end
def question_params
params.permit(:content, :type)
end
What's the proper way of handling this so that my API returns a 400 or 422 code if the question type isn't one of the whitelisted options?
You would do this as a validation. Simplest way is to do it on the Question model.
class Question < ApplicationRecord
validates :type, inclusion: { in: %w(OpenEnded StarRating) }
end
The save will fail if the type is missing or incorrect.
Note that type has a special meaning, you may wish to consider a different column name if you're not using Single Table Inheritance.

Where is the API document for the usage like `render json: #user`?

I wonder if there is any explanation of the usage below under http://api.rubyonrails.org/ instead of http://guides.rubyonrails.org.
render json: #user
Although there is a page in Rails Guide mentioning this, it does not cover other available options like :include, for example:
render json: #user, include: { blog: { only: [:name, :permalink]} }
I can't believe such common API is hard to find in its official API document.
That's probably because it's using as_json method. Refer to:
https://apidock.com/rails/ActiveModel/Serializers/JSON/as_json
So, the render method isn't strictly related to as_json options. You should head to the link above.
Dug around a little and found the ActionController::Renderers documentation which, to add a new one states:
To create a renderer pass it a name and a block. The block takes two arguments, the first is the value paired with its key and the second is the remaining hash of options passed to render.
and then if you view it's source on GitHub you can see when they create the :json renderer, they do:
add :json do |json, options|
json = json.to_json(options) unless json.kind_of?(String)
...
end
So, Rails, is just calling to_json on "the value paired with it's key" (which is #user in render json: #user) passing in all the extras you passed to the render call, in this case the include.
So if you want to know what the include option is doing, you would need to check the #user.to_json method, which then calls ActiveSupport::JSON.encode which just kind of delegates to ActiveSupport::JSON::Encoding::JSONGemEncoder#encode which then just calls as_json on the object and turns it into a string.

RoR: removing sensitive field from response

Is there any way to remove sensitive fields from the result set produced by the default ActiveRecord 'all', 'where', 'find', etc?
In a small project that I'm using to learn ruby I've a reference to User in every object, but for security reasons I don't want to expose the user's id. When I'm using a simple HTML response it is easy to remove the user_id simply by not using it. But for some task I'd like to return a json using something like:
def index
#my_objects = MyObject.all
respond_to do |format|
...
format.json { render json: #my_objects, ...}
...
end
end
How do I prevent user_id to be listed? Is there any way to create a helper that removes sensitive fields?
You can use the as_json to restrict the attributes serialized in the JSON response.
format.json { render json: #my_objects.as_json(only: [:id, :name]), ...}
If you want to make it the default, then simply override the method in the model itself
class MyObject
def serializable_hash(options = nil)
super((options || {}).merge(only: [:id, :name]))
end
end
Despite this approach is quick and effective, it rapidly becomes unmaintainable as soon as your app will become large enough to have several models and possibly different serialization for the same type of object.
That's why the best approach is to delegate the serialization to a serializer object. It's quite easy, but it will require some extra work to create the class.
The serializer is simply an object that returns an instance of a model, and returns a JSON-ready hash. There are several available libraries, or you can build your own.

JSON Decode Parameter Issue

I have a rails 4 application that uses postgresql. I also have a backbone.js application that pushes JSON to the rails 4 app.
Here's my controller:
def create
#product = Product.new(ActiveSupport::JSON.decode product_params)
respond_to do |format|
if #product.save
format.json { render action: 'show', status: :created, location: #product }
else
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
def product_params
params.require(:product).permit(:title, :data)
end
I'm trying to parse the JSON and insert the product, but on insert, I'm getting the error:
TypeError (no implicit conversion of ActionController::Parameters into String):
Thanks for all help!
Your mileage may vary, but I fixed a smilar problem by a bandaid code like this:
hash = product_params
hash = JSON.parse(hash) if hash.is_a?(String)
#product = Product.new(hash)
The particular problem I had was that if I was just doing JSON.parse on the params that contained the object I wanted to create, I was getting this error while unit testing, but the code was working just fine when my web forms were submitting the data. Eventually, after losing 1 hour on logging all sorts of stupid things, I realized that my unit tests were somehow passing the request parameter in a "pure" form -- namely, the object was a Hash already, but when my webforms (or manual headless testing via cURL) did sumbit the data, the params were as you expect -- a string representation of a hash.
Using this small code snippet above is, of course, a bandaid, but it delivers.
Hope that helps.
Convert hash into JSON using to_json
The error is telling you that ActiveSupport::JSON.decode expects to be provided with a string, but is unable to coerce the argument you are providing it into a string. The argument provided to it here is "product_params" which returns a ActionController::Parameters (a loosely wrapped Hash).
If you are using "out of the box" style Backbone there is no need to decode what is being POSTed to that action. Just change the action to:
#product = Product.new(product_params)
The structure of your product_params method indicates that the action is expecting the data you are POSTing to look like this:
{
product: {
title: "Foo",
data: "bar"
}
}
and that your Product model has two attributes that will be populated by .new: title and data.
If you are explicitly encoding something into JSON on the client side you need to figure out what POST parameter it is being submitted as and the decode it on the server (again - there is almost certainly not a good reason to jump through hoops like that).

Resources