This document provides a case study on using Node.js to build enterprise applications. It discusses how the author's company, ARHS Developments, migrated their testing data from multiple copies of MS Access to a centralized web application called Fatman built with Node.js, Express, MongoDB, and other technologies. Fatman uses Mongoose for object modeling and Async for asynchronous control flow. The document outlines Fatman's architecture and how it handles CRUD operations, schemas, middleware, and controllers to provide a more elegant and scalable solution compared to MS Access.
11. Elegant MongoDB object modeling for
Node.js
ORM seemed desirable
Clean object model to use in app code
11
12. Schema
var mongoose = require(‘mongoose’);
mongoose.connect(‘localhost’, ‘fatman’);
var ProjectSchema = new mongoose.Schema({
id : String
, name : String
, users : [String]
});
var Project = mongoose.model('Project', ProjectSchema);
12
13. CREATE/EDIT
var project = new Project({
id: ‘AKCT’
, name: ‘GOCA Newsoft AKCT’
, users: [‘vanbasla’, ‘grosjech’]
});
project.save(function (err){
//… callback after save
});
13
14. RETRIEVE
// find project by id
Project.where(‘id’, ‘AKCT’)
.findOne(function(err, project) {
// do something with search result
});
// find my projects
Project.find({‘users’: username})
.exec(function(err, projects){
// do something with search results
});
14
15. MORE Schema
function trim(value){
return value ? value.trim() : value;
}
function lessThan80chars(value){
return value.length <= 80;
}
var ProjectSchema = new mongoose.Schema({
id : {type: String, required: true, unique: true}
, name : {type: String, set: trim, validate: [
lessThan80chars,
'Value too long. Max 80 characters.']}});
15
16. Advanced
// statics
ProjectSchema.statics.findById = function(projectId, cb){
Project.where('id', projectId).findOne(cb);
};
// methods
ProjectSchema.methods.issueTrackerEnabled = function() {
return this.issueTracker != null;
};
// middleware
ProjectCaseSchema.pre(‘remove’, function(next) {
// do something when a Project is deleted
next();
});
16
17. In fatman
We use
setters
+
pre-save middleware
to keep history of edits.
17
18. In fatman
// creates a setter for field
function setter(field) {
return function setField(newValue) {
this._oldValues = this._oldValues || {};
this._oldValues[field] = this[field];
return newValue;
}
}
var TestCaseSchema = new Schema({
id: {type:Number,index:true},
description: {type:String, set: setter('description')},
history: [Schema.Types.Mixed]
});
18
19. In fatman
// Populate history before save.
TestCaseSchema.pre('save', function (next) {
var self = this
, oldValues = this._oldValues || {};
delete this._oldValues;
this.modifiedPaths.forEach(function (field) {
if (field in oldValues) {
self.history.push({
‘old': oldValues[field],
‘new’: self[field]
});
}
});
next();
});
19
20. Express is a minimal and flexible
node.js web application framework.
Simple and modular
Node de-facto standard
21. Hello Express
var express = require('express'),
consolidate = require('consolidate');
// create an express app
var app = express();
// configure view engine
app.engine('html', consolidate.handlebars);
app.set('views', __dirname + '/views');
// configure a route with an url parameter
app.get('/hello/:name', hello);
function hello(req, res, next){
res.render('hello.html', {
'name' : req.params.name
});
}
app.listen(1337);
console.log('Listening on port 1337');
22. controller
function list (req, res, next){
Project.findById(req.params.projectId, function(err, project){
if (err) return next(err);
TestCase.find({‘project’: project}, function(err, testcases){
if (err) return next(err);
res.render(‘testcases.html’, {
‘project’: project,
‘testcases’: testcases
});
});
});
}
23. controller
function show (req, res, next){
Project.findById(req.params.projectId, function(err, project){
if (err) return next(err);
TestCase.findOne({‘project’:project, ‘id’:id}, function(err, tc){
if (err) return next(err);
res.render(‘testcase.html’, {
‘project’: project,
‘testcase’: tc
});
});
});
}
24. controller
function save (req, res, next){
Project.findById(req.params.projectId, function(err, project){
if (err) return next(err);
var tc = new TestCase(req.body);
tc.project = project;
tc.save(function(err, tc){
if (err) return next(err);
// redirect after post
res.redirect(req.url);
});
});
}
25. MIDDLEWARE
function loadProject(req, res, next){
Project.findById(req.params.projectId, function(err, project){
if (err) return next(err);
res.locals.project = project;
next();
});
}
// before all routes requiring a project
app.all('/:projectId/*', loadProject);
26. BETTER
function list (req, res, next){
var project = res.locals.project;
TestCase.find({‘project’:project}, function(err, testcases){
if (err) return next(err);
res.render(‘testcases.html’, {
‘testcases’: testcases
});
});
}
27. BETTer
function show (req, res, next){
var project = res.locals.project;
TestCase.findOne({‘project’:project, ‘id’:id}, function(err, tc){
if (err) return next(err);
res.render(‘testcase.html’, {
‘testcase’: tc
});
});
}
28. BETTer
function save (req, res, next){
var tc = new TestCase(req.body);
tc.project = res.locals.project;
tc.save(function(err){
if (err) return next(err);
res.redirect(req.url);
});
}
29. pyramid of doom
function search (req, res, next){
Project.findById(projectId, function(err, project){
if (err) return next(err);
TestPlan.findByIdentifier(project, testPlanId, function(err, testPlan) {
if (err) return next(err);
var tags = getTagsFromRequest(req);
TestCase.findByTag(testPlan, tags, function(err, tagQuery, testCases){
if (err) return next(err);
TestCase.countTags(tagQuery, function(err, tagsResult) {
if (err) return next(err);
res.render(‘search’, {
‘testCases’ : testCases,
‘tagsResult’ : tagsResult,
‘project’ : project,
‘testPlan’ : testPlan,
‘tags’ : tags
});
});
});
});
});
}
30. Async.js
Async is a utility module which provides
straight-forward, powerful functions for
working with asynchronous JavaScript
32. MORE async Async.js
var ids = [‘AKCT’, ‘FATMAN’];
var projects = [];
ids.forEach(function(id){
Project.findById(id, function(err, project){
projects.push(project);
});
});
res.render(‘projects’, {‘project’ : projects});
WRONG
33. MORE async Async.js
var ids = [‘AKCT’, ‘FATMAN’];
var projects = [];
async.each(ids, function(id, next){
Project.findById(id, function(err, project){
projects.push(project);
next();
})
}, function(err){
res.render(‘projects’, {‘projects’: projects});
});
34. MORE async Async.js
Collections Control flow
each series
map parallel
filter whilst
reject doWhilst
reduce until
detect doUntil
sortBy waterfall
… …