Ember ActiveModelAdapter customization - ember.js

My Ember application interacts with an API (through a DS.ActiveModelAdapter adapter) which respond to GET "/api/v1/users?username=mcclure.rocio" with a JSON like:
{
"user": {
"id": 5,
"name": "Rocio McClure",
"username": "mcclure.rocio",
"email": "rocio.mcclure#yahoo.com"
}
}
My router is:
Router.map(function() {
this.route("login");
this.route("user", {path: "user/:username"}, function() {
this.route("profile");
});
});
So I have route like http://localhost:4200/user/mcclure.rocio which is kind of summary of a user.
The problem is loading the correct model in the route:
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params) {
return this.store.find('user', { username: params.username })
}
});
My Ember inspector states that the loaded model is an empty DS.AdapterPopulatedRecordArray. That's because findQuery (which is actually called as I provide a query object) expect to fetch a JSON array while my API return a single user JSON object, so it translates it to an empty array.
However this.store.find('user', { username: params.username }) build the right request to my API but how can I make the Store accept the API response and serve it as model to my route?
note:
If my API returned an array a could do something like this:
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params) {
return this.store.find('user', { username: params.username }).then(function(data){
return data.objectAtContent(0);
});
}
});
but, I prefer not to modify it.

You should make use of the normalizePayload function on DS.RestSerializer to modify the response to the format Ember Data expects.

Related

Recommended pattern to implement JSONAPI filter with query params in Ember?

I spent a chunk of time yesterday trying to include filter (reflecting the JSONAPI spec) in the query params of part of an Ember app. With Ember Data it is easy enough to pass a filter array to an endpoint, the problem I have is reflecting that filter array in the query params for a particular route. Note: other, non array, query params are working fine.
TL;DR I have tried various options without success and have a solution that really feels unsatisfactory and not at all DRY. I figure that many others must have tackled this problem and have surely found a better solution. Read on for details of what I have tried so far.
I started with something like this (I initially assumed it would work having read the Ember docs on query params):
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', 'filter'],
sort: 'id',
init() {
this._super(...arguments);
this.set('filter', []);
},
});
Route:
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
filter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
console.log(JSON.stringify(params)); // filter is always []
return this.get('store').query('contact', params);
}
});
Acceptance Test (this was just a proof of concept test before I started on the more complex stuff):
test('visiting /contacts with query params', async function(assert) {
assert.expect(1);
let done = assert.async();
server.createList('contact', 10);
server.get('/contacts', (schema, request) => {
let params = request.queryParams;
assert.deepEqual(
params,
{
sort: '-id',
"filter[firstname]": 'wibble'
},
'Query parameters are passed in as expected'
);
done();
return schema.contacts.all();
});
await visit('/contacts?filter[firstname]=wibble&sort=-id');
});
No matter how I tweaked the above code, params.filter was always [] in the Route model function.
I have searched around for best-practice on what would seem to be a common use case, but have not found anything recent. sarus' solution here from Nov 2015 works, but means that every possible filter key has to be hardcoded in the controller and route, which seems far from ideal to me. Just imagine doing that for 20 possible filter keys! Using sarus' solution, here is code that works for the above acceptance test but as I say imagine having to hardcode 20+ potential filter keys:
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort',
{ firstnameFilter: 'filter[firstname]' }
],
sort: 'id',
firstnameFilter: null,
init() {
this._super(...arguments);
}
});
Route:
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
firstnameFilter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
if (params.firstnameFilter) {
params.filter = {};
params.filter['firstname'] = params.firstnameFilter;
delete params.firstnameFilter;
}
return this.get('store').query('contact', params);
}
});
I hope there's a better way!
If you don't have the requirement to support dynamic filter fields, #jelhan has provided a really good answer to this question already.
If you do need to support dynamic filter fields read on.
First of all, credit should go to #jelhan who put me on the right track by mentioning the possibility of serializing the application URL with JSON.stringify() and encodeURIComponent() together.
Here's example code with this working...
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', {
filter: {
type: 'array'
}
}],
sort: 'id',
init() {
this._super(...arguments);
this.set('filter', []);
},
});
Route (no changes required):
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
filter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
return this.get('store').query('contact', params);
}
});
Acceptance Test:
test('visiting /contacts with query params', async function(assert) {
assert.expect(1);
let done = assert.async();
server.createList('contact', 10);
server.get('/contacts', (schema, request) => {
let params = request.queryParams;
assert.deepEqual(
params,
{
sort: '-id',
"filter[firstname]": 'wibble',
"filter[lastname]": 'wobble'
},
'Query parameters are passed in as expected'
);
done();
return schema.contacts.all();
});
// The filter is represented by a Javascript object
let filter = {"firstname":"wibble", "lastname":"wobble"};
// The object is converted to a JSON string and then URI encoded and added to the application URL
await visit('/contacts?sort=-id&filter=' + encodeURIComponent(JSON.stringify(filter)));
});
Great! This test passes. The filter defined in the application URL is passed through to the Route. The Route's model hook makes a JSONAPI request with the filter correctly defined. Yay!
As you can see, there's nothing clever there. All we need to do is set the filter in the correct format in the application URL and the standard Ember Query Params setup will just work with dynamic filter fields.
But how can I update the filter query param via an action or link and see that reflected in the application URL and also make the correct JSONAPI request via the Route model hook. Turns out that's easy too:
Example Action (in controller):
changeFilter() {
let filter = {
firstname: 'Robert',
lastname: 'Jones',
category: 'gnome'
};
// Simply update the query param `filter`.
// Note: although filter is defined as an array, it needs to be set
// as a Javascript object to work
// this.set('filter', filter); - this seems to work but I guess I should use transitionToRoute
this.transitionToRoute('contacts', {queryParams: {filter: filter}});
}
For a link (say you want to apply a special filter), you'll need a controller property to hold the filter, we'll call it otherFilter and can then reference that in the link-to:
Example Controller property (defined in init):
init() {
this._super(...arguments);
this.set('filter', []);
this.set('otherFilter', {occupation:'Baker', category: 'elf'});
}
Example link-to:
{{#link-to 'contacts' (query-params filter=otherFilter)}}Change the filters{{/link-to}}
There you have it!
There is no reason to represent filter values in applications URL the same way as they must be for backend call to be JSON API complaint. Therefore I would not use that format for application URLs.
If you don't have the requirement to support dynamic filter fields, I would hard code all of them to have nice URLs like /contacts?firstname=wibble&sort=-id.
Your code would look like this, if you like to support filtering for firstname and lastname:
// Controller
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', 'page', 'firstname', 'lastname'],
sort: 'id',
});
// Route
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
firstname: {
refreshModel: true
},
lastname: {
refreshModel: true
}
sort: {
refreshModel: true
}
},
model({ firstname, lastname, sort, page }) {
console.log(JSON.stringify(params)); // filter is always []
return this.get('store').query('contact', {
filter: {
firstname,
lastname
},
sort,
page
});
}
});
If you have to support dynamic filter fields, I would represent the filter object in application URL. For serialization you could use JSON.stringify() and encodeURIComponent() together. The URL would then look like /contacts?filter=%7B%22firstname%22%3A%22wibble%22%7D&sort=-id.

A model id come from another model, how can I set in route by ember

My ember version:
DEBUG: -------------------------------
Ember : 2.10.2
Ember Data : 2.11.0
jQuery : 2.2.4
Ember Simple Auth : 1.1.0
Model Fragments : 2.3.2
DEBUG: -------------------------------
And my route code:
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import RSVP from 'rsvp';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model() {
console.log(1);
return RSVP.hash({
...,
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
});
},
afterModel(model, transition) {
return this.store.findRecord('company', model.user.get('companyId')).then(company => {
console.log(2);
this.set('company', company);
});
},
setupController(controller, model) {
console.log(3);
controller.set('user', model.user);
controller.set('company', this.get('company'));
}
});
Look at console.log code, I think the correct order should be 1->2->3. But sometimes it turns out to be 1->3->2.
But my company id must come from user api. So what is way I set it in route? Thanks.
I am writing just another solution, From RSVP.hash api docs
Returns a promise that is fulfilled when all the given promises have been fulfilled, or rejected if any of them become rejected. The returned promise is fulfilled with a hash that has the same key names as the promises object argument. If any of the values in the object are not promises, they will simply be copied over to the fulfilled object.
So you can write your requirement like the below code,
model() {
var promises = {
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
};
return Ember.RSVP.hash(promises).then(hash => {
//then method will be called once all given promises are fulfilled, or rejected if any of them become rejected.
return this.store.findRecord('company', hash.user.get('companyId')).then(company => {
hash.company = company; // Here i am setting company property inside model itself, so you dont need to set it in route and carry over to controller
return hash;
});
})
}
Note:I am curious to know if you can reproduce 1->3->2 behavior in ember-twiddle.
Actually the right way to do this is to put all your model fetching in your model hook:
model() {
return RSVP.hash({
...,
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
}).then(hash => {
hash.company = this.store.findRecord('company', hash.user.get('companyId'));
return RSVP.hash(hash);
})
},
setupController(controller, model) {
controller.set('user', model.user);
controller.set('company', model.company);
}

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!

Ember data single data extract through browser URL

Getting all articles is ok, but when try to retrieve just one article through route url directly, system error - undefined
var articles = Ember.$.getJSON('http://localhost/emberdata/api/articles');
will return:
[{"articles":
[
{
"id":1,
"title":"Ember is the best",
"author":"brk","excerpt":"Programming is awesome"
},
{
"id":2,
"title":"Backbone not a framework",
"author":"krb",
"excerpt":"Server-side controller sucks"
},
{
"id":3,
"title":"Javascript pwned",
"author":"rbk",
"excerpt":"I know right"
}
]
}]
API is created using PHP Slim Rest API
this Route working find, showing all the data in the handlebar template
App.ArticlesRoute = Ember.Route.extend({
model: function() {
return articles;
}
});
However for child view routing, undefined is returned
App.ArticleRoute = Ember.Route.extend({
model: function(params) {
var article = articles.findBy('id', params.article_id);
console.log(article);
return article;
}
});
Directly invoking the child view URL is not working:
http://localhost/emberdata/#/articles/1
However clicking the link of articles, those child view route works:
this.resource('article', { path: ':article_id' });
This is the error:
Ember.$.getJSON() will return a promise (see: http://emberjs.com/api/classes/Ember.PromiseProxyMixin.html). You can't call the findBy() method on a promise.
That being said, you're making it yourself very difficult. I recommend to start using the DS.RESTAdapter. In your case it would be something like this:
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'emberdata/api'
});
then clear (or remove) ArticlesRoute because you will use convention (instead of configuration):
App.ArticlesRoute = Ember.Route.extend({
});
idem for ArticleRoute. Except, if your backend doesn't support calls like /emberdata/api/article/1 use the following code:
App.ArticleRoute = Ember.Route.extend({
model: function(params) {
var article = this.store.find('article').findBy('id', params.article_id);
console.log(article);
return article;
}
});