Ember, Ember-data and Rails relations error: "Cannot read property 'typeKey' of undefined" - ruby-on-rails

I connect Ember to a Rails API which delivers some JSON.
I'm trying to get relations working (product hasMany images) but it keeps giving me this error:
Cannot read property 'typeKey' of undefined
My Models:
App.Product = DS.Model.extend({
images: DS.hasMany('image'),
title: DS.attr('string'),
});
App.Image = DS.Model.extend({
product: DS.belongsTo('product')
});
Rails renders json as:
{
"products":[
{
"id": 1,
"title": "product title",
"images":[
{
"id": 1,
"urls":
{
"thumb":"http://domain.com/thumb/image.jpg",
"original":"http://domain.com/original/image.jpg"
}
}
]
}
]
}

Turns out I needed to "sideload" my images in Rails, so the JSON became:
{
"products":[
{
"id": 1,
"title": "product title",
"image_ids": [1]
}
],
"images":[
{
"id": 1,
"urls":
{
"thumb":"http://domain.com/thumb/image.jpg",
"original":"http://domain.com/original/image.jpg"
}
}
]
}
Rails' ProductSerializer:
class ProductSerializer < ActiveModel::Serializer
embed :ids, :include => true
attributes :id, :title
has_many :images
methods :image_urls
end

It seems that you were using embedded JSON in your example. You need to use the EmbeddedRecordsMixin https://github.com/emberjs/data/blob/master/packages/ember-data/lib/serializers/embedded_records_mixin.js and set the appropriate flag to mark images as being embededd

Related

Custom json on rails 5 api only, using to_json

I am working on a rails 5 api only. I can get all the data that i want, but i would like to customize the output.I tried some solutions, but couldn't find it yet.
This is my portfolio controller:
def show
render json: #portfolio.to_json(
:only => [:title],
:include => {
:cryptos => {
:only => [],
:include => [
{:radarcrypto => { :only => [:ticker, :price] }},
]},
}
)
end
this is the output
{
title: "Portfolio 1",
cryptos: [
{
radarcrypto: {
ticker: "BTC",
price: "190919.85",
}
},
{
radarcrypto: {
ticker: "ETH",
price: "12220.18",
}
},
],
}
But i would like something like that:
{
title: "Portfolio 1",
cryptos:
{
ticker: "BTC",
price: "190919.85",
},
{
ticker: "ETH",
price: "12220.18",
},
}
this are my models
class Portfolio < ApplicationRecord
has_many :cryptos
end
class Radarcrypto < ApplicationRecord
has_many :cryptos
end
class Crypto < ApplicationRecord
belongs_to :portfolio
belongs_to :radarcrypto
end
I already tried to use without success:
class Radarcrypto < ApplicationRecord
ApplicationRecord.include_root_in_json = false
end
I don't know if there is a better solution to this customization, if would be better to use the views, json.jbuilder for example. thanks.
It's not possible to include a collection of objects as a value to a key without wrapping them in an array in JSON. (source (W3Schools))
It is possible, on the other hand, to have a collection of objects, but each one would have to have its own unique key. So the response could be shaped as such:
{
title: "Portfolio 1",
cryptos: {
BTC: {
ticker: "BTC",
price: "190919.85",
},
ETH: {
ticker: "ETH",
price: "12220.18",
},
}
(A different key is going to have to be used if more than one radarcrypto can have the same ticker)
I'm not sure how that would be achieved using the to_json options (e.g. include and only). You could probably do it manually by doing:
def show
portfolio = {};
portfolio['title'] = #portfolio.title
portfolio['cryptos'] = {}
#portfolio.cryptos.each do |crypto|
radarcrypto = crypto.radarcrypto
portfolio['cryptos']["#{radarcrypto.ticker}"] = {
'ticker': radarcrypto.ticker,
'price': radarcrypto.price
}
end
render json: portfolio.to_json
end
Side note
A gem such as Jbuilder may be a good candidate if nested shaping of JSON responses is done in multiple controllers.

Elasticsearch : Multi match query on nested fields

I am having a problem with multi-match query in RoR. I have Elastic Search configured and working however I am working on setting up aggregations which so far seem to work, but for whatever reason I am not able to search on the field which I am aggregating. This is the extract from my model:
settings :index => { :number_of_shards => 1 } do
mapping do
indexes :id, index: :not_analyzed
indexes :name
indexes :summary
indexes :description
indexes :occasions, type: 'nested' do
indexes :id, type: 'integer'
indexes :occasion_name, type: 'string', index: :not_analyzed
...
end
end
end
def as_indexed_json(options = {})
self.as_json(only: [:id, :name, :summary, :description],
include: {
occasions: { only: [:id, :occasion_name] },
courses: { only: [:id, :course_name] },
allergens: { only: [:id, :allergen_name] },
cookingtechniques: { only: [:id, :name] },
cuisine: { only: [:id, :cuisine_name]}
})
end
class << self
def custom_search(query)
__elasticsearch__.search(query: multi_match_query(query), aggs: aggregations)
end
def multi_match_query(query)
{
multi_match:
{
query: query,
type: "best_fields",
fields: ["name^9", "summary^8", "cuisine_name^7", "description^6", "occasion_name^6", "course_name^6", "cookingtechniques.name^5"],
operator: "and"
}
}
end
I am able to search on all fields as specified in the multi_match_query apart of "occasion_name" which happens to be the field I am aggregating. I have checked that the field is correctly indexed (using elastic search-head plugin). I am also able to display the facets with the aggregated occasion_names in my view. I tried everything I can think of, including removing the aggregation and searching on occasion_name, but still no luck.
(I am using the elasticsearch-rails gem)
Any help will be much appreciated.
Edit:
I got this ES query from rails:
#search=
#<Elasticsearch::Model::Searching::SearchRequest:0x007f91244df460
#definition=
{:index=>"recipes",
:type=>"recipe",
:body=>
{:query=>
{:multi_match=>
{:query=>"Christmas",
:type=>"best_fields",
:fields=>["name^9", "summary^8", "cuisine_name^7", "description^6", "occasion_name^6", "course_name^6", "cookingtechniques.name^5"],
:operator=>"and"}},
:aggs=>
{:occasion_aggregation=>
{:nested=>{:path=>"occasions"}, :aggs=>{:id_and_name=>{:terms=>{:script=>"doc['occasions.id'].value + '|' + doc['occasions.occasion_name'].join(' ')", :size=>35}}}}}}},
This is an example of all that gets indexed for 1 of my dummy recipes I use for testing (the contents are meaningless - I use this only for testing):
{
"_index": "recipes",
"_type": "recipe",
"_id": "7",
"_version": 1,
"_score": 1,
"_source": {
"id": 7,
"name": "Mustard-stuffed chicken",
"summary": "This is so good we'd be surprised if this chicken fillet recipe doesn't become a firm favourite. Save it to your My Good Food collection and enjoy",
"description": "Heat oven to 200C/fan 180C/gas 6. Mix the cheeses and mustard together. Cut a slit into the side of each chicken breast, then stuff with the mustard mixture. Wrap each stuffed chicken breast with 2 bacon rashers – not too tightly, but enough to hold the chicken together. Season, place on a baking sheet and roast for 20-25 mins.",
"occasions": [
{
"id": 9,
"occasion_name": "Christmas"
}
,
{
"id": 7,
"occasion_name": "Halloween"
}
,
{
"id": 8,
"occasion_name": "Bonfire Night"
}
,
{
"id": 10,
"occasion_name": "New Year"
}
],
"courses": [
{
"id": 9,
"course_name": "Side Dish"
}
,
{
"id": 7,
"course_name": "Poultry"
}
,
{
"id": 8,
"course_name": "Salad"
}
,
{
"id": 10,
"course_name": "Soup"
}
],
"allergens": [
{
"id": 6,
"allergen_name": "Soya"
}
,
{
"id": 7,
"allergen_name": "Nut"
}
,
{
"id": 8,
"allergen_name": "Other"
}
,
{
"id": 1,
"allergen_name": "Dairy"
}
],
"cookingtechniques": [
{
"id": 15,
"name": "Browning"
}
],
"cuisine": {
"id": 1,
"cuisine_name": "African"
}
}
}
EDIT 2:
I managed to make the search work for occasions as suggested by #rahulroc, but now I can't search on anything else...
def multi_match_query(query)
{
nested:{
path: 'occasions',
query:{
multi_match:
{
query: query,
type: "best_fields",
fields: ["name^9", "summary^8", "cuisine_name^7", "description^6", "occasion_name^6", "course_name^6", "cookingtechniques.name^5"],
operator: "and"
}
}
}
}
end
UPDATE: Adding multiple nested fields - I am trying to add the rest of my aggregations but I am facing similar problem as before. My end goal will be to use the aggregations as filters so I need to add about 4 more nested fields to my query (I also would like to have the fields searchable) Here is the working query as provided by #rahulroc + the addition of another nested field which I can't search on. As before in terms of indexing everything is working and I can display the aggregations for the newly added field, but I can't search on it. I tried different variations of this query but I couldn't make it work (the rest of the fields are still working and searchable - the problem is just the new field):
def multi_match_query(query)
{
bool: {
should: [
{
nested:{
path: 'occasions',
query: {
multi_match:
{
query: query,
type: "best_fields",
fields: ["occasion_name"]
}
}
}
},
{
nested:{
path: 'courses',
query: {
multi_match:
{
query: query,
type: "best_fields",
fields: ["course_name"]
}
}
}
},
{
multi_match: {
query: query,
fields:["name^9", "summary^8", "cuisine_name^7", "description^6"],
}
}
]
}
}
end
You need to create a separate nested clause for matching a nested field
"query": {
"bool": {
"should": [
{
"nested": {
"path": "occassions",
"query": {
"multi_match": {
"query": "Christmas",
"fields": ["occassion_name^2"]
}
}
}
},
{
"multi_match": {
"query": "Christmas",
"fields":["name^9", "summary^8", "cuisine_name^7", "description^6","course_name^6"] }
}
]
}
}

Rails permit nested array

I am trying to use accepts_nested_attributes_for in conjunction with a has_many association and having a lot of trouble...
Here is a simplified version of my user.rb:
class User < ActiveRecord::Base
...
has_many :user_permissions
accepts_nested_attributes_for :user_permissions
...
end
My user_permission.rb:
class UserPermission < ActiveRecord::Base
belongs_to :user
...
end
And my users_controller.rb:
class UsersController < ApiController
...
def update
#user.assign_attributes user_params
if #user.save
render partial: 'user', locals: { user: #user }
else
render json: {errors: #user.errors}.to_json, status: 500
end
end
...
private
def user_params
params.require(:user).permit(:first_name, :last_name, user_permissions_attributes: [ :user_id, :resource_id, :can_read, :can_update, :can_create, :can_delete ])
end
end
I am referencing this rails documentation on how to use accepts_nested_attributes_for with Strong Parameters.
However, when I 'puts user_params' from inside the users_controller this is all I see (no reference to the user_permissions):
{"first_name"=>"Joe", "last_name"=>"Shmoe"}
Here is an example of JSON I am submitting to the server (via angular $resource):
{
"id": 10,
"first_name": "Joe",
"last_name": "Shmoe",
"user_permissions": [
{
"organization_resource_id": 20,
"user_id": 10,
"can_update": true,
"can_read": true
},
{
"organization_resource_id": 21,
"user_id": 10,
"can_create": true,
"can_read": true
}
],
}
Which returns this JSON:
{
"id": 10,
"first_name": "Joe",
"last_name": "Shmoe",
"user_permissions": [],
}
I am fairly confident this is an issue in my rails layer, but just for reference here is the angular User.js service I created to perform this RESTful interaction with the server:
angular.module('slics').service('User', [
'$resource', function($resource) {
return $resource('/api/users/:id', {
id: '#id'
}, {
update: {
method: 'PUT',
isArray: false
}
});
}
]);
Really not sure what I am missing here. It does not seem like it should be this difficult to submit nested attributes... but the more research I do the more I realize this does seem to be a pretty common Rails frustration.
Please feel free to comment if any additional context/information would be useful to include in my problem description to help troubleshoot this problem and I would be happy to provide it!
Strong params expects user_permissions_attributes, and you're submitting user_permissions.
Strong params is separate from accepts_nested_attributes_for (in fact, it has nothing to do with it), so however you define your require!/permit calls is exactly how your attributes should be submitted.
ProTip: To save you some future frustration, if you plan on updating through accepts nested attributes, you probably want to permit :id as well.
Well, you post an array of hashes, not a hash.
So this code
user_permissions_attributes: [ :user_id, :resource_id, :can_read, :can_update, :can_create, :can_delete ]
will permit such structure
{
"id": 10,
"first_name": "Joe",
"last_name": "Shmoe",
"user_permissions_attributes": [
"organization_resource_id": 20,
"user_id": 10,
"can_update": true,
"can_read": true
]
}
Try to whitelist all params at "user_permissions"
user_permissions_attributes: []
Or check out this article, to learn how to build advanced whitelists with StrongParams
http://patshaughnessy.net/2014/6/16/a-rule-of-thumb-for-strong-parameters
user_permissions_attributes: [ :user_id, :id, :can_read, :can_update, :can_create, :can_delete ]) permit :id and submitting hashes with index value..
JSON format submitting to the serve
"user": {
"id": 10,
"first_name": "Joe",
"last_name": "Shmoe",
"user_permissions": {
"0": {
"id": 20,
"user_id": 10,
"can_update": true,
"can_read": true
},
"1": {
"id": 21,
"user_id": 10,
"can_create": true,
"can_read": true
}
}
}

ember-rails not loading hasMany Association

Gems Used:
Using ember-source (1.5.1.1)
Using ember-data-source (1.0.0.beta.7)
Using ember-rails (0.15.0)
Using handlebars-source (1.3.0)
Using active_model_serializers (0.8.1)
This is the ember app code:
window.App = Ember.Application.create
LOG_TRANSITIONS: true
rootElement: '#app-ember-root'
App.store = DS.Store.extend({});
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({});
App.Router.map ()->
#resource 'quotes',
path: '/'
App.QuotesRoute = Ember.Route.extend
model: ()->
this.store.find 'quote'
App.CompaniesQuote = DS.Model.extend
quote: DS.belongsTo 'quote'
App.Quote = DS.Model.extend
message: DS.attr 'string'
createdAt: DS.attr 'date'
companiesQuotes: DS.hasMany('companiesQuote')
companiesQuotesCount: (->
this.get('companiesQuotes.length')
).property('companiesQuotes')
Serializers:
class QuoteSerializer < ActiveModel::Serializer
attributes :id, :message, :created_at
has_many :companies_quotes
end
class CompaniesQuoteSerializer < ActiveModel::Serializer
attributes :id, :company_id
end
Template quotes.handlebars:
<div class="quotes-list">
<div class="quote-container">
{{#each}}
{{ companiesQuotesCount }} companies quote for this quote {{ id }}
{{/each }}
</div>
</div>
/quotes JSON response:
{
"quotes":[
{
"id":10,
"message":"Quote 10!",
"created_at":"2014-06-04T17:00:01.000Z",
"companies_quotes":[
{
"id":27,
"company_id":1
},
{
"id":28,
"company_id":2
},
{
"id":26,
"company_id":3
}
]
},
{
"id":11,
"message":"Quote 11!",
"created_at":"2014-06-11T14:45:02.000Z",
"companies_quotes":[
{
"id":30,
"company_id":1
},
{
"id":31,
"company_id":2
},
{
"id":29,
"company_id":3
}
]
}
]
}
With this env/code, the property companiesQuotesCount is always 0.
What am I missing?
Solved
Using the #kingpin2k's response, I changed the JSON structure modifying QuoteSerializer:
class QuoteSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :message, :created_at
has_many :companies_quotes
end
Ember Data doesn't do embedded records by default.
You'll either need to fix the json using a client side serializer, or fix it server side.
It should be in this format:
{
"quotes":[
{
"id":10,
"message":"Quote 10!",
"created_at":"2014-06-04T17:00:01.000Z",
"companies_quotes":[27, 28, 29]
},
{
"id":11,
"message":"Quote 11!",
"created_at":"2014-06-11T14:45:02.000Z",
"companies_quotes":[30, 31, 32]
]
}
],
companies_quotes:[
{
"id":27,
"company_id":1
},
{
"id":28,
"company_id":2
},
{
"id":26,
"company_id":3
},
....
]
}
Here's an example for using extractArray http://emberjs.com/api/data/classes/DS.RESTSerializer.html#method_extractArray
Additionally your computed property is just watching the reference, which won't change as the length changes.
companiesQuotesCount: (->
this.get('companiesQuotes.length')
).property('companiesQuotes.length')

Customizing output of JSON

In the controller I have a respond_with like this:
respond_with(#layer1 , #layer2)
The JSON output I need is like this:
{
"LayerOne": [
{
"name": "haha",
"number":"44"
}, // more ....
],
"LayerTwo": [
{
"name": "James Bond",
"score": 20
} // , ....
]
}
So to get the first section I write the serializer like this:
class Layer1Serializer < ActiveModel::Serializer
attributes :number, :name
def name
object.person.name
end
end
And I change the controller to be like this, so I can pass a ROOT so it shows in the JSON as "LayerOne"
respond_with(#Layer1, root: 'LayerOne')
but remember at the beginning I had two things to pass to controller, so now I can't figure our how to do this for the second section of JSON that says "Layer2"
You can create the following intermediate class:
class BothLayers
include ActiveModel
def initialize(layer1,layer2)
#layer1 = layer1
#layer2 = layer2
end
attr_accessor :layer1, :layer2
end
and the following serializer:
class BothLayersSerializer < ActiveModel::Serializer
root false
has_many :layer1, key: "LayerOne"
has_many :layer2, key: "LayerTwo"
end
Then in your controller:
both_layers = BothLayers.new(#layer1,#layer2)
respond_with( both_layers, serializer: BothLayersSerializer )
Using the JBuilder DSL is an excellent way to solve your problem.
https://github.com/rails/jbuilder
The JSON response you want is implemented as a view, giving you complete control over how it renders.
create a new hash and pass your array values into it.
respond_with({:LayerOne => #layer1.as_json(:only => [:name, :percentage]), :LayerTwo => #layer2.as_json(:only => [:name, :off_target_by])})
i got this json :
{
"LayerOne": [
{
"name": "layer1",
"percentage": "10.11"
},
{
"name": "layer 1 bis",
"percentage": "1212.0"
}
],
"LayerTwo": [
{
"name": "layer 2",
"off_target_by": 2
},
{
"name": "layer 2 bis",
"off_target_by": 9
}
]
}
hope it helps :)
EDIT 2 :
You can create an array serializer to pass your variables :
class LayerArraySerializer < ActiveModel::ArraySerializer
self.root = false
end
and in your view :
respond_with({:LayerOne => #layer1 , :LayerTwo => #layer2}, :serializer => LayerArraySerializer)
json print :
[
[
"LayerOne",
[
{
"percentage": "10.11",
"name": "layer1"
},
{
"percentage": "1212.0",
"name": "layer 1 bis"
}
]
],
[
"LayerTwo",
[
{
"off_target_by": 2,
"name": "layer 2"
},
{
"off_target_by": 9,
"name": "layer 2 bis"
}
]
]
]
Railcasts has an excellent video+text tutorial on AR Serializer, I'm sure you'll find your answer there
http://railscasts.com/episodes/409-active-model-serializers

Resources