Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PUT with nested object

I have Dish and Comment models like below in my Rails 5.1 API app - code repo here . I need help with adding a new Comment to a Dish.

Post

class Dish < ApplicationRecord
    has_many :comments
end

Comment

class Comment < ApplicationRecord
    belongs_to :dish
end

Post Serializer (uses ActiveModel Seriazlier)

class DishSerializer < ActiveModel::Serializer
  attributes :id, :name, :image, :category, :label, :price, :featured, :description, :created_at

  has_many :comments
end

Comment Serializer

class CommentSerializer < ActiveModel::Serializer
  attributes :id, :rating, :comment, :author, :date

  def date
    object.created_at
  end
end

Post Controller - default rails scaffold

class DishesController < ApplicationController
  before_action :set_dish, only: [:show, :update, :destroy]

  # GET /dishes
  def index
    @dishes = Dish.all

    render json: @dishes
  end

  # GET /dishes/1
  def show
    render json: @dish
  end

  # POST /dishes
  def create
    @dish = Dish.new(dish_params)

    if @dish.save
      render json: @dish, status: :created, location: @dish
    else
      render json: @dish.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /dishes/1
  def update
    # byebug
    if @dish.update(dish_params)
      render json: @dish
    else
      render json: @dish.errors, status: :unprocessable_entity
    end
  end

  # DELETE /dishes/1
  def destroy
    @dish.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_dish
      @dish = Dish.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def dish_params
      params.require(:dish).permit(:name, :image, :category, :label, :price, :featured, :description)
    end
end

Comment Controller - default rails scaffold

class CommentsController < ApplicationController
  before_action :set_comment, only: [:show, :update, :destroy]

  # GET /comments
  def index
    @comments = Comment.all

    render json: @comments
  end

  # GET /comments/1
  def show
    render json: @comment
  end

  # POST /comments
  def create
    @comment = Comment.new(comment_params)

    if @comment.save
      render json: @comment, status: :created, location: @comment
    else
      render json: @comment.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /comments/1
  def update
    if @comment.update(comment_params)
      render json: @comment
    else
      render json: @comment.errors, status: :unprocessable_entity
    end
  end

  # DELETE /comments/1
  def destroy
    @comment.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_comment
      @comment = Comment.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def comment_params
      params.require(:comment).permit(:rating, :comment, :author)
    end
end

Issue

When a user visits /dishes/:id and adds a comment to a dish via the front-end app ( Angular 2) , the comment is push to the array of current comments and I'm calling PUT /dishes:id with the dish object nested with the existing comments and the new comment. However the new comment is not saved by rails - no error returned, rather the dish object is returned. However I do see Unpermitted parameters: :id, :created_at in rails s console. How do I get rails to save the new comment ?

The page ( dishes/9 ) from where I'm adding the comment to a dish looks like below on the Angular client side. enter image description here

Rails Server logs

On the rails side, below is what I see in params - I do see the new comment - {"author"=>"JANE7777", "rating"=>3, "comment"=>"COMMENT7777", "date"=>"2017-11-12T12:58:12.555Z"} in there.

Started PUT "/dishes/9" for 127.0.0.1 at 2017-11-12 18:28:12 +0530
Processing by DishesController#update as HTML
  Parameters: {"id"=>"9", "name"=>"Uthappizza", "image"=>"images/uthappizza.png", "category"=>"mains", "label"=>"Hot", "price"=>"4.99", "featured"=>true, "description"=>"A unique combination of Indian Uthappam (pancake) and Italian pizza, topped with Cerignola olives, ripe vine cherry tomatoes, Vidalia onion, Guntur chillies and Buffalo Paneer.", "created_at"=>"2017-11-01T04:30:09.407Z", "comments"=>[{"id"=>46, "rating"=>5, "comment"=>"Imagine all the eatables, living in conFusion!", "author"=>"John Lemon", "date"=>"2012-10-16T17:57:28.556Z"}, {"id"=>47, "rating"=>4, "comment"=>"Sends anyone to heaven, I wish I could get my mother-in-law to eat it!", "author"=>"Paul McVites", "date"=>"2014-09-05T17:57:28.556Z"}, {"id"=>48, "rating"=>3, "comment"=>"Eat it, just eat it!", "author"=>"Michael Jaikishan", "date"=>"2015-02-13T17:57:28.556Z"}, {"id"=>49, "rating"=>4, "comment"=>"Ultimate, Reaching for the stars!", "author"=>"Ringo Starry", "date"=>"2013-12-02T17:57:28.556Z"}, {"id"=>50, "rating"=>2, "comment"=>"It's your birthday, we're gonna party!", "author"=>"25 Cent", "date"=>"2011-12-02T17:57:28.556Z"}, {"id"=>51, "rating"=>4, "comment"=>"great dish", "author"=>"Jogesh", "date"=>"2017-10-30T05:03:39.656Z"}, {"author"=>"JANE7777", "rating"=>3, "comment"=>"COMMENT7777", "date"=>"2017-11-12T12:58:12.555Z"}], "dish"=>{"id"=>"9", "name"=>"Uthappizza", "image"=>"images/uthappizza.png", "category"=>"mains", "label"=>"Hot", "price"=>"4.99", "featured"=>true, "description"=>"A unique combination of Indian Uthappam (pancake) and Italian pizza, topped with Cerignola olives, ripe vine cherry tomatoes, Vidalia onion, Guntur chillies and Buffalo Paneer.", "created_at"=>"2017-11-01T04:30:09.407Z"}}
  Dish Load (1.0ms)  SELECT  "dishes".* FROM "dishes" WHERE "dishes"."id" = $1 LIMIT $2  [["id", 9], ["LIMIT", 1]]

[25, 34] in C:/apps/railsApi/app/controllers/dishes_controller.rb
   25:   end
   26:
   27:   # PATCH/PUT /dishes/1
   28:   def update
   29:     byebug
=> 30:     if @dish.update(dish_params)
   31:       render json: @dish
   32:     else
   33:       render json: @dish.errors, status: :unprocessable_entity
   34:     end
(byebug) params
<ActionController::Parameters {"id"=>"9", "name"=>"Uthappizza", "image"=>"images/uthappizza.png", "category"=>"mains", "label"=>"Hot", "price"=>"4.99", "featured"=>true, "description"=>"A unique combination of Indian Uthappam (pancake) and Italian pizza, topped with Cerignola olives, ripe vine cherry tomatoes, Vidalia onion, Guntur chillies and Buffalo Paneer.", "created_at"=>"2017-11-01T04:30:09.407Z", "comments"=>[{"id"=>46, "rating"=>5, "comment"=>"Imagine all the eatables, living in conFusion!", "author"=>"John Lemon", "date"=>"2012-10-16T17:57:28.556Z"}, {"id"=>47, "rating"=>4, "comment"=>"Sends anyone to heaven, I wish I could get my mother-in-law to eat it!", "author"=>"Paul McVites", "date"=>"2014-09-05T17:57:28.556Z"}, {"id"=>48, "rating"=>3, "comment"=>"Eat it, just eat it!", "author"=>"Michael Jaikishan", "date"=>"2015-02-13T17:57:28.556Z"}, {"id"=>49, "rating"=>4, "comment"=>"Ultimate, Reaching for the stars!", "author"=>"Ringo Starry", "date"=>"2013-12-02T17:57:28.556Z"}, {"id"=>50, "rating"=>2, "comment"=>"It's your birthday, we're gonna party!", "author"=>"25 Cent", "date"=>"2011-12-02T17:57:28.556Z"}, {"id"=>51, "rating"=>4, "comment"=>"great dish", "author"=>"Jogesh", "date"=>"2017-10-30T05:03:39.656Z"}, {"author"=>"JANE7777", "rating"=>3, "comment"=>"COMMENT7777", "date"=>"2017-11-12T12:58:12.555Z"}], "controller"=>"dishes", "action"=>"update", "dish"=>{"id"=>9, "name"=>"Uthappizza", "image"=>"images/uthappizza.png", "category"=>"mains", "label"=>"Hot", "price"=>"4.99", "featured"=>true, "description"=>"A unique combination of Indian Uthappam (pancake) and Italian pizza, topped with Cerignola olives, ripe vine cherry tomatoes, Vidalia onion, Guntur chillies and Buffalo Paneer.", "created_at"=>"2017-11-01T04:30:09.407Z"}} permitted: false>
(byebug) c
Unpermitted parameters: :id, :created_at
   (0.0ms)  BEGIN
   (0.0ms)  COMMIT
[active_model_serializers]   Comment Load (0.0ms)  SELECT "comments".* FROM "comments" WHERE "comments"."dish_id" = $1  [["dish_id", 9]]
[active_model_serializers] Rendered DishSerializer with ActiveModelSerializers::Adapter::Attributes (31.29ms)
Completed 200 OK in 1901725ms (Views: 37.5ms | ActiveRecord: 5.0ms)

Client-side models

The Dish model has Comment[] as one of the members. When a new comment is added via the form the comment is push to dish.comments array before sending the Dish object to the Rails API back-end.

Comment model in client side

export class Comment {
    rating: number;
    comment: string;
    author: string;
    date: string;
}

Post model in client side

import { Comment } from './comment';
export class Dish {
  id: number;
  name: string;
  image: string;
  category: string;
  label: string;
  price: string;
  featured: boolean;
  description: string;
  comments: Comment[];
}
like image 404
user3206440 Avatar asked Nov 12 '17 13:11

user3206440


People also ask

Can JSON have nested objects?

Objects can be nested inside other objects. Each nested object must have a unique access path. The same field name can occur in nested objects in the same document.

What is nested object?

Nested objects are the objects that are inside an another object. In the following example 'vehicles' is a object which is inside a main object called 'person'. Using dot notation the nested objects' property(car) is accessed.

Can you have a JSON within a JSON?

JSON can actually take the form of any data type that is valid for inclusion inside JSON, not just arrays or objects. So for example, a single string or number would be valid JSON. Unlike in JavaScript code in which object properties may be unquoted, in JSON only quoted strings may be used as properties.

What does nested mean in JSON?

Nested JSON is simply a JSON file with a fairly big portion of its values being other JSON objects. Compared with Simple JSON, Nested JSON provides higher clarity in that it decouples objects into different layers, making it easier to maintain.


1 Answers

I’d recommend you use the Comments controller and form a nested route to be RESTful.

Firstly, POST the request, including the dish_id to /comments. You’re trying to create a Comment, not update the Dish.

Secondly, form the nested route: /dishes/:dish_id/comments

Here’s a guide to nesting resources like this: http://guides.rubyonrails.org/routing.html#nested-resources

The reason for your error is that you’ve broken the expected parameter structure. For a PUT your params should be:

{
   id: <record to update>,
   dish: {
      name: “blah”,
      comments: [{...},{...}]
   }
 }

Again, I really recommend you don’t PUT to Dish if you just want to add a Comment. For instance: the above would be desired to replace all comments on the Dish! If you’re creating a record, create that record, don’t update it’s parent.

If you wanted to update the Dish when a new Comment is added you can add a callback to the Comment model on create or do it in the Comment create controller action.

like image 167
TomDunning Avatar answered Oct 11 '22 20:10

TomDunning