Dynamic routes with Express.js -- is this even possible? - node.js

Every time I update the database with a new menu item, I'm trying to get the routing to update with one more route. Here's my sad little ugly attempt:
Here in app.js, I check the menu database and shazaam...routes are made on the fly at startup. Cool!:
// in app.js //
var attachDB = function(req, res, next) {
req.contentdb = db.content;
req.menudb = db.menu;
req.app = app; // this is the express() app itself
req.page = PageController;
next();
};
db.menu.find({}, function (err, menuitems){
for(var i=0; record = menuitems[i]; i++) {
var menuitem = record.menuitem;
app.all('/' + menuitem, attachDB, function(req, res, next) {
console.log('req from app all route: ',req)
PageController.run(menuitem, req, res, next);
});
}
http.createServer(app).listen(config.port, function() {
console.log(
'\nExpress server listening on port ' + config.port
);
});
});
Not real elegant but it's a proof of concept. Now here's the problem: When I save a new menu item in my Admin.js file, the database get's updated, the router seems to get updated but something about the request just blows up after clicking on a menu link with a dynamically created route
Many things in the request seem to be missing and I feel like there is something fundamental I don't understand about routing, callbacks or perhaps this is just the wrong solution. Here's what the function responsible for creating a new menu item and creating a new route in my Admin.js file looks like:
// in Admin.js //
menuItem: function(req, res, callback) {
var returnMenuForm = function() {
res.render('admin-menuitem', {}, function(err, html) {
callback(html);
});
};
var reqMenudb = req.menudb,
reqContentdb = req.contentdb,
reqApp = req.app,
reqPage = req.page;
if(req.body && req.body.menuitemsubmitted && req.body.menuitemsubmitted === 'yes') {
var data = { menuitem: req.body.menuitem };
menuModel.insert( data, function(err) {
if (err) {
console.log('Whoa there...',err.message);
returnMenuForm();
} else {
// data is inserted....great. PROBLEM...the routes have not been updated!!! Attempt that mimics what I do in app.js here...
reqApp.all('/' + data.menuitem, function(req, res, next) {
// the 2 db references below are set with the right values here
req.contentdb = reqContentdb;
req.menudb = reqMenudb;
next();
}, function(req, res, next) {
reqPage.run(data.menuitem, req, res, next);
});
returnMenuForm();
}
});
} else {
returnMenuForm();
}
},
Saving the data in the admin section works fine. If you console log app.routes, it even shows a new route which is pretty cool. However after refreshing the page and clicking the link where the new route should be working, I get an undefined error.
The admin passes data to my Page controller:
// in PageController.js //
module.exports = BaseController.extend({
name: "Page",
content: null,
run: function(type, req, res, next) {
model.setDB(req.contentdb); /* <-- problem here, req.contentdb is undefined which causes me problems when talking to the Page model */
var self = this;
this.getContent(type, function() {
var v = new View(res, 'inner');
self.navMenu(req, res, function(navMenuMarkup){
self.content.menunav = navMenuMarkup;
v.render(self.content);
});
});
},
getContent: function(type, callback) {
var self = this;
this.content = {}
model.getlist(function(records) {
if(records.length > 0) {
self.content = records[0];
}
callback();
}, { type: type });
}
Lastly, the point of error is here in the model
// in Model.js //
module.exports = function() {
return {
setDB: function(db) {
this.db = db;
},
getlist: function(callback, query) {
this.db.find(query || {}, function (err, doc) { callback(doc) });
},
And here at last, the 'this' in the getlist method above is undefined and causes the page to bomb out.
If I restart the server, everything works again due to my dynamic loader in app.js. But isn't there some way to reload the routes after a database is updated?? My technique here does not work and it's ugly to be passing the main app over to a controller as I'm doing here.

I would suggest two changes:
Move this menu attachment thing to a separate module.
While you're at it, do some caching.
Proof of concept menu db function, made async with setTimeout, you'll replace it with actuall db calls.
// menuitems is cached here in this module. You can make an initial load from db instead.
var menuitems = [];
// getting them is simple, always just get the current array. We'll use that.
var getMenuItems = function() {
return menuitems;
}
// this executes when we have already inserted - calls the callback
var addMenuItemHandler = function(newItem, callback) {
// validate that it's not empty or that it does not match any of the existing ones
menuitems.push(newItem);
// remember, push item to local array only after it's added to db without errors
callback();
}
// this one accepts a request to add a new menuitem
var addMenuItem = function(req, res) {
var newItem = req.query.newitem;
// it will do db insert, or setTimeout in my case
setTimeout(function(newItem){
// we also close our request in a callback
addMenuItemHandler(newItem, function(){
res.end('Added.');
});
}, 2000);
};
module.exports = {
addMenuItem: addMenuItem,
getMenuItems: getMenuItems
}
So now you have a module menuhandler.js. Let's construct it and use it in our app.
var menuHandler = require('./menuhandler');
var app = express();
// config, insert middleware etc here
// first, capture your static routes - the ones before the dynamic ones.
app.get('/addmenuitem', menuHandler.addMenuItem);
app.get('/someotherstaticroute', function(req, res) {
var menu = menuHandler.getMenuItems();
res.render('someview', {menu: menu});
});
// now capture everything in your menus.
app.get('/:routename', function(req, res){
// get current items and check if requested route is in there.
var menuitems = menuHandler.getMenuItems();
if(menuitems.indexOf(req.params.routename) !== -1) {
res.render('myview', {menu: menuitems});
} else {
// if we missed the route, render some default page or whatever.
}
});
app.get('/', function(req, res) {
// ...
});
Now you don't go to db if there were no new updates (since menuitems array is always up to date) so your initial view is rendered faster (for that 1 db call, anyway).
Edit: oh, I just now saw your Model.js. The problem there is that this refers to the object you have returned:
{
setDB: function(db) {
this.db = db;
},
getlist: function(callback, query) {
this.db.find(query || {}, function (err, doc) { callback(doc) });
}
}
So, no db by default. And since you attach something to the app in the initial pageload, you do get something.
But in your current update function, you attach stuff to the new app (reqApp = req.app), so now you're not talking to the original app, but another instance of it. And I think that your subsequent requests (after the update) get the scope all mixed up so lose the touch with the actual latest data.

In your code when you start your server it reads from the menu db and creates your routes. When your menu changes, you do not re-read from db again.
I suggest you do something like the following
app.all('*', function(req, res) {
//read from your menu db and do the the route management yourself
});

Related

Retrieve from firebase and render in ejs

So I'm basically trying to do a "foreach" loop for my ejs frontend but I can't seem to res.render the snapshot in the server.js. I can't seem to get the variable into the res.render(). I have no problem retrieving the data from firebase FYI.
I've already tried various methods such as moving it in the ref.on etc
app.get('/', function (req, res) {
var ref = database.ref('Courses');
// var ref = firebase.database().ref("users");
ref.on("value", function (snapshot) {
snapshot.forEach(function (childSnapshot) {
var childData = childSnapshot.val();
});
});
res.render('pages/index', {
ChildData: childData
});
});
Data is loaded from Firebase asynchronously, since it may take some time. Instead of waiting for the data to come back from the server, the main code of your app continues straight away. Then when the data is available, your callback is called with that data.
This means that any code that needs the data from the database needs to be inside that callback.
app.get('/', function(req, res) {
var ref = database.ref('Courses');
ref.once("value", function (snapshot) {
var childData;
snapshot.forEach(function (childSnapshot) {
childData = childSnapshot.val();
});
res.render('pages/index', {
ChildData:childData
});
});
});
The main changes:
I moved the res.render into the callback, so that it runs after the data is available.
I declare the var childData before the loop, so that it's also available to the code outside of the loop.
I use once instead of on, so that the data is only loaded once.
Use this
app.get('/', function(req, res) {
var ref = database.ref('Courses');
// var ref = firebase.database().ref("users");
ref.on("value", function (snapshot) {
snapshot.map(function (childSnapshot) {
var childData = childSnapshot.val();
});
res.render('pages/index', {
ChildData:childData
});
});
});
Move the response send method inside the callback of ref.on and change forEach to map becuase map is synchronous so it will complete the iterations first and will send the data in response

Can't update the mongodb document on the fly

I am quite new to Node.js and MongoDB. I am trying to have this code in which I have a boolean document called test which is initially "false" in Mongodb and I want to alert that when a page /hello is loaded. then go to another page submit a form and update test to true and load /hello again. so this time it should alert true (when I check the database it has been updated to true) but it doesn't alert anything when I test it. Would be great if you let me know when am doing wrong!
here are the relevant codes in my app.js
app.post("/Submitted", function(req,res){
var conditions = mongoose.model('users').findOne({username: req.session.user.username}, function (err, doc){
doc.test = true;
doc.save();
res.redirect('hello');
});
app.get('/hello', function (req, res) {
var umbrella = req.session.user.test;
if (req.session.user) {
res.render('5points', {test: test});
} else {
res.redirect('/');
}
});
and here is my jade file:
script.
function check() {
var test= !{JSON.stringify(test)};
alert(test);
body(onload= "check()")
The res.redirect('hello') is getting triggered before the doc.save gets completed.Remember node.js is non blocking I/O.So you need to call the res.redirect inside the callback of save.
app.post("/Submitted", function(req, res) {
var conditions = mongoose.model('users').findOne({
username: req.session.user.username
}, function(err, doc) {
doc.test = true;
doc.save(function(err) {
if (err) return res.json(message: "error");
res.redirect('hello');
});
}
});
});

how to send a message from inside a callback function with mongodb in nodejs

I am coding a basic project manager, nothing fancy. I am writing the page where the project is created (with AngularJS) and am sending all the $scope to /create (the backend is Express.js). The router gets the JSON perfectly, and save it to a local MongoDB without problems.
My problem is that I want to set a message telling that the project was created successfully and send it back to AngularJS. This is my code.
router.js
module.exports = function(app, db) {
app.post('/create', function (req, res) {
var create = require("./../scripts/create")(req, res, db);
console.log(create); //just for testing whether I can receive the message.
});
}
create.js
module.exports = function(req, res, db) {
db.collection('projects').insert(req.body.project, function(err, docs) {
if (err) throw err;
return 'Project created.'; //I want to return this string.
});
};
I don't know how to return something from inside the db.collection.insert's callback function.
So you have to remember that anonymous function calls in JavaScript are not assigned to anywhere. They are passed, and then lost. This is usually why we don't really have return statements in them.
var x = function () { return y }; gives x the value of y but since there is never an assignment of the value of a callback, a return statement is meaningless. Callbacks, no matter if they have a return value, will not give you a value. They may feed that return value up to the function that they were given to, but they are entirely lost to you.
The way to get around this is to do some trickery with the scope. Basically what you want to do is 'bump' the value you want to return up a scope you can assign and then return it there. For example, you can do this:
module.exports = function(req, res, db) {
var stringToReturn;
db.collection('projects').insert(req.body.project, function(err, docs) {
if (err) throw err;
stringToReturn = 'Project created.'; //I want to return this string.
});
return stringToReturn;
};
This will work because the return value gets bound to module.exports, which is in turn bound to the result of
var create = require('./create');
console.log(create('something')) //should log 'Project created.'
Solved!!
Router.js
module.exports = function(app, db) {
app.post('/create', function(req, res) {
var create = require("./../scripts/create")(req, res, db);
});
});
Create.js
module.exports = function(req, res, db) {
db.collection('projects').insert(req.body.project, function(err, records) {
if (err) throw err;
res.send("Project created.");
});
};
Now Angular is receiving the response from the server.

Node.js code hints

Hello I would like to know if I'm right with this kind of code and if there is a better way to do it.
There is a module to get the homepage.
app.js:
[...]
// Get homepage
app.get('/', frontend.index);
[...]
The frontend.index function have to get a couple of data obj from redis (util.getRemoteConfig and util.getGlobalStats) and use them to render the Jade template, so I did this.
frontend.js:
[...]
// [GET] Homepage
exports.index = function(req, res){
var remoteConfig = {};
var globalStats = {};
util.getRemoteConfig(function(remoteConfig) {
var version = 'n.a.';
if (remoteConfig.version)
version = remoteConfig.version;
util.getGlobalStats(function(globalStats) {
res.render('index.jade',
{ title: config.items.default_title, version: version, global_stats: JSON.stringify(globalStats) }
);
});
});
};
[...]
The util module gets the data from redis and pass it via callback.
util.js:
[...]
exports.getRemoteConfig = function(cb) {
client.hget('remote_config', function(err, obj) {
if (err) return cb(err);
// do something with obj and return it
cb(obj);
});
};
exports.getGlobalStats = function(cb) {
client.hgetall("global_stats", function (err, obj) {
if (err) return cb(err);
// do something with obj and return it
cb(obj);
});
};
[...]
This is working fine but is it really correct? Can I do something better than this?
Thanks any hint will be useful.
If remote config and global stats are route independent, you should probably put them into a middle-ware instead of controller:
middlewares.js:
exports.remoteConfig = function(req,res,next){
util.getRemoteConfig(function(err,config){
if(err){
return next(err);
}
req._remote_config = config;
return next(null);
});
}
controllers.js:
exports.index = function(req, res){
var remoteConfig = req._remote_config;
var globalStats = req._global_stats;
res.render('index.jade', {
title: config.items.default_title,
version: version,
global_stats: JSON.stringify(globalStats)
};
};
app.js:
app.get('/', middleware.remoteConfig, middleware.globalStats,controllers.index);
or
app.use(middleware.remoteConfig);
app.use(middleware.globalStats);
if the middleware should be used for all routes.
Note: the code is untested.

Send multiple DB query results to a single view using Express

I have a dashboard view ( dashboard.jade ) that will display two panels with different information, all that info should be retrieved from a database and then sent to the view.
Let's say i have a route file ( document.js ) with two actions defined:
exports.getAllDocuments = function(req, res){
doc = db.model('documents', docSchema);
doc.find({}, function(err, documents) {
if (!err) {
// handle success
}
else {
throw err;
}
});
};
exports.getLatestDocumentTags = function(req, res){
tags = db.model('tags', tagSchema);
tags.find({}, function(err, docs) {
if (!err) {
// handle success
}
else {
throw err;
}
});
};
These functions would only serve the porpuse of retrieving data from the database.
Now i would like to send that data to the dashboard view from my dashboard.js route file under exports.index function where i render my dashboard view.
The problem is, since the db calls will be async i wouldn't have access to the data before i could call the view.
I guess i could have an action that simply did all my db calls and through callbacks deliver all the data at once to the view but that would make my data retrieval actions not reusable.
I'm really confused on how to tackle this problem correctly, probably i'm getting this async thing all wrong. Can someone give me some hints on how to do this properly ?
Here's something to pique your interest.
//Check out the async.js library
var async = require('async');
//Set up your models once at program startup, not on each request
//Ideall these would be in separate modules as wel
var Doc = db.model('documents', docSchema);
var Tags = db.model('tags', tagSchema);
function index(req, res, next) {
async.parallel({ //Run every function in this object in parallel
allDocs: async.apply(Doc.find, {}) //gets all documents. async.apply will
//do the equivalent of Doc.find({}, callback) here
latestDocs: async.apply(Tags.find, {})
], function (error, results) { //This function gets called when all parallel jobs are done
//results will be like {
// allDocs: [doc1, doc2]
// latestDocs: [doc3, doc4]
// }
res.render('index', results);
});
}
exports.index = index;
};
Try some more tutorials. If you haven't had the "a ha" moment about how async programming works in node, keep going through guided, hand-held tutorials before trying to write brand new programs without guidance.
//Check out the async.js library and mangoose model
var mongoOp = require("./models/mongo");
var async = require('async');
router.get("/",function(req,res){
var locals = {};
var userId = req.params.userId;
async.parallel([
//Load user Data
function(callback) {
mongoOp.User.find({},function(err,user){
if (err) return callback(err);
locals.user = user;
callback();
});
},
//Load posts Data
function(callback) {
mongoOp.Post.find({},function(err,posts){
if (err) return callback(err);
locals.posts = posts;
callback();
});
}
], function(err) { //This function gets called after the two tasks have called their "task callbacks"
if (err) return next(err); //If an error occurred, we let express handle it by calling the `next` function
//Here `locals` will be an object with `user` and `posts` keys
//Example: `locals = {user: ..., posts: [...]}`
res.render('index.ejs', {userdata: locals.user,postdata: locals.posts})
});

Resources