Skip to content

Commit

Permalink
Option to always include headers, address range-not-satisfiable (#87)
Browse files Browse the repository at this point in the history
* Add alwaysIncludeHeaders option for out-of-range pages and empty result sets

* Add rangeNotSatisfiableStatusCode option, update docs
  • Loading branch information
devinivy committed May 8, 2020
1 parent 3e18216 commit ade4c6e
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 9 deletions.
6 changes: 4 additions & 2 deletions README.md
Expand Up @@ -20,7 +20,7 @@ Version 4.x.x is inteded for use with scoped @hapi packages (v19, requires Node

Version 3.x.x is inteded for use with scoped @hapi packages (v18).

## Version 2.0.0
## Version 2.0.0

Version 2.0.0 is intended for use with Hapi 17.x.x and above, do not use this version for version below 17.x.x of Hapi.

Expand Down Expand Up @@ -81,7 +81,7 @@ By default the plugin will generate a metadata object alongside your resources i
You can also decide to put the metadata in the response header, so the body remains clean.
In this case, the plugin will use 2 kinds of headers:
* `Content-Range`: `startIndex-endIndex/totalCount` (see https://tools.ietf.org/html/rfc7233#section-4.2)
This header gives the start and end indexes of the current page records, followed by the total number of records.
This header gives the start and end indexes of the current page records, followed by the total number of records. In the case that `meta.alwaysIncludeHeaders` is `true` and the result set is empty the value will instead take the form `*/totalCount` per RFC 7233.
* `Link`: `<url>; rel=relationship` (see https://tools.ietf.org/html/rfc2068#section-19.6.2.4)
This header gives the url to different resources related to the current page. The available relationships are :
+ `rel=self`: the current page
Expand Down Expand Up @@ -113,6 +113,8 @@ You can customize the metadata with the following options:
* `page`: The page number requested. Default name is page, disabled by default.
* `limit`: The limit requested. Default name is limit, disabled by default.
* `location`: 'body' put the metadata in the response body, 'header' put the metadata in the response header. Default is 'body'.
* `alwaysIncludeHeaders`: when `location` is 'header' and this option is set to `true`, the Content-Range and Link headers will be set even when the result set is empty, and when the entire result set fits on a single page.
* `rangeNotSatisfiableStatusCode`: when `alwaysIncludeHeaders` is `true` and the requested page is out of range of the result set, this status code will be used.
* `successStatusCode`: HTTP response status code when returning paginated data, undefined by default so the code set by the application prevails.

#### The results
Expand Down
10 changes: 10 additions & 0 deletions lib/config.js
Expand Up @@ -34,6 +34,16 @@ internals.schemas.options = Joi.object({
}),
meta: Joi.object({
location: Joi.string().valid('body', 'header'),
alwaysIncludeHeaders: Joi.boolean()
.when('location', {
is: 'header',
otherwise: Joi.forbidden()
}),
rangeNotSatisfiableStatusCode: Joi.number().integer().min(200).less(500)
.when('alwaysIncludeHeaders', {
is: true,
otherwise: Joi.forbidden()
}),
successStatusCode: Joi.number().integer().min(200).less(300),
baseUri: Joi.string().allow(''),
name: Joi.string().required(),
Expand Down
12 changes: 9 additions & 3 deletions lib/ext.js
Expand Up @@ -188,7 +188,8 @@ module.exports = class Ext {
if (this.config.meta.location === 'header') {
delete request.response.headers['total-count']

if (totalCount > currentLimit && results.length > 0) {
if (this.config.meta.alwaysIncludeHeaders ||
(totalCount > currentLimit && results.length > 0)) {
// put metadata in headers rather than in body
const startIndex = currentLimit * (currentPage - firstPage)
const endIndex = startIndex + results.length - firstPage
Expand All @@ -208,10 +209,15 @@ module.exports = class Ext {

request.response.headers[
'Content-Range'
] = `${startIndex}-${endIndex}/${totalCount}`
] = startIndex > endIndex
? `*/${totalCount}`
: `${startIndex}-${endIndex}/${totalCount}`

request.response.headers.Link = links

if (this.config.meta.successStatusCode) {
if (startIndex > endIndex && this.config.meta.rangeNotSatisfiableStatusCode) {
request.response.code(this.config.meta.rangeNotSatisfiableStatusCode)
} else if (this.config.meta.successStatusCode) {
request.response.code(this.config.meta.successStatusCode)
}
}
Expand Down
125 changes: 121 additions & 4 deletions test/test.js
Expand Up @@ -927,8 +927,41 @@ describe('Override default values', () => {
expect(statusCode).to.equal(200)
const headers = res.request.response.headers
const response = res.request.response.source
expect(headers['Content-Range']).to.not.exist
expect(headers.Link).to.not.exist
expect(headers['Content-Range']).to.not.exist()
expect(headers.Link).to.not.exist()
expect(response).to.be.an.array()
expect(response).to.have.length(20)
})

it('Override meta location - move metadata to http headers with unique page and forced header inclusion', async () => {
const options = {
meta: {
location: 'header',
alwaysIncludeHeaders: true,
successStatusCode: 206
}
}

const server = register()
await server.register({
plugin: require(pluginName),
options
})

const res = await server.inject({
method: 'GET',
url: '/users'
})
const statusCode = res.request.response.statusCode
expect(statusCode).to.equal(206)
const headers = res.request.response.headers
const response = res.request.response.source
expect(headers['Content-Range']).to.equal('0-19/20')
expect(headers.Link).to.be.an.array()
expect(headers.Link).to.have.length(3)
expect(headers.Link[0]).match(/rel="self"$/)
expect(headers.Link[1]).match(/rel="first"$/)
expect(headers.Link[2]).match(/rel="last"$/)
expect(response).to.be.an.array()
expect(response).to.have.length(20)
})
Expand Down Expand Up @@ -1046,8 +1079,92 @@ describe('Override default values', () => {
expect(statusCode).to.equal(200)

const headers = res.request.response.headers
expect(headers['Content-Range']).to.not.exist
expect(headers.Link).to.not.exist
expect(headers['Content-Range']).to.not.exist()
expect(headers.Link).to.not.exist()

const response = res.request.response.source
expect(response).to.be.an.array()
expect(response).to.have.length(0)
})

it('Override meta location - using range not satisfiable status code when requested page is out of range', async () => {
const options = {
query: {
limit: {
default: 5
},
page: {
default: 5
}
},
meta: {
location: 'header',
alwaysIncludeHeaders: true,
rangeNotSatisfiableStatusCode: 416
}
}

const server = register()
await server.register({
plugin: require(pluginName),
options
})

const res = await server.inject({
method: 'GET',
url: '/users'
})
const statusCode = res.request.response.statusCode
expect(statusCode).to.equal(416)

const headers = res.request.response.headers
expect(headers['Content-Range']).to.equal('*/20')
expect(headers.Link).to.exist()

const response = res.request.response.source
expect(response).to.be.an.array()
expect(response).to.have.length(0)
})

it('Override meta location - set metadata if requested page is out of range when using forced header inclusion', async () => {
const options = {
query: {
limit: {
default: 5
},
page: {
default: 5
}
},
meta: {
location: 'header',
alwaysIncludeHeaders: true,
successStatusCode: 206
}
}

const server = register()
await server.register({
plugin: require(pluginName),
options
})

const res = await server.inject({
method: 'GET',
url: '/users'
})

const statusCode = res.request.response.statusCode
expect(statusCode).to.equal(206)

const headers = res.request.response.headers
expect(headers['Content-Range']).to.equal('*/20')
expect(headers.Link).to.be.an.array()
expect(headers.Link).to.have.length(4)
expect(headers.Link[0]).match(/rel="self"$/)
expect(headers.Link[1]).match(/rel="first"$/)
expect(headers.Link[2]).match(/rel="last"$/)
expect(headers.Link[3]).match(/rel="prev"$/)

const response = res.request.response.source
expect(response).to.be.an.array()
Expand Down

0 comments on commit ade4c6e

Please sign in to comment.