Break program flow with express.js - javascript

I wonder what would be the proper way of breaking the flow of the program and redirect to some route.
Something like the equivalent of a header redirection but using route names instead of the complete URL.
This is quite common to be found in PHP frameworks and quite powerful, and I haven't found yet the way of doing it with express.js.
Imagine this scenario:
function getUser() {
//...
//getting user..
//Here I would like to break the flow of the program
if (error) {
res.redirect('add');
}
//If there was no error we keep the normal flow
//...
}
router.get('/add', function (req, res, next) {
var user = getUser();
var date = //whatever
var type = //whatever
var event = createEvent(user, date, type);
res.render('add', {
date: date,
type: type,
whatever: whatever,
csrfToken: req.csrfToken(),
message: req.flash('error')
});
});
A normal render wouldn't be what I want, because I don't want having to get again all the variables needed to render the view, I would like the route I want to redirect to deal with it.
I was thinking about using writeHead, but that would require the whole URL, not just the route URL.
Any ideas or recommendations?

You could return false or null or some other value from getUser() to indicate a break. Or check the state of the response (e.g. check res._headerSent === true or res.finished === true).
Another possibility might be to look into making getUser() a middleware function. Example:
function getUser(req, res, next) {
// ...
if (error)
return res.redirect('add');
// If there was no error we keep the normal flow
// ...
// you would add user data to `req` here instead of returning it
req.user = ...
next();
}
router.get('/add', getUser, function(req, res, next) {
var user = req.user;
var date = //whatever
var type = //whatever
var event = createEvent(user, date, type);
res.render('add', {
date: date,
type: type,
whatever: whatever,
csrfToken: req.csrfToken(),
message: req.flash('error')
});
});

Related

Node.js express - get route params without going into the route function for quick analytics tracking

I aim to send an analytics event for each route in my API. As opposed to saving an event with the entire full route that was called, I want to save the base url as the event and the parameters of the route as variables of the event. For example, when saving an analytics event...
Not this:
{
event_title: "API User Event"
category: "domain.com/api/user_routes/route_1/value_of_param_one"
}
But this:
{
event_title: "API User Event"
category: "domain.com/api/user_routes/route_1"
params: {
param_one: "value_of_param_one"
}
}
I'd like to have a global function that gets the parameters from the request variable, however, if you do this on a higher level (not route level)
app.use('/api/user_routes/*', myWrapperFunction)
myWrapperFunction will detect anything after /api/user_routes as parameters. From my experiments, I was only able to successfully detect the actual parameters inside a specific route function. However, that approach requires me to either edit each route function or wrap it like so...
router.get('/route_1/:param_one', (req, res) => Utility.analyticsEvent(userController.routeOneFunction, req, res));
router.get('/route_2/:param_one', (req, res) => Utility.analyticsEvent(userController.routeTwoFunction, req, res));
router.get('/route_3/:param_one', (req, res) => Utility.analyticsEvent(userController.routeThreeFunction, req, res));
Is there a way to detect the actual parameters of the route without actually going into the function itself? Or is this a limitation on express because it won't know the specifics of the route until it finds the first matching option traversing down the routes?
Edit If there is no way to know the parameters before express matches the specific route: is there a function that you can run before executing the route function that will tell you which route will be matched and will specify the parameters?
Welcome all comments!
I think one approach is to write a middleware like below.
// This will get executed before every request. As we'll add this with app.use() with top level middlewares
function customMiddleware (req, res, next) {
let url = req.baseUrl;
// some if else logic to re-route
if( url.includes('/api/user_routes')) {
let urlSplit = url.split();
if( url[urlSplit.length() - 1] == 'param_one' ) {
res.redirect('/api/user_routes/route_1')
}
else if(url[urlSplit.length() - 1] == 'param_tow' ) {
res.redirect('/api/user_routes/route_1')
}
// and so on
} else {
// executes other middleware or go to matching route
next()
}
}
app.use(customMiddleware)
Found a way to do it after the call is made by overwriting the response.json function
app.use(function (req, res, next) {
var json = res.json;
res.json = function (body) {
// Send analytics event before returning response
try {
// Routes that don't need to be tracked with analytics
let notIncludeRoutes = [
"some_not_tracked_route"
];
let route = req.baseUrl + req.route.path;
if(notIncludeRoutes.indexOf(route) === -1) {
// Track route and params
let route_params = res.req.params;
Utility.analyticsEvent(route, route_params);
}
} catch(error) {
// Don't block call if there was an error with analytics, but do log it
console.log("ANALYTICS ERROR: ", error);
}
json.call(this, body);
};
next();
});

Expressjs middleware static typed path and path with param conflict

I'm about to implement REST endpoints for authenticated users and non-authenticated users in expressjs. My current understanding of REST has led me to following design pattern:
User resource
Token user:
/users/self GET, PATCH
Non-token user:
/users POST
/users/:user_id GET
Profile image resource
Token user:
/users/self/profile-images POST
/users/self/profile-images/:profile_image_id PUT, DELETE
Non-token user:
/users/:user_id/profile-images GET
I'm struggling to figure out how to use this pattern without having :user_id parameter become self, i.e {user_id: 'self'}. I would want them to act as two isolated path types without interference, one strict and one dynamic. Is this possible? If so, how?
A code example of my current implementation looks like following:
// instPrivateUserRestRoutes.js (token-user)
router.use('/users/self', [
instAccountRestRoutes(restControllers),
instAuthenticationSessionRestRoutes(restControllers),
instPrivateProfileImageRestRoutes(restControllers)
])
// instPublicUserRestRoutes.js (non-token user)
router.use('/users/:user_id', [
instPublicProfileImageRestRoutes(restControllers)
])
// instRestApp.js (mount point for top resources)
router.use(instPrivateUserRestRoutes(restControllers))
router.use(instPublicUserRestRoutes(restControllers))
What you could do it to create a conditional routing middleware. The factory dunction takes as first argument a callback method that determines which router should be use, and as second argument a list of routers, and returns a new middle war that conditionally uses one of the routes.
function conditionalRouting(cond, routes) {
return function (req, res, next) {
try{
// get the index for the router that should be used
var idx = cond(req, res)
// call this router and pass req, res and next to it
routes[idx](req, res, next)
} catch(err) {
// in the case an error occurs in on of the steps "throw" that error
next(err)
}
}
}
You could then use it that way:
app.use(
'/users/:user_id',
conditionalRouting(
(req, _) => req.params.user_id === 'self' ? 0:1,
[routerForSelf, routerForUser]
))
An other way would be to handle this case explicitly with a middle ware that triggers a not found error:
function forceNotFound(req, res, next) {
var error = new Error('resource not found')
error.status = 404
next(error)
}
And add this as your last middleware
router.use('/users/self', [
instAccountRestRoutes(restControllers),
instAuthenticationSessionRestRoutes(restControllers),
instPrivateProfileImageRestRoutes(restControllers),
forceNotFound
])
This way it is clear that express should stop at that point.
This will look different to what the standard Cannot GET /the/path would look like, but you normally don't want to display those default error messages anyway.
If you want to have the same kind of message then you could write:
var parseUrl = require('parseurl')
var encodeUrl = require('encodeurl')
app.use((req, res, next) => {
var msg = 'Cannot ' + req.method + ' ' + encodeUrl(parseUrl.original(req).pathname)
var error = new Error(msg)
error.status = 404
next(error)
})
If you only want to handled numeric ids you could use this:
router.use('/users/:user_id(\\d+)', [
instPublicProfileImageRestRoutes(restControllers)
])
That way instPublicProfileImageRestRoutes would only be called if user_id is numberic.

Making ajax response global and usable in all routes

I am writing a node js express api application which needs to a couple of ajax calls in the beginning of the application when it starts up and it needs to use this "global" data for all the requests it receives.
In the beginning of my app.js, I have
var users = require('./modules/users');
in my users.js, I make the ajax calls to retrieve my users.
//ajax call 1
request.post({url:url1, formData: data}, function optionalCallback(err, httpResponse, body) {
//some code
 and ajax call 2
request.get(url2, function(error, response, body) {
var response = JSON.parse(body);
var users = response.users;
 // I want to make this users global and available in all routes 
}).auth(null, null, true, access_token);

});
My route looks like this
router.post('/create', users.validate, function(req, res, next) {
// ...
}
and users.js is a module where I want access to the users list.
var users = {
validate: function () {
return function (req, res, next) {
// Global variable
var user_list; // This should be the response from the ajax requests
var user_id = req.query.user_id;
return user_list.indexOf(user_id) != -1
}
}
}
module.exports = users;
How do I make the users global so that I can use it in each route?
If there is an alternate better way of doing this, please suggest.
You can create a module for storing the user information. You can attain the functionality by simply using global variables in NodeJS, but I like this approach because its modular and extensible for checks and hooks down the development line.
Below is a simple example.
user-store.js
var userData = null;
module.exports = {
setUserData : function (data){
userData = data;
},
getUserData : function (){
return userData;
}
};
In your AJAX call, you can cache the store the data in the module by doing something like this as you need:
users.js
var userStore= require('./user-store');
ajaxCall()
.then((response) => {
var users = response.users;
userStore.setUserData(users);
});
To use the user data globally, you can import user-store.js and call getUserData function.
wherever-you-need.js
var userStore= require('./user-store');
var userData = userStore.getUserData();
Let me know if this approach serves the purpose for you.

Express app.get given an array

I'm reading code from https://github.com/FrankHassanabad/Oauth2orizeRecipes which demonstrate the use of OAuth2orize, which can be used to implement OAuth2 authorization server.
The question I'm asking is nothing fancy though. I just have trouble with the basics of Express 3.x.
In app.js:
oauth2 = require('./oauth2')
. . .
app.get('/dialog/authorize', oauth2.authorization);
In Oauth2.js:
exports.authorization = [
login.ensureLoggedIn(),
server.authorization(function (clientID, redirectURI, scope, done) {
db.clients.findByClientId(clientID, function (err, client) {
if (err) {
return done(err);
}
if(client) {
client.scope = scope;
}
// WARNING: For security purposes, it is highly advisable to check that
// redirectURI provided by the client matches one registered with
// the server. For simplicity, this example does not. You have
// been warned.
return done(null, client, redirectURI);
});
}),
function (req, res, next) {
//Render the decision dialog if the client isn't a trusted client
//TODO Make a mechanism so that if this isn't a trusted client, the user can recorded that they have consented
//but also make a mechanism so that if the user revokes access to any of the clients then they will have to
//re-consent.
db.clients.findByClientId(req.query.client_id, function(err, client) {
if(!err && client && client.trustedClient && client.trustedClient === true) {
//This is how we short call the decision like the dialog below does
server.decision({loadTransaction: false}, function(req, callback) {
callback(null, { allow: true });
})(req, res, next);
} else {
res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client });
}
});
}
];
So, is it because app.get() can take an array of middlewares? I'm trying to find where the code to app.get() is to figure out but I can't find it.
EDIT:
I'm on Express 3.6. So according to Infer-on's answer, correct me if I'm wrong.
You mean oauth2.authorization array instead of module?
app.VERB goes to this._router[method].apply(this._router, arguments);
where arguments is an array-like object with exactly one item, which is the oauth2.authorization array.
Then goes to router/index.js in the function defined by:
methods.forEach(function(method){
Router.prototype[method] = function(path){
var args = [method].concat([].slice.call(arguments));
this.route.apply(this, args);
return this;
};
});
Here, what previously was arguments is now path. And then becomes args. So the original array given by oauth2.authorization is still there and is an item inside args which has the length of 2, the first item is the method name "get" and the second is the array.
this.route is defined in the same file:
Router.prototype.route = function(method, path, callbacks){
var method = method.toLowerCase()
, callbacks = utils.flatten([].slice.call(arguments, 2));
// ensure path was given
if (!path) throw new Error('Router#' + method + '() requires a path');
// ensure all callbacks are functions
callbacks.forEach(function(fn){
if ('function' == typeof fn) return;
var type = {}.toString.call(fn);
var msg = '.' + method + '() requires callback functions but got a ' + type;
throw new Error(msg);
});
// create the route
debug('defined %s %s', method, path);
var route = new Route(method, path, callbacks, {
sensitive: this.caseSensitive,
strict: this.strict
});
// add it
(this.map[method] = this.map[method] || []).push(route);
return this;
};
Since there is utils.flatten([].slice.call(arguments, 2)); the array from oauth2.authorization gets flattened. So it's as if the things sent weren't array but normal arguments. (I don't know what the "2" is doing). The 3rd of the oauth2.authorization is the callback that's easy to understand. The first is login.ensureLoggedIn() which is a middleware? The second is server.authorization()..but I'm not entirely sure what it's doing.
for the get method, after the first argument, application will add the route, then will pass the other arguments to related controller
this._router[method].apply(this._router, arguments);
app.js
app.get('/', routes.index);
index.js
// controller
exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
application.js
methods.forEach(function(method){
app[method] = function(path){
if ('get' == method && 1 == arguments.length) return this.set(path);
// deprecated
if (Array.isArray(path)) {
console.trace('passing an array to app.VERB() is deprecated and will be removed in 4.0');
}
// if no router attached yet, attach the router
if (!this._usedRouter) this.use(this.router);
// setup route
this._router[method].apply(this._router, arguments);
return this;
};
});
so
app.get('/dialog/authorize', oauth2.authorization);
for the /dialog/authorize view will be passed the authorization method exported by oauth2.authorization module
EDIT
I'm not sure of the array export, try something like Implement Authorization Endpoint:
app.get('/dialog/authorize',
login.ensureLoggedIn(),
server.authorization(function (clientID, redirectURI, scope, done) {
db.clients.findByClientId(clientID, function (err, client) {
if (err) {
return done(err);
}
if(client) {
client.scope = scope;
}
// WARNING: For security purposes, it is highly advisable to check that
// redirectURI provided by the client matches one registered with
// the server. For simplicity, this example does not. You have
// been warned.
return done(null, client, redirectURI);
});
}),
function (req, res, next) {
//Render the decision dialog if the client isn't a trusted client
//TODO Make a mechanism so that if this isn't a trusted client, the user can recorded that they have consented
//but also make a mechanism so that if the user revokes access to any of the clients then they will have to
//re-consent.
db.clients.findByClientId(req.query.client_id, function(err, client) {
if(!err && client && client.trustedClient && client.trustedClient === true) {
//This is how we short call the decision like the dialog below does
server.decision({loadTransaction: false}, function(req, callback) {
callback(null, { allow: true });
})(req, res, next);
} else {
res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client });
}
});
});

node.js & express: for loop and `app.get()` to serve articles

I'm working on a node js express application that uses app.get() to serve the different web addresses. So app.get('/',{}); serves the home page, app.get('/login'{ }); serves the login page, etc.
Is it good practice to programatically serve pages using a for loop, like in this example?
var a = ["a", "b", "c"];
a.forEach(function(leg) {
app.get('/' + leg, function(req, res){
res.render('index.ejs', {
word : leg
});
});
});
index.ejs is just
<p><%= word %> </p>
So that site.com/a, site.com/b, and site.com/c are all webpages.
I want to utilize this to run .foreach() on a list of all of the article titles (coming from a database of stored articles).
EDIT: The website allows users to submit posts, which become "articles" in a database. I want this loop to route to new submitted articles after they've been posted. If I want to do app.get('/' + newPageName); for user submitted pages AFTER I've already started the server with node server.js, how is that achieved?
Make use of middlewares to better handle the requests. I assume you will have tens/hundreds of posts, adding routes for them like what you've done, is not so elegant.
Consider the following code; I am defining routes of /posts/:legId form. We will match all requests to this route, fetch the article and render it.
If there are handful of routes to be defined you could make use of regular expression to define them.
// dummy legs
var legs = {
a: 'iMac',
b: 'iPhone',
c: 'iPad'
};
app.get('/posts/:leg', fetch, render, errors);
function fetch(req, res, next) {
var legId = req.params.leg;
// add code to fetch articles here
var err;
if (!legs[legId]) {
err = new Error('no such article: ' + legId);
}
req.leg = legs[legId];
next(err);
}
function render(req, res, next) {
res.locals.word = req.leg;
res.render('index');
}
function errors(err, req, res, next) {
console.log(err);
res.locals.error = err.message;
// render an error/404 page
res.render('error');
}
Hope this helps, feel free to ask me any questions you may have.
No, you should not be generating route handlers like that. This is what route parameters are for.
When you start a path component (folder) with a : in an Express route, Express will match any URL that follows the pattern automatically, and place the actual value in req.params. For example:
app.get('/posts/:leg', function(req, res, next) {
// This will match any URL of the form /post/something
// -- but NOT /post/something/else
if (/* the value of req.params.leg is valid */) {
res.render('index.ejs', { word: req.params.leg });
} else {
next(); // Since the user requested a post we don't know about, don't do
// anything -- just pass the request off to the next handler
// function. If no handler responds to the request, Express
// defaults to sending a 404.
}
});
In the real world, you'd probably determine if the leg param is valid by doing a database lookup, which entails making an async call:
app.get('/posts/:leg', function(req, res, next) {
db.query(..., req.params.leg, function(err, result) {
if (err) next(err); // Something went wrong with the database, so we pass
// the error up the chain. By default, Express will
// return a 500 to the user.
else {
if (result) res.render('index.ejs', result);
else next();
}
});
});

Categories