├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── index.js ├── package.json └── test ├── index.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.11' 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-range [![Build Status](https://travis-ci.org/purposeindustries/express-range.svg)](https://travis-ci.org/purposeindustries/express-range) 2 | 3 | Express middleware for handling `Range`, `Content-Range`, and `Accept-Ranges` headers. 4 | 5 | ## Install 6 | 7 | Install the [package](http://npmjs.org/package/express-range) with [npm](http://npmjs.org): 8 | 9 | ```sh 10 | $ npm install express-range 11 | ``` 12 | 13 | ## Usage 14 | 15 | Create middleware: 16 | 17 | ```js 18 | var app = express(); 19 | app.use(range({ 20 | accept: 'items', 21 | limit: 10, 22 | })); 23 | ``` 24 | 25 | Uses sane defaults: 26 | 27 | ```js 28 | var items = [{ 29 | name: 'foo', 30 | id: 1 31 | }, { 32 | name: 'bar', 33 | id: 2 34 | }, { 35 | name: 'baz' 36 | }]; 37 | 38 | app.get('/foo', function(req, res) { 39 | res.range({ 40 | first: req.range.first, 41 | last: req.range.last, 42 | length: items.length 43 | }); 44 | res.json(items.slice(req.range.first, req.range.last + 1)); 45 | }); 46 | ``` 47 | 48 | ## API 49 | 50 | ### range(options) 51 | 52 | Creates an `express` middleware. The middleware parses the range `Range` 53 | header, and sets response code `206` if present. It also sets the `Accept-Ranges` 54 | header. 55 | 56 | - `options.accept` - accepted range unit(s) 57 | - `options.limit` - *optional* If range not specified in the request, range `0-(limit-1)` is assumed 58 | - `options.length` - *optional* Collection length, or `Function` (with `function(cb(err, length))` signature). 59 | If not provided, unknown length (`*`) is assumed. 60 | 61 | ### req.range 62 | 63 | POJO, containing the requested range. 64 | 65 | - `req.range.unit` - `Range` unit 66 | - `req.range.first` - First items index (defaults to 0 if no range is specified) 67 | - `req.range.last` - Last items index (defaults to `limit-1` if no range is specified) 68 | - `req.range.suffix` - If the range is suffix-style (`Range: items=-5` - the last 5 items) 69 | 70 | ### res.range(options) 71 | 72 | Set custom response headers (`Content-Range`). By default, the middleware sets 73 | the same response range, that was requested. 74 | 75 | - `options.unit` - Specify range unit (it defaults to the requested unit) 76 | - `options.first` - Specify the first items index (it defaults to the requested one) 77 | - `options.last` - Specify the last items index(it defaults to the requested one) 78 | . `options.length` - Specify the resource lenth (it defaults to middleware default, or `*`) 79 | 80 | ## License 81 | 82 | MIT 83 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parse = require('http-range-parse'); 4 | var format = require('http-content-range-format'); 5 | 6 | module.exports = function middleware(options) { 7 | options = options || {}; 8 | var accept = options.accept || ['items']; 9 | if(typeof accept == 'string') { 10 | accept = [accept]; 11 | } 12 | var limit = options.limit || 10; 13 | var lengthFn = typeof options.length == 'number' 14 | ? function(cb) { cb(null, options.length); } 15 | : typeof options.length == 'undefined' 16 | ? function(cb) { cb(); } 17 | : options.length; 18 | return function contentRange(req, res, next) { 19 | var parsed = req.headers.range 20 | ? parse(req.headers.range) 21 | : accept.length == 1 22 | ? { 23 | unit: accept[0], 24 | first: 0, 25 | last: limit - 1 26 | } 27 | : null; 28 | req.range = parsed; 29 | res.setHeader('Accept-Ranges', accept.join(', ')); 30 | if(req.headers.range) { 31 | res.status(206); 32 | } 33 | lengthFn(function(err, length) { 34 | if(err) { 35 | return next(err); 36 | } 37 | if(accept.length == 1 && typeof parsed.first == 'number' 38 | && typeof parsed.last == 'number') { 39 | res.setHeader('Content-Range', format({ 40 | first: parsed.first, 41 | last: parsed.last, 42 | unit: accept[0], 43 | length: length 44 | })); 45 | } 46 | res.range = function(opts) { 47 | if(!opts.unit && !parsed && accept.length > 1) { 48 | throw new Error('Content-Range unit is ambigous'); 49 | } 50 | res.setHeader('Content-Range', format({ 51 | unit: opts.unit || parsed.unit || accept[0], 52 | first: opts.hasOwnProperty('first') ? opts.first : parsed.first, 53 | last: opts.hasOwnProperty('last') ? opts.last : parsed.last, 54 | length: opts.hasOwnProperty('length') ? opts.length : parsed.length, 55 | })); 56 | }; 57 | next(); 58 | }); 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-range", 3 | "version": "2.0.1", 4 | "description": "Express REST middleware for Content-Range pagination", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "test-bg": "mocha -w" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/purposeindustries/express-range" 13 | }, 14 | "keywords": [ 15 | "express", 16 | "content-range", 17 | "range", 18 | "pagination" 19 | ], 20 | "author": "Purpose Industries ", 21 | "contributors": [ 22 | { 23 | "name": "Bence Dányi", 24 | "email": "bd@purposeindustries.co" 25 | } 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/purposeindustries/express-range/issues" 30 | }, 31 | "homepage": "https://github.com/purposeindustries/express-range", 32 | "devDependencies": { 33 | "express": "^4.6.1", 34 | "mocha": "^1.20.1", 35 | "should": "^4.0.4", 36 | "supertest": "^0.13.0" 37 | }, 38 | "dependencies": { 39 | "http-content-range-format": "^1.0.0", 40 | "http-range-parse": "^1.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var req = require('supertest'); 3 | var middleware = require('../'); 4 | 5 | 6 | describe('Range parsing', function() { 7 | it('should parse range', function(done) { 8 | var app = express(); 9 | 10 | app.use(middleware({ 11 | accept: 'items', 12 | })); 13 | 14 | app.get('/', function(req, res) { 15 | res.json(req.range); 16 | }); 17 | 18 | req(app) 19 | .get('/') 20 | .set('Range', 'items=0-4') 21 | .expect(206) 22 | .expect('Content-Range', 'items 0-4/*') 23 | .end(done); 24 | }); 25 | it('should accept static length', function(done) { 26 | var app = express(); 27 | 28 | app.use(middleware({ 29 | accept: 'items', 30 | length: 10 31 | })); 32 | 33 | app.get('/', function(req, res) { 34 | res.json(req.range); 35 | }); 36 | 37 | req(app) 38 | .get('/') 39 | .set('Range', 'items=0-4') 40 | .expect(206) 41 | .expect('Content-Range', 'items 0-4/10') 42 | .end(done); 43 | }); 44 | it('should accept dynamic length', function(done) { 45 | var app = express(); 46 | 47 | app.use(middleware({ 48 | accept: 'items', 49 | length: function(cb) { 50 | cb(null, 10) 51 | } 52 | })); 53 | 54 | app.get('/', function(req, res) { 55 | res.json(req.range); 56 | }); 57 | 58 | req(app) 59 | .get('/') 60 | .set('Range', 'items=0-4') 61 | .expect(206) 62 | .expect('Content-Range', 'items 0-4/10') 63 | .end(done); 64 | }); 65 | it('should fall back to defaults', function(done) { 66 | var app = express(); 67 | 68 | app.use(middleware({ 69 | accept: 'items', 70 | })); 71 | 72 | app.get('/', function(req, res) { 73 | res.json(req.range); 74 | }); 75 | 76 | req(app) 77 | .get('/') 78 | .expect(200) 79 | .expect('Content-Range', 'items 0-9/*') 80 | .end(done); 81 | }); 82 | it('should allow overrides', function(done) { 83 | var app = express(); 84 | 85 | app.use(middleware({ 86 | accept: 'items', 87 | })); 88 | 89 | app.get('/', function(req, res) { 90 | res.range({ 91 | unit: 'foo', 92 | first: 1, 93 | last: 2, 94 | length: 8 95 | }) 96 | res.json(req.range); 97 | }); 98 | 99 | req(app) 100 | .get('/') 101 | .expect(200) 102 | .expect('Content-Range', 'foo 1-2/8') 103 | .end(done); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --------------------------------------------------------------------------------