Skip to content

Commit

Permalink
Merge pull request #8 from seegno/bugfix/range-parse
Browse files Browse the repository at this point in the history
Fix `limit` and `offset` interpretation
  • Loading branch information
fixe committed Jan 28, 2015
2 parents b4b63e1 + eb20bf0 commit 161dbb0
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 74 deletions.
1 change: 1 addition & 0 deletions .jshintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
14 changes: 5 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand Down
12 changes: 0 additions & 12 deletions errors/invalid-range-error.js

This file was deleted.

12 changes: 12 additions & 0 deletions errors/range-not-satisfiable-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

/**
* Module dependencies.
*/

var errors = require('create-error');

/**
* Export `RangeNotSatisfiableError`.
*/

module.exports = errors('RangeNotSatisfiableError', { status: 416 });
55 changes: 33 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,73 @@
*/

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`.
*/

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) {
throw new MalformedRangeError();
}

// 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;
};
};
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
84 changes: 55 additions & 29 deletions test/koa-paginate_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
});

Expand All @@ -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();
});
});

0 comments on commit 161dbb0

Please sign in to comment.