Page loading before model is deserialized - ember.js

Use case :
I am trying to render a page that compare outputs of 2 performance benchmark runs.
Issue :
Only part of the data is getting deserialized at the time of the page render. On multiple refreshs all of the data gets rendered.
The network tab seems to show that the output is sent correctly by the backend.
Execution:
1) The data is rendered using the api /reports/:reportId.
This api is called twice using 2 reportIds to be compared and the difference is then displayed on the UI.
The /reports/:reportId api returns an output like :
{
"data": {
"id": 87,
"type": "reports",
"attributes": {
"status": "STARTED",
"startdatetime": 1532511531000,
"enddatetime": 1533485380052,
"queries": [
{
"startdatetime": 1532511531000,
"enddatetime": 1533485380056,
"reportId": 87,
"id": "Q1"
}
]
}
}
}
The output intermittently renders everything but the "queries" array while deserializing the array perfectly in some runs.
2) To parse the queries array I used a custom array transform like :
import DS from 'ember-data';
export default DS.Transform.extend({
deserialize: function(serialized) {
return (Ember.typeOf(serialized) == "array")
? serialized
: [];
},
serialize: function(deserialized) {
var type = Ember.typeOf(deserialized);
if (type == 'array') {
return deserialized
} else if (type == 'string') {
return deserialized.split(',').map(function(item) {return jQuery.trim(item);});
} else {
return [];
}
}
});
3) The model for reports looks like:
status: DS.attr('string'),
startdatetime: DS.attr('number'),
enddatetime: DS.attr('number'),
queries: DS.attr('array')
4) My route for compare looks like :
import Route from '#ember/routing/route';
import { hash } from 'rsvp';
import { inject as service } from '#ember/service';
export default Route.extend({
model(params) {
return hash({
leftrun: this.store.findRecord('report', params.id1),
rightrun: this.store.findRecord('report', params.id2)
})
}
});
On running the page on debug mode when the queries array doesn't get deserialized,
1) I can see that the "status" and other attributes is accessible.
2) The network tab shows that the output for reports/:runId is "pending" (Which doesn't make sense as the "status" is set.) Once the page is fully loaded, the network tab shows that the full data including the queries array was received by the webapp.
Is there an issue with the flow or my transform that is causing this issue?

Related

Calling Ember Model in Computed Property returns incorrect array

I'm trying to format data held in an Ember Model so that I can plug it into a Chart Component.
Route code:
import Route from '#ember/routing/route';
export default Route.extend({
model() {
let dateFrom = this.paramsFor('dashboard').dateFrom
let dateTo = this.paramsFor('dashboard').dateTo
let hash = {dateFrom, dateTo}
return Ember.RSVP.hash({
custakelist: this.get('store').query('custakelist', hash),
barchart: this.get('store').query('barchart', hash),
});
},
setupController(controller, models) {
this._super(controller, models);
controller.set('barchart', models.barchart);
controller.set('custakelist', models.custakelist);
},
Controller Code:
import Controller from '#ember/controller';
import groupBy from 'ember-group-by';
export default Controller.extend({
entriesByDate: groupBy('custakelist', 'take_list_date'),
entriesByAge: groupBy('custakelist', 'patient_age'),
wardData: Ember.computed.map('entriesByDate', function(group) {
return {
label: group.value,
count: group.items.length
};
}),
ageData: Ember.computed.map('entriesByAge', function(group) {
return {
label: group.value,
count: group.items.length
};
}),
clerkData: Ember.computed.map('barchart', function(barchart) {
return {
label: barchart.label,
count: barchart.count
};
}),
});
I know that the models are being loaded on the page correctly thanks to Ember Data. I also know that the 'custakelist' model is being used by other charts.
When I try to use the model 'barchart' and log the result to the console I can see that an array with the correct number of items is created but they don't contain any values, they just display as follows:
0: {label: Computed Property, count: Computed Property}
How can I make the data that is already loaded as per my model usable in this context?
I solved this by iterating on the model with a forEach and putting the result in a new array ready to plug into my chart:
clerkData: Ember.computed('barchart', function(test) {
let newArray = []
this.get('barchart').forEach(function(x) {
let newLabel = moment(x.data.label).format("MMM Do YY")
let newCount = x.data.count
let newData = {label:newLabel, count:newCount}
newArray.push(newData)
})
return newArray
}),

ember data no model was found for attribute name

I have defined a model(app/models/job.js)
import DS from 'ember-data';
export default DS.Model.extend({
status: DS.attr(),
result: DS.attr()
});
And I am trying to load it from the index controller(app/controllers/index.js)
import Ember from 'ember';
export default Ember.Controller.extend({
productName: "",
customerName: "",
startDate: "",
endDate: "",
actions: {
search: function() {
let data = this.store.find("job", '9e5ce869-89b3-4bfc-a70f-034593c21eae');
return data;
}
}
});
The HTTP response I get is:
{
"status": "OK",
"result": {
"b": 2,
"a": 2,
"see": 1,
"c": 1
}
}
How ever I get following error and warning:
WARNING: Encountered "status" in payload, but no model was found for model name "status" (resolved model name using next-gen-analytics#serializer:job:.modelNameFromPayloadKey("status"))
WARNING: Encountered "result" in payload, but no model was found for model name "result" (resolved model name using next-gen-analytics#serializer:job:.modelNameFromPayloadKey("result"))
TypeError: Cannot read property '_internalModel' of undefined
at finders.js:50
at Object.Backburner.run (ember.debug.js:224)
at ember$data$lib$system$store$$Service.extend._adapterRun (store.js:2043)
at finders.js:45
at tryCatch (ember.debug.js:56151)
at invokeCallback (ember.debug.js:56166)
at publish (ember.debug.js:56134)
at ember.debug.js:32577
at Queue.invoke (ember.debug.js:910)
at Object.Queue.flush (ember.debug.js:974)onerrorDefault # ember.debug.js:32616exports.default.trigger # ember.debug.js:56792Promise._onerror # ember.debug.js:57758publishRejection # ember.debug.js:56065(anonymous function) # ember.debug.js:32577Queue.invoke # ember.debug.js:910Queue.flush # ember.debug.js:974DeferredActionQueues.flush # ember.debug.js:770Backburner.end # ember.debug.js:160Backburner.run # ember.debug.js:228run # ember.debug.js:20238ember$data$lib$system$adapter$$default.extend.ajax.Ember.RSVP.Promise.hash.success # rest-adapter.js:831jQuery.Callbacks.fire # jquery.js:3148jQuery.Callbacks.self.fireWith # jquery.js:3260done # jquery.js:9314jQuery.ajaxTransport.send.callback # jquery.js:9718
Any suggestion will be appreciated
UPDATE
I was thinking this is kind of a bug, so I went ahead to log a bug on ember-data github repo, got the response from #wecc in an hour or so (NICE)
https://github.com/emberjs/data/issues/3683
So to fix this issue, I wrote my own serializer.
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizePayload: function(payload) {
return {
'job': {
id: '9e5ce869-89b3-4bfc-a70f-034593c21eae',
status: payload.status,
result: payload.result
}
};
}
});
And now, it starts working.
OPINION I can think of why they implemented the default RESTSerializer this way, but we probably should give more information to the users on the documentation of the ember-data, otherwise, newbie who is trying to use it will get lost.
1) Your server response should have a root element with the same name, as model's. So, for single object it shuld be:
{
"job": {
"id": "<backend must provide an id, integer preferred>"
"status": "OK",
"result": {
"b": 2,
"a": 2,
"see": 1,
"c": 1
}
}
And for multiple objects:
{
"jobs": [{
"id": "<backend must provide an id, integer preferred>"
"status": "OK",
"result": {
"b": 2,
"a": 2,
"see": 1,
"c": 1
}, {/*next object*/}]
}
I tried to debug it, and I found that the code goes to rest-serializer.js and extractSingle function, and goes to the line "if (!store.modelFactoryFor(modelName)) {" and it returns "false".
extractSingle: function (store, primaryTypeClass, rawPayload, recordId) {
Ember.deprecate("`serializer.normalizePayload` has been deprecated. Please use `serializer.normalizeResponse` with the new Serializer API to modify the payload.", this.normalizePayload === JSONSerializer.prototype.normalizePayload, {
id: "ds.serializer.normalize-payload-deprecated",
until: "2.0.0"
});
var payload = this.normalizePayload(rawPayload);
var primaryRecord;
for (var prop in payload) {
var modelName = this.modelNameFromPayloadKey(prop);
if (!store.modelFactoryFor(modelName)) {
Ember.warn(this.warnMessageNoModelForKey(prop, modelName), false, {
id: "ds.serializer.model-for-key-missing"
});
continue;
}
var isPrimary = this.isPrimaryType(store, modelName, primaryTypeClass);
var value = payload[prop];

Ember data Embedded JSON without Id and no root object

I am working with Ember and Ember-data. But the JSON which i receive is not par with the Ember side-loading standards. The JSON does'nt have a root model. Also the models are embedded and some times haves Ids and sometimes does not have Id.
I have seen couple of links on how to add root model using extract hook and also how to play with embedded model using
App.ColorSerializer = DS.RestSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
foos: {embedded: 'always'}
}
});
This code is taken from this link.
This is the JSON used there
{
colors:[
{
id: 1,
color: "red",
foos:[
{
id:1,
name:'something 1'
},
{
id:2,
name:'something 2'
}
]
},
...
Now the problem that i am facing is that my JSON could also look like below(no root model "color")
{
id: 1,
color: "red",
foos:[
{
id:1,
name:'something 1'
},
{
id:2,
name:'something 2'
}
]
},
...
or even like this(without Ids for foo objects)
{
id: 1,
color: "red",
foos:[
{
name:'something 1'
},
{
name:'something 2'
}
]
},
...
Is there any way i can handle this? How do i add Ids to the embedded model foo? Also is there some solution/plugin which would accept any kind of embedded JSON and convert it into side loaded JSON and added Ids if needed.
I have seen this solution. Does it really work? Because it does not use the latest EmbeddedRecordsMixin
I used a generic transform for arrays:
// /transforms/array.js
import DS from "ember-data";
import Ember from "ember";
export default DS.Transform.extend({
deserialize: function (value) {
if (Ember.isArray(value)) {
return Ember.A(value);
} else {
return Ember.A();
}
},
serialize: function (value) {
if (Ember.isArray(value)) {
return Ember.A(value);
} else {
return Ember.A();
}
}
});
Then in my model, I simply use:
foos: DS.attr("array")

Customize JSON collection name in Serializer

When accessing API resources like /api/users/ which list/search a resource Ember Data expectects the response to be in this format:
{
"users": [{
"name": "Rails"
}, {
"name": "Omakase"
}]
}
but my response looks like this:
{
"results": [{
"name": "Rails"
}, {
"name": "Omakase"
}]
}
How can I tell my Serializer to turn results into users?
The following Serialzer renames single attributes but not the whole list as shown above:
import DS from 'ember-data';
export default DS.ActiveModelSerializer.extend({
attrs: {
"users" : "results",
}
});
this should do it for you, dont change attributes, just these two methods for extracting single models and arrays of models. You take the payload.results property and process it further, instead of the default payload.
extractArray: function(store, type, payload) {
return Array.prototype.map.call(payload.results, function(hash) {
return this.normalize(type, hash, payload.type);
}, this);
},
extractSingle: function(store, type, payload, recordId) {
return this.normalize(type, payload.results, payload.type);
}

Ember: create a DS.belongsTo *outside* of a Model

I'm wanting to ideally create a DS.belongsTo / BelongsToRelationship in my own class (which is an Ember.Object, but not a DS.Model), or alternatively recreate the functionality to let me hold a reference to a record in my own class. I don't know if it's possible to use a DS.belongsTo outside of a DS.Model, or if so, how to set it up.
Background:
I have an ember-cli app using ember-data + ember-fire + firebase. One of my models has an attribute which is an object holding "type specific" information for the record. I transform this object into my own class based on the type it describes, and some times that type will have references to other records in the database. In these cases I would like to have a DS.belongsTo property set up in my typeSpecific class that I could link to in the same way as linking to a relationship in a regular model.
Alternative:
After a lot of searching and not finding any information on how to do this I made my own class which got me most of the way there. I've just noticed that although I can change the record it references on the client side and have it update, if I change it on the server-side I don't get updates coming through, so it's back to the drawing board.
If anyone is able to tell me how to make this alternative approach work that would serve the purpose too. The idea with this class is that I pass it a model name and id, and it should create the model reference and then keep model and id in sync if either side changes, and also pass through updates if anything on the model it's connected to gets changed just like a regular relationship would.
export default Ember.Object.extend({
id: null,
table: undefined,
model: undefined,
store: undefined,
init: function() {
this._super();
if(this.id && !this.model) {
this.updateModel();
}
else if(this.model && !this.id) {
this.updateId();
}
},
updateModel: function() {
var self = this;
if( this.get('id') ) {
this.store.find(this.get('table'), this.get('id')).then( function(model) {
self.set('model', model);
});
}
else {
self.set('model', undefined);
}
}.observes('id','table'),
updateId: function() {
if(this.get('model')) {
this.set('id', this.get('model.id'));
}
else {
this.set('id', null);
}
}.observes('model'),
});
Edit: code to manipulate the object above:
//Creating a reference:
this.set('target', ModelPointer.create({store:this.get('store'), table:this.get('targetTable'), id:targetId}));
//or:
this.set('target', ModelPointer.create({store:store, table:'myTable'}));
...
this.set('target.id', '42');
I believe that at the moment if I change either the id or model on the client the other will update automatically, eg:
//either:
this.set('target.id', '43');
//or:
this.store.find('myTable','43').then( function(newModel) {
self.set('target.model', newModel);
});
The problem is that if I log into Firebase and change myTable['42'].name='Fred' then the value showing on my web page which is linked to target.model.name doesn't update to 'Fred'. I suspect that if I set target.model.name to 'Fred' on the client side and save it wouldn't update the value on the server either(?)
The cleanest solution I've come up is to not store the id separately (left to the model itself). I've verified that changes I make in Firebase propagate to the displayed entry just fine.
With this solution setting the referenced model can be done with either its id or simply the model instance itself. See the controller code for examples.
Firstly, for reference, some test data for Firebase:
{
"testModels": {
"1": {
"name": "Model one"
},
"2": {
"name": "The second model"
},
"3": {
"name": "Third is the charm"
}
}
}
Thus its model app/models/test-model.js just needs the name in there.
Here is my belongsTo-like proxy class, I put mine under app/utils/proxy-class.js but it should probably be a Mixin:
import Ember from 'ember';
export default Ember.Object.extend({
remote: null, // reference to the remote DS.Model
store: null, // reference to the actual store
storeModel: null, // name of the model in the store
_watchRemote: function() {
var self = this;
if ( typeof self.get('remote') === 'object' ) {
// do nothing, already an object
if ( ! Ember.isPresent( self.get('store') ) ) {
// but set the store from the model
self.set( 'store', self.get('remote.store') );
}
} else if ( typeof self.get('remote') === 'string' ||
typeof self.get('remote') === 'number' ) {
// it's an id, so fetch the model
self._fetchModel( self.get('remote') );
}
}.observes('remote').on('init'), // on change, and during object init
_fetchModel: function( id ) {
var self = this;
self.store.find( self.get('storeModel'), id ).then(
function( model ) {
self.set( 'remote', model );
}, function ( err ) {
console.error( "couldn't read from the store:", err );
});
},
});
I created this controller, and used the browser console to change the model on the fly to test that model changes are picked up:
import Ember from 'ember';
import proxyClass from '../utils/proxy-class';
export default Ember.Controller.extend({
model: {
remoteFromId: null,
remoteFromModel: null,
},
init: function() {
var self = this;
self.set( 'model.remoteFromId',
proxyClass.create({
remote: 1,
store: self.get('store'),
storeModel: 'test-model',
})
);
self.get('store').find( 'test-model', 2 )
.then( function( model ) {
self.set( 'model.remoteFromModel',
proxyClass.create({
remote: model,
storeModel: 'test-model',
// no store provided here: set from the model
})
);
});
}
});
And the template for the controller:
<p>remoteFromId: {{model.remoteFromId.remote.id}}
{{model.remoteFromId.remote.name}}</p>
<p>remoteFromModel: {{model.remoteFromModel.remote.id}}
{{model.remoteFromModel.remote.name}}</p>