-
Notifications
You must be signed in to change notification settings - Fork 69
How to configure NodeJS
This directive uses HTTP headers to ask your server to paginate its response. Your server needs to read these headers and also return headers of its own to tell the directive which range is returned and the total number of elements.
Here is an example of how to talk with angular-paginate-anything in ExpressJS.
Example serverside code used in an ExpressJS (version 3) web framework.
Please note these are code extracts you can review and/or use.
This example code will refer to an 'articles' MongoDB collection and has code that uses MongooseJS.
OK, so lets assume in this example the angular-paginate-anything directive uses this URL:
http://localhost:3000/api/v1/articles
The ExpressJS app will have a route like this:
app.get('/api/v1/articles', articles.all);
The controller for this route uses Promises
which calls on the utilsArticles
module:
exports.all = function (req, res) {
utilsArticles.find(req)
.then(
function (articlesObj) {
res.header('Accept-Ranges', 'items');
res.header('Range-Unit', 'items');
res.header('Content-Range', articlesObj.contentRange);
res.jsonp(articlesObj.articles);
},
function (err) {
res.jsonp(500, {error: err});
})
.done();
};
The utilsArticles.find
essentially runs two queries, one for the total count of all items and one for the range of items requested:
The results of both are assembled into a single response object:
var find = function (req) {
// input validation
assert.equal(typeof (req), 'object', 'argument \'req\' must be an object');
var deferred = q.defer();
if (!req.headers.range || !req.headers['range-unit']) {
req.headers.range = '0-4';
req.headers['range-unit'] = 'items';
}
buildFindQuery(req)
.then(function (query) {
// Run two promises and return both
return q.spread([
count(req),
execQuery('find', req, query)
], function (articlesCount, articles) {
// Build Content Range Header
// Ex: Content-Range:0-4/7
var range = utilsValidation.assertValidRange(req.headers.range);
var contentRange = range.rangeFrom + '-' + (range.rangeFrom + (articles.length - 1));
// Response Object
// Content Range Header Ex: Content-Range:0-4/6
// Articles list
var articlesObj = {
contentRange: contentRange + '/' + articlesCount,
articles: articles
};
return articlesObj;
});
})
.then(
function (articlesObj) {
deferred.resolve(articlesObj);
},
function (err) {
deferred.reject(err);
}
)
.done();
return deferred.promise;
}
Query one: total count
var count = function (req) {
// input validation
assert.equal(typeof (req), 'object', 'argument \'req\' must be an object');
var sessionId = req.sessionID || 'undefined',
deferred = q.defer();
Article.count().exec(function (err, count) {
if (err) {
logger.error(err.toString(), {sessionId: sessionId});
deferred.reject('Sorry we are unable to complete this request');
} else {
logger.info('Count', {sessionId: sessionId});
deferred.resolve(count);
}
});
return deferred.promise;
};
Query two: the range of items query built and executed using buildFindQuery
and execQuery
var buildFindQuery = function (req) {
// input validation
assert.equal(typeof (req), 'object', 'argument \'req\' must be an object');
var deferred = q.defer();
var range = utilsValidation.assertValidRange(req.headers.range);
if (!range.isValid) {
deferred.reject('Invalid Range');
}
if (range.isValid) {
/* Determine the limit and skip values based on the range
* Ex: 0-4 will be limit of 5, skip will be zero
* Ex: 5-9 will be limit of 5, skip will be six
*/
var limit = (range.rangeTo - range.rangeFrom) + 1;
var skip = 0;
if (range.rangeFrom > 0) {
skip = range.rangeFrom;
}
var query = Article.find().limit(limit).skip(skip).sort('-created_at');
deferred.resolve(query);
}
return deferred.promise;
};
var execQuery = function (QueryName, req, query) {
// input validation
assert.equal(typeof (req), 'object', 'argument \'req\' must be an object');
var sessionId = req.sessionID || 'undefined',
deferred = q.defer();
query.exec(function (err, articles) {
if (err) {
logger.error(err.toString(), {sessionId: sessionId});
deferred.reject('Sorry we are unable to complete this request');
} else {
logger.info('Query: ' + QueryName, {sessionId: sessionId});
deferred.resolve(articles);
}
});
return deferred.promise;
};
The range headers are validated by buildFindQuery
with utilsValidation.assertValidRange
var assertValidRange = function (range) {
var state = {
isValid: true
};
if (/^[0-9]{1,6}-[0-9]{1,6}$/.test(range) === false) {
state.isValid = false;
}
var parts = range.split('-');
if ((parseInt(parts[0], 10) < parseInt([parts[1]], 10)) === false) {
state.isValid = false;
}
else {
state.rangeFrom = parseInt(parts[0], 10);
state.rangeTo = parseInt([parts[1]], 10);
}
return state;
};
Here's a sampe of the it
blocks for unit tests for the above code:
Controller
it('#all() finds articles', function (done) {
req.headers = {};
var res = {};
res.headers = {};
res.header = function (key, value) {
res.headers[key] = value;
};
res.jsonp = function (articles) {
articles.length.should.equal(1);
res.headers['Accept-Ranges'].should.equal('items');
res.headers['Range-Unit'].should.equal('items');
res.headers['Content-Range'].should.equal('0-0/1');
done();
};
controller.all(req, res);
});
it('#all() handles database errors', function (done) {
// mock expressjs/connect response object
var res = {};
res.jsonp = function (responseCode, responseObject) {
// tests
responseCode.should.equal(500);
responseObject.error.should.equal('Sorry we have a problem finding all articles');
utilsArticle.find.restore();
done();
};
// Stub out find,return a rejected promise object
var stubFindPromise = function () {
var deferred = q.defer();
deferred.reject('Sorry we have a problem finding all articles');
return deferred.promise;
};
sinon.stub(utilsArticle, 'find', stubFindPromise);
// run controller
controller.all(req, res);
});
utilsArticles
it('#find returns a default number of articles sorted from newest to oldest', function (done) {
req.headers = {};
utilsArticle.find(req)
.then(function (response) {
response.contentRange.should.equal('0-4/8');
response.articles.length.should.equal(5);
response.articles[0]._id.should.eql(store.articleId_8);
})
.finally(function () {
done();
})
.done();
});
it('#find returns a subset articles sorted from newest to oldest', function (done) {
req.headers = {
range: '5-7',
'range-unit': 'items'
};
utilsArticle.find(req)
.then(function (response) {
response.contentRange.should.equal('5-7/8');
response.articles.length.should.equal(3);
response.articles[0]._id.should.eql(store.articleId_3);
})
.finally(function () {
done();
})
.done();
});
it('#find handles range header errors', function (done) {
req.headers = {
range: '8-7',
'range-unit': 'items'
};
utilsArticle.find(req)
.then(function () {
throw new Error('Should not be success');
}, function (err) {
err.should.match(/Invalid\ Range/);
})
.finally(function () {
done();
})
.done();
});
it('#find handles database errors', function (done) {
req.headers = {};
// Simulate database driver error.
// Stub mongoose find().sort().exec() to return an error in the callback
var stubFind = {
// limit() returns self
limit: function () {
return this;
},
// skip() returns self
skip: function () {
return this;
},
// sort() returns self
sort: function () {
return this;
},
// exec() callback error
exec: function (callback) {
callback(new Error('Stubbed Article.exec()'));
}
};
// stub find() with mock
sinon.stub(mongoose.Model, 'find').returns(stubFind);
utilsArticle.find(req)
.then(function () {
throw new Error('Should not be success');
}, function (err) {
err.should.match(/unable/);
})
.finally(function () {
mongoose.Model.find.restore();
done();
})
.done();
});
utilsValidation
it('#assertValidRangeUnit validates a range and return a range object', function (done) {
// Add this should assertion, else jshint complains should is defined but not used.
should.exist(done);
utilsValidation.assertValidRange('0-4').should.eql({isValid: true, rangeFrom: 0, rangeTo: 4});
utilsValidation.assertValidRange('10-4').should.eql({isValid: false});
utilsValidation.assertValidRange('4-4').should.eql({isValid: false});
done();
});