I have a fairly specific problem that I was hoping one of you really intelligent folk might know a solution for (or even a workaround at this stage)
Specifically, I'm dealing with action cables, nested JSON, and the JSONAPI.
I have an asset model, which has some attributes like name, desc etc. but it also has an attribute called state which is a complex nested JSON object.
// app/models/asset.js
export default DS.Model.extend({
// Attributes
name: DS.attr('string'),
desc: DS.attr('string'),
state: DS.attr(),
lastSeen: DS.attr('date'),
});
When anything on the asset changes in the backend, it is pushed down the cable to Ember, where it does a pushPayload(data), the payload looks like this;
{
"data": {
"id": "5",
"type": "assets",
"attributes": {
"asset_id": "962ABC",
"name": "962 ABC",
"desc": "Test Vehicle",
"activation_status": "active",
"state": {
"avl": {
"longitude": 152.9475426,
"reported_at": "2017-06-22T21:59:52Z"
},
"dfm": {
"in_alarm": false,
"reported_at": "2017-06-21T05:46:57Z",
"sensitivity": "normal",
"voice_prompt": false,
"driver_detected": true,
},
"tpms": {
"system_desc": "123ABC",
"system_type": "123_abc"
}
},
"last_seen": "2017-06-22T21:59:54.000Z"
},
"relationships": {
"company": {
"data": {
"id": "1",
"type": "companies"
}
},
"events": {
"links": {
"related": "/events/?asset_id=5"
}
},
"messages": {
"links": {
"related": "/messages/?asset_id=5"
}
}
}
}
}
This all works fine and dandy, updates to the asset & state are displayed as they happen thanks to the cable, and state is read only so I don't have to worry about saving anything. HOWEVER, I have noticed that when any single attribute on state changes in the backend, the entire asset is pushed down from the backend (this should be fine), and then this fires the observer for state and also all observers for state descendants - whereas I need it to only fire the observer for the state attribute that changed.
I have tried a number of things and each seemed to either not work at all, or still continue to update state in a way that fired off all state observers.
What I have tried;
ember-model-data-fragments (while it should work, I think the way that the action cable pushes the data must subvert this?)
embedded records (requires an ID for state, not currently compatible with JSONAPI)
raw json transform (making the json into ember objects, didn't seem to help)
Can anyone suggest a strategy or solution for me to try? I've spent nearly 2 days on this problem.. I would even settle for just splitting it up between avl/tpms/dfm, as long as when an attribute in one of those sections is changed, it doesn't notify properties from the other 2 sections.
Thanks
Related
I am trying to use the JSON API Adapter with ember-cli 2.5.1 , but I'm having a bit of trouble.
I have a todo-list.js model, which has a "hasMany" relationship to todo-list-item.js. Getting the todo-list, the server returns this:
{
"links": {
"self": "http://localhost:4200/service/v1/todolists/b-tlst-af69786c-cbaf-4df9-a4a3-d8232677006a"
},
"data": {
"type": "todo-list",
"id": "b-tlst-af69786c-cbaf-4df9-a4a3-d8232677006a",
"attributes": {
"name": "b1-TodoList",
"created-on": 1468474962458,
"modified-on": 1468474962458
},
"relationships": {
"todolistitems": {
"data": {
"type": "todo-list-item",
"id": "b-todo-b5e3c146-d93a-4f97-8540-875bbcd156ca"
}
}
}
}
}
If there had been two TodoListItem children instead of one, the value of that "data" key would have been an array, rather than an object.
After receiving this, I was expecting the Ember Chrome plug-in's "Data" tab to show 1 TodoList and 1 child TodoListItem. Instead, it shows 1 TodoList and 0 TodoListItems.
I note from the Network tab that the browser never makes a request to get the items listed in the "data" section of the response.
Is the relationships section above correct and sufficient?
It turns out to have been caused by promise misunderstandings on the client side, and additionally, on the server I had to put dashes in the "relationships" key (i.e. "todo-list-items") and make the value of "data" an array.
I am new to Ember and I will like to implement a server side search endpoint that returns results for several models/tables in DB.
Trying to figure out what is the best practice to do this in Ember.
Server results can contain several types of objects/models, each model is already defined in ember-data.
So far I am able to retrieve the results (I can see the results in ember-inspector) but I am not able to render it properly (I only have access to first object for some reason).
Current architecture:
rails-5 server with JSONAPI response
ember 2.3 with multiple models
Flow:
POST /search with body:
{"search":{"query": "xxx","size": "10"}}
RESULT:
{
"data": [
{
"id": "111111",
"type": "foo",
"attributes": {
"x": "y"
}
},
{
"id": "222222",
"type": "bar",
"attributes": {
"z": "y"
}
}
]
}
I'm working on a web service API using the HATEOAS REST representation.
My client can create an item (e.g. a stub of a blogpost):
POST /item
204 Created
Content-Type: application/vnd.foo.item+json
{
"id": 42,
"title": "Lorem Ipsum",
"status": "STUB",
"body": "Very long text."
"_links": {
"self": {
"href": "/item/42"
},
"activate": {
"href": "/item/42/activate"
},
}
}
After that the client can activate the item following the activate link (e.g. go live with the post). So it makes another call to the API:
POST /item/42/activate
200 Ok
Content-Type: application/vnd.foo.item+json
{
"id": 42,
"title": "Lorem Ipsum",
"status": "ACTIVE",
"body": "Very long text."
"_links": {
"self": {
"href": "/item/42"
},
"permalink": {
"href": "/item/42/permalink"
}
}
}
Up to here it is fine. But the problem is that I'm looking for a way to tell the client a suggestion about the next action to do (it's backend business logic).
In my case could be:
Bring the user to post page following the permalink
Bring the user to a shop cart to buy post extra features (visibility, more images, homepage positions and so on...)
Tell the user that the post is pending content review
I don't have an idea on how I could encapsulate this information in HATEOAS.
I was thinking to something like:
POST /item/42/activate
200 Ok
Content-Type: application/json
{
"suggested-action": "check-censure-panel",
"censure-reason": "censored (gambling)",
"_embedded": {
"foo.item": {
"id": 42,
"title": "Lorem Ipsum",
"status": "ACTIVE",
"body": "Very long text."
"_links": {
"self": {
"href": "/item/42"
},
"permalink": {
"href": "/item/42/permalink"
}
}
}
}
But the problem is that every suggested action is heterogeneous for extra attributes, another example may be:
"suggested-action": "go-to-checkout",
"product-order": 424242100,
They don't have a common interface, so I can't make a a vnd.foo.suggestedAction+json type.
What is the best way to design this response?
The next action is a state transition, and you seem to be using HAL so any state transitions should be presented as HAL.
Clients of your app need to react to what state transitions your app provides. So one very simple thing you could do is send a Location header to the next resource the app should present. You could even 302 redirect them there instead of 200'ing them with the updated resource.
You could provide the next action as a link...and not necessarily a HAL link. You could do it as a Link header (https://www.rfc-editor.org/rfc/rfc5988) but i think that would be weird, i just bring it up to knock home the point that your app needs to tell your client about a link.
You seem to want to use custom media types, but you could use profile links (https://www.rfc-editor.org/rfc/rfc6906) and mix in a profile into your vnd.foo type. You can stick to your vnd.foo type and just have it defined that there is an optional suggested-action link relationship. The problem in your example is you're defining it with data fields, but use a link:
{
"id": 42,
"title": "Lorem Ipsum",
"status": "ACTIVE",
"body": "Very long text."
"_links": {
"self": {
"href": "/item/42"
},
"permalink": {
"href": "/item/42/permalink"
},
"x:suggested-action" : {
"href" : "/path/to/best/action"
}
}
the client can follow that link, present the user with an option to follow that link, or ignore it. In the middle case, it's nice if your app provides some context to the user, like a title field:
"x:suggested-action" : {
"href" : "http://path/to/check/censure/panel",
"title" : "Check Censure Panel"
}
Also you can give a hint as to the resource the app can expect:
"x:suggested-action" : {
"href" : "http://path/to/check/censure/panel",
"title" : "Check Censure Panel",
"type" : "vnd.censure.panel/json"
}
I personally don't like doing that as i like my client to react to whatever i send them, but it's useful when you give multiple suggested actions:
"x:suggested-action" : [
{
"href" : "http://path/to/check/censure/panel",
"title" : "Check Censure Panel",
"type" : "vnd.censure.panel/json"
},
{
"href" : "http://path/to/checkout",
"title" : "Start Checkout",
"type" : "vnd.checkout/json"
}
]
now the app can decide based on well defined media types which of the suggested actions it wants to do, present, or ignore.
I may be making a Promise faux pas but after authenticating a user I want to load the user's profile into the App.Session singleton that I've created:
App.Session.set(
'userProfile',
self.get('store').find('user',App.Session.get('userId'))
);
This results in the API call being made and a valid resultset being returned but for some reason in the debugger I get an empty result. Specifically, I do see the User.id but the rest of the columns are blank.
From the debugger, here's the JSON response:
{
"user": {
"id": "1",
"username": "jsmith",
"name": {
"first_name": "Joe",
"last_name": "Smith"
},
"emails": [
{
"id": "52153c0330063",
"name": "work-1",
"type": "other",
"address": "new#notreally.com",
"comments": "",
"status": "active",
"is_primary": false
},
{
"id": "52153d1b90ad0",
"name": "work-2",
"type": "other",
"address": "old#yesreally.com",
"comments": "",
"status": "active",
"is_primary": true
},
]
}
I'm a little new to Promises and so I thought maybe if I changed the code to:
self.get('store').find('user',App.Session.get('userId')).then( function(profile){
App.Session.set('userProfile', profile);
});
I felt pretty good about my new Promise acumen as I wrote this new code. Sadly my proud moment was greeted with failure. My second code snippet behaves precisely the same as the first one. Huh?
Can anyone help?
--------- ** UPDATE ** ---------
I've now including the model definition for User and a picture of the debugger window I made reference to.
User Model
App.RawTransform = DS.Transform.extend({
deserialize: function(serialized) {
return serialized;
},
serialize: function(deserialized) {
return deserialized;
}
});
App.NameTransform = DS.Transform.extend({
deserialize: function(serialized) {
return App.Name.create(serialized);
},
serialize: function(deserialized) {
return JSON.stringify(deserialized);
}
});
App.User = DS.Model.extend({
username: DS.attr("string"),
name: DS.attr("name"),
roles: DS.attr("raw"),
goals: DS.attr("raw"),
places: DS.attr("raw"),
emails: DS.attr("raw"),
networks: DS.attr("raw"),
relationships: DS.attr("raw"),
services: DS.attr("raw"),
uom: DS.attr("raw"),
});
Debug Window
Prior to login the model viewer looks like this:
Then after login it looks like this:
And then looking at the record details we see:
Ok, the answer seems to be down to two things. First of all, the second code snippet for handling the promise that I tried:
self.get('store').find('user',App.Session.get('userId')).then( function(profile){
App.Session.set('userProfile', profile);
});
is the correct way to go. The first method just leaves you with a "broken promise" in a "broken heart" sort of way not technically speaking but the point is it doesn't work.
The reason that my second promise implementation didn't work though was down to the Model indirectly and very specifically down to the deserializer I had put in place for Names.
I was scratching my head on this for second as the deserializer had worked back in the Ember-Data v0.1x world so I did what seemed appropriate ... I blamed Ember-Data. Come on, we've all done it. The fact is that Ember-Data had nothing to do with it and once I was willing to accept the blame I realised that it was simply a matter of not having moved my Name object over to the project I'm currently working on. Doh!
We are using Ember V1.0.0-pre.2 and our handlebars is as follows:
{{#each data.Product}}
<div>
{{Details.uid}} - {{Details.Name}}
</div>
{{/each}}
Our the 'data' bit is from this json:
{
"Product": [
{
"Details": {
"uid": "1",
"Name": "one"
}
},
{
"Details": {
"uid": "2",
"Name": "two"
}
},
{
"Details": {
"uid": "3",
"Name": "three"
}
},
{
"Details": {
"uid": "4",
"Name": "four"
}
},
{
"Details": {
"uid": "5",
"Name": "five"
}
}
]
}
This fails with the following warning:
WARNING: Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos
When I change Details.whatever to details.whatever the warning disappears.
Is this by design or can we get around it somehow? The data is returned from the server in a fixed format and we wouldn't want to use another interim model if we can avoid it.
Ember has a naming policy where "instances/attributes" always start with a lowercase letter whilst "classes" always start with an uppercase letter. I think that's probably where you are running into some issues, if possible you should be de-serialising your JSON into attributes starting with lowercase letters.
Relevant part pulled from the guides (http://emberjs.com/guides/object-model/classes-and-instances/):
By convention, properties or variables that hold classes are capitalized, while instances are not. So, for example, the variable Person would contain a class, while person would contain an instance (usually of the Person class). You should stick to these naming conventions in your Ember applications.
Naming convention apply also to model data, but if you can't change what is coming from your API you can get around this by defining a map on a per property basis, e.g.
App.Adapter.map('App.Product', {
details: {key: 'Details'},
name: {key: 'Name'},
fooBar: {key: 'FOO_BaR'}
...
});
see here for more reference on how to map your json to your models: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/mixins/mappable.js
hope it helps