Retrieve count (total) from non-default REST API - ember.js

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!

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.

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});
}
});

Trying to use jsonp with ember-data, and unable to use Ember App's store.createRecord in custom adapter

I'm trying to use ember-data with jsonp by overridding DS.RESTAdapter's findAll (based on the answer to this question).
App.ApplicationStore = DS.Store.extend({});
App.Event = DS.Model.extend({
name: DS.attr('string')
});
App.EventAdapter = DS.RESTAdapter.extend({
findAll: function() {
var events = [];
$.ajax({
url: '...',
dataType: 'jsonp',
success: function(response) {
response.results.forEach(function(event) {
events.addObject(App.ApplicationStore.createRecord('event', event));
}, this);
}
});
return events;
}
});
App.EventsRoute = Ember.Route.extend({
model: function() {
return this.store.find('event');
}
});
I first tried using events.addObject(App.Event.create(event)), but ember returned an error: "You should not call create on a model. Instead, call store.createRecord with the attributes you would like to set".
The issue is, App.ApplicationStore.createRecord is undefined, so I'm stuck without a way to instantiate Events. Anyone know what's going on? If there's a completely different approach to getting jsonp to work with ember-data, that's fine too.
This parsing of the response seems more like a job for the RESTSerializer than the RESTAdapter(though you will still need the adapter if you need to set the dataType/url)
Not 100% sure, but it looks like your reponse is an array that doesn't have the correct key
as stated in the jsonapi.org documenation?
If this is the case, you'd want to create a serializer for events like this
App.EventsSerializer = DS.RESTSerializer.extend({
extractFindAll: function(store, type, rawPayload, id, requestType) {
this._super(store, type, { 'events': rawPayload }, id, requestType);
}
});
The above serializer will reformat the response to be ember-data readable(as per the above documentation), and ember-data will take care of the rest
DS.RESTSerializer documentation
As an aside, the current store is passed as the first parameter to DS.RESTAdapter.findAll, so you should access the store through that parameter
<\EDIT>
including DS.RESTAdapter.findall source
kaungst's answer was really helpful, but ember was still throwing an error. It led me to a solution that works, though:
App.EventSerializer = DS.RESTSerializer.extend({
normalizePayload: function(payload) {
return {'events': payload};
}
});
App.EventAdapter = DS.RESTAdapter.extend({
findAll: function(store) {
var events = [];
$.ajax({
url: '...',
dataType: 'jsonp',
success: function(response) {
response.results.forEach(function(event) {
events.addObject(store.createRecord('event', event));
}, this);
}
});
return events;
}
});
I overrode DS.RESTSerializer's normalizePayload instead of extractFindAll, which fixed the subsequent error I was getting. Additionally, I defined App.EventSerializer (singular) instead of App.EventsSerializer.

Ember.js - Error - "Assertion failed: You must include an `id` in a hash passed to `push`"

I'm getting this error after I save a post (title, text) to a mongodb database via a REST API written with Express and refresh the browser. I've already set my primary key to '_id' and have been reading about possibly normalizing the data?
Here is the payload from the server (only 1 post in db):
{
"posts": [
{
"title": "The Title",
"text": "Lorem ipsum",
"_id": "52c22892381e452d1d000010",
"__v": 0
}
]
}
The controller:
App.PostsController = Ember.ArrayController.extend({
actions: {
createPost: function() {
// Dummy content for now
var to_post = this.store.createRecord('post', {
title: 'The Title',
text: 'Lorem ipsum'
});
to_post.save();
}
}
});
The model:
App.Post = DS.Model.extend({
title: DS.attr('string'),
text: DS.attr('string')
});
Serializer:
App.MySerializer = DS.RESTSerializer.extend({
primaryKey: function(type){
return '_id';
}
});
Adapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'api'
});
Any help is much appreciated! Please let me know if you need any other info.
Thanks
When using custom adapters/serializers the naming is important. If you want it to apply to the entire application it should be called ApplicationSerializer
App.ApplicationSerializer = DS.RESTSerializer.extend({
primaryKey: '_id'
});
Adapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'api'
});
If you just want it to apply to a single model (this applies to adapter's as well)
App.PostSerializer = DS.RESTSerializer.extend({
primaryKey: '_id'
});
I had this same error but after some debugging discovered it was caused by my rest API not returning the saved objects json.

Accessing controller properties within Ember Data REST Adapter

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?