Skip to content

How to configure NodeJS

Joe Nelson edited this page Jun 19, 2014 · 3 revisions

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 NodeJS Serverside Code

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;

};

Unit Tests

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();

});
Clone this wiki locally