Recursively intercepting $http - angularjs

In order to better consume a hateoas enabled rest api I got the idea to intercept http calls and add some methods to my resource objects before returning them.
The idea is that if the resource has an array of links I'll add methods to easy further http requests.
The below code is push to the interceptors array of a $httpProvider and does pretty much what I want.
define(['angular'], function (angular) {
var $http = angular.injector(['ng']).get('$http');
function flattenLinks(links) {
for (var key in links) {
var rel = links[key].rel;
var href = links[key].href;
links[rel] = href;
}
}
return {
'response': function responseHandler(response) {
var data = response.data ? response.data : response;
if(typeof data === 'string' || !data.hasOwnProperty('links')) {
return response;
}
if(data.links instanceof Array) {
flattenLinks(data.links);
}
if(data instanceof Array) {
for(var key in data) {
responseHandler(data[key]);
}
}
data.hasLink = function(link) {
return link in data.links;
};
data.get = function(rel) {
return $http.get(data.links[rel]);
};
data.delete = function(rel) {
return $http.delete(data.links[rel]);
};
data.post = function(rel) {
return $http.post(data.links[rel], data);
};
data.put = function(rel) {
return $http.put(data.links[rel], data);
};
return response;
}
};
});
The problem is that when I use, as seen below, my added methods to do requests the response isn't handled by my interceptor. The $http.get, $http.delete, etc. done from within my interceptor isn't intercepted (naturally!).
vm.someResourceObj.get('self').then(function(response) {
console.log(response);
});
So the question is. How do I get the internal calls to $http handled?

First of all - this is not an answer to you question =)
But it's a kind of advice: man, you are really try to develop a bicycle.
According to developers guide you should use $resource instead of $http to interact with REST api. Especially with RESTful.
$resource service will provide you functionality to make exactly what you want - to make requests like User.get() and after this User.save(), User.update(), etc.
My suggest is to create a factory for each entity(like "user", "account", etc), or for each workflow(like "sign up").
'use strict';
angular.module('app.factories.user', ['ngResource'])
.factory('UserFactory', function ($resource) {
var _userApi = {
user: $resource('/user', {}, {})
};
return {
getCurrentUser: function () {
return _userApi.user.get(function (data) {
// here we got data.user;
}, function (response) {
console.error(response.status);
}).$promise;
}
};
})
;
And use it in a controller:
.controller('UserPageCtrl', function ($scope, UserFactory) {
var currentUser;
$scope.getCurrentUser = function () {
return UserFactory.getCurrentUser().success(function (user) {
//here we got user
currentUser = user;
//now we able do stuf like
//currentUser.save()
//etc.
}).error(function (err) {
console.error(err);
});
};
});
So, don't mind if examples above little bit... you know... let's say if you don't like it ))
Just take a look at $resource documentation (and this)
If you don't like resource, you can use third-party restanguar (or perhaps something else). But restangular is much more strict, so choose it only if you know for sure that your backend developers aren't idiots.
UPD1: About interceptors (I'll put here my working code for error intercepting, the idea of this code is to logout user when any request return 403 error status):
You are able to add all facctory to global $httpProvider(at module config section).
Like this:
app.config(function ($urlRouterProvider, $httpProvider) {
//Process backend errors
$httpProvider.interceptors.push('errorsFactory');
})
And here is errorsFactory:
'use strict';
angular.module('app.factories.errors', [])
.factory('errorsFactory', function ($q) {
var HTTP_STATUS = {
FORBIDDEN: 403
};
return {
response: function (response) {
if (response.status === HTTP_STATUS.FORBIDDEN) {
//logout here
}
return response || $q.when(response);
},
responseError: function (rejection) {
if (rejection.status === HTTP_STATUS.FORBIDDEN) {
//logout here
}
return $q.reject(rejection);
}
};
})
;

Use $injector to get $http instead of accessing it via angular..
Detailed Answer:
The problem is how you inject $http. It seems that you get some $http that is outside of your app (I can't exactly say why, though). var $http = angular.injector(['ng']).get('$http'); doesn't do what you want (or what you think it does).
You could do var $http = angular.element(document).injector().get('$http'); (or probably better use $document there), but imo you should use the $injector-service normally: you can just add $injector to your dependencies/inject statement and inside your interceptor use that to retrieve the service you want like: var $http = $injector.get("$http");
With that your interceptor will intercept the $http-calls it makes.

Related

AngularJs Service called from two places returns same value

I am new to angular and pardon my ignorance if any. I am trying to create a simple service that will do a get funtionality and serve data into array.
The problem i am having is, no mattter what i do - i always get the same data for any parameter i pass.
Here is my sample service
function myService($http, $q) {
var service = {
getSomeData: getSomeData:
};
var def = $q.defer();
return service;
function getSomeData(category) {
if (category === 'books') {
url = 'http://www.someurl1';
} else {
url = 'http://www.someurl2'
};
$http.get(url, {
params: {
'type': category
}
}).success(function(data) {
def.resolve(data);
}).error(function() {
def.reject('Failed to get data');
});
return def.promise;
}
}
})();
Once i have this, in my controller, i am trying to call it for sample purposes like this
$scope.someData = [] ;
$scope.someData.push(myService.getSomeData('DVDs');
$scope.someData.push(myService.getSomeData('books');
Now when i my look at my $scope.someData,
i have an array of two objects - the problem being that they are always the same and doesnt have data specific to books and dvds.
Another minor issue I have is my object someData has
array --> Promise -- > $$state --> value
which then has the actual data. how can i get the data directly.
i tried return def.promise(data);
One problem is you are only creating one promise outside the getSomeData function.
A promise can only be resolved once. You need a new promise for each request.
Also $http already returns a promise but you can't push it directly into an array as data.
your code should look more like:
Service:
function myService($http, $q) {
var service = {
getSomeData: getSomeData
};
return service;
function getSomeData(category) {
if (category === 'books') {
url = 'http://www.someurl1';
} else {
url = 'http://www.someurl2'
};
return $http.get(url, {
params: {
'type': category
}
}).then(function(response) {
return response.data;
}).catch(function(err) {
console.log("Ooops", err)
});
}
}
Controller
$scope.someData = [] ;
myService.getSomeData('DVDs').then(function(data){
data.forEach(function(item){
$scope.someData.push(item);
});
});

Restangular, return promise response within a service to scope

I am trying to handle restangular calls entirely from a service to keep the controller light, and also so I can manipulate the data further within the service later.
Struggling with promises, which I think is my issue. If I can avoid it, I dont want the service to return the promise, and then use .then() in the controller.
If I make the restangular call directly from the controller it works fine.
angular.module('vehicle', ['restangular'])
.config(
function(RestangularProvider) {
RestangularProvider.setBaseUrl('https://jsonplaceholder.typicode.com/');
RestangularProvider.setResponseExtractor(function(response, operation, what) {
if (operation === 'getList' && !Array.isArray(response)) {
return [response];
}
return response;
});
})
.controller('VehicleController', function($scope, VehicleService, Restangular) {
$scope.vehicles = VehicleService.getVehicles();
Restangular.all('posts').getList().then(function(vehicle) {
$scope.moreVehicles = vehicle.plain();
});
})
.service('VehicleService', function(Restangular) {
VehicleService = {};
VehicleService.vehicles = [];
VehicleService.getVehicles = function() {
Restangular.all('posts').getList().then(function(vehicle) {
VehicleService.vehicles = vehicle.plain();
return VehicleService.vehicles;
});
}
return VehicleService;
});
https://plnkr.co/edit/q1cNw6gN12pKsiPaD1o0?p=preview
Any ideas why my combination of service and promise wont return the data to scope?

ngResource $promise error handling?

I have the following controllers which all the ngResource services to get data.
.controller('venueCtrl', function ($scope, $stateParams, VenueService) {
$scope.venue = VenueService.get({ id: $stateParams.id });
})
.controller('tomorrowCtrl', function ($scope, EventService) {
var evts = EventService.query({ period: "Tomorrow" });
evts.$promise.then(function (response) { $scope.events = response; });
}).....
Now I need to add error handling (for example, display an alert box) for the error situations, e.g., no network, web service failed, etc. How to add the code to handle the errors?
You can use Interceptors this way:
angular.module('ngRessourceErrorsHandler', [])
.config(function($resourceProvider) {
angular.forEach($resourceProvider.defaults.actions, function(action) {
action['interceptor'] = {
responseError: function(httpResponse) {
//Do whatever you want here !
}
};
})
});
Please try this and let me know if this works for you. Don't forget to add ngRessourceErrorsHandler dependency to your module or just use config directly.

Angular.js: how to call a service once for all the app controllers

I don't know if this is even one of the Angular concepts or possible to do but i have a service that call the user information (name, id, age, ...):
.factory('me', function($resource, API_URL, $q) {
return {
getUser: function() {
var deferred = $q.defer();
var url = API_URL + 'api/me';
$resource(url)
.get(function(user) {
deferred.resolve(user);
}, function(response) {
deferred.reject(response);
});
return deferred.promise;
}
};
})
I use this service in many controllers to get the user data and use it to send with other http calls. for example in my newItemCtrl
.controller('newItemCtrl', function($scope, $http, API_URL, me) {
i called the me service as i did in many other controllers
and am wondering is there a way to call this service only once and use it in all the controllers instead of x times in each controller
I do something similar all the time with services by setting the value to the service once it's returned the first time. Now once the data is set in the service it will return the stored user data instead of making a request to your server.
service('myService', function($q, $http) {
this.data;
var self = this;
this.getMyData = function() {
if (angular.isDefined(self.data)) {
// use $q to return a promise
return $q.when(self.data)
}
return $http.get('myurl').then(function(resp) {
self.data = resp;
})
}
}
In your controller you can call myService.getMyData()
//Assume Value1 and Service1 have been correctly injected
if (Value1.data === null) {
Your_Var = Service1.call;
} else {
Your_Var = Value1.data;
}
For simplicity sake I would put that in a ternary expression when implementing.
For DRY compliance.
Going with DRY we need a SharedFactoryFuncion to handle $http requests
Your_Var = Service1.call;
//Inside Service1.call
return Value1.data === undefined ? SharedFactoryFuncion.get("url") : Value1.data;
There are numerous ways to handle the return of data from .get() so I won't go there as it's not pertinent.

How to mock an angular $http call and return a promise object that behaves like $http

Is there a way to return an HttpPromise (or something similar) to mimic a call to $http? I want to set a global variable that indicates whether the real HTTP request is made or whether a fake HttpPromise object is returned with fake data.
For example, I have a service that is similar to this:
angular
.module('myservice')
.factory('MyService', ['$http', function($http) {
return {
get : function(itemId) {
if (isInTestingMode) {
// return a promise obj that returns success and fake data
}
return $http.get("/myapp/items/" + itemId);
}
};
} ]);
And in my controller, I have a call to the aforementioned service that looks similar to this:
// Somewhere in my controller
MyService.get($scope.itemId)
.success(function(data) {
$scope.item = data;
})
.error(function(data, status, headers, config) {
$scope.notFound = true;
});
I'm trying to not change the controller code; I want the success and error chaining to still work when in my "isInTestMode".
Is it possible to fake an HttpPromise in the way that I described in the service?
Below is a revised edition of the "MyService" above (a snippet) containing a success and error on the promise object. But, how do I execute the success method?
return {
get : function(itemId) {
if (isInTestingMode) {
var promise = $.defer().promise;
// Mimicking $http.get's success
promise.success = function(fn) {
promise.then(function() {
fn({ itemId : "123", name : "ItemName"}, 200, {}, {});
});
return promise;
};
// Mimicking $http.get's error
promise.error = function(fn) {
promise.then(null, function(response) {
fn("Error", 404, {}, {});
});
return promise;
};
return promise;
}
return $http.get("/myapp/items/" + itemId);
}
}
Just use the deferred method of the $qservice
var fakeHttpCall = function(isSuccessful) {
var deferred = $q.defer()
if (isSuccessful === true) {
deferred.resolve("Successfully resolved the fake $http call")
}
else {
deferred.reject("Oh no! Something went terribly wrong in your fake $http call")
}
return deferred.promise
}
And then you can call your function like an $http promise (you have to customize whatever you want to put inside of it, of course).
fakeHttpCall(true).then(
function (data) {
// success callback
console.log(data)
},
function (err) {
// error callback
console.log(err)
})
I found that this post is similar to what I was asking.
However, I wanted a way to mock my service call so that fake data could be returned instead of issuing a true HTTP request call. The best way to handle this situation, for me, is to use angular's $httpBackend service. For example, to bypass a GET request to my "items" resource BUT to not bypass GETs of my partials/templates I would do something like this:
angular
.module('myApp', ['ngMockE2E'])
.run(['$httpBackend', function($httpBackend) {
$httpBackend
.whenGET(/^partials\/.+/)
.passThrough();
$httpBackend
.whenGET(/^\/myapp\/items\/.+/)
.respond({itemId : "123", name : "ItemName"});
}]);
See this documentation for more information on $httpBackend.
I finally found a way using jasmin. $httpBackend was no option for me, as there were also non-$http-methods I needed mock on the same service. I also think that the controller test needing to specify the url is not perfect as imho the controller and its test should not need to know about it.
Here is how it works:
beforeEach(inject(function ($controller, $rootScope, $q) {
scope = $rootScope.$new();
mockSvc = {
someFn: function () {
},
someHttpFn: function () {
}
};
// use jasmin to fake $http promise response
spyOn(mockSvc, 'someHttpFn').and.callFake(function () {
return {
success: function (callback) {
callback({
// some fake response
});
},
then: function(callback) {
callback({
// some fake response, you probably would want that to be
// the same as for success
});
},
error: function(callback){
callback({
// some fake response
});
}
}
});
MyCtrl = $controller('MyCtrl', {
$scope: scope,
MyActualSvc: mockSvc
});
}));
You can implement your FakeHttp class:
var FakeHttp = function (promise) {
this.promise = promise;
this.onSuccess = function(){};
this.onError = function(){};
this.premise.then(this.onSuccess, this.onError);
};
FakeHttp.prototype.success = function (callback) {
this.onSuccess = callback;
/**You need this to avoid calling previous tasks**/
this.promise.$$state.pending = null;
this.promise.then(this.onSucess, this.onError);
return this;
};
FakeHttp.prototype.error = function (callback) {
this.onError = callback;
/**You need this to avoid calling previous tasks**/
this.promise.$$state.pending = null;
this.promise.then(this.onSuccess, this.onError);
return this;
};
Then in your code, you would return a new fakeHttp out of the promise.
if(testingMode){
return new FakeHttp(promise);
};
The promise must be asynchronous, otherwise it won't work. For that you can use $timeout.
easy peasy!
You can do it using angular-mocks-async like so:
var app = ng.module( 'mockApp', [
'ngMockE2E',
'ngMockE2EAsync'
]);
app.run( [ '$httpBackend', '$q', function( $httpBackend, $q ) {
$httpBackend.whenAsync(
'GET',
new RegExp( 'http://api.example.com/user/.+$' )
).respond( function( method, url, data, config ) {
var re = /.*\/user\/(\w+)/;
var userId = parseInt(url.replace(re, '$1'), 10);
var response = $q.defer();
setTimeout( function() {
var data = {
userId: userId
};
response.resolve( [ 200, "mock response", data ] );
}, 1000 );
return response.promise;
});
}]);

Resources