ActiveRecord - include nested associations instead of just id? - ruby-on-rails

I have a rails api with two ActiveRecords, User and Organization. Organization has_many users and User belongs_to Organization. When I find a user, by their email, I automatically get the organization_id as part of the record. When I render the json for the response, right now I am calling user.as_json(include: :organization) which does include the organization. However, this is what I get:
{
"id": 1,
"name": "My Name",
"organization_id": 1,
"organization": {
"id": 1,
"name": "My Organization"
}
}
How can I structure my query to return this?
{
"id": 1,
"name": "My Name",
"organization": {
"id": 1,
"name": "My Organization"
}
}

user.as_json(include: :organization, except: [:organization_id])

Related

Rails Serialization - fast_jsonapi / active_model_serializers

I am a bit lost getting on board fast_jsonapi / active_model_serializers to build an API. I have the basics down but seem stuck on a custom solution.
I have this as a serializer:
class AreaSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name, :cost_center, :notes
has_many :children
end
In my area model I have:
has_many :children, -> { Area.where(ancestry: id) }
My controller looks like:
class Api::V1::AreasController < ApiController
def index
render json: AreaSerializer.new(Area.root).serialized_json
end
end
Areas are nested in a hierarchy with the ancestry gem. The output is:
{
"data": [{
"id": "1",
"type": "area",
"attributes": {
"id": 1,
"name": "Calgary",
"cost_center": "123456",
"notes": ""
},
"relationships": {
"children": {
"data": [{
"id": "3",
"type": "child"
}]
}
}
}, {
"id": "2",
"type": "area",
"attributes": {
"id": 2,
"name": "Edmonton",
"cost_center": "78946",
"notes": ""
},
"relationships": {
"children": {
"data": []
}
}
}]
}
I am looking for an out put like this:
{
"data": [{
"id": "1",
"type": "area",
"attributes": {
"id": 1,
"name": "Calgary",
"cost_center": "123456",
"notes": ""
},
"relationships": {
"areas": {
"data": [{
"id": "3",
"type": "area",
"attributes": {
"id": 3,
"name": "Child Area",
"cost_center": "123456",
"notes": ""
}
}]
}
}
}, {
"id": "2",
"type": "area",
"attributes": {
"id": 2,
"name": "Edmonton",
"cost_center": "78946",
"notes": ""
}
}]
}
The idea being where the nested relationship shows the details etc.
I just started using fast_jsonapi in my rails project.
fast_jsonapi adheres to JSON:API spec which you can see here.
So, you will not be able to use the relationship helper functions (:has_many, :belongs_to) to achieve the output you want, which is nesting the attributes of area inside the relationships key.
"relationships": {
"areas": {
"data": [{
"id": "3",
"type": "area",
"attributes": { // nesting attributes of area
"id": 3,
"name": "Child Area",
"cost_center": "123456",
"notes": ""
}
}]
}
}
JSON:API specifies that:
To reduce the number of HTTP requests, servers MAY allow responses
that include related resources along with the requested primary
resources. Such responses are called “compound documents”.
So instead, you will have the attributes of area inside a key called included.
In order to get the included key in your response, you will need to provide an options hash in your controller to the serializer.
I don't quite understand your model Area relationships, but assume an Area has many SubArea.
class Api::V1::AreasController < ApiController
def index
// serializer options
options = {
include: [:sub_area],
collection: true
}
render json: AreaSerializer.new(Area.root, options).serialized_json
end
end
You Cannot use Association in fast_jsonapi . To Get a response in nested format . You need to add methods and need to create another serializer .
class AreaSerializer
include FastJsonapi::ObjectSerializer
set_type 'Area'
attributes :id, :name, :cost_center, :notes
attribute :childrens do |area, params|
ChildrenSerializer.new(area.childrens, {params:
params})
end
end
class ChildrenSerilizer
include FastJsonapi::ObjectSerializer
set_type 'Area'
attributes :id, :name ...
end
I started using the technique listed above but ended up forking and re-writing the jsonapi-serializer gem so it allows nesting (up to 4 levels deep) and does away with the concept of having relationships and attributes keys. I was also frustrated that it only output ID and TYPE keys of which TYPE is redundant most of the time since the object typically stays the same class in an array as an example and people seldom use real polymorphism (although it still supports this and outputs ID/type for polymorphic relationships).
Probably the best part of my re-write is that it allows deterministic field select-ability anywhere within the json key tree via a new fields input.
https://github.com/rubesMN/jsonapi-serializer

Rails Pundit policy_scope on render json include fields

Continuing my previous question at: Active Model Serializer and Pundit deleting records during a Show CRUD action
I have a situation where a User should not be able to view another user's unpublished chapters belonging to a Story an author created.
E.g. If UserA creates a story called Targon and provides 2 published chapters and 2 unpublished chapters, then UserB should only see published chapters for Targon story.
Normally with Pundit policy scoping, it scopes the index CRUD action.
What I need to scope however, are Chapters belonging to a Story during the render json line:
render json: story, include: [:user, :chapters], status: :ok
I have tried:
# ---------------------------------------------------------------------------
# ActiveRecord auto-save will kick in and delete all unpublished chapters
# ---------------------------------------------------------------------------
story.chapters = policy_scope(story.chapters)
render json: story, include: [:user, :chapters], status: :ok
According to https://gist.github.com/demisx/9896113 (has_many section) the above code will delete all the unpublished chapters belonging to Targon when I reassign story.chapters:
story.chapters = policy_scope(story.chapters) # BAD
I am hoping there is some way I can do something like this:
render json: story, include: [:user, policy_scope(:chapters)], status: :ok
At the moment, without scoping the story.chapters any users who fetch for Story with ID 16 (Targon) will get back JSONAPI:
{
"data": {
"id": "16",
"type": "stories",
"attributes": {
"title": "Mount Targon",
"summary": "Mount Targon is the mightiest peak in Runeterra, a towering peak of sun-baked rock amid a range of summits unmatched in scale anywhere else in the world. Located far from civilization, Mount Targon is utterly remote and all but impossible to reach save by the most determined seeker. Many legends cling to Mount Targon, and, like any place of myth, it is a beacon to dreamers, madmen and questors of adventure. Some of these brave souls attempt to scale the impossible mountain, perhaps seeking wisdom or enlightenment, perhaps chasing glory or some soul-deep yearning to witness its summit. The ascent is all but impossible, and those hardy few who somehow survive to reach the top almost never speak of what they have seen. Some return with a haunted, empty look in their eyes, others changed beyond all recognition, imbued by an Aspect of unearthly, inhuman power with a destiny few mortals can comprehend.",
"published": true,
"published-date": "2017-11-02T10:35:33.184Z",
"created-at": "2017-11-02T10:35:33.184Z",
"updated-at": "2017-11-04T07:35:04.083Z",
"cover": {
"url": "http://res.cloudinary.com/chewedon/image/upload/v1509780931/c8ubn3tfivxziyxwynsa.png",
"standard": {
"url": "http://res.cloudinary.com/chewedon/image/upload/c_fill,g_north,h_300,w_200/c8ubn3tfivxziyxwynsa.png"
}
}
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "users"
}
},
"chapters": {
"data": [{
"id": "26",
"type": "chapters"
}, {
"id": "27",
"type": "chapters"
}, {
"id": "37",
"type": "chapters"
}, {
"id": "38",
"type": "chapters"
}]
}
}
},
"included": [{
"id": "1",
"type": "users",
"attributes": {
"username": "Chewedon",
"photo": {
"url": "http://res.cloudinary.com/chewedon/image/upload/v1509857442/nx1tqlcdxrhz6r3kjx87.jpg",
"standard": {
"url": "http://res.cloudinary.com/chewedon/image/upload/c_fill,g_north,h_150,w_150/nx1tqlcdxrhz6r3kjx87.jpg"
}
}
},
"relationships": {
"stories": {
"data": [{
"id": "1",
"type": "stories"
}, {
"id": "2",
"type": "stories"
}, {
"id": "3",
"type": "stories"
}, {
"id": "4",
"type": "stories"
}, {
"id": "5",
"type": "stories"
}, {
"id": "6",
"type": "stories"
}, {
"id": "8",
"type": "stories"
}, {
"id": "9",
"type": "stories"
}, {
"id": "10",
"type": "stories"
}, {
"id": "11",
"type": "stories"
}, {
"id": "12",
"type": "stories"
}, {
"id": "13",
"type": "stories"
}, {
"id": "14",
"type": "stories"
}, {
"id": "15",
"type": "stories"
}, {
"id": "16",
"type": "stories"
}]
}
}
}]
}
Here in the relationship section, chapters 37 and 38 are unpublished, leading to a 403 Forbidden on my Ember frontend.
Ideally the server should have scoped out these before returning the records but due to the bug I described above and in my previous Stackoverflow question, I am stuck on how to scope included fields with Pundit.
Any ideas?
Thanks to user oowowaee from previous linked question, who suggested overriding the Story serializer's chapters field (which I didn't know you could do that), the code is working now and the records don't get deleted from database.
class StorySerializer < ActiveModel::Serializer
include Pundit
attributes :id, :title, :summary, :published, :published_date, :created_at, :updated_at, :cover
belongs_to :user
has_many :chapters
# ------------------------------------------------------------------------
# Note: need to use 'object.chapters' not 'self.chapters` below.
# ------------------------------------------------------------------------
def chapters
policy_scope(object.chapters)
end
end

jsonapi - Invalid links object on create request for parent and child records

I'm trying to write an API endpoint for creating Redemptions in my app.
In Rails, my model is such that Redemption has many Items (class_name: RedemptionItems).
Here's my POST Body, following what I assume is the correct JSONAPI Specification (since there is no explicit spec for creating parent and child records in one request).
{
"data": {
"type": "redemptions",
"relationships": {
"items": {
"data": [{
"type": "items",
"attributes": { "offer_id": "1", "quantity": "2" }
},
{
"type": "items",
"attributes": { "offer_id": "1", "quantity": "3" }
},
{
"type": "items",
"attributes": { "offer_id": "123", "quantity": "3" }
}
]
}
}
}
}
I'm using JSONAPI::Resources. I have defined my JSONAPI::Resources thus:
class Platform::Api::Members::RedemptionItemResource < JSONAPI::Resource
model_name 'Platform::RedemptionItem'
has_one :redemption
end
class Platform::Api::Members::RedemptionResource < JSONAPI::Resource
model_name 'Platform::Redemption'
has_many :items, class_name: 'RedemptionItem'
end
It's currently giving me an inavlid links object error, which doesnt tell me anything on how I must improve my request body.
{
"errors": [
{
"title": "Invalid Links Object",
"detail": "Data is not a valid Links Object.",
"code": "115",
"status": "400"
}
]
}

AngularJS get related data from user through JSON API

I'm using Ionic framework, but question is about AngularJS. I have written json api on ruby on rails. For authentication I choose ng-token-auth + devise-token-auth.
User json:
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"name": "User1",
"email": "email#email.com",
"current-sign-in-at": "..",
"last-sign-in-at": "..",
"created-at": ".."
},
"relationships": {
"friends": {
"data": [
{
"id": "2",
"type": "users"
}
]
}
}
},
"included": [
{
"id": "2",
"type": "users",
"attributes": {
"name": "User2",
"email": "user#user.com",
"current-sign-in-at": "..",
"last-sign-in-at": "..",
"created-at": ".."
},
"relationships": {
"friends": {
"data": [ ]
}
}
}
]
}
My serializer for user:
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email,:current_sign_in_at,
:last_sign_in_at, :created_at
has_many :friends, through: :friendships
end
Current user object as response:
{"success":true,"data":{"id":1,"provider":"email","uid":"0a56bb6b-dc72-4ef3-906e-1c17cb2fef46","name"
:"User1","nickname":null,"image":null,"email":"email#email.com"}}
There is my problem, I can't reach user relationship information.
I don't know why, but object name in my view is user (not current_user) and don't have access for it in my controllers.
Question is how can I have some additional information from this object and how to provide current user object access for my controllers.
I figured it out, so the answer is really simple. ng-token-auth provide $rootScope.user as the current user and it can be used in controllers.
But I still don't know how to provide related data to user object, fortunately found workaround for my needs.

Removing duplicate records after query and before returning data

I'm fairly new to Rails and have been familiarizing myself with the generators, but this is the first time I'm trying to create a custom route.
I have a star schema with the three tables Clients, Products, and Features that each have an n/n relationship to each other, so my "star" junction table has the three foreign keys, client_id, product_id, and feature_id. I created an API that'll return a list of clients, which each have a nested list of products, which each have a nested list of features. Here's the method that builds and returns the result:
def return_all
allData = Client
.includes(:products)
.includes(:features)
.to_json(:include => {:products => {:include => :features}})
render json: allData
end
The issue with this is that there are multiple features for every product, so the resulting JSON returns this (I removed all but one client and some of the data for brevity sake):
{
"id": 13,
"name": "client_1",
"created_at": "2015-10-09T18:44:56.000Z",
"updated_at": "2015-10-09T18:44:56.000Z",
"products": [
{
"id": 1,
"name": "product_1",
"created_at": "2015-10-12T03:10:34.000Z",
"updated_at": "2015-10-12T03:10:34.000Z",
"features": [
{
"id": 9,
"name": "email",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
},
{
"id": 10,
"name": "sms",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
},
{
"id": 11,
"name": "ivr",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
},
{
"id": 12,
"name": "print",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
}
]
},
{
"id": 1,
"name": "product_1",
"created_at": "2015-10-12T03:10:34.000Z",
"updated_at": "2015-10-12T03:10:34.000Z",
"features": [
{
"id": 9,
"name": "email",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
},
{
"id": 10,
"name": "sms",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
},
{
"id": 11,
"name": "ivr",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
},
{
"id": 12,
"name": "print",
"created_at": "2015-10-12T03:10:55.000Z",
"updated_at": "2015-10-12T03:10:55.000Z",
"value": null,
"group": "channel"
}
]
}
]
}
How do I go about removing the duplicate listings of products? Please let me know if you need any more information about my question.
Thanks
Edit: by request, here are my models:
class ClientsFeaturesProduct < ActiveRecord::Base
belongs_to :client
belongs_to :feature
belongs_to :product
end
class Client < ActiveRecord::Base
has_many :clients_features_products
has_many :features, through: :clients_features_products
has_many :products, through: :clients_features_products
end
class Product < ActiveRecord::Base
has_many :clients_features_products
has_many :clients, through: :clients_features_products
has_many :features, through: :clients_features_products
end
class Feature < ActiveRecord::Base
has_many :clients_features_products
has_many :clients, through: :clients_features_products
has_many :products, through: :clients_features_products
end
try using
has_many :products, -> { distinct }, through: :clients_features_products
on the Client model
see http://guides.rubyonrails.org/association_basics.html#distinct
A couple of other things
code style: use all_data instead of allData. See the style guide for more info: https://github.com/styleguide/ruby
you might want to use preload instead of includes; includes will either act like preload or like joins and you don't know which, so I tend to stay away from it; good article here: http://blog.arkency.com/2013/12/rails4-preloading/
I would try this, but it's just a guess.
def return_all
allData = Client
.joins(:products) # might need to use raw sql
.joins(:features)
.to_json(:include => {:products => {:include => :features}})
render json: allData
end
Alternatively, you could use Active Model Serializers to explicitly define a json view.

Resources