Paramaterize the base URL in an Angular JS $resource - angularjs

I'm using several Angular JS $resource definitions all of which retrieve their base URL from a configuration service. For example:
$resource(config.baseURL() + '/api/v2/foo/:id', {id: '#id'})
$resource(config.baseURL() + '/api/v2/bar/:id', {id: '#id'})
The reason this is done is that the base URL can be changed via a query string parameter when the application is first loaded.
I figured out that (obviously in retrospect) the URL used by the $resource is initialized just once so it's possible to get a race condition where the URL for a particular $resource is initialized before the base URL query string parameter is dealt with. So I tried to change the $resource declaration to this:
$resource(':baseURL/api/v2/foo/:id', {baseURL: config.baseURL(), id: '#id'})
Unfortunately the base URL is getting escaped – the // is converted to %2F%2F – so the whole URL then doesn't work properly.
Is there any way to suppress the escaping for that parameter? (or maybe a better way to solve the problem in general)?

Another way you can tackle this is use a provider and config it in the config stage.
Here is an example of something similar I did a while back.
.provider('Environment', function () {
var environments = {
dev: {
root: 'http://localhost',
port: 3000,
api: '/api',
version: 'v1'
}
};
var selectedEnv = 'dev';
var self = this;
this.setEnvironments = function (envs) {
if (!Object.keys(envs).length)
throw new Error('At least one environment is required!');
environments = envs;
};
this.setActive = function (env) {
if (!environments[env])
throw new Error('No such environment present: ' + env);
selectedEnv = env;
return self.getActive();
};
this.getEnvironment = function (env) {
if (!env)
throw new Error('No such environment present: ' + env);
return environments[env];
};
this.getActive = function () {
if (!selectedEnv)
throw new Error('You must configure at least one environment');
return environments[selectedEnv];
};
this.getApiRoute = function () {
var active = self.getActive();
return active.root + (active.port ? ':' + active.port : '') +
active.api + (active.version ? '/' + active.version : '');
};
this.$get = [function () {
return self;
}];
})
Then in the config phase:
.config(function (EnvironmentProvider) {
EnvironmentProvider.setEnvironments({
dev: {
root: 'http://10.0.0.3',
api: '/api',
version: 'v1'
},
localonly: {
root: 'http://localhost',
api: '/api',
version: 'v1'
},
prod: {
root: 'https://myapp.mybackend.com',
api: '/api',
version: 'v1'
}
});
//Set prod as the active schema
EnvironmentProvider.setActive('prod');
});
Later in some controller/service/factory:
.factory('API',function($resource, Environment){
return {
User: $resource(Environment.getApiRoute() + '/users/:id', {id: '#_id'}),
OtherResource: $resource(Environment.getApiRoute() + '/otherresource/:id', {id: '#_id'})
}
});

Why not make use of the $location service?
For example, how about the following to handle the base url, and, if the application is running from localhost, include the port number? Additionally, be able to include either http or https based on the current URL?
var host = $location.host();
if (host === "localhost")
host += ":" + $location.port();
var url = $location.protocol() + "://" + host + "/whateverElseYouWantInThePath";
and then use url where you need it?

From the Definition of resource,
#param {string} url A parametrized URL template with parameters prefixed by : as in
/user/:username. If you are using a URL with a port number (e.g.
http://example.com:8080/api), you'll need to escape the colon character before the port
number, like this: djResource('http://example.com\\:8080/api').
So you have to define your config.baseURL() as follows,
config.baseUrl = function(){
return 'http://server.com\\:port/rest_part/';
}

Here is a horrible but working workaround. Instead of...
$resource(config.baseURL() + '/api/v2/foo/:id', {id: '#id'})
... which is only evaluated once, use an object that implements String-methods that ngResource evaluates before each request:
var url = {};
url.value = function() {return config.baseURL() + '/api/v2/foo/:id'};
url.split = function (separator,limit) { return url.value().split(separator,limit) };
url.replace = function (match, other) { return url.value().replace(match, other) };
url.toString = function() { return url.value(); }
$resource(url, {id: '#id'})

Related

What is the best practice to manage API paths in an angular app?

I was looking for a way to manage the external API paths into a single location. Currently, I am storing them as a constant object.
var Resources = function () {
var baseURL = 'http://localhost:3000/'
var apiURL = baseURL + 'api/v1/';
return {
URL: {
API: apiURL,
ITEMS: {
INDEX: apiURL + 'items/'
},
CATEGORIES: apiURL + 'categories/',
AUTHORS: apiURL + 'authors/'
}
};
angular
.module('testApp')
.constant('RESOURCES', Resources());
However, now I am facing problem adding nested endpoints
eg: http://localhost:3000/api/v1/items/1/lease
Here, the above method fails, as "item_id" cannot be placed in the constant object
Interesting approach. We store this as a constant as well, but use string replacement for path variables. For instance:
{
"getStatusById": "/status/:id"
}
Then:
var url = endpoints.getStatusById.replace(':id', id)

How to overcome "Access-Control-Allow-Origin" error when client talks to server

So I'm using a yeoman project from swiip called generator-gulp-angular - just do "npm search gulp-angular" and you'll see it.
Out of the box the client is running from 127.0.0.1:3000 and I wish to make a $http call to a (python) service on 127.0.0.1:8080. Its using browser-sync for live reload and proxy middleware to make the proxy calls from client to server. Proxy middleware is per default disabled, so the trick is to enable it and successfully make requests to and receive responses from server. So far i've not been able to successfully enable it :-(
Update: I recreated the yeoman project following the motto "give yourself a simple example" in order to concentrate on the Access-Control issue and I got it to work more or less out of the box without having to resort to modifying the server logic in order to allow cross origin requests. Pleasingly it's as simple as the instructions describe. Here's the proxy file that extends the gulp middleware to perform the proxy, or rather mapping from client to server:
/*jshint unused:false */
/***************
This file proxy.js allows you to configure a proxy system plugged into BrowserSync
in order to redirect backend requests while still serving and watching
files from the web project
IMPORTANT: The proxy is disabled by default.
If you want to enable it, watch at the configuration options and finally
change the `module.exports` at the end of the file
***************/
'use strict';
var proxyMiddleware = require('http-proxy-middleware');
var options = {
target: 'http://127.0.0.1:8080'
};
var proxy = proxyMiddleware('/quote', options);
module.exports = function(){
return [proxy];
}
From the gulpfile (gulpfile.js) we have:
'use strict';
var gulp = require('gulp');
var browserSync = require('browser-sync');
var browserSyncSpa = require('browser-sync-spa');
var util = require('util');
var middleware = require('./proxy');
module.exports = function(options) {
function browserSyncInit(baseDir, browser) {
browser = browser === undefined ? 'default' : browser;
var routes = null;
if(baseDir === options.src || (util.isArray(baseDir) && baseDir.indexOf(options.src) !== -1)) {
routes = {
'/bower_components': 'bower_components'
};
}
var server = {
baseDir: baseDir,
routes: routes
};
//
// Here's the relevant bit
//
server.middleware = middleware();
browserSync.instance = browserSync.init({
startPath: '/',
server: server,
browser: browser
});
}
browserSync.use(browserSyncSpa({
selector: '[ng-app]'// Only needed for angular apps
}));
gulp.task('serve', ['watch'], function () {
browserSyncInit([options.tmp + '/serve', options.src]);
});
..
gulp.task('serve:e2e-dist', ['build'], function () {
browserSyncInit(options.dist, []);
});
};
As you see we're configuring gulp to be aware of the "/quote" context that when used on the client (http://localhost:3000/quote) will get mapped to the backend (http://localhost:8080/quote) - for further information: https://github.com/chimurai/http-proxy-middleware/blob/v0.0.5/README.md
Here's where we make the call on the client using the $http service:
function quotePriceGenerator($q, $http) {
var lowPrice = 1.45000, highPrice = 1.47000;
var askPrice, sellPrice;
var service = {};
service.getPrice = function() {
var deferred = $q.defer();
$http({
url: '/quote',
method: 'GET'
})
.then(function(quote) {
var date = new Date();
const quoteZoom = 100000;
const quotePipsWindow = -3;
..
qBox['trading-size-string'] = qBox['trading-size'].toString();
service.qBox = qBox;
return deferred.resolve(service);
});
// Returns a random integer between min (included) and max (included)
// Using Math.round() will give you a non-uniform distribution!
function getRandomIntInclusive(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
return deferred.promise;
};
return service;
..
function randomizeAskSell(low, high){
}
}
quotePriceGenerator.$inject = ['$q', '$http'];
export default quotePriceGenerator;
The backend, a python tornado REST API, didn't require any configuration for Access-Control-Allow-Origin. Here it is:
from __future__ import division
import tornado.ioloop
import pyrestful.rest
import random
from pyrestful import mediatypes
from pyrestful.rest import get
class Quote(object):
quote_date_time = float
..
sell_price = float
trading_size = int
class QuoteResource(pyrestful.rest.RestHandler):
#get(_path="/quote", _produces=mediatypes.APPLICATION_JSON)
def getQuoteJson(self):
price_min = random.randint(145000,146000)/100000
price_max = random.randint(146000,147000)/100000
..
quote.sell_price = sell_price
quote.trading_size = trading_size
return quote
if __name__ == "__main__":
try:
print("Start the service")
app = pyrestful.rest.RestService([QuoteResource])
app.listen(8080)
tornado.ioloop.IOLoop.instance().start()
except KeyboardInterrupt:
print("\nStop the service")
Some information on the erroneous request/response would be helpful too.
You could try to set the changeOrigin param to true. This will modify the request's host header to match the server's hostname.
var proxyMiddleware = require('http-proxy-middleware');
var options = {
target: 'http://127.0.0.1:8080',
changeOrigin: true // <-- changeOrigin
};
If that doesn't work; you can add the Access-Control-Allow-Origin header to the response:
var proxyMiddleware = require('http-proxy-middleware');
var options = {
target: 'http://127.0.0.1:8080',
onProxyRes: function (proxyRes, req, res) {
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
}
};
http-proxy-middleware onProxyRes option is added in v0.5.0 , so make sure to update it if you're still using v0.0.5
You have to setup a proxy server to forward your requests, setup a reverse proxy, or setup CORS on the back-end to allow the cross-origin request.

Dynamic URL queries with AngularJS and NodeJS, MongoDB

I really do not understand how to handle URLs with queries appended to it.
I have endpoints that accept several parameters like:
?max_length=50,
?min_length=1,
?active=true,
?only_today=true,
?etc...
Via AngularJS how can I set those value dynamically only if the user has checked for those values?
Actually I'm just building an object {} appending those parameters when the $scope is not null. But I don't think it is a good idea.
Same for NodeJS and MongoDB...
How can I get the correct object based on the query string on the URL?
What I'm doing here as well is to split up the URL and checking for the words, etc... I'm sure there is a better way and I can not find it in both documentations and wondering to bigger and bigger URL parameters it start to be hell.
I know this is a real low level question but I don't really understand how to handle it.
Thanks
You can use the $location service for that.
https://docs.angularjs.org/api/ng/service/$location
You can use $resource to easily map your endPoints to your services. You should map your params to what is expected in your api. And if you have conditional parameters, you need to handle undefined params in your backend and ignore these params. For mapping your endpoints in nodeJS check out Restify
For example:
angular.module("myApp", []).factory("myFactory", function($resource) {
var YourResource = $resource('/rest/yourResource');
var factory = {
retriveMyResources: function(paramsQuery) {
return YourResource.query(paramsQuery).$promise;
}
};
return factory;
}).controller("myCtrl", function($scope, myFactory) {
myFactory.retrieveMyResources({
maxLength: $scope.maxLength,
sortBy: $scope.sortBy
}).then(function(result) {
$scope.result = result
})
})
Your node server
//App.js you initialize your server, and include your route files
(function() {
var restify = require("restify");
var server = restify.createServer({
name: "test-server"
});
server.pre(restify.CORS());
server.use(restify.gzipResponse());
server.use(restify.acceptParser(server.acceptable));
server.use(restify.queryParser());
server.use(restify.bodyParser());
server.use(restify.jsonp());
require("./routes/your_resource_route.js")(server);
server.listen("1921", function() {
console.log('%s listening at %s environment: %s', server.name, server.url, process.env.NODE_ENV);
});
})();
Example Route file
module.exports = function(server) {
var yourResourceService = require("services/your_resource_service.js");
server.get("rest/yourResource",
function(req, res, next) {
return yourResourceService.findResources(req.params.maxLength, req.params.sortBy).then(function(resources) {
res.send(200, resources);
next();
}).catch(function(err) {
res.send(500, err);
next();
}).done();
}
);
}
And your service file
module.exports = function(app_context) {
var exampleService = {
findItems: function(maxLength, sortBy) {
var sortObject = {};
sortObject[sortBy || DEFAULT_SORT] = -1;
return Q(brandMongooseCollection.find({}).sort(sortObject).limit(maxLength || DEFAULT_MAX_LENGTH).lean().exec());
}
};
return exampleService;
};

angularjs resource limit changes after first call

Problem description
Im using the angular resource to get data from my server. I've extended it a bit to make sure all of my resources have security headers.
Problem is that on the second get request and on, my get requests are sent with limit=0, and only the first get request is sent correctly (with limit=12).
Code part
This is my base resource factory (for making sure all resource contain the keys and everything):
app.factory('SecuredFactory', function($resource){
var DEFAULT_ACTIONS = {
'get': {method:'GET'},
'query': {method:'GET', isArray:true},
};
var DEFAULT_PARAMS = {
'limit': 12,
'format': 'json'
};
for(var key in DEFAULT_ACTIONS){
DEFAULT_ACTIONS[key]['headers'] = <headers object>;
}
var securedResource = function(url, paramDefaults, actions){
for (var attrname in actions) {
DEFAULT_ACTIONS[attrname] = actions[attrname];
}
for (var attrname in paramDefaults) {
DEFAULT_PARAMS[attrname] = paramDefaults[attrname];
}
var defaultResource = $resource(url, DEFAULT_PARAMS, DEFAULT_ACTIONS);
return defaultResource;
};
return securedResource;
});
And this is an example of how I creat a specific factory out of the secured one:
app.factory('QuestionFactory', function(SecuredFactory, Constants){
var url = Constants.SERVER_URL + 'question/';
var Task = SecuredFactory(url);
return Task;
});
And this is finally how I use it, for example:
// filtering example (not important for this matter):
var filtering = {author: "Daniel"};
var contents = [];
var resource = QuestionFactory;
resource.get(filtering, function (res) {
// success fetching
$scope.contents = $scope.contents.concat(res['objects']);
}
// failed fetching
, function (err) {
}
);
The requests
first request:
question?format=json&limit=12&offset=0
second request and on:
question?format=json&limit=0&offset=0
My problem was that the DEFAULT_PARAMS variable was declared as global. I didn't realize that invoking the secured factory with {limit: 0} will override the global, therefore changing the limit to 0 for ALL of my resources.
Changing the securedFactory to a service and moving the "globals" into the returned function solved it. Had to add new ofcourse before every securedService call.

angular-resource.js encodeUriSegment Issue

I have an issue querying restful resources when the resource uri has several subpaths :
Example :
.factory('SomeFactory', function ($resource) {
return $resource('/path/subPath/otherSubPath/:id', {}, {
show: { method: 'GET', params: { id: '#id' } }
})
}) ;
When I invoke SomeFactory.show in a controller I get the error
the server responded with a status of 400 (Bad Request)
This is because the browser is looking for the uri :
http://server/path/subPath%2FotherSubPat/id
Notice the %2F replacing the / (slash) in the uri , I have tried many tricks in javascript to make this work ; But the only solution was to add the following last line replace(/%2F/gi, '/'); in angular-resource.js encodeUriSegment method .
Please tell me if this approach is correct .
function encodeUriSegment(val) {
return encodeUriQuery(val, true).
replace(/%26/gi, '&').
replace(/%3D/gi, '=').
replace(/%2B/gi, '+').
replace(/%2F/gi, '/');
}
Thanks .
it seems like you have already bumped into this issue opened on github:
https://github.com/angular/angular.js/issues/1388#issue-6979382
where the approach you've taken is suggested. As far as I am concerned, as I had the same issue as you, I preferred to have multiple services (one for each subpath) rather than modifying angular-resource.js - I prefer to not modify core lib files as any updates will wipe those changes.
Hopefully a flag to se the uri encoding will be added to Angular to solve this problem.
You can use http interceptor:
module.constant("interceptULRS",
[new RegExp('.+/path/.*')]);
module.config(['$httpProvider', 'interceptULRS', function($httpProvider, interceptULRS) {
var ENCODED_SLASH = new RegExp("%2F", 'g');
$httpProvider.interceptors.push(function ($q) {
return {
'request': function (config) {
var url = config.url;
for (var i = 0; i < interceptULRS.length; i++) {
var regex = interceptULRS[i];
if (url.match(regex)) {
url = url.replace(ENCODED_SLASH, "/");
// end there is only one matching url
break;
}
}
config.url = url;
return config || $q.when(config);
}
};
});
}]);
Keep in mind that it will intercept all URLs.
Building off of #Pavol I think you can pare the request function down to
var ENCODED_SLASH = new RegExp("%2F", 'g');
var request = function (config) {
var matches = config.url.match(ENCODED_SLASH);
if (matches && matches.length) {
config.url = config.url.replace(ENCODED_SLASH, "/");
}
return config || $q.when(config);
};
if you don't need ENCODED_SLASH elsewhere, and don't care where the "%2F"s are in the url.
As I work more and more with the angular-resource I'm finding using and understanding the $httpProvider.interceptors to be more important.

Resources