diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.jshintignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index 58741c6..d26927c 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,15 @@ Choose your preferred method: ### Configuration The middleware can be configured with the following parameters: -- Limit: Default number of items per page (20 items by default). - Maximum: Maximum number of items allowed per page (50 items by default). You can change the defaults by doing: ```js paginate({ - limit: 10, maximum: 100 }); ``` - -**Note** the default limit value is used only in the absence of the `Range` header. - ## Usage ```js @@ -39,18 +34,19 @@ app.get('/', paginate(), function *() { // `paginate` middleware will inject a `pagination` object in the `koa` context, // which will allow you to use the `pagination.offset` and `pagination.limit` // in your data retrieval methods. - this.body = Foobar.getData({ + this.body = foobar.getData({ limit: this.pagination.limit, offset: this.pagination.offset }); - // This is needed in order to expose the count in `Content-Range` header. - this.pagination.count = Foobar.count(); + // This is needed in order to expose the length in `Content-Range` header. + this.pagination.length = foobar.count(); }); app.listen(3000); ``` + ### Request You can provide the `Range` header specifying the items you want to retrieve. For instance to retrieve the first 5 elements: @@ -67,7 +63,7 @@ This will generate a response with the following `Content-Range` header: 'Content-Range: items 0-4/*' ``` -The `*` will be replaced with the total number of items provided in the `pagination.count` variable. +The `*` will be replaced with the total number of items provided in the `length` variable. ## Running tests diff --git a/errors/invalid-range-error.js b/errors/invalid-range-error.js deleted file mode 100644 index acba985..0000000 --- a/errors/invalid-range-error.js +++ /dev/null @@ -1,12 +0,0 @@ - -/** - * Module dependencies. - */ - -var errors = require('create-error'); - -/** - * Export `InvalidRangeError`. - */ - -module.exports = errors('InvalidRangeError', { status: 412 }); diff --git a/errors/range-not-satisfiable-error.js b/errors/range-not-satisfiable-error.js new file mode 100644 index 0000000..356d9ea --- /dev/null +++ b/errors/range-not-satisfiable-error.js @@ -0,0 +1,12 @@ + +/** + * Module dependencies. + */ + +var errors = require('create-error'); + +/** + * Export `RangeNotSatisfiableError`. + */ + +module.exports = errors('RangeNotSatisfiableError', { status: 416 }); diff --git a/index.js b/index.js index 5c7cd0a..b88f673 100644 --- a/index.js +++ b/index.js @@ -4,10 +4,10 @@ */ var _ = require('lodash'); -var InvalidRangeError = require('./errors/invalid-range-error'); var MalformedRangeError = require('./errors/malformed-range-error'); -var contentRange = require('content-range'); -var parseRange = require('range-parser'); +var RangeNotSatisfiableError = require('./errors/range-not-satisfiable-error'); +var contentRangeFormat = require('http-content-range-format'); +var rangeSpecifierParser = require('range-specifier-parser'); /** * Export `PagerMiddleware`. @@ -15,21 +15,21 @@ var parseRange = require('range-parser'); module.exports = function(options) { options = _.assign({ - limit: 20, maximum: 50 }, options); return function *paginate(next) { - // Ensure that `limit` is never higher than `maximum`. - var limit = options.limit > options.maximum ? options.maximum : options.limit; + var first = 0; + var last = options.maximum; var maximum = options.maximum; - var offset = 0; + var unit = 'bytes'; + // Handle `Range` header. if (this.get('Range')) { - var range = parseRange(maximum + 1, this.get('Range')); + var range = rangeSpecifierParser(this.get('Range')); if (range === -1) { - throw new InvalidRangeError(); + throw new RangeNotSatisfiableError(); } if (range === -2) { @@ -37,29 +37,40 @@ module.exports = function(options) { } // Update `limit` and `offset` values. - limit = range[0].end; - offset = range[0].start; + first = range.first; + last = range.last; + unit = range.unit; } - // Set range values on context. + // Set pagination object on context. this.pagination = { - limit: limit, - offset: offset + limit: last + 1, + offset: first }; + // Prevent pages with more items than allowed. + if ((last - first + 1) > maximum) { + last = first + maximum - 1; + } + yield* next; - // Fix limit value if is higher than count. - if (limit > this.pagination.count) { - limit = this.pagination.count; + var length = this.pagination.length; + + // Fix `last` value if `length` is lower. + if (last > length) { + last = length - 1; } // Set `Content-Range` based on available items. - this.set('Content-Range', contentRange.format({ - count: this.pagination.count, - limit: limit, - name: 'items', - offset: this.pagination.offset + this.set('Content-Range', contentRangeFormat({ + first: first, + last: last, + length: length, + unit: unit })); + + // Set the response as `Partial Content`. + this.status = 206; }; }; diff --git a/package.json b/package.json index d35b5ab..9427c55 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,12 @@ "node": ">= v0.11.13" }, "dependencies": { - "content-range": "0.2.0", "create-error": "0.3.1", "debug": "2.1.0", + "http-content-range-format": "1.0.0", "lodash": "2.4.1", - "range-parser": "1.0.2" + "range-specifier-parser": "0.1.0", + "util": "0.10.3" }, "devDependencies": { "chai": "1.10.0", diff --git a/test/koa-paginate_test.js b/test/koa-paginate_test.js index 3a67b9b..a25473e 100644 --- a/test/koa-paginate_test.js +++ b/test/koa-paginate_test.js @@ -7,6 +7,7 @@ var chai = require('chai'); var koa = require('koa'); var paginate = require('../'); var request = require('./request')(); +var util = require('util'); chai.should(); @@ -15,16 +16,15 @@ chai.should(); */ describe('paginate', function() { - it('should accept a `limit` option', function *() { + it('should use the default values', function *() { var app = koa(); - app.use(paginate({ - limit: 5 - })); + app.use(paginate()); yield request(app.listen()) .get('/') - .expect('Content-Range', 'items 0-4/*') + .expect(206) + .expect('Content-Range', 'bytes 0-49/*') .end(); }); @@ -37,85 +37,111 @@ describe('paginate', function() { yield request(app.listen()) .get('/') - .expect('Content-Range', 'items 0-2/*') + .expect(206) + .expect('Content-Range', 'bytes 0-2/*') .end(); }); - it('should set `Content-Range` headers by default', function *() { + it('should accept a `Range` header', function *() { var app = koa(); app.use(paginate()); yield request(app.listen()) .get('/') - .expect('Content-Range', 'items 0-19/*') + .set('Range', 'items=0-5') + .expect(206) + .expect('Content-Range', 'items 0-5/*') .end(); }); - it('should accept a `Range` header', function *() { + it('should give an error if the `Range` is malformed', function *() { var app = koa(); app.use(paginate()); yield request(app.listen()) .get('/') - .set('Range', 'items=0-5') - .expect('Content-Range', 'items 0-4/*') + .set('Range', 'invalid') + .expect(412, 'Precondition Failed') .end(); }); - it('should allow specifying a `count` variable in the pagination', function *() { + it('should give an error if the `Range` is invalid', function *() { var app = koa(); app.use(paginate()); - app.use(function *(next) { - this.pagination.count = 10; + yield request(app.listen()) + .get('/') + .set('Range', 'bytes=5-1') + .expect(416, 'Range Not Satisfiable') + .end(); + }); - yield* next; - }); + it('should not allow `limit` value superior to `maximum`', function *() { + var app = koa(); + + app.use(paginate({ + maximum: 3 + })); yield request(app.listen()) .get('/') - .expect('Content-Range', 'items 0-9/10') + .expect(206) + .set('Range', 'items=0-5') + .expect('Content-Range', 'items 0-2/*') .end(); }); - it('should give an error if the `Range` is malformed', function *() { + it('should not allow `limit` value superior to `length`', function *() { var app = koa(); app.use(paginate()); + app.use(function *() { + this.pagination.length = 3; + }); + yield request(app.listen()) .get('/') - .set('Range', 'invalid') - .expect(412, 'Precondition Failed') + .expect(206) + .set('Range', 'items=0-5') + .expect('Content-Range', 'items 0-2/3') .end(); }); - it('should give an error if the `Range` is invalid', function *() { + it('should set `limit` to `N+1` when `Range` is `items=0-N`', function *() { var app = koa(); + var n = 5; app.use(paginate()); + app.use(function *() { + this.pagination.limit.should.equal(n + 1); + }); + yield request(app.listen()) .get('/') - .set('Range', 'items=5-1') - .expect(412, 'Precondition Failed') + .expect(206) + .set('Range', util.format('items=0-%s', n)) .end(); }); - it('should not allow `limit` value superior to `maximum`', function *() { + it('should set `offset` to `N` when `Range` is `items=N-5`', function *() { var app = koa(); + var n = 2; - app.use(paginate({ - limit: 5, - maximum: 3 - })); + app.use(paginate()); + + app.use(function *() { + this.pagination.offset.should.equal(n); + }); yield request(app.listen()) .get('/') - .expect('Content-Range', 'items 0-2/*') + .expect(206) + .set('Range', util.format('items=%s-5', n)) .end(); }); });