I'm writing an API for simple GET and POST routes. The app is a shared to-do-list. Users and lists are stored separately. A list can belong to many users, and a user can have many lists.
// User
{
"_id": "54435e1424afb4c412f388bc",
"email": "[email protected]",
"lists": [
"54435eca24afb4c412f388bd",
"54435f1a24afb4c412f388be",
"54435f1f24afb4c412f388bf"
],
"name": "Batman"
},
// List
{
"name": "Weekend",
"users": [
"54435e1424afb4c412f388bc",
"543931f2894684f416b7b272"
],
"tasks": [
"Beat the Joker",
"Buy groceries"
],
"_id": "54435eca24afb4c412f388bd"
}
The POST route adds a list to the database, and makes sure to also writes the list's ID to the affected users.
My first version was a very deep callback pyramid (WIP can be seen here http://pastebin.com/BfB0yHKg). I decided to use Q promises to get around this, and now my (still WIP) code looks like below. Lots of helpers, and the execution is at the bottom. I'd appreciate some feedback on the general approach and promise pattern, but details are welcome too.
Note 1: The seemingly unnecessary conversions of async functions to promise versions are meant to unify the patterns and throw everything in the promise chain. I'd love some feedback on that thought. Note 2: Helpers will get more general and pulled out of the route function eventually!
router.post('/', function(req, res) {
console.log('Creating a new list');
var pParseRequest = function() {
var deferred = Q.defer();
try {
var list = JSON.parse(req.body.list);
deferred.resolve(list);
} catch (error) {
var e = new Error(error);
e.name = 'ParseError';
deferred.reject(e);
}
return deferred.promise;
};
var pValidateList = function(list) {
var deferred = Q.defer();
if (!list.name || list.name === '') {
var e = new Error('Name missing or empty');
e.name = 'DataError';
deferred.reject(e);
}
if (!list.users || list.users.length < 1) {
var e = new Error('Users missing or empty')
e.name = 'DataError';
deferred.reject(e);
}
if (!list.tasks) {
var e = new Error('Tasks missing')
e.name = 'DataError';
deferred.reject(e);
}
deferred.resolve(list);
return deferred.promise;
};
var pConnectDB = function(collection) {
var deferred = Q.defer();
try {
var coll = req.db.get(collection);
deferred.resolve(coll);
} catch (error) {
var e = new Error(error);
e.name = 'DBError';
deferred.reject(e);
}
return deferred.promise;
};
var pInsert = function(item, collection) {
var deferred = Q.defer();
collection.insert(item, function(error, doc) {
if (error) {
var e = new Error(error);
e.name = 'DBError';
deferred.reject(e);
} else {
deferred.resolve(doc);
}
});
return deferred.promise;
};
var pUpdateUsers = function(list, usersCollection) {
var promises = [];
list.users.forEach(function(v, i) {
var deferred = Q.defer();
promises.push(deferred.promise);
usersCollection.find({_id: v}, function(error, docs) {
if (error) {
var e = new Error(error);
e.name = 'DBError';
deferred.reject(new Error(e));
} else if (docs.length !== 1) {
var e = new Error('Found no or too many users');
e.name = 'DBError';
deferred.reject(e);
} else {
var lists = docs[0].lists;
lists.push(list._id);
Q.fcall(function() {
pUpdateUserLists(v, lists, usersCollection);
})
.then(function() {
deferred.resolve();
});
}
});
});
return Q.all(promises)
.then(function(f) {
return list;
});
};
var pUpdateUserLists = function(user, lists, collection) {
var deferred = Q.defer();
collection.update({_id: user}, { $set: { 'lists': lists } }, function(error, docs) {
if (error) {
var e = new Error(error);
e.name = 'DBError';
deferred.reject(e);
} else {
deferred.resolve();
}
});
return deferred.promise;
};
var getStatusCode = function(message) {
switch (message) {
case 'ParseError':
case 'DataError':
return 400;
break;
case 'DBError':
return 500;
break;
default:
return 500;
break;
}
};
// GO!
pParseRequest()
.then(function(list) {
return pValidateList(list);
})
.then(function(list) {
return pConnectDB('lists')
.then(function(listsCollection) {
return pInsert(list, listsCollection);
})
.then(function(insertedList) {
return pConnectDB('users')
.then(function(usersCollection) {
return pUpdateUsers(insertedList, usersCollection);
})
.then(function(list) {
console.log('Responding with list');
res.jsonp(list);
return list;
});
});
})
.fail(function(error) {
console.error('Error in the promise chain', error);
var code = getStatusCode(error.name);
res.status(code).send(error.message || 'Something went wrong');
})
.done();
});