JSON from Sails responses don't have root elements. Ember data requires that the JSON be wrapped in a root element with the same name as the object. The plan is to extend RESTSerializer to munge the JSON on the client side, because I don't have similar control over the server that I'm getting this data from. I'm not sure what I'm doing wrong, but it doesn't seem like my Accounts model is using my AccountSerializer...
This is my Serializer:
var AccountSerializer = DS.RESTSerializer.extend({
/**
The current ID index of generated IDs
#property
#private
*/
_generatedIds: 0,
/**
Sideload a JSON object to the payload
#method sideloadItem
#param {Object} payload JSON object representing the payload
#param {subclass of DS.Model} type The DS.Model class of the item to be sideloaded
#param {Object} item JSON object representing the record to sideload to the payload
*/
sideloadItem: function(payload, type, item){
var sideloadKey = type.typeKey.pluralize(), // The key for the sideload array
sideloadArr = payload[sideloadKey] || [], // The sideload array for this item
primaryKey = Ember.get(this, 'primaryKey'), // the key to this record's ID
id = item[primaryKey];
// Missing an ID, generate one
if (typeof id == 'undefined') {
id = 'generated-'+ (++this._generatedIds);
item[primaryKey] = id;
}
// Don't add if already side loaded
if (sideloadArr.findBy("id", id) != undefined){
return payload;
}
// Add to sideloaded array
sideloadArr.push(item);
payload[sideloadKey] = sideloadArr;
return payload;
},
/**
Extract relationships from the payload and sideload them. This function recursively
walks down the JSON tree
#method sideloadItem
#param {Object} payload JSON object representing the payload
#paraam {Object} recordJSON JSON object representing the current record in the payload to look for relationships
#param {Object} recordType The DS.Model class of the record object
*/
extractRelationships: function(payload, recordJSON, recordType){
// Loop through each relationship in this record type
recordType.eachRelationship(function(key, relationship) {
var related = recordJSON[key], // The record at this relationship
type = relationship.type; // belongsTo or hasMany
if (typeof related === "object" && related !== null){
// One-to-one
if (relationship.kind == "belongsTo") {
// TODO: figure out when we need to only sideload 1 item we don't need to pluralize
// Sideload the object to the payload
this.sideloadItem(payload, type, related);
// Replace object with ID
recordJSON[key] = related.id;
// Find relationships in this record
this.extractRelationships(payload, related, type);
}
// Many
else if (relationship.kind == "hasMany") {
// Loop through each object
related.forEach(function(item, index){
// Sideload the object to the payload
this.sideloadItem(payload, type, item);
// Replace object with ID
related[index] = item.id;
// Find relationships in this record
this.extractRelationships(payload, item, type);
}, this);
}
}
}, this);
return payload;
},
/**
Overrided method
*/
extractArray: function(store, type, payload, id, requestType) {
var typeKey = type.typeKey,
typeKeyPlural = typeKey.pluralize(),
newPayload = {};
newPayload[typeKeyPlural] = payload;
payload = newPayload;
console.log(payload);
// Many items (findMany, findAll)
if (typeof payload[typeKeyPlural] != "undefined"){
payload[typeKeyPlural].forEach(function(item, index){
this.extractRelationships(payload, item, type);
}, this);
}
for(var key in payload) {
if(key === typeKeyPlural) {
for(var i =0; i < payload[key].length; i++) {
if(typeof payload[key][i] !== 'object') {
delete payload[key][i];
}
}
}
}
return this._super(store, type, payload, id, requestType);
},
extractSingle: function (store, type, payload, id, requestType) {
console.log('what is happening');
var typeKey = type.typeKey,
typeKeyPlural = typeKey.pluralize(),
newPayload = {};
if(typeof payload[typeKey] !== "object") {
newPayload[typeKey] = payload;
payload = newPayload;
if(payload[typeKey] instanceof Array) {
payload[typeKey] = payload[typeKey][0];
}
}
if (typeof payload[typeKey] === "object"){
this.extractRelationships(payload, payload[typeKey], type);
delete payload[typeKeyPlural];
}
console.log(payload);
return this._super(store, type, payload, id, requestType);
}
});
export default AccountSerializer;
In my adapters/account.js I have the following:
import DS from "ember-data";
var AccountAdapter = DS.RESTAdapter.extend({
namespace: 'api/v1',
host: 'http://localhost:5000',
pathForType: function(type) {
return type + '.json';
},
serializer: AccountSerializer
});
export default AccountAdapter;
I'm not 100% sure where you got the serializer property from, but I'm not sure that actually exists. There's a defaultSerializer property on the adapter which would work, but that comes with a bit of weird precedence rules. If I were you, I would declare the serializer in the Ember CLI way by putting it in serializers/account.js and remove the serializer property from your adapter.
Related
How exactly ID build? Data stored in store by ID, but how the ID is generated?
//Code just to pass requirements
normalize(model, hash, prop) {
hash.id = 'someId';
return this._super(...arguments);
}
If you ask about new records ( which creates on client side ) :
https://github.com/emberjs/data/blob/v2.5.3/addon/-private/system/store.js#L329
/**
If possible, this method asks the adapter to generate an ID for
a newly created record.
#method _generateId
#private
#param {String} modelName
#param {Object} properties from the new record
#return {String} if the adapter can generate one, an ID
*/
_generateId(modelName, properties) {
var adapter = this.adapterFor(modelName);
if (adapter && adapter.generateIdForRecord) {
return adapter.generateIdForRecord(this, modelName, properties);
}
return null;
},
Also check documentation for generateIdForRecord http://emberjs.com/api/data/classes/DS.Adapter.html#method_generateIdForRecord
we have a search formular with multiple (dynamic generated) input fields.
Our server rest api is Spring MVC powered and accepts a call like this:
/rest/search/1?identnummer=0000000¶meter2=xy¶meter3=zy
In the search method of the formular (action: search)
i collect all parameters and build up a string like this:
queryparams = 1?identnummer=0000000¶meter2=xy¶meter3=zy
and then call the store.find method:
that.store.find('trefferliste', queryparams).then(function(response) {
console.log("Success loading hit list...", response);
},
function(error) {
console.error(error);
that.transitionTo('trefferliste');
}
});
I overwrote the buildURL Method (removed encodeURIComponent()
if (id && !Ember.isArray(id)) { url.push(encodeURIComponent(id)); }
for the TrefferlisteAdapter
App.TrefferlisteAdapter = App.ApplicationAdapter.extend({
buildURL: function(type, id) {
var url = [];
var host = helicDevServer;
var prefix = this.urlPrefix();
if (type) { url.push(this.pathForType(type)); }
if (id && !Ember.isArray(id)) { url.push(id); }
if (prefix) { url.unshift(prefix); }
url = url.join('/');
if (!host && url) { url = '/' + url; }
return url;
}
});
After the request is send and i receive the expected json response, i have two entrys in the store:
Entry 1 : id = 1
Entry 2 : id = 1?identnummer=0000000¶meter2=xy¶meter3=zy
The first entry is the expected one, the second is corrupt and causes this error:
Error: Assertion Failed: Expected an object as `data` in a call to `push` for App.Trefferliste , but was undefined
at new Error (native)
at Error.EmberError (/js/libs/ember-stable/ember.debug.js:12061:21)
at Object.Ember.default.assert (/js/libs/ember-stable/ember.debug.js:5162:13)
at ember$data$lib$system$store$$Service.extend.push (/js/libs/ember-canary/ember-data.js:10849:15)
at /js/libs/ember-canary/ember-data.js:4805:24
at Object.Backburner.run (/js/libs/ember-stable/ember.debug.js:217:27)
at ember$data$lib$system$store$$Service.extend._adapterRun (/js/libs/ember-canary/ember-data.js:11143:33)
at /js/libs/ember-canary/ember-data.js:4802:22
at tryCatch (/js/libs/ember-stable/ember.debug.js:46977:16)
at invokeCallback (/js/libs/ember-stable/ember.debug.js:46989:17)
So my questions are:
Is this a acceptable method to pass the dynamic parameters to the
find method?
Does someone know how i can prevent the corrupt item to
get in the store?
Some more information about my setup:
DEBUG: Ember : 1.11.3
ember.debug.js:5197 DEBUG: Ember Data : 1.0.0-beta.17+canary.4ad70aab2a
ember.debug.js:5197 DEBUG: jQuery : 2.1.3
Serializer Method for Sideloading my records:
/**
Deserialize nested JSON payload into a flat object with sideloaded relationships. */
App.NestedSerializer = DS.RESTSerializer.extend({
/**
Generate a unique ID for a record hash
#param {DS.Store} store
#param {DS.Model} type
#param {Object} hash The record hash object
#return {String} The new ID
*/
generateID: function(store, type, hash){
var id, primaryKey,
serializer = store.serializerFor(type);
type._generatedIds = type._generatedIds || 0;
// Get the ID property name
primaryKey = Ember.get(serializer, 'primaryKey') || 'id';
id = hash[primaryKey];
// Generate ID
if (!id) {
id = ++type._generatedIds;
hash[primaryKey] = id;
}
return id;
},
/**
Sideload a record hash to the payload
#method sideloadRecord
#param {DS.Store} store
#param {DS.Model} type
#param {Object} payload The entire payload
#param {Object} hash The record hash object
#return {Object} The ID of the record(s) sideloaded
*/
sideloadRecord: function(store, type, payload, hash) {
var id, sideLoadkey, sideloadArr, serializer;
// If hash is an array, sideload each item in the array
if (hash instanceof Array) {
id = [];
hash.forEach(function(item, i){
id[i] = this.sideloadRecord(store, type, payload, item);
}, this);
}
// Sideload record
else if (typeof hash === 'object') {
sideLoadkey = type.typeKey.pluralize(); // Sideloaded keys are plural
sideloadArr = payload[sideLoadkey] || []; // Get/create sideload array
id = this.generateID(store, type, hash);
// Sideload, if it's not already sideloaded
if (sideloadArr.findBy('id', id) === undefined){
sideloadArr.push(hash);
payload[sideLoadkey] = sideloadArr;
}
}
// Assume it's already pointing to a sideloaded ID
else {
id = hash;
}
return id;
},
/**
Process nested relationships on a single hash record
#method extractRelationships
#param {DS.Store} store
#param {DS.Model} type
#param {Object} payload The entire payload
#param {Object} hash The hash for the record being processed
#return {Object} The updated hash object
*/
processRelationships: function(store, type, payload, hash) {
// If hash is an array, process each item in the array
if (hash instanceof Array) {
hash.forEach(function(item, i){
hash[i] = this.processRelationships(store, type, payload, item);
}, this);
}
else {
// Find all relationships in this model
type.eachRelationship(function(key, relationship) {
var related = hash[key], // The hash for this relationship
relType = relationship.type; // The model type
this.generateID(store, type, hash);
hash[key] = this.sideloadRecord(store, relType, payload, related);
this.processRelationships(store, relType, payload, related);
}, this);
}
return hash;
},
/**
(overloaded method)
Starts the deserialized of all payloads.
#method normalizePayload
#param {Object} payload
#return {Object} the normalized payload
*/
extract: function(store, type, payload, id, requestType) {
payload = this.normalizePayload(payload);
var rootKeys = Ember.keys(payload);
// Loop through root properties and process extract their relationships
rootKeys.forEach(function(key){
var type = store.container.lookupFactory('model:' + key.singularize()),
hash = payload[key];
// If the key resolves to a model type, sideload any embedded relationships
if (type) {
this.processRelationships(store, type, payload, hash);
}
}, this);
console.log(payload);
return this._super(store, type, payload, id, requestType);
}
,
normalizePayload: function(payload) {
if(payload.elementBeanWrapper){
payload.dashboard = payload.elementBeanWrapper;
delete payload.elementBeanWrapper;
}
return payload;
}
});
App.ApplicationSerializer = App.NestedSerializer;
You can monitor query params like this:
#..controller
queryParamsDidChange: (->
#store.find('myModel', #get('customQueryParams')).then (models)=>
#set 'content', models
).observes 'customQueryParams'
customQueryParams will be object like {myParam1: '123', myParam2: '456'}
Will this work for you?
I'm trying to modify the behaviour of pushPayload in the RESTSerializer. However the function is never called.
pushPayload: function(store, payload) {
console.log('pushPayload');
payload = this.normalizePayload(payload);
for (var prop in payload) {
var typeName = this.typeForRoot(prop),
type = store.modelFor(typeName),
typeSerializer = store.serializerFor(type);
/*jshint loopfunc:true*/
var normalizedArray = map.call(Ember.makeArray(payload[prop]), function(hash) {
return typeSerializer.normalize(type, hash, prop);
}, this);
store.pushMany(typeName, normalizedArray);
}
},
normalizePayload: function(payload) {
console.log('normalize');
return payload;
},
This will output only:
normalize
I really have no idea what's going on. I literraly copied from the master both methods. There is no typo and if normalizePayload is called pushPayload should be called also!
normalizePayload is called from both extractSingle (store.find('foo', 1);), store.extractArray (store.find('foo');) and pushPayload (store.pushPayload('foo', obj);).
You need to actually call pushPayload on the store with the type of the serializer defined. Additionally be careful, map is defined earlier in the document:
var map = Ember.ArrayPolyfills.map;
App.FooSerializer = DS.RESTSerializer.extend({
pushPayload: function(store, payload) {
console.log('pushPayload');
payload = this.normalizePayload(payload);
for (var prop in payload) {
var typeName = this.typeForRoot(prop),
type = store.modelFor(typeName),
typeSerializer = store.serializerFor(type);
/*jshint loopfunc:true*/
var normalizedArray = map.call(Ember.makeArray(payload[prop]), function(hash) {
return typeSerializer.normalize(type, hash, prop);
}, this);
store.pushMany(typeName, normalizedArray);
}
},
});
http://emberjs.jsbin.com/OxIDiVU/730/edit
I'm trying to write code to generically normalize embedded records. I'm trying to do this in the "normalizePayload" method of the serializer, as "extractObject" and "extractArray" don't get called on a store.pushPayload call (I'm pre-loading data using pushPayload).
Here's my code:
App.ApplicationSerializer = DS.ActiveModelSerializer.reopen
embeddedRelationships: []
extractHasOne: (payload, relationship_name) ->
object = payload[relationship_name]
objectId = object['id']
payload["#{relationship_name}_id"] = objectId
delete payload[relationship_name]
return object
extractHasMany: (payload, relationship_name) ->
objects = payload[relationship_name]
objectIds = objects.mapProperty('id')
payload["#{Ember.Inflector.inflector.singularize(relationship_name)}_ids"] = objectIds
delete payload[relationship_name]
return objects
extractRelationships: (payload) ->
extracted_objects = {}
this.embeddedRelationships.forEach((relationship_name) ->
relationship_payload = payload[relationship_name]
if relationship_payload instanceof Object
# has one
extracted_objects[relationship_name] = this.extractHasOne(relationship_payload, relationship_name)
else if relationship_payload instanceof Array
# has many
extracted_objects[relationship_name] = this.extractHasMany(relationship_payload, relationship_name)
, this)
return extracted_objects
normalizePayload: (type, payload) ->
if payload[type]?
# single object
this.extractRelationships(payload[type])
else if payload[Ember.Inflector.inflector.pluralize(type)]?
# many objects
payload[Ember.Inflector.inflector.pluralize(type)].forEach((object_payload) ->
this.extractRelationships(object_payload)
, this)
NOTE: this isn't done yet, as I'm not merging all the extracted objects and sideloading them yet.
In either case, the idea is that in a subclass, I override "embedded relationships" with a list of strings that represent the relationships that must be normalized for that model.
The problem I'm running into is simple: "normalizePayload" is being called with a null "type" value.
I'm pre-loading the data like so:
this.store.pushPayload('category', window.preloadCategories)
Is this an ember bug, or am I missing something?
pushPayload takes the type and grabs that serializer then it calls pushPayload on the custom serializer which doesn't take a type, so no type is passed into the normalizePayload.
I'd suggest creating a custom serializer for Category instead of Application, then you'll know its a category type.
The reason behind this was pushPayload was geared to pushing more than a single type (aka the payload isn't just Category or Post
pushPayload on the store
/**
Push some raw data into the store.
The data will be automatically deserialized using the
serializer for the `type` param.
This method can be used both to push in brand new
records, as well as to update existing records.
You can push in more than one type of object at once.
All objects should be in the format expected by the
serializer.
```js
App.ApplicationSerializer = DS.ActiveModelSerializer;
var pushData = {
posts: [
{id: 1, post_title: "Great post", comment_ids: [2]}
],
comments: [
{id: 2, comment_body: "Insightful comment"}
]
}
store.pushPayload('post', pushData);
```
#method pushPayload
#param {String} type
#param {Object} payload
*/
pushPayload: function (type, payload) {
var serializer;
if (!payload) {
payload = type;
serializer = defaultSerializer(this.container);
Ember.assert("You cannot use `store#pushPayload` without a type unless your default serializer defines `pushPayload`", serializer.pushPayload);
} else {
serializer = this.serializerFor(type);
}
serializer.pushPayload(this, payload);
},
pushPayload on the serializer
It will first normalize the payload, so you can use this to push
in data streaming in from your server structured the same way
that fetches and saves are structured.
#method pushPayload
#param {DS.Store} store
#param {Object} payload
*/
pushPayload: function(store, payload) {
payload = this.normalizePayload(null, payload);
for (var prop in payload) {
var typeName = this.typeForRoot(prop),
type = store.modelFor(typeName);
/*jshint loopfunc:true*/
var normalizedArray = map.call(payload[prop], function(hash) {
return this.normalize(type, hash, prop);
}, this);
store.pushMany(typeName, normalizedArray);
}
},
I was using Ember Data 1.0.0 beta 1. I switched to beta 2 (just released).
It seems that the model serializers (in which I normalize the id's) no longer work.
I have the impression that the param order in normalize is changed from type, prop, hash to type, hash, prop.
This is what the migration guide recommends:
normalize: function (type, property, hash) {
// normalize the `_id`
var json = { id: hash._id };
delete hash._id;
// normalize the underscored properties
for (var prop in hash) {
json[prop.camelize()] = hash[prop];
}
// delegate to any type-specific normalizations
return this._super(type, property, json);
}
The params order in beta 2 is now (type, hash, property). As a result, models normalized do not contain an id in the beta 2 version.
If I switch the params to type, hash, property, then the id gets filled, but all other properties become ampty at that moment.
It thus look slike you can no longer use normalize to normalize both the id and any underscored properties.
There's a revised version of the normalize function (which is slightly more robust) in the Transition document.
https://github.com/emberjs/data/blob/master/TRANSITION.md#underscored-keys-_id-and-_ids
App.ApplicationSerializer = DS.RESTSerializer.extend({
normalize: function(type, hash, property) {
var normalized = {}, normalizedProp;
for (var prop in hash) {
if (prop.substr(-3) === '_id') {
// belongsTo relationships
normalizedProp = prop.slice(0, -3);
} else if (prop.substr(-4) === '_ids') {
// hasMany relationship
normalizedProp = Ember.String.pluralize(prop.slice(0, -4));
} else {
// regualarAttribute
normalizedProp = prop;
}
normalizedProp = Ember.String.camelize(normalizedProp);
normalized[normalizedProp] = hash[prop];
}
return this._super(type, normalized, property);
}
});
You'll get the blank params if you only switch the order on the line with normalize: function(.... You also have to switch it on the return this._super(... line.