Accessing controller properties within Ember Data REST Adapter - ember.js

I need to access a controller property to build custom URLs in my RESTAdapter instances, but I can't find a way to access the controller within the context of an adapter. Here's what I have:
I have a simple model that looks like this:
App.Customer = DS.Model.extend(
{
first_name: DS.attr('string'),
last_name: DS.attr('string'),
date_of_birth: DS.attr('string'),
created_at: DS.attr('string'),
updated_at: DS.attr('string')
});
The resource REST URL for this model looks something like this:
https://api.server.com/v1/accounts/:account_id/customers/:customer_id
I'm extending the RESTAdapter in Ember Data for most of my models so I can customize resource URLs individually. Like this:
App.CustomerAdapter = DS.RESTAdapter.extend(
{
buildURL: function(type, id)
{
// I need access to an account_id here:
return "new_url";
}
});
As you can see, in this example I need an account ID in the URL to be able to query a customer object. The account ID is something that the user would have had to supply by logging-in and is stored in an AccountController which is an instance of Ember.Controller.
My question is, how can I access a property from my AccountController within my CustomerAdapter? Here are the things I've tried, none have worked:
App.CustomerAdapter = DS.RESTAdapter.extend(
{
buildURL: function(type, id)
{
var account_id = this.controllerFor('account').get('activeAccount').get('id');
return "new_url";
}
});
,
App.CustomerAdapter = DS.RESTAdapter.extend(
{
needs: ['account'],
accountController: Ember.computed.alias("controllers.account"),
buildURL: function(type, id)
{
var account_id = this.get('accountController').get('activeAccount').get('id');
return "new_url";
}
});
,
App.CustomerAdapter = DS.RESTAdapter.extend(
{
activeAccountBinding = Ember.Binding.oneWay('App.AccountController.activeAccount');
buildURL: function(type, id)
{
var account_id = this.get('activeAccount').get('id');
return "new_url";
}
});
At this point, the only hack I can think of, is to put the account ID in a global variable outside of Ember and access it from there within the Adapter.
Other suggestions?

We have a similar problem, and essentially we do a global variable, and feel guilty about it. Ours is in Ember Model, but the same concept and problem exists. Another solution is to use findQuery, but this returns a collection, so then you have to pull the item out of the collection.
App.CustomerAdapter = DS.RESTAdapter.extend(
{
buildURL: function(type, id)
{
var params = type.params;
return "new_url" + params.account_id;
}
});
In Some Route:
App.BlahRoute = Em.Route.extend({
model: function(params){
App.Customer.params = {account_id:123};
this.get('store').find('customer', 3);
}
});

I know that you can access controller's properties in context of another controller.
I see that you tried somewhat similar, but anyway maybe this would work for adapter:
App.YourController = Ember.ObjectController.extend({
needs: ['theOtherController'],
someFunction: function () {
var con = this.get('controllers.theOtherController');
return con.get('propertyYouNeed');
},
});
Also, have you thought about adding AccountId property to your Customer model?
Maybe automatic URL can be achieved with proper routing?

Related

Ember: how to add params from router to adapter

I'm trying to fetch data from the following URL structure:
${ENV.APP.API_HOST}/api/v1/customers/:customer_orgCode/sites/
I'm using an adapter to shape the request with buildURL with the following files:
// router.js
this.route('sites', { path: 'customers/:customer_orgCode/sites' }, function() {
this.route('show', { path: ':site_id' });
});
// adapters/site.js
export default ApplicationAdapter.extend({
buildURL (modelName, id, snapshot, requestType, query) {
// return `${ENV.APP.API_HOST}/api/v1/customers/${snapshot???}/sites/`;
return `${ENV.APP.API_HOST}/api/v1/customers/239/sites/`;
}
}
// routes/sites/index.js
export default Ember.Route.extend({
model: function() {
let superQuery = this._super(...arguments),
org = superQuery.customer_orgCode;
this.store.findAll('site', org);
}
});
I'm able to get the customer_orgCode on the model, but unable to pull it into the adapter. I've noted that the model isn't being populated in the Ember inspector, but the sites data is present when I make the request. Does anyone know how I can dynamically populate the buildURL with the customer_orgCode from the params on the router? And then specify sites/index to use the 'site' model?
OK, I've figured this out. I needed to use query() instead of findAll() in the route. This allows buildURL to pickup the query parameter from the route and pass it in as the 5th argument ie. - buildURL(modelName, id, snapshot, requestType, query). Then in the route, I was neglecting return as part of my model setup. So the solution is below for anyone interested.
// router.js
this.route('sites', { path: 'customers/:customer_orgCode/sites' }, function() {
this.route('show', { path: ':site_id' });
});
// adapters/site.js
export default ApplicationAdapter.extend({
buildURL (modelName, id, snapshot, requestType, query) {
let org = query.org;
return `${ENV.APP.API_HOST}/api/v1/customers/${org}/sites/`;
}
});
// routes/sites/index.js
export default Ember.Route.extend({
model: function() {
let superQuery = this._super(...arguments),
org = superQuery.customer_orgCode;
return this.store.query('site', {org:org});
}
});

Retrieve count (total) from non-default REST API

I would like to query my server for this URL http://localhost:1337/api/posts/count?category=technology using Ember. I'm using the default RESTAdapter like this:
export default DS.RESTAdapter.extend({
coalesceFindRequests: true,
namespace: 'api',
)};
The Post model looks like this:
import DS from 'ember-data';
var Post = DS.Model.extend({
title: DS.attr('string'),
permalink: DS.attr('string'),
body: DS.attr('string')
});
export default Post;
How do I make such a request?
I think you have at least two ways to achieve that when you don't have a control over your backend. Otherwise, you can still use the RESTful API (see the last section of my answer).
Override buildURL
The first one is to use the existing RESTAdapter functionalities and only override the buildURL method:
var PostAdapter = DS.RESTAdapter.extend({
namespace: 'api',
buildURL: function(type, id, record) {
var originalUrl = this._super(type, id, record);
if (this._isCount(type, id, record)) { // _isCount is your custom method
return originalUrl + '/count';
}
return originalUrl;
}
});
where in the _isCount method you decide (according to record property for example) if it's what you want. And then, you can pass the params using the store:
this.store.find('post', {
category: technology
});
Override findQuery
The second way is to override the whole findQuery method. You can either use the aforementioned DS.RESTAdapter or use just DS.Adapter. The code would look as the following:
var PostAdapter = DS.Adapter.extend({
namespace: 'api',
findQuery: function(store, type, query) {
// url be built smarter, I left it for readability
var url = '/api/posts/count';
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.getJSON(url, query).then(function(data) {
Ember.run(null, resolve, data);
}, function(jqXHR) {
jqXHR.then = null;
Ember.run(null, reject, jqXHR);
});
});
},
});
and you use the store as in the previous example as well:
this.store.find('post', {
category: technology
});
Obtaining 'count' from meta
If you have a complete control on your backend, you can leverage the power of metadata.
The server response then would be:
// GET /api/posts/:id
{
"post": {
"id": "my-id",
"title": "My title",
"permalink": "My permalink",
"body": "My body"
},
"meta": {
"total": 100
}
}
and then you can obtain all the meta information from:
this.store.metadataFor("post");
Similarly, you can use this approach when getting all the posts from /api/posts.
I hope it helps!

Embedded data from RestApi

My data comes from REST API like this:
customers:[
id:3,
name:"Joue",
currency:{
id:5
iso_code:"BDT"
}
]
My model:
App.Customer = DS.Model.extend({
name: DS.attr('string'),
currency: DS.attr('string')
});
i populated a select box with the availabe currencies and now i want to select by "id" 5.
Since currency is embedded and its interpreted as string i cant access it.
As far as i know embedded records are no longer supported in ember-data 1.0.
do i have to rewrite my REST Api and get rid of the relationships or there is a workaround .
You can just create a custom serializer for the data.
Using your data (slightly modified, since the json isn't valid, and I'm guessing that's just cause it was hand written?)
{
customers:[
{
id:3,
name:"Joue",
currency:{
id:5,
iso_code:"BDT"
}
}
]
}
Here's a serializer for that particular response type (read more about it here https://github.com/emberjs/data/blob/master/TRANSITION.md)
App.CustomerSerializer = DS.RESTSerializer.extend({
extractArray: function(store, type, payload, id, requestType) {
var customers = payload.customers,
currencies = [];
customers.forEach(function(cust) {
var currency = cust.currency;
delete cust.currency;
if(currency){
currencies.push(currency);
cust.currency = currency.id;
}
});
payload = { customers:customers, currencies: currencies };
return this._super(store, type, payload, id, requestType);
}
});
And your models defined with a relationship
App.Customer = DS.Model.extend({
name: DS.attr('string'),
currency: DS.belongsTo('currency')
});
App.Currency = DS.Model.extend({
iso_code: DS.attr('string')
});
Example:
http://emberjs.jsbin.com/OxIDiVU/535/edit
currency is not "embedded", it's just an object. Don't declare it as a string in your model:
currency: DS.attr()
You say you want to "select" by id--what do you actually want to do? You can access the properties of currency directly:
{{! handlebars }}
Your currency id is {{currency.id}}.
// Javascript
id = model.get('currency.id');
No need for additional complexity involving serializers or additional models. However, you need to be careful when changing currency id, since
model.set('currency.id', 6)
will not dirty the model and it won't save. You'll need to also incant
model.notifyPropertyChange('currency')

Cache a record when using Query Params in the call? Ember-data

I have got this route retrieving 2 models:
App.PanelRoute = Ember.Route.extend({
model: function(){
var topologymin = this.store.find('topologymin');
var metricmap = this.store.find('metricmap', { param1: 'something'})
return Ember.RSVP.hash({
topologymin: topologymin,
metricmap: metricmap
});
});
This makes 2 calls:
http://localhost/topologymins
http://localhost/metricmaps?param1=something
If I go to another route and again to this one, it makes again the call with the params, not the other one:
http://localhost/metricmaps?param1=something
But, as its the same call to retrieve the same records I would like them to be cached like in the other call.
How does it know when to call the server and when its not necessary? Is it possible to do that?
My models:
App.Topologymin = DS.Model.extend({
siteGroup: DS.attr('string'),
sites: DS.hasMany('site')
});
App.Metricmap = DS.Model.extend({
profile: DS.attr('string'),
link: DS.attr('string'),
services: DS.attr()
});
When you fire a request based on params Ember Data doesn't know how those params necessarily translate into the models (aka it doesn't know that you have all of the records that have some sort of relationship param1). You can cache it yourself, but then you'd still need some sort of way of knowing those records from other records in your store.
App.PanelRoute = Ember.Route.extend({
model: function(){
var met = this.get('fetchedBeforePromise'),
topologymin = this.store.find('topologymin'),
metricmap = met || this.store.find('metricmap', { param1: 'something'});
this.set('fetchedBeforePromise', metricmap);
return Ember.RSVP.hash({
topologymin: topologymin,
metricmap: metricmap
});
});

Duplicate null-id records in ember-data

I'm using ember 1.0 and ember-data 1.0.0 beta 1. I have the following routes and controller to create and save simple notes ('AuthenticatedRoute' is just a custom made route for logged-in users):
App.Note = DS.Model.extend({
title: DS.attr(),
author: DS.attr(),
body: DS.attr(),
createdAt: DS.attr()
});
App.NotesRoute = App.AuthenticatedRoute.extend({
model: function() { return this.store.find('note'); },
});
App.NotesNewRoute = App.AuthenticatedRoute.extend({
model: function() {
return this.store.createRecord('note');
}
});
App.NotesNewController = Ember.ObjectController.extend({
actions: {
save: function() {
var self = this, model = this.get('model');
model.set('author', localStorage.username);
model.set('createdAt', new Date());
model.save().then(function() {
self.get('target.router').transitionTo('notes.index');
});
}
}
});
When I save a new note everything works as expected. But when I navigate away from the notes route and then back into it, the notes list is populated with a duplicate entry. One entry has an id and can be edited, deleted etc, the other has all the data of the first entry except the id attribute is null. It seems to me ember-data keeps the newly created record (that hasn't been committed to the database and thus has no id yet) alive even when the record becomes committed but I am uncertain as to why. When I reload the page, the list is correctly displayed, no duplicates appear. What am I doing wrong?
For the record, I am using mongodb so I use a custom serializer to convert '_id' attributes to ember-data friendly 'id's, essentially copied from here:
App.NoteSerializer = DS.RESTSerializer.extend({
normalize: function(type, hash, property) {
// 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, json, property);
}
});
I should also mention that this problem existed in ember-data 0.13 as well.
It was a stupid mistake in my RESTful server. I was responding to POST requests with a 204 (empty) response instead of what ember-data expected, that is a 201 ("created") response with the newly created record as the payload. This post made me realize it.
It would be nice though to include this information in the official REST adapter documentation.
That is certainly strange behaviour indeed. Unfortunately I'm not able to explain why you're experiencing this, however:
You can use the willTransition callback in the actions object in your Route to ensure that when it is transitioned away from, if NotesNewController's content property is dirty (i.e. has not been persisted yet), it will have its transaction rolled back.
App.NotesNewRoute = App.AuthenticatedRoute.extend({
model: function() {
return this.store.createRecord('note');
},
actions: {
willTransition: function (transition) {
var model = this.controllerFor('notesNew').get('content');
if (model.get('isDirty') === true) {
model.get('transaction').rollback();
}
return this._super(transition);
}
}
});