Ember Data nested Models - ember.js

I'm using EmberJs and Ember-Data in a Google App Engine project which uses NDB. In the database I have Host, Probe and Check entities. The database model doesn't really matter as long as I have my REST api in order but for clarity here are my database Classes:
class Host(ndb.Model):
hostName = ndb.StringProperty()
hostKey = ndb.Key('Host', 'SomeHostId')
class Probe(ndb.Model):
checkName = ndb.StringProperty()
probeKey = ndb.Key('Host', 'SomeHostId', 'Probe', 'SomeProbeId')
class Check(ndb.Model):
checkName = ndb.StringProperty()
checkKey = ndb.Key('Host', 'SomeHostId', 'Probe', 'SomeProbeId', 'Check', 'SomeCheckId')
I've added the keys in order to show that each host has some probes running on them and each probe performs some checks.
Host
Probe
Check
In my App.Js I have defined the following models:
App.Host = DS.Model.extend({
hostName: DS.attr('string')
probes: DS.hasMany('probe',{async:true})
});
App.Probe = DS.Model.extend({
host: DS.belongsTo('host'),
probeName: DS.attr('string')
checks: DS.hasMany('check',{async:true})
});
App.Check = DS.Model.extend({
probe: DS.belongsTo('probe'),
hostName: DS.attr('string')
});
I have defined the following router:
App.Router.map(function() {
this.resource('hosts', function(){
this.resource('host', { path:':host_id'}, function(){
this.resource('probes', function(){
this.resource('probe', { path:':probe_id'}, function(){
this.resource('checks', function(){
this.resource('check', { path:':check_id'}, function(){
});
});
});
});
});
});
});
And in AppEngine if have built the following URL paths:
app = webapp2.WSGIApplication([
('/', MainHandler),
webapp2.Route('/hosts', HostsHandler),
webapp2.Route('/hosts/<hostId>/', HostHandler),
webapp2.Route('/hosts/<hostId>/probes', ProbesHandler),
webapp2.Route('/hosts/<hostId>/probes/<probeId>/checks', ChecksHandler),
webapp2.Route('/hosts/<hostId>/probes/<probeId>/checks/<checkId>/', CheckHandler)
])
http://example.com/hosts returns:
{
"hosts": [
{
"hostName": "SomeHostName1",
"id": "SomeHostId1"
},
{
"hostName": "SomeHostName2",
"id": "SomeHostId2"
}
]
}
http://example.com/hosts/SomeHostId1/probes returns:
{
"probes": [
{
"probeName": "SomeProbeName1",
"id": "SomeProbeId1",
"host_id": "SomeHostId1"
},
{
"probeName": "SomeProbeName2",
"id": "SomeProbeId2",
"host_id": "SomeHostId1"
}
]
}
http://example.com/hosts/SomeHostId1/probes/SomeProbeId1/checks returns:
{
"checks": [
{
"checkName": "SomeCheckName1",
"id": "SomeCheckId1",
"probe_id": "SomeProbeId1"
},
{
"checkName": "SomeCheckName2",
"id": "SomeCheckId2",
"probe_id": "SomeProbeId1"
}
]
}
My templates are:
<script type="text/x-handlebars" id="host">
<h3>{{hostName}}</h3>
{{#link-to 'probes' probes}}probes{{/link-to}}
{{outlet}}
</script>
<script type="text/x-handlebars" id="probes">
{{#each probe in probes}}
Probe: {{probe.probeName}}
{{#link-to 'checks' probe.checks}}checks{{/link-to}}
{{/each}}
{{outlet}}
</script>
<script type="text/x-handlebars" id="checks">
{{#each check in checks}}
Check: {{check.checkName}}
{{/each}}
</script>
Now I have all this... but no clue how to tie it up together so that Ember-Data makes the right http requests. So far I've only seen request go to http://example.com/modelName/

Currently Ember Data does not support this type of nested routes for API endpoints. There's been some talk about this, but it doesn't seem to be making any forward progress.

I don't know anything about App engine, but if you could obtain a config like this, for ember-data rest adapter
app = webapp2.WSGIApplication([
('/', MainHandler),
webapp2.Route('/hosts', HostsHandler),
webapp2.Route('/hosts/<hostId>', HostHandler),
webapp2.Route('/probes', ProbesHandler),
webapp2.Route('/probes/<probeId>', ProbesHandler),
webapp2.Route('/checks/', CheckHandler)
webapp2.Route('/checks/<checkId>/', CheckHandler)
])
And the response to http://example.com/hosts should return a json array hosts:[{},{}] and to http://example.com/hosts/1 a json representing a host object host:{} and the same for the other AppEngine routes

You have defined the Host model twice, I think that shouldn't have been the case. I am pretty new to ember and haven't used async:true feature, but I have been able to do things like(but I hadn't used nested route):
App.Host = DS.Model.extend({
hostName: DS.attr('string')
probes: DS.hasMany('probe')
});
App.Probe = DS.Model.extend({
probeName: DS.attr('string')
checks: DS.hasMany('check')
});
App.Check = DS.Model.extend({
checkName: DS.attr('string')
});
and you can spin up a rest api for host that returns :
{
"hosts": [
{
"hostName": "SomeHostName1",
"id": "SomeHostId1",
"probes":["p1","p2"]
},
{
"hostName": "SomeHostName2",
"id": "SomeHostId2",
"probes":["p2","p3"]
}
],
"probes": [
{
"probeName": "SomeProbeName1",
"id": "p1",
"checks":["c1","c2"]
},
{
"probeName": "SomeProbeName2",
"id": "p2",
"checks":["c2","c3"]
}
],
"checks": [
{
"checkName": "SomeCheckName1",
"id": "c1"
},
{
"checkName": "SomeCheckName2",
"id": "c2"
}
]
}
In my case I didn't have nested route but I think we should be able to set the controller content from the master payload somehow since all the required content are in store already! I don't know if it was of any help, but this is something I would also like to know the answer of.

Related

Ember model relationship not matching API

I have two models who look like this:
// models/database.js
export default DS.Model.extend({
title: DS.attr(),
/*...*/
documents: DS.hasMany('document')
});
// models/document.js
export default DS.model.extend({
name: DS.attr(),
/*...*/
database: DS.belongsTo('database')
});
Is also have some routes
Router.map(function() {
this.route('databases', function() {
this.route('database', { path: ":database_id" }, function() {
this.route('documents');
});
});
});
When I call /database/[ID]/documents if got nothing.
I think the problem is I'm working with an JSON HAL API and when I ask for databases on API I receive this kind of response.
{
"_embedded": {
"databases": [
{
"_links": {
"self": {
"href": "<API_URL>/databases/7ec39267b544424c/",
"reference": "7ec39267b544424c",
"title": "00000006"
}
},
"database": {
"name": "00000006",
"reference": "7ec39267b544424c",
"title": "****"
}
},
{
"_links": {
"self": {
"href": "<API_URL>/databases/aace19af004144a4/",
"reference": "aace19af004144a4",
"title": "00000007"
}
},
"database": {
"name": "00000007",
"reference": "aace19af004144a4",
"title": "******"
}
}
}]
}
As you can see there no documents attributes on JSON returned.
If I want a database's documents I've got to call [API_URL]/databases/[ID]/domains/[ID]/documents
There's still a way to link documents to a database by specifying that the documents are recoverable at this URL (/databases/[ID]/domains/[ID]/Documents)?
I found the solution.
I've override extractRelationships method in hal serializer like this:
extractRelationships(modelClass, resourceHash, included) {
resourceHash._links["documents"] = {
"href" : "/databases/" + resourceHash.id + "/domains/" + (resourceHash.name || resourceHash[modelClass.modelName].name) + "/documents/"
};
return this._super(modelClass, resourceHash, included);
}
I just add a link to documents for each databases returned by the API.
I've modified my route for documents like this
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model() {
return this.modelFor('databases.database').get('documents');
}
});
And now it works perfectly.
I think what you're looking for is a HAL API to JSON API converter. To avoid doing this yourself manually I'd recommend the http://github.com/201-created/ember-data-hal-9000 Ember plugin, which I've used myself to get Ember Data to play nice with Spring Boot.
If you wanted to see a simple example here's my client-side and server-side demo app.
Additionally, if you were curious what Ember, and JSON API don't use HAL you can read here JSON API - FAQ on HAL.

Not able to show errors with Ember 2

In an application built with Ember 2.1.0, I followed this guide to implement server side validation.
I have this route :
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('user', params.id);
},
actions: {
submit: function() {
this.controller.model.save().then(function() {
this.transitionTo('admin.users');
}, (data) => {
console.log(data.errors);
this.set('errors', data.errors);
});
},
cancel: function() {
this.controller.model.rollbackAttributes();
this.transitionTo('admin.users');
}
}
});
And I have this code in the template :
{{#if errors.length}}
<div class='alert alert-danger'>
<h4>There was a problem</h4>
<p>The user could not be saved due to validation errors</p>
</div>
{{/if}}
The server returns {"errors":[{"email":["can't be blank"]}]} and errors are displayed in the console but nothing on the page.
According to the documentation, I tried to return {"errors":{"email":["can't be blank"]}} but I have "InvalidError expects json-api formatted errors array."
How can I show errors with Ember 2?
I finally found the solution. The data sent by the server was not using the json API format as written in the doc like this :
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "422",
"source": { "pointer": "/data/attributes/first-name" },
"title": "Invalid Attribute",
"detail": "First name must contain at least three characters."
}
]
}
I wrote this code, in ruby, to format it correctly :
class ErrorSerializer
def self.serialize(errors)
result = errors.messages.map do |message|
message.last.map do |detail|
{
"source": { "pointer": "/data/attributes/#{message.first}" },
"detail": detail
}
end
end.flatten
{ errors: result }
end
end
I can be used like this :
render status: :unprocessable_entity, json: ErrorSerializer.serialize(user.errors)

Ember save() throws JSON.parse error: "unexpected end of data at line 1 column 1 of the JSON data"

I have a model record created and being saved through a route and controller. When I save the record through the controller (via a savePlace action), I am seeing this error in the JS console:
SyntaxError: JSON.parse: unexpected end of data at line 1 column 1 of the JSON data
I've tried not setting anything on the model as well as setting dummy data on the model, but I get the same error. I am also user ember-cli http-mocks as a test backend to handle JSON responses. I realize it may be the response, but I'm not sure how else to configure the response.
Here's the relevant code:
routes/places/new.js:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.createRecord('place');
},
});
controllers/places/new.js:
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
saveGeom(geom) {
this.get('model').set('geometry', geom);
},
savePlace(data) {
this.get('model').set('name', this.get('name')).set('description', this.get('description'));
this.get('model').save().then(function() {
alert("SUCCESS");
}, function(error) {
console.log(error);
});
}
}
});
server/mocks/place.js:
placeRouter.post('/places', function(req, res) {
res.setHeader('Access-Control-Allow-Methods', 'POST');
res.send({
"places": {
id: 1,
name: "Triangle",
description: "Ryan Christiani",
geometry: {
"type": "Polygon",
"coordinates": [
[
[-84.32281494140625,34.9895035675793],
[-81.73690795898438,36.41354670392876],
[-83.616943359375, 34.99850370014629],
[-84.05639648437499,34.985003130171066],
[-84.22119140625, 34.985003130171066],
[-84.32281494140625,34.9895035675793]
]
]
}
}
});
});
Thanks!
I think you are using the wrong brackets in the wrong places in your JSON Object.
Check out this page
http://www.tutorialspoint.com/json/json_syntax.htm
The http-mocks configuration is wrong. It should be this the code snippet below. The server was instead responded with an array of objects (the response for 'GET /'). Not sure why that would trigger a JSON.parse error, but this is the correct configuration.
placeRouter.post('/', function(req, res) {
res.setHeader('Access-Control-Allow-Methods', 'POST');
res.send({
'places': [
{
id: 1,
name: "Using Ember CLI to create a Fixture Adapter.",
description: "Ryan Christiani",
geometry: {
"type": "Polygon",
"coordinates": [
[
[-84.32281494140625,34.9895035675793],
[-81.73690795898438,36.41354670392876],
[-83.616943359375, 34.99850370014629],
[-84.05639648437499,34.985003130171066],
[-84.22119140625, 34.985003130171066],
[-84.32281494140625,34.9895035675793]
]
]
}
}]});
});

ember-data 1.0 relationships

I'm using a rest adapter with ember-data 1.0 and ember.js 1.0
given these models:
App.Customer = DS.Model.extend({
name: DS.attr('string'),
state: DS.belongsTo("State")
});
App.State = DS.Model.extend({
region: DS.attr('string'),
state: DS.attr('string'),
stateName: DS.attr('string'),
customers: DS.hasMany("Customer")
});
when I go to /#/states, I get this json response
{
"states": [
{
"region": "West",
"state": "AZ",
"stateName": "Arizona",
"id": "0x0000000000000324",
"customers": [
"0x00000000000001e5"
]
},
{
"region": "West",
"state": "CA",
"stateName": "California",
"id": "0x0000000000000325",
"customers": [
"0x00000000000001c0",
"0x00000000000001c4",
"0x00000000000001d4"
]
}
]
"customers" : [
{
}
]
}
Now, I have a couple of questions
1) What should I put in the Customers part ? A complete list of all the customers, or just a list of the customers that are specified in the state list ?
2) what data should I send back if I visit /#/customers ?
3) If I were to edit a customer. would I set it up so that the lookup/combo makes a separate request to the server ?
thanks for the help !
1) In your original JSON the customers array should only contain customers who are present in the state list.
2) This depends on your app. Generally you would want /customers to return all customers. Or maybe a paginated collection of customers. Or maybe only customers that the currently logged in user is allowed to see. Etc...
3) When you edit a customer Ember should already have the customer data loaded, so no additional lookup request should be required.

Ember-Data, embedded relation records and server JSON response

Working on an App with Ember RC6 and Ember-Data v0.13-54 along a custom server PHP API am running into some problems with Models relations (mainly many-to-many) and side loading.
The models in questions are:
App.Member = DS.Model.extend({
apiToken: DS.attr('string'),
apiTokenExpire: DS.attr('string'),
favourites: DS.hasMany('App.Presentation')
});
App.Presentation = DS.Model.extend(
{
title: DS.attr('string'),
description: DS.attr('string'),
date: DS.attr('date'),
category: DS.belongsTo('App.Category'),
tags: DS.hasMany('App.Tag'),
employees: DS.hasMany('App.Member'),
presentation: DS.belongsTo('App.File'),
presenterNotes: DS.belongsTo('App.File'),
cover: DS.belongsTo('App.Image')
});
To get the RESTAdapater to send the relation with the Member model I have:
App.APIRESTAdapter.map('App.Member', {
favourites: {embedded: 'always'}
});
When loading /members/1 the server returns:
{
"member": {
"api_token": "9fa236ad58726584d7b61bcf94b4dda37cbd3a24",
"api_token_expire": "1383832335",
"id": 1,
"favourite_ids": [ 3 ],
"group_ids": [ 2 ]
},
"presentations": [
{
"title": "That's a new one!",
"category_id": 2,
"id": 3,
"tag_ids": [ 1 ],
"employee_ids": [ 1 ]
}
]
}
But if I log get('member.favourites').mapProperty('id') I get an empty array and none of the favourites are actually added to the Member model.
If I disable the embedding of favourites on the RESTAdapter, all works fine. I am just wondering if there is something that I am missing or if there is some issues with how the JSON response is formatted?
EDIT:
Done some digging and seems that the issue comes from the fact that the relation names (favourites, employees) is different from the model names (Member, Presentation) which are used when sideloading data. Weird, since rev. 12 https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md models should be Sideload by Type.
Doing some tests I've added a new many-to-many relation for those 2 models:
App.Member gets presentations: DS.hasMany('App.Presentation')
App.Presentation get members: DS.hasMany('App.Member')
The JSON returned by the server is exactly the same, and when logging get('member.presentations') I now get the list of presentations as it should.
At this point this looks like a bug to me, but maybe am missing something? I've tried mappings on the RESTAdapater for favourites and employees but that didn't help... Maybe there is some other Adapter config that could help?
This isn't a sideloading issue but more a misunderstanding on my side about embedded data and what the configuration meant.
Since the Adapter was configured with:
App.APIRESTAdapter.map('App.Member', {
favourites: {embedded: 'always'}
});
The expected JSON response from the server is:
{
"member": {
"api_token": "b84fd204b37d3fa3cee8a2b2cac8bd4fd02635a1",
"api_token_expire": "1384027367",
"id": 1,
"favourites": [
{
"title": "Some kind of title",
"category_id": 1,
"id": 2,
"tag_ids": [ 1 , 2 ],
"employee_ids": [ 1 ]
}
]
}
}
So "favourite_ids": [ X, X, X ] should have been "favourites": [ HASH, HASH, HASH ] when records are flagged as embedded.