I'm developing a RESTful API with Node.js, Express and MySql. No ORM used.
I want to reuse as much code as possible in order to handle requests for resources.
I'm going to implement a callback factory and I'd like to hear from you about my approach.
api.js
It is the main application file.
'use strict';
var
http = require('http'),
express = require('express'),
app = express(),
env = app.get('env'),
config = require('./config')[env],
pool = require('./pool')(config.database), /* node-mysql pool */
people = require('./routes/people')(pool); /* route handlers for people resource */
app.use(express.json());
app.use(express.urlencoded());
app.use(app.router);
app.get('/people', people.findAll); /* middleware handling request for the resource */
http.createServer(app).listen(8000);
routes/people.js
It is the file containing code to handle requests for /peopleresource.
'use strict';
module.exports = {
var
async = require('async'),
CallbackFactory = require('../CallbackFactory'),
people = {};
people.findAll = function (req, res, next) {
async.waterfall(
[
CallbackFactory.createCallback('getPoolConnection', pool),
CallbackFactory.createCallback('lastQuery', 'SELECT * FROM person'),
CallbackFactory.createCallback('json', res)
],
CallbackFactory.getCallback('next', next);
);
};
return people;
};
CallbackFactory.js
The module exports the createCallback method only.
The first argument to createCallback is always the callback name.
Other arguments are specific to the callback to create.
'use strict';
var
factoryMethods: {
getPoolConnection: function (pool) {
return function (callback) {
pool.getConnection(function (err, connection) {
callback(err, connection);
};
};
},
lastQuery: function (sql, values) {
return function (connection, callback) {
connection.query(sql, values, function (err, result) {
connection.release();
callback(err, result);
});
};
},
json: function (res) {
return function (result, callback) {
res.json(result);
callback();
};
},
next: function (next) {
return function (err) {
if (err) {
return next(err);
}
};
},
};
module.exports = {
createCallback: function () {
var
args = Array.prototype.slice.call(arguments),
name = args.shift();
return factoryMethods[name].apply(null, args);
}
};
Conclusion
I googled and searched SO in order to find approaches that are useful to code reuse.
I didn't find anything. Maybe it is not the correct/best approach. What do you think about it?
I think you're on the wrong track here. Callback factory is not a common pattern because it's adding a layer of abstraction without reducing complexity in any way. Phrased differently, think of async.waterfall as already providing the abstraction layer that you're looking for. Or, if you like named functions, that I recommend async.auto, which also automatically determines which functions can operation in serial and parallel based on the dependencies you specify.
I would recommend having async.waterfall or async.auto call your functions directly. If you want additional abstraction and less code, use an ORM like Sequelize. But I see your factory complicating understanding your code for anyone else who comes along later.
Related
The code comes from an MDN tutorial on how to use Node.js and mongoose. The idea is to make parallel request to get the count of documents in different models. I don't understand where the callback passed to each async.parallel comes from, where it is defined and what it does, it seems like a dummy function to me. Could you help me understand it? Here is the code:
var Book = require('../models/book');
var Author = require('../models/author');
var Genre = require('../models/genre');
var BookInstance = require('../models/bookinstance');
var async = require('async');
exports.index = function(req, res) {
async.parallel({
book_count: function(callback) {
Book.countDocuments({}, callback); // Pass an empty object as match condition to find all documents of this collection
},
book_instance_count: function(callback) {
BookInstance.countDocuments({}, callback);
},
book_instance_available_count: function(callback) {
BookInstance.countDocuments({status:'Available'}, callback);
},
author_count: function(callback) {
Author.countDocuments({}, callback);
},
genre_count: function(callback) {
Genre.countDocuments({}, callback);
}
}, function(err, results) {
res.render('index', { title: 'Local Library Home', error: err, data: results });
});
};
the callback is passed by the async package.
Explanation:
As async parallel function takes array or object (in your example) of asynchronous tasks and these async tasks require a callback which will get called when its execution completes or if there is an error. So parallel function provides these callback functions and will call your callback ( provided as a second parameter to parallel function call) when all of them are called or got an error in any of them.
You can check the detailed explanation from here - https://caolan.github.io/async/v3/docs.html#parallel
Update:
Consider parallel as a wrapper function like:
function parallel(tasks, userCallback) {
let tasksDone = [];
function callback(err, data){ // this is the callback function you're asking for
if(err){
userCallback(err); // calling callback in case of error
}else {
tasksDone.push(data);
if(tasks.length === tasksDone.length){
userCallback(null, tasksDone); // calling callback when all the tasks finished
}
}
}
tasks.forEach(task => {
task(callback); // calling each task without waiting for previous one to finish
})
}
Note: This is not a proper implementation of parallel function of async, this is just an example to understand how we can use callback function internally and what is its usecase
Being new to Node, I am still having some troubles with callbacks.
In the mapBpiIfindex function I am trying to loop through all of the VLANs found by the vlans function. Once it has looped through all VLANs, creating the map, I want to output the map to the browser. But, the only output I am getting is {}. How can I send the mapping to the browser? I am not even sure if I am using my callbacks correctly.
var express = require('express');
var router = express.Router();
var snmp = require('snmp-native');
// Create a Session with explicit default host, port, and community.
let session = new snmp.Session({ host: 'AASW0120', port: 161, community: 'community' })
let Mibs = {
hostname: [1,3,6,1,2,1,1,5,0],
vlans: [1,3,6,1,4,1,9,9,46,1,3,1,1,2],
dot1dBasePortIfIndex: [1,3,6,1,2,1,17,1,4,1,2]
}
/* Get all VLANs on switch */
function vlans(snmpSession, cb) {
let vlans = []
session.getSubtree({ oid: Mibs.vlans }, function (error, varbinds) {
if (error) {
console.log('Fail :(');
} else {
varbinds.forEach(function (varbind) {
vlans.push(varbind.oid[varbind.oid.length -1])
})
}
cb(vlans)
})
}
/* Map BPIs to Ifindices */
function mapBpiIfindex(session, cb) {
let map = {}
vlans(session, function (vlans) {
vlans.forEach(function (vlan) {
session.getSubtree({oid: Mibs.dot1dBasePortIfIndex, community: 'community#' + vlan}, function (error, varbinds) {
if (error) {
console.log('Fail :(')
} else {
varbinds.forEach(function (varbind) {
map[varbind.oid[varbind.oid.length -1]] = {ifindex: varbind.value, vlan: vlan}
})
}
})
})
cb(map)
})
}
router.get('/vlans', function (req, res, next) {
vlans(session, function (vlans) {
res.send(vlans)
})
})
router.get('/bpi-ifindex', function (req, res, next) {
mapBpiIfindex(session, function (mapping) {
res.send(mapping)
})
})
The answer is no, youre not using it correctly ;)
A few things here:
You should be clear that only the code within the callback is executed after the operation has finished, so cb(map)does not wait until all youre looped callbacks have finished. Thats why nothing is returned (because when cb is called, the async functions have not finished yet and map values are undefined. Have a look at this How do I return the response from an asynchronous call?, its the same principle.
Have a look at async module. Specifically, the do* or whilst methods. It'll help you process loops with async function calls.
Apart from that, you should not use forEach if you mind about performance.
UPDATE
I updated the code below to reflect my solution. It was rather confusing to figure it out but hopefully it will help someone else too.
I'm trying to figure out how to test my routes. The issue I'm running into is, when I make the GET request my node-googleplaces service calls out to the google api. Is there a way to mock out this service so that I can test my route and just fake the data it returns?
controller.js
'use strict';
var path = require('path'),
GooglePlaces = require('node-googleplaces');
exports.placesDetails = function (req, res) {
var places = new GooglePlaces('MY_KEY');
var params = {
placeid: req.params.placeId,
};
//this method call will be replaced by the test stub
places.details(params, function (err, response) {
var updatedResponse = 'updated body here'
res.send(updatedResponse)
});
};
test.js
var should = require('should'),
//seem weird but include it. The new version we're making will get injected into the app
GooglePlaces = require('node-googleplaces');
request = require('supertest'),
path = require('path'),
sinon = require('sinon'),
describe(function () {
before(function (done) {
//create your stub here before the "app" gets instantiated. This will ensure that our stubbed version of the library will get used in the controller rather than the "live" version
var createStub = sinon.stub(GooglePlaces, 'details');
//this will call our places.details callback with the 2nd parameter filled in with 'hello world'.
createStub.yields(null, 'hello world');
app = express.init(mongoose);
agent = request.agent(app);
done();
});
it('should get the data', function (done) {
agent.get('/api/gapi/places/search/elmersbbq')
.end(function (err, res) {
if (err) {
return done(err);
}
console.log(res.body)
done();
});
});
})
The only way I'm thinking about to do it is to change your method to:
exports.placesDetails = function (req, res, places)
create additional method:
exports.placesDetailsForGoogle = function (req, res) {
exports.placesDetails(req, res, new GooglePlaces('MY_KEY'));
}
and write a test that executes placesDetails, passing properly mocked 'places' object. You'll test placesDetails logic with this and at the same time you'll have comfy function to be used in actual code without need to actualy instantiate GooglePlaces object every time.
I've inherited some Node.js code and I need to add some functionality. However, I'm not sure syntactically how to accomplish my goal due to the asynchronous nature of Node. Currently, I have a function defined like this:
return {
myEntryPoint: function(req, res) {
var results = getResults();
res.send(200, results);
}
};
Several pieces are calling this function already. Inside of it, I'm calling a function called getResults which is defined like this:
var getResults = function() {
var results = [];
async.series([
function(callback) {
// add to results
},
function(callback) {
// add to results
}
]);
return results;
};
My problem is, I need to wait until all of the functions inside of the async.series call are made before I return the results. How do I do this?
You could change it to add a callback. Series has an optional callback to run once all the functions have completed within.
var getResults = function(finished) {
var results = [];
async.series([
function(callback) {
// add to results
},
function(callback) {
// add to results
}
], function() {
//Called when series is finished
finished(results);
});
};
And to get the results,
return {
myEntryPoint: function(req, res) {
getResults(function(results) {
res.send(200, results);
});
}
};
The usual way to handle this is to take a callback that uses the argument in question.
results = getResults(args);
someFunction(results);
Is equivalent to
function getResults(args, callback) {
var results;
//do stuff and populate results.
callback(results);
}
getResults(args, someFunction);
Except you don't have the issue of trying to wait for async stuff to happen, which you can't do, without weird logic.
I have code like
common.findOne('list', {'listId': parseInt(request.params. istId)}, function(err, result){
if(err) {
console.log(err);
}
else {
var tArr = new Array();
if(result.tasks) {
var tasks = result.tasks;
for(var i in tasks) {
console.log(tasks[i]);
common.findOne('tasks', {'taskId':parseInt(tasks[i])}, function(err,res){
tArr[i] = res;
console.log(res);
});
}
console.log(tArr);
}
return response.send(result);
}
});
It is not executed sequentially in node.js so I get an empty array at the end of execution. Problem is it will first execute console.log(tArr); and then execute
common.findOne('tasks',{'taskId':parseInt(tasks[i])},function(err,res){
tArr[i] = res;
console.log(res);
});
Is there any mistake in my code or any other way for doing this.
Thanks!
As you are probably aware, things run asynchronously in node.js. So when you need to get things to run in a certain order you need to make use of a control library or basically implement it yourself.
I highly suggest you take a look at async, as it will easily allow you to do something like this:
var async = require('async');
// ..
if(result.tasks) {
async.forEach(result.tasks, processEachTask, afterAllTasks);
function processEachTask(task, callback) {
console.log(task);
common.findOne('tasks', {'taskId':parseInt(task)}, function(err,res) {
tArr.push(res); // NOTE: Assuming order does not matter here
console.log(res);
callback(err);
});
}
function afterAllTasks(err) {
console.log(tArr);
}
}
The main things to see here is that processEachTask gets called with each task, in parallel, so the order is not guaranteed. To mark that the task has been processed, you will call callback in the anonymous function from findOne. This allows you to do more async work in processEachTask but still manage to signify when it is done. When every task is done, it will then call afterAllTasks.
Take a look at async to see all the helper functions that it provides, it is very useful!
I've recently created a simple abstraction named "wait.for" to call async functions in sync mode (based on Fibers): https://github.com/luciotato/waitfor
Using wait.for and async your code will be:
var wait = require('waitfor');
...
//execute in a fiber
function handleRequest(request,response){
try{
...
var result = wait.for(common.findOne,'list',{'listId': parseInt(request.params.istId)});
var tArr = new Array();
if(result.tasks) {
var tasks = result.tasks;
for(var i in tasks){
console.log(tasks[i]);
var res=wait.for(common.findOne,'tasks',{'taskId':parseInt(tasks[i])});
tArr[i] = res;
console.log(res);
}
console.log(tArr);
return response.send(result);
};
....
}
catch(err){
// handle errors
return response.end(err.message);
}
};
// express framework
app.get('/posts', function(req, res) {
// handle request in a Fiber, keep node spinning
wait.launchFiber(handleRequest,req,res);
});