Say I have the following ember-data model:
App.Person = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
starred: DS.attr('boolean')
});
This communicates with a Rails app with the following pretty standard CRUD API:
GET /people - get a list of people
POST /people - create a new person
GET /people/id - get a specific person
PUT /people/id - update a specific person
DELETE /people/id - delete a specific person
This all maps to Ember-Data with the standard Store/Adapter.
However, lets say that in order to "star" or "unstar" a person, the API doesn't let us do this by the standard update action. There's a specific API endpoint for this action:
POST /people/id/star - mark a person as "starred"
POST /people/id/unstar - mark a person as "unstarred"
How do I fit this API in with Ember Data?
It looks like I'd need to extend DS.Store and DS.RESTAdapter somehow, but I'm not sure of the best approach to make them aware of these different actions. It also feels a bit wrong that a generic Adapter for the app has to be aware of starring people.
Note that I have no control over the API, so I can't make POST /people/id aware of "starring" so that it would fit in with a standard update.
Been a while and it may not have been possible at the time, but you can call the ajax method of your adapter directly:
YourApp.Store.prototype.adapter.ajax(url, action, {data: hashOfParams})
For example:
YourApp.Store.prototype.adapter.ajax('/products', 'GET', {data: {ids: [1,2,3]}})
For your question:
YourApp.Store.prototype.adapter.ajax('/people' + id + '/star','POST')
Edit - using buildURL is useful, particularly if you've set a namespace on your adapter:
url = YourApp.Store.prototype.adapter.buildURL('people',id) + '/star'
Edit 2 - You can also get the adapter with container.lookup('adapter:application'), which is useful if you don't have global access to the app (ex ES6 modules / ember-cli)
Edit 3 - The above refers to an outdated version of Ember / Ember-CLI. Now I define this function in a mixin -
customAjax: (method, type, id, action, hash = null) ->
#note that you can now also get the adapter from the store -
#adapter = #store.adapterFor('application')
adapter = #container.lookup('adapter:application')
url = adapter.buildURL(type, id) + '/' + action
hash['data'] = $.extend({}, hash) if hash #because rails
adapter.ajax(url, method, hash).then (result) =>
return result
And call it like so -
#customAjax('PUT', 'modelClass', modelId, 'nonCRUDActionName', optionalHashOfAdditionalData).then (response) =>
#do something with response
I went through the same issue, and solved it using a modified version of what is explained in here:
Non-crud rails actions in Ember.js
Basically, you need to use a Jquery AJAX call, the important part for me was to change the contentType in order to get the auth data to be sent in the request headers, rather than in form-data (the default, if you include data in your call). Also, make sure to give the proper context in the AJAX part, otherwise 'this' won't work in the 'success' callback:
App.ItemController = Ember.ObjectController.extend
actions:
starItem: ->
controller = this
item = #get 'model'
id = item.id
token = #auth.get 'authToken'
$.ajax
type: 'PUT'
context: controller
data: '{ "auth_token": "' + token + '"}'
url: '/api/v1/items/' + id + '/star'
contentType: 'application/json; charset=UTF-8'
dataType: 'json'
success: ->
if #get 'item.starred_by_user'
#set 'model.starred_by_user', false
item.decrementProperty('total_stars')
else
#set 'model.starred_by_user', true
item.incrementProperty('total_stars')
Maybe a good start: https://github.com/beautifulnode/ember.request
Did not tryed it yet, but seems promising in your case...
I think, that in CQRS therms star and unstar are commands. So you should declare model PersonStar and work with it.
In the discussion group, Yehuda mentioned this isn't possible yet.
Related
I'm working on a backbonejs app which utilises a RESTful API for all the model data. I need to be able to pass a customer HTTP header for authentication, which includes a localStorage item, to all ajax calls made through the model's fetch(), save(), etc.
I know how to pass the header for each one individuall, but I can't figure out a way to do it for all calls without copying/pasting the same code over and over. I've read some things about overriding the backbonejs sync, but I can't quite understand how I could do that.
I've decided to create a 'BaseModel' which all my other models would extend from, thinking this might be an easy way to do this (it's made it easy for my api url root).
Here is the basics of my models:
BaseModel:
var Backbone = require("backbone");
// Declare our options we'll use to extend the base view
var modelOptions = {
apiUrl: 'api.php/v1.1',
};
// Export our extended view
module.exports = Backbone.Model.extend(modelOptions);
Sample Model:
var BaseModel = require("./baseModel");
// Declare our options we'll use to extend the base view
var modelOptions = {
initialize: function() {
this.url = this.apiUrl+'/Cars';
},
};
// Export our extended view
module.exports = BaseModel.extend(modelOptions);
$.ajaxSetup({
headers: { 'custom-header': ' value' }
});
Also, you can add a header (or set of headers) to every request then use the beforeSend hook with $.ajaxSetup():
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('custom-header', 'value');
}
});
I believe you can set the global AJAX header through jQuery like:
$.ajaxSetup({
headers: yourCustomHeader
});
or
Backbone.$.ajaxSetup({
headers: yourCustomHeader
});
How can I use promise with $resource?
This is my service,
app.service("friendService",function( $resource, $q ) {
// Return public API.
return({
addFriend: addFriend,
updateFriend: updateFriend,
getFriends: getFriends,
removeFriend: removeFriend
});
function updateFriend( friend ) {
var postData = {
id: friend.id,
name: friend.name
};
var request = $resource("api/update.php", null,{
update: {
method:'PUT',
data:postData
}
});
return( request.$promise.then( handleSuccess, handleError ) );
}
I get this error,
TypeError: request.$promise is undefined
What is the correct way doing it with $resource?
Change from
return( request.$promise.then( handleSuccess, handleError ) );
to
return request.update().$promise.then(handleSuccess, handleError);
That said, using $resource like this is quite inefficient while not taking any advantage of it. It's better to replace with $http.
You should simplify your service to actually BE the $resource
app.factory('friendService', [ '$resource', function($resource) {
return $resource('/api/friends/:id', null, {
'update' : {
method : 'PUT'
}
});
} ]);
This automatically provides the following endpoints (which actually is the cool thing about $resource):
{ 'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'}
};
Here are some usage examples:
friendService.query(success, error); // GET /friends
friendService.get({ id : "exampleId" }, success, error); // GET /friends/exampleId
friendService.save({/* no params */}, friendObjectWithId, success, error); // POST /friends/idTakenFromObject
friendService.delete({ id : "exampleId" }, {}, success, error); // DELETE /friends/exampleId
friendService.update({/* no params */}, friendObjectWithId, success, error); // PUT /friends/idTakenFromObject
So, as this line of the documentation describes, you dont need the $promise to specify the callbacks:
non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
So you can simply do something like this:
friendService.update({}, friendObject, successHandler, errorHandler)
Short answer:
I think you are misunderstanding what $resource is, since you're trying to use it as you would use $http.
$resource is a "wrapper" around $http to provide a Object Oriented CRUD way to interact with a RESTful api. (DOCS explain it well and provide good examples)
From your URL, I don't think you're actually using a REST api so it would probably be better to use $http service instead of using $resource service.
Regardless, here's a working fiddle.
Resource and REST API
A resource, in the context of angular, corresponds to a resource in context of REST and so, it will expect your webservice to behave like a RESTful app. To explain it further, let' take your "Friend" as example... (I will be reworking your URLS to better match a REST API)
API Definition
Take the following REST+CRUD conformant scheme (for a Friend resource)
Resource URI Methods allowed
Friend Collection api/friend GET, POST
Friend api/friend/:id GET, PUT
The basic idea here is that each Resource is univocally represented by a URI (that's actually the definition of URI: -> Uniform Resource Identifier) and the HTTP Method (Verb) is used to define the action that will be performed on said Resource.
Of course, REST is much more than this and I suggest you read this SO POST or this funny article or even Roy Fielding's dissertation (the guy that came up with REST) that explain the concept a lot better than I ever can hope for.
URL Structure
This issue is prone to hot debate, and you can read some interesting points here in this SO Post and an article from Roy Fielding partially addressing this too. To sum up, REST does not require clean URLs. Actually, it doesn't require ANY kind of semantically logic URL structure.
What REST APIs must be is hypertext-driven, that is, given an entry point (URL), the API must be self explanatory so that a client can "discover" resources and relations by itself, with type of resources given by media-types. That means, if an url changes, the API doesn't break!!
So, in practical terms, this can be a valid:
Home /
Friend Collection /foo
Friend Resource 1 /bar
Friend Resource 2 /baz
As well as this can be valid :
Home index.php
Friend Collection index.php?q=api/friend
Friend Resource 1 index.php?q=api/friend/1
Friend Resource 2 index.php?q=api/friend/2
Or it's cousin, using mod_reqrite to make "clean URLs", can be valid
Home /
Friend Collection /api/friend
Friend Resource 1 /api/friend/1
Friend Resource 2 /api/friend/1
or even this can be valid...
Home /index.php
Friend Collection /friend.php
Friend Resource 1 /friend_1.php
Friend Resource 2 /friend_2.php
The server, in no way, is obliged to follow any pattern. However, that doesn't mean you shouldn't adhere to a structure, must mostly for SEO purposes (or human readability). And, in the last example, it might be hard to develop a sane webservice that relies on individual scripts for each individual resource. (you might not violate REST principles, but you will probably violate some basic programming rules, such as DRY...)
Also, angular-resource is (kind of) opinionated about url structure. It's not an absolute requirement but...
Regarding your specific question, yes, you would need mod_rewrite to match the example I gave you. But you don't need mod_rewrite to make a REST compliant API.
Using angular-resource module
Now that our API scheme is set and follows the REST+CRUD principles, we can exploit the full potential of the angular-resource module.
We can create a client side representation(interface) of "Friend".
//Custom actions
var actions = {
update: {
method: 'PUT'
}
}
var friendUrl = "/api/friend/:id"; // should be obtained by inspecting the API iteself, usually the parent collection.
var Friend = $resource(friendUrl, {id: '#id'}, actions);
To get a friend we would issue a GET request (and specify it's id);
Friend.get({id: 1}).$promise.then(
function (response) {
//console.log(response);
}
);
DELETE and PUT requests(which we created and called update) are basically the same thing. $resource also supports retrieving collections using the object's method query. You can use that to retrieve the collection of friends.
Please notice that I'm using a hardcoded URL for simplicity
request is just setting up your endpoint. You need to actually call some method on it, e.g. request.get({id: 1}).$promise; or request.query({term: 'test'}).$promise;
I'm trying to figure out a way to cache my knockoutJS SPA data and I've been experimenting with amplifyJS. Here's one of my GET functions:
UserController.prototype.getUsers = function() {
var self = this;
return $.ajax({
type: 'GET',
url: self.Config.api + 'users'
}).done(function(data) {
self.usersArr(ko.utils.arrayMap(data.users, function(item) {
// run each item through model
return new self.Model.User(item);
}));
}).fail(function(data) {
// failed
});
};
Here's the same function, "amplified":
UserController.prototype.getUsers = function() {
var self = this;
if (amplify.store('users')) {
self.usersArr(ko.utils.arrayMap(amplify.store('users'), function(item) {
// run each item through model
return new self.Model.User(item);
}));
} else {
return $.ajax({
type: 'GET',
url: self.Config.api + 'users'
}).done(function(data) {
self.usersArr(ko.utils.arrayMap(data.users, function(item) {
// run each item through model
return new self.Model.User(item);
}));
}).fail(function(data) {
// failed
});
};
This works as expected, but I'm not sure about the approach I used, because it will also require extra work on the addUser, removeUser and editUser functions. And seeing as I have many more similar functions throughout my app, I'd like to avoid the extra code if possible.
I've found a way of handling things with the help of ko.extenders, like so:
this.usersArr = ko.observableArray().extend({ localStore: 'users' });
Then use the ko.extenders.localStore function to update the local storage data whenever it detects a change inside the observableArray. So on init it will write to the observableArray in case local storage data exists for users key and on changes it will update the local storage data.
My problem with this approach is that I need to run my data through the model and I couldn't find a way to do that from the localStore function, which is kept on a separate page.
Has any of you worked with KO and Amplify? What approach did you use? Should I use the first one or try a combination of the two and rewrite the extender in a way that it only updates the local storage without writing to the observableArray on init?
Following the discussion in the question's comments, I suggested to use native HTTP caching instead of adding another caching layer on the client by means of an extra library.
This would require implementing a conditional request scheme.
Such a scheme relies on freshness information in the Ajax response headers via the Last-Modified (or E-Tag) HTTP headers and other headers that influence browser caching (like Cache-Control: with its various options).
The browser transparently sends an If-Modified-Since (or If-None-Match) header to the server when the same resource (URL) is requested subsequently.
The server can respond with HTTP 304 Not Modified if the client's information is still up-to-date. This can be a lot faster than re-creating a full response from scratch.
From the Ajax request's point of view (jQuery or otherwise) a response works the same way, no matter if it actually came from the server or if it came from the browser's cache, the latter is only a lot faster.
Carefully adapting the server side is necessary for this, the client side on the other hand does not need much change.
The benefit of implementing conditional requests is reduced load on the server and faster response behavior on the client.
A specialty of Knockout to improve this even further:
If you happen to use the mapping plugin to map raw server data to a complex view model, you can define - as part of the options that control the mapping process - a key function. Its purpose is to match parts of your view model against parts of the source data.
This way parts of the data that already have been mapped will not be mapped again, the others are updated. That can help reduce the client's processing time for data it already has and, potentially, unnecessary screen updates as well.
Not the easiest issue to put into a title.
Anyhow, my app is built on nodejs/expressjsand has an API set up for the url:
EDIT: The current code I'm using is:
$scope.updateProduct = $resource('/api/updateProduct/:product/:param/:value',{},{
query: {method:'GET'},
post: {method:'POST'},
save: {method:'PUT', params: {brand: '#brand', param:'#param', value:'#value'}},
remove: {method:'DELETE'}
});
$scope.updateProduct.save({
product : $scope.post._id,
param: 'likes',
value: $scope.user._id
});
At present it calls /api/updateProduct instead of /api/updateProduct/<product>/<param>/<value> like it's supposed to / like it does when I perform $scope.updateProduct.get().
In my console I see (as an example):
PUT /api/updateBrand/Quay%20Eyewear%20Australia/userTags/sunglasses,%20classic 200 30ms - 2.31kb
However, the API isn't actually accessed/nothing happens. Interestingly, if I go to localhost:5000/api/updateBrand/Quay%20Eyewear%20Australia/userTags/sunglasses,%20classic in my browser, it posts the correct data and updates the product in my database, so it's definitely an error with the way the $resource is being called.
As you can see in ngResource docs, $resource receive 3 parameters:
$resource(url[, paramDefaults][, actions]);
You are passing your action as a parameter.
The correct version would be:
$scope.updateProduct = $resource('/api/updateProduct/:product/:param/:value',{}, {'save':{method:'POST'}});
Note that it isn't even necessary, because when you use $resource you already create the default methods:
{
'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'}
};
First of all, your have defined the save() to have a parameter called "brand", but in your url template and in the call to save(), you are using "product". I guess it's a typo.
You say when you are using browser to visit the url, it works well; but when angular is make a PUT request to the same url, nothing is happening. This indicates that your backend is configure to process only GET requests for this particular url pattern. Therefore, you need to make sure that your backend is accepting PUT requests.
I was struggling with this issue and was able to pass parameters to the resource by doing the equivalent call below (notice the '$' before 'save').
$scope.updateProduct.$save({
product : $scope.post._id,
param: 'likes',
value: $scope.user._id
});
I also did not define a 'save' method in the resource. According to Angular docs:
"Calling these methods (meaning non-GET methods) invoke $http on the url template with the given method, params and headers. When the data is returned from the server then the object is an instance of the resource type and all of the non-GET methods are available with $ prefix. This allows you to easily support CRUD operations (create, read, update, delete) on server-side data."
My app is Backbone.js for client-side, Express.js for back-end.
I have problems with syncing with all parts of my API, using the backbone model and collection(they use urlRoot: "/users").
I'm allowed to use only GET or POST, no PUT or DELETE.
*I'm not allowed to use more models*
Not allowed to use jQuery ajax
My API
add new user:
I need to make a POST to /users with JSON of new data. So I did it just fine with - this.model.save({new data...})
list all users:
My API for that, responses to GET /users, with the right handler - so, this.collection.fetch() - works fine.
Log-In:
My API accepts POST to /users/login for that. How can I add a function "logIn" to my model, that will use custom sync/pass options.url to sync - or any other way - that will POST to /users/login ?
Log-Out:
API accepts POST to /users/logout - how to send this request using my backbone model ?
User By ID:
Same question here for GET /users/:id
Update user:
POST /users/:id - same question again.
--- So actually, the question in short is ---
What is the best way (or the most "right"), to implement methods of a backbone model, that are similar to "model.save()" - but just POST/GET to a bit different path then urlRoot ?
You probably have a couple options here. One would be structuring your models in a way that supports the urls you want. For instance, have a User model and a Session model that deal with updating the user and managing the logged in state separately.
The other thing you should probably do is to use the url method in your models.
Something like this in your User model. (Note: using urlRoot instead of url here is identical, but this is the correct approach for anything more complicated that is needed in the url)
url : function() {
var base = "/users/";
if(this.isNew()) {
return base;
} else {
return base + this.get("id");
}
}
You could extend this same concept to work in your Session model for handling logout vs login based on if the Session is new or not.
Update:
If you must use the same model, the best thing would be to totally bypass the Backbone.sync method and write a custom AJAX call with success/error handlers that know how to clean things up.
login : function() {
$.post("/users/login", {success: function (response) {
// Update user as needed
}, error: function (xhr, response) {
// Handle errors
}
}
}