Hapi plugin to handle 'custom' resources pagination in json only.
npm install hapi-pagination --save
Version 4.x.x is inteded for use with scoped @hapi packages (v19, requires Node versions >= 12).
Version 3.x.x is inteded for use with scoped @hapi packages (v18).
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.
Post an issue if you encounter a bug or an error in the documentation. Create a new pull request if you want to add a new functionality or do any change that might get the plugin better.
I'm looking for new ideas to make the plugin better, if you have any, post an issue !
The plugin is supporting get method only for now, tell me if you need it tu support other methods too.
Check the release log: https://github.com/fknop/hapi-pagination/releases
Note: If you're reading this on npm, the README may not be up to date.
The plugin works with settings that you can override. You can override the default value of a setting and even the default name. It allows you to customize your calls to your API to suit your needs.
See the default options object below.
zeroIndex
: Defaults to false
; change this to true
if you want to start paginating at page 0 instead of page 1The plugin accepts query parameters to handle the pagination, you can customize these parameters with the following options:
limit
: The number of resources by page. Default value is 25, default name is
limit.page
: The number of the page that will be returned. Default value is 1,
default name is page.pagination
: Allows you to enable, disable pagination for one request. Default
value is true (enabled), default name is pagination, enabled by default.invalid
: This is NOT
a query parameter, but it allows you to customize the
behavior if the validation of limit and page fails. By default, it sets the
defaults, you can set it to 'badRequest' that will send you a 400 - Bad Request
.Notes:
request.query
.pagination
parameter will still be accessible through
request.query
By default the plugin will generate a metadata object alongside your resources in the response body. 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. 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 pagerel=first
: the first pagerel=prev
: the previous pagerel=next
: the next pagerel=last
: the last pageTo choose where the metadata will be put, use the option meta.location (see below). You can customize the metadata with the following options:
name
: The name of the metadata object. Default is 'meta'.baseUri
: The base uri for the generated links. Default is undefined. Relative URI can be achieved with an empty stringcount
: The number of rows returned. Default name is count. Enabled by default.totalCount
: The total numbers of rows available. Default name is totalCount.
Enabled by default.pageCount
: The total number of pages available. Default name is pageCount,
enabled by default.self
: The link to the requested page. Default name is self, enabled by
default.previous
: The link to the previous page. Default name is previous, enabled by
default. null if no previous page is available.next
: Same than previous but with next page.hasNext
: A boolean indicating there is a next page. Disabled by defaulthasPrevious
: A boolean indicating there is a previous page. Disabled by defaultfirst
: Same than previous but with first page.last
: Same than previous but with last page.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.name
: the name of the results array, results by default. Useful with location='body' only.reply
: Object with:
paginate
: The name of the paginate method (see below), paginate by
default.include
: An array of routes that you want to include, support * and regex.
Default to '*'.exclude
: An array of routes that you want to exclude. Useful when include is
'*'. Default to empty array. Support regex.You can override the page, limit, and pagination default value on route level. You can also force enable or disable the pagination on a route level. This is useful when you're using regex for example.
config: {
plugins: {
pagination: {
// enabled: boolean - force enable or force disable
defaults: {
// page: override page
// limit: override limit
// pagination: override if pagination is false or true by
// default
}
}
}
}
The method is an helper method. This is a shortcut for:
h.response({results: results, totalCount: totalCount});
You can change names of fields (results
, totalCount
) using reply options.
reply: {
results: {
name: 'rows'
},
totalCount: {
name: 'count'
}
}
You can also reply the array and set the totalCount by adding the totalCount (with whatever name you chose) to the request object.
request.totalCount = 10;
h.response(results);
The paginate
method also offers a way to add custom properties to your response. You just have to
pass an object as first parameter and pass a options.key
parameter which is the name of the key of the paginated results.
For example:
return h.paginate({ results: [], otherKey: 'value', otherKey2: 'value2' }, 0, { key: 'results' });
The response will also contains otherKey
and otherKey2
. Nested keys for the paginated results are not allowed.
If you pass an object but forgot to pass a key for your results, the paginate method will throw an error. Same thing if the key does not exist.
Please note that if you pass metadata in headers the custom properties don't work, because we don't want to change the response in this case.
If totalCount is not exposed through the request object or the h.paginate method, the following attributes will be set to null if they are active.
last
pageCount
totalCount
next
You can still have those four attributes by exposing totalCount even if totalCount is set to false.
const options = {
query: {
page: {
name: 'page',
default: 1
},
limit: {
name: 'limit',
default: 25
},
pagination: {
name: 'pagination',
default: true,
active: true
}
invalid: 'defaults'
},
meta: {
location: 'body',
successStatusCode: undefined,
name: 'meta',
count: {
active: true,
name: 'count'
},
totalCount: {
active: true,
name: 'totalCount'
},
pageCount: {
active: true,
name: 'pageCount'
},
self: {
active: true,
name: 'self'
},
previous: {
active: true,
name: 'previous'
},
next: {
active: true,
name: 'next'
},
hasNext: {
active: false,
name: 'hasNext'
},
hasPrevious: {
active: false,
name: 'hasPrevious'
},
first: {
active: true,
name: 'first'
},
last: {
active: true,
name: 'last'
},
page: {
active: false,
// name == default.query.page.name
},
limit: {
active: false
// name == default.query.limit.name
}
},
results: {
name: 'results'
},
reply: {
paginate: 'paginate',
results: {
name: 'results'
},
totalCount:{
name: 'totalCount'
}
},
routes: {
include: ['*'],
exclude: []
},
zeroIndex: false
};
const Hapi = require('hapi');
const server = new Hapi.Server();
// Add your connection
await server.register(require('hapi-pagination'))
const Hapi = require('hapi');
const server = new Hapi.Server();
// Add your connection
const options = {
query: {
page: {
name: 'the_page' // The page parameter will now be called the_page
},
limit: {
name: 'per_page', // The limit will now be called per_page
default: 10 // The default value will be 10
}
},
meta: {
location: 'body', // The metadata will be put in the response body
name: 'metadata', // The meta object will be called metadata
count: {
active: true,
name: 'count'
},
pageCount: {
name: 'totalPages'
},
self: {
active: false // Will not generate the self link
},
first: {
active: false // Will not generate the first link
},
last: {
active: false // Will not generate the last link
}
},
routes: {
include: ['/users', '/accounts', '/persons', '/'],
}
};
await server.register({plugin: require('hapi-pagination'), options: options})
Global configuration:
const Hapi = require('hapi');
const server = new Hapi.Server();
// Add your connection
const options = {
routes: {
include: [], // Emptying include list will disable pagination
}
};
await server.register({plugin: require('hapi-pagination'), options: options})
Activate on route level:
config: {
plugins: {
pagination: {
enabled: true
}
}
}
If you want to provide more examples, I'll accept a PR.
You have two choices when you uses this plugin with Joi:
limit
, page
and pagination
to the query schema (with the names that you chose !).allowUnknown
option to true.
See hereYou don't need this if you don't need to validate anything !
validate: {
query: {
// Your other parameters ...
limit: Joi.number().integer(),
page: Joi.number().integer(),
pagination: Joi.boolean()
}
}
// OR
validate: {
options: {
allowUnknown: true
}
query: {
// Your other parameters...
}
}
Make sure you have lab
and code
installed and run :
npm test