I have an service in my Angular app that is responsible for authorizing the user and returning the auth token back.
However, due to the async nature of $http, I cannot properly isolate the logic of my service. Instead I must return a promise and push the logic of interpreting the response to the caller of the service (single responsibility red flag).
Given this, it's made me reconsider whether I am thinking about this correctly. I come from a Java world where the majority of interactions are synchronous, so something isn't sitting well with me as I try to architect a clean design with single responsibility.
What am I missing here?
UPDATE
I realize the below will not work as intended, but that's my thought of how I'd like it to work at least:
app.service('AuthenticationService', ['$http', '$httpParamSerializerJQLike', function($http, $httpParamSerializerJQLike)
{
this.authServerBaseURL = "...";
this.clientId = "...";
this.authenticate = function(username, password)
{
var config =
{
headers:
{
"Content-Type" : 'application/x-www-form-urlencoded',
"Authorization" : "Basic " + btoa(this.clientId + ":")
}
}
var body = $httpParamSerializerJQLike(
{
grant_type : "password",
username : username,
password : password
});
return $http.post(this.authServerBaseURL + '/oauth/token', body, config).
success(function(data, status, headers, config)
{
return data.access_token;
}).
error(function(data, status, headers, config)
{
return false;
});
}
}]);
Update after you added code: Your thought process can work, see below. However, $http docs say not to use .success and .error. If you instead use .then, as in my examples below, it will work.
Assuming your code is something similar to this:
// AuthService
this.authenticate = function() {
return $http.post('http://example.com', body, config);
}
// Using it:
AuthService.authenticate().then(function(data) {
var token = data.access_token;
});
You can move the knowledge about how the data is extracted to the service like this:
// AuthService
this.authenticate = function() {
return $http.post('http://example.com', body, config).then(function(data) {
return data.access_token;
});
}
// Using it:
AuthService.authenticate().then(function(token) {
var token = token;
});
what happens here is that you make a new promise by calling .then on the $http promise, which is what is returned. The promises are chained, so the $http promise will resolve this new promise, which then resolves itself with the extracted token.
Related
I have an AngularJS frontend to my API-based application. There are services which make API calls, such as the following. I want to compare a variable from each of these calls:
Get the user data:
this.getUserData = function () {
var apiCall = $http({
method: 'GET',
url: 'http://example.com/api/userdata',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Access-Control-Allow-Origin': 'Any'
}
});
return apiCall;
};
Get the page data:
this.getPageData = function(slug){
var apiCall = $http.get('http://example.com/api/public/page?slug=' + slug, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}
)
return apiCall;
};
In my controller I wish to compare a var from each of these calls, like so :
if (apiService.getUserData.likes.page_id == apiService.getPageData.page_id){ // do stuff }
What is the most efficient way of doing this, given that API calls can take a while.. I don't have to make an API call everytime I want to access a variable from the API do I? Bear in mind that these API calls would normally be made in DIFFERENT controllers, so the results are stored in different scopes.
Basically, I'm confused whether that bit of logic should go in a controller, in the service itself, or somewhere else. Any advice is appreciated. Thanks.
If you are comparing them in a controller you can easily call one followed by the other and compare. Since you say you have an "API-based application" and use an auth token from localStorage you should call the API in each different controller you have in case your auth token expires or something.
I would most likely create a service like (flushing it out to more to show all pieces):
angular.module('app').service('CompareService', CompareService);
CompareService.$inject = ['apiService', '$q'];
function CompareService(apiService, $q) {
return {
arePageIdsEqual: apie
};
function apie() {
var deferred = $q.defer();
apiService.getUserData().$promise.then(function(userData) {
apiService.getPageData().$promise.then(function(pageData) {
deferred.resolve(userData.likes.page_id === pageData.page_id);
}, function(err) {
deferred.reject(err);
});
}, function(err) {
deferred.reject(err);
});
return deferred.promise;
}
}
And in your controller just inject it and call it like:
CompareService.arePageIdsEqual(function(yes) {
if(yes) {
// do something
}
}, function(err) {
// err making the calls
});
You should implement it with a Promise.all.
function doCompare(userData,pageData){
if (userData.likes.page_id == pageData.page_id){
// do stuff
}
}
var promises = [apiService.getUserData,apiService.getPageData]
$q.all(promises).then(doCompare);
When a user logs into the main page of my site, I typically load quite a bit of data on the home page. much more than when they come to a specific url on the page. When they hit the ome page, that actually fullfills the data requests of much of the data that I grab individually when they hit a specific page.
I like how the $http module works with $cache and I'm wanting to use the knowledge of my home page hit to populate the cache of calls I know the individual page will make.
That is, as an example, my home page calls /rest/clients which returns all clients while individual pages call /rest/client/101. What I want to do is make it so that if /rest/clients is called first, then when /rest/client/101 is called an new fresh xhr call does not have to be made but the data can be gotten from the cache as if /rest/client/101 had already been called.
I've never done a decorator before but I'm thinking maybe a decorator on the $http service? I looked through the $http code and it seemed the cache is stored in closure to the actual http call and not exposed except on the next Get.
Has anyone done this or similar? I could not find it. Any specific pseudo coding suggestions would be very welcome.
In your data service you have 2 methods, getAll and getOne.
In the service define a reference to your getAll results promise.
Then in your getOne service check to see if that promise exists and if it does use it to filter out the one item that you need to satisfy your getOne need.
module.service('dataService', function($http){
var getAllPromise = null;
this.getAll = function(){
if (getAllPromise !== null){
getAllPromise;
}
getAllPromise = $http.get('clients');
return getAllPromise
};
this.getOne = function(id){
if (getAllPromise !== null){
return getAllPromise
.then(function(allData){
//logic here to find the one in the full result set
return theFoundItem;
};
}
return $http.get('clients/' + id);
};
});
I found the solution I asked for but implementing and making it testable is proving to be beyond my skills. I'm going to go with #brocco solution but for the permanent record I'm leaving the actual answer to what I was asking. I'm not marking this as the correct solution because #brocco solution is better for my real problem. So, thank you #brocco for the help.
You can see below what I'm basically doing is to create my own $cache with $cacheFactory. I then use the .put method of the new cache object to prime my cache. Then, subsequent calls to the client/1 url will get the cache'd record without ever having to call cache/1 in real live. The cache is loaded in the for loop from the first big call.
Thanks for everyones input on this.
var myApp = angular.module('myApp', []);
myApp.factory('speakersCache', function($cacheFactory) {
return $cacheFactory('speakersCacheData');
});
myApp.controller('personController', ['$scope','$http','speakersCache', function ($scope,$http,speakersCache) {
$scope.getAllSpeakers = function() {
$http.get('speakers.json',{cache: speakersCache}).
success(function (data, status, headers, config) {
debugger;
var i;
for(i=0;i<data.length;i++) {
var url = 'speaker/' + i;
speakersCache.put(url, data[i]);
}
}).
error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
};
$scope.getAllSessions = function() {
$http.get('sessions.json',{cache: speakersCache}).
success(function (data, status, headers, config) {
debugger;
}).
error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
};
$scope.getOneSpeaker = function() {
$http.get('speaker/1',{cache: speakersCache}).
success(function (data, status, headers, config) {
debugger;
}).
error(function (data, status, headers, config) {
debugger;
});
}
$scope.checkit = function() {
var x = speakersCache;
debugger;
};
}]);
If I understand you well, I have done something similar:
I have this code:
.factory('myOwnEntity', ['$filter',
function ($filter) {
var myOwnList = [];
return {
set : function (data) {
myOwnList = data;
},
get : function () {
return myOwnList;
},
find : function (id) {
return $filter('filter')(myOwnList, { itemId : id }).pop();
}
}
}
])
When I make the petition to the Web Service, I store the information like this:
$http.get(url, {
cache : true
})
.success(function (data) {
myOwnEntity.set(data);
defer.resolve(data);
});
return defer.promise;
Now, the next time I need some information, I just query my entity with the find method. Hope this is what you are looking for.
I'm hitting an API which requires all authenticated actions to include an auth token in the request, however, I do not have the auth token until I login.
I've only seen examples of setting default request parameters in Restangular in app.config.
Is it possible to set this until after the user has logged in and User.auth_token is set?
So basically instead of:
app.config(function(RestangularProvider) {
RestangularProvider.setDefaultRequestParams({
auth_token: 'thisistheauthenticationtoken'
});
});
I need:
app.config(function(RestangularProvider) {
RestangularProvider.setDefaultRequestParams({
auth_token: User.auth_token
});
});
Why would you set token as part of the response versus in the header? Like so.
Restangular.setDefaultHeaders({ authentication: 'bearer ' + token.authentication });
I know this is an old thread but this SO question kept appearing when I was Googling (yes, I just used Google as a verb... deal with it :P) for a resolution, so I thought I should provide my solution. Hopefully it will help the OP or anyone else that may come across this page.
angular.module("app").factory("UserService", [
"$rootScope",
"$state",
"$q",
"Restangular",
function ($rootScope, $state, $q, Restangular) {
var UserSvc = {};
var Identity;
/*
This creates a scoped copy of Restangular
Normally this is where you would use setDefaultRequestParams,
but it would only affect this scope and not ALL API requests in your app
*/
var UsersAPI = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl("api/1.0/users");
});
UserSvc.login = function (credentials) {
var $defer = $q.defer();
UsersAPI.all("start-session").post(credentials).then(function(respData){
if (respData.apikey) {
Identity = respData.plain();
/*
User is authenticated and API key is obtained from server response
Note how I do NOT use the setDefaultRequestParams function:
If we do the withConfig/setDefaultRequestParams, it only affects local scope, not global
This method modifies the ROOT Restangular object and
will then propegate through all future use of Restangular in your app
*/
Restangular.configuration.defaultRequestParams.common.apikey = Identity.apikey;
if ($rootScope.toState && $rootScope.toState.name != "login") {
$state.go($rootScope.toState.name, $rootScope.toStateParams || {});
} else {
$state.go("app.dashboard");
}
$defer.resolve(Identity);
}
else {
Identity = undefined;
$defer.reject(Identity);
}
},function (respData) {
$defer.reject(respData);
});
return $defer.promise;
};
return UserSvc;
}
]);
In my case, I use
Restangular.setDefaultRequestParams({token: localstorage.get('token')});
This works with me. Please have a look my snippet here.
https://github.com/fugokidi/ng-snippets/blob/master/rest.js
If you want to do something like this, you need to remove your code from app.cofig and move to when you find user is logged in.
You can set defaultRestParams for restangular at any point of application using Restangular service.
For more info refer https://github.com/mgonto/restangular#setdefaultrequestparams.
A more Angular-ish example from a project that I've been working on:
angular.module('app', [ 'restangular' ])
.factory('API', function(Restangular){
return Restangular.withConfig(function(config){
config
.setBaseUrl('https://api.example.com')
// etc etc etc
; // END config
});
})
.factory('Auth', function(API){
return {
login: function(credentials){
// Assuming I just POST /session/new to get an OAuth token,
// which is totally not a thing that OAuth should do.
API.one('session').post('new', credentials)
.then(function(auth){ // Assuming `auth = { access_token: '...' }`
API.setDefaultHeaders({
Authorization: 'bearer ' + auth.access_token
// Assuming OAuth Bearer Token
});
})
},
logout: function(){ /* . . . */ }
};
})
.controller('MainController', function(API, Auth){
var self = this;
self.user = { };
this.login = function(credentials){
Auth.login(credentials).then(function(){
self.user = API.one('user').$object;
});
});
})
; // END module(app)
The following code will read the token from storage for every request.
app.config(function(RestangularProvider) {
//Injext $cookies manually (there might be better ways to do this)
var $cookies;
angular.injector(['ngCookies']).invoke(['$cookies', function(_$cookies_) {
$cookies = _$cookies_;
}]);
RestangularProvider.setDefaultHeaders({
Authorization: function() {
return $cookies.get('token');
}
});
});
I too struggled with this.
Instead of using
RestangularProvider.setDefaultRequestParams({
auth_token: 'thisistheauthenticationtoken'
});
try using
Restangular.setDefaultRequestParams({auth_token:'thisistheauthenticationtoken'});
I implemented Primus (Sockets) on my Server and would like to access it via the client, which uses AngularJS. I would like to be able to still use libraries like Restangular or the $resource from Angular. So IMHO the best way to achieve this is to extend the $http service, which is used by most libraries as the basis.
I would like this new service to be able to gracefully fall back to the normal $http, when there is no socket connection available.
In Pseudocode:
socketHttpService = function(config) {
if(socketEnabled) {
var message = buildMessageFromConfig();
primus.write(message);
return promise;
}
return $http(config);
}
Call it like you would $http
socketHttpService({method: 'GET', url: '/someUrl'}).then(function() {
// do whatever
});
My question is, how can i replace the standard $http service with this newly created one? Is there an elegant way, while still retaining the default $http behaviour?
In the meantime, I found a solution to the problem
.config(function($provide) {
$provide.decorator('$httpBackend', function($delegate, $q, $log, SocketService) {
// do not blast mock $httpBackend if it exists
if (angular.isDefined(angular.mock)) {
return $delegate;
}
var httpBackendSocket = function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
if(SocketService.isOpen) {
console.log('open');
method = method.toLowerCase();
// we only know get, post, put, delete
if(method === 'get' || method === 'post' || method === 'put' || method === 'delete') {
// we can not handle the authentication links via sockets, so exclude them
if( url.substring( 0, '/api/v1/currentuser'.length ) !== '/api/v1/currentuser' &&
!angular.equals(url, '/api/v1/login') &&
!angular.equals(url, '/api/v1/logout') &&
!angular.equals(url, '/api/v1/session') ) {
var promise = SocketService.writeRest(method, url, post || {});
return promise.then(function promiseSuccess(response) {
return callback(response.status, response.data, response.headers, '');
}, function promiseError(response) {
// is caught via http handlers
// LATER: If error, retry with $httpBackend ($delegate)
return callback(response.status, response.data, response.headers, '');
});
}
}
}
return $delegate(method, url, post, callback, headers, timeout, withCredentials, responseType);
}
return httpBackendSocket;
});
})
Why? Because it feels like 5 times faster than http, because there is a standing connection and I am not losing any of the realtime options. It's like a cherry on top.
Kind Regards
I'm trying to figure out if it is possible to use a $http interceptor to cancel a request before it even happens.
There is a button that triggers a request but if the user double-clicks it I do not want the same request to get triggered twice.
Now, I realize that there's several ways to solve this, and we do already have a working solution where we wrap $http in a service that keeps track of requests that are currently pending and simply ignores new requests with the same method, url and data.
Basically this is the behaviour I am trying to do with an interceptor:
factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.finally(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
}
}
}])
When I look at the API for $http interceptors it does not seem to be a way to achieve this. I have access to the config object but that's about it.
Am I attempting to step outside the boundaries of what interceptors can be used for here or is there a way to do it?
according to $http documentation, you can return your own config from request interceptor.
try something like this:
config(function($httpProvider) {
var cache = {};
$httpProvider.interceptors.push(function() {
return {
response : function(config) {
var key = createKey(config);
var cached = cache[key];
return cached ? cached : cached[key];
}
}
});
}
Very old question, but I'll give a shot to handle this situation.
If I understood correctly, you are trying to:
1 - Start a request and register something to refer back to it;
2 - If another request takes place, to the same endpoint, you want to retrieve that first reference and drop the request in it.
This might be handled by a request timeout in the $http config object. On the interceptor, you can verify it there's one registered on the current request, if not, you can setup one, keep a reference to it and handle if afterwards:
function DropoutInterceptor($injector) {
var $q = $q || $injector.get('$q');
var dropouts = {};
return {
'request': function(config) {
// I'm using the request's URL here to make
// this reference, but this can be bad for
// some situations.
if (dropouts.hasOwnProperty(config.url)) {
// Drop the request
dropouts[config.url].resolve();
}
dropouts[config.url] = $q.defer();
// If the request already have one timeout
// defined, keep it, othwerwise, set up ours.
config.timeout = config.timeout || dropouts[config.url];
return config;
},
'requestError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
},
'response': function(response) {
delete dropouts[response.config.url];
return response;
},
'responseError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
}
};
}