1 | # bissle
|
2 | #### Minimalist HALicious pagination reply interface for [HapiJS](https://github.com/hapijs/hapi)
|
3 |
|
4 | [![Travis](https://img.shields.io/travis/felixheck/bissle.svg)](https://travis-ci.org/felixheck/bissle/builds/) ![npm](https://img.shields.io/npm/dt/bissle.svg)
|
5 |
|
6 | ---
|
7 |
|
8 | 1. [Introduction](#introduction)
|
9 | 2. [Installation](#installation)
|
10 | 3. [Usage](#usage)
|
11 | 4. [API](#api)
|
12 | 5. [Example](#example)
|
13 | 6. [Testing](#testing)
|
14 | 7. [Contribution](#contribution)
|
15 | 8. [License](#license)
|
16 |
|
17 | ## Introduction
|
18 |
|
19 | This [HapiJS](https://github.com/hapijs/hapi) plugin enables an additional reply interface to paginate a response in a RESTful and [HAL](https://tools.ietf.org/html/draft-kelly-json-hal-06) compliant manner. So the plugin accordingly splices the initial response; extends it with meta information about the count of entries per page, the total count and the current page; adds a link map for HALicious navigation and appends the corresponding `Link` header. It is not a middleware-like plugin, so you are allowed to control the usage explicitly by yourself. Because of this, it works perfectly in combination with HAL plugins like [halacious](https://github.com/bleupen/halacious), as it is shown in the [example](#example) below.
|
20 |
|
21 | The plugin is implemented in ECMAScript 6, therefore the development dependencies are based on `babel`. Additionally `eslint` and `tape` are used to grant a high quality implementation.
|
22 |
|
23 | **bissle** is the Swabian term for *a little bit*, it should visualize the sense of pagination.
|
24 |
|
25 | ## Installation
|
26 | For installation use the [Node Package Manager](https://github.com/npm/npm):
|
27 | ```
|
28 | // production version with ES5 syntax
|
29 | $ npm install --save bissle
|
30 | ```
|
31 |
|
32 | or clone the repository:
|
33 | ```
|
34 | // development version with ES6 syntax
|
35 | $ git clone https://github.com/felixheck/bissle
|
36 | ```
|
37 |
|
38 | Alternatively use the [Yarn Package Manager](https://yarnpkg.com):
|
39 | ```
|
40 | // production version with ES5 syntax
|
41 | $ yarn add bissle
|
42 | ```
|
43 |
|
44 | ## Usage
|
45 | #### Import
|
46 | First you have to import the module and the peer dependency [akaya](https://github.com/felixheck/akaya):
|
47 | ``` js
|
48 | const bissle = require('bissle');
|
49 | const akaya = require('akaya');
|
50 | ```
|
51 |
|
52 | #### Create HapiJS server
|
53 | Afterwards create your **HapiJS** server and the corresponding connection if not already done:
|
54 | ``` js
|
55 | const server = new Hapi.Server();
|
56 |
|
57 | server.connection({
|
58 | port: 1337,
|
59 | host: 'localhost',
|
60 | });
|
61 | ```
|
62 |
|
63 | #### Registration
|
64 | Finally register the plugins per `server.register()`:
|
65 | ``` js
|
66 | server.register([akaya, bissle], err => {
|
67 | if (err) {
|
68 | throw err;
|
69 | }
|
70 |
|
71 | server.start();
|
72 | });
|
73 | ```
|
74 |
|
75 | After registering **bissle**, the [HapiJS reply interface](http://hapijs.com/api#reply-interface) will be decorated with the new method `reply.bissle()`.
|
76 |
|
77 | #### Joi Validation
|
78 | If you use **Joi** for request validation, simply add `per_page` and `page` to the query scheme. The plugin exposes the all *bissle* related scheme via `server.plugins.bissle.scheme`. Alternatively it is possible to enable the `allowUnknown` option.<br>The exposed object contains additionally the scheme for plugin related options.
|
79 |
|
80 | ## API
|
81 | #### Plugin Options
|
82 | While the plugin registration it is possible to pass a [plugin specific options object](http://hapijs.com/api#serverregisterplugins-options-callback):
|
83 | - `options {Object}` - The plugin specific options object.
|
84 | - `absolute {boolean}` - If the pagination links (not the `Link` header) should be absolute or not.<br>Default: `false`.
|
85 | - `paramNames {Object}` - Config object for overriding default parameter names output in the response
|
86 | - `per_page {string}` - Parameter name for describing the page limit <br>Default: `per_page`
|
87 | - `page {string}` - Parameter name for describing the current page <br>Default: `page`
|
88 | - `total {string}` - Parameter name for describing the total item count <br>Default: `total`
|
89 |
|
90 | #### `reply.bissle(response, [options])`
|
91 |
|
92 | An additional reply interface for paginated responses.
|
93 | - `response {Object}` - The result to be decorated and replied.
|
94 | - `options {Object}` - The custom default values.
|
95 | - `key {string}` - The access key of `response` to get the result to be paginated.<br>Default: `'result'`.
|
96 | - `per_page {number}` - The default entries per page if none is defined in the query string.<br>Default: `100`.<br>Range: `1-500`.
|
97 | - `total {number}` - Overwrite the internally generated `total` value and avoid data splicing. The passed response get returned without internally done pagination. Just meta information and the `Link` header get added.<br>Default: `null`.<br>Range: `>=0`.
|
98 |
|
99 | ##Example
|
100 | The following example demonstrates the usage of **bissle** in combination with **mongoose**, **halacious** and various utilities.
|
101 |
|
102 | ```js
|
103 | const Hapi = require('hapi');
|
104 | const bissle = require('bissle');
|
105 | const halacious = require('halacious');
|
106 | const akaya = require('akaya');
|
107 | const Boom = require('boom');
|
108 | const _ = require('lodash');
|
109 | const YourModel = require('./models/yourModel');
|
110 |
|
111 | const server = new Hapi.Server();
|
112 | server.connection({ port: 1337 });
|
113 |
|
114 | server.route({
|
115 | method: 'GET',
|
116 | path: '/',
|
117 | config: {
|
118 | id: 'root',
|
119 | handler: function (request, reply) {
|
120 | YourModel.find({}, (err, result) => {
|
121 | if (err) return reply(Boom.badRequest(err));
|
122 | if (!result) return reply(Boom.notFound());
|
123 |
|
124 | return reply.bissle({ result });
|
125 | });
|
126 | },
|
127 | plugins: {
|
128 | hal: {
|
129 | prepare: function(rep, next) {
|
130 | _.forEach(rep.entity.result, task => {
|
131 | rep.embed('task', `./${task._id}`, task);
|
132 | });
|
133 |
|
134 | return next();
|
135 | },
|
136 | ignore: ['result']
|
137 | }
|
138 | }
|
139 | });
|
140 |
|
141 | server.register([akaya, halacious, {
|
142 | register: bissle,
|
143 | options: { absolute: false }
|
144 | }], err => {
|
145 | if (err) throw err;
|
146 |
|
147 | server.start();
|
148 | });
|
149 | ```
|
150 |
|
151 | ---
|
152 |
|
153 | Assuming that **mongoose**'s `find()` returns the following data as `result`:
|
154 |
|
155 | ```js
|
156 | [
|
157 | {
|
158 | _id: "abc",
|
159 | title: "abc"
|
160 | },
|
161 | {
|
162 | _id: "def",
|
163 | title: "def"
|
164 | },
|
165 | {
|
166 | _id: "ghi",
|
167 | title: "ghi"
|
168 | },
|
169 | {
|
170 | _id: "jkl",
|
171 | title: "jkl"
|
172 | },
|
173 | {
|
174 | _id: "mno",
|
175 | title: "mno"
|
176 | }
|
177 | ]
|
178 | ```
|
179 |
|
180 | ---
|
181 |
|
182 | Requesting the route `/items?page=2&per_page=2`, the plugin replies:
|
183 |
|
184 | ```js
|
185 | {
|
186 | _links: {
|
187 | self: {
|
188 | href: "/items?page=2&per_page=2"
|
189 | },
|
190 | first: {
|
191 | href: "/items?per_page=2"
|
192 | },
|
193 | prev: {
|
194 | href: "/items?per_page=2"
|
195 | },
|
196 | next: {
|
197 | href: "/items?page=3&per_page=2"
|
198 | },
|
199 | last: {
|
200 | href: "/items?page=3&per_page=2"
|
201 | },
|
202 | },
|
203 | page: 2,
|
204 | per_page: 2,
|
205 | total: 5,
|
206 | result: [
|
207 | {
|
208 | _id: "ghi",
|
209 | title: "ghi"
|
210 | },
|
211 | {
|
212 | _id: "jkl",
|
213 | title: "jkl"
|
214 | }
|
215 | ]
|
216 | }
|
217 |
|
218 | ```
|
219 |
|
220 | Additionally the plugin sets the corresponding `Link` header.
|
221 |
|
222 | ---
|
223 |
|
224 | The **halacious** plugin enables to extend this response to:
|
225 |
|
226 | ```js
|
227 | {
|
228 | _links: {
|
229 | self: {
|
230 | href: "/items?page=2&per_page=2"
|
231 | },
|
232 | first: {
|
233 | href: "/items?per_page=2"
|
234 | },
|
235 | prev: {
|
236 | href: "/items?per_page=2"
|
237 | },
|
238 | next: {
|
239 | href: "/items?page=3&per_page=2"
|
240 | },
|
241 | last: {
|
242 | href: "/items?page=3&per_page=2"
|
243 | },
|
244 | },
|
245 | page: 2,
|
246 | per_page: 2,
|
247 | total: 5,
|
248 | _embedded: [
|
249 | {
|
250 | _links: {
|
251 | self: {
|
252 | href: "/items/ghi"
|
253 | }
|
254 | },
|
255 | _id: "ghi",
|
256 | title: "ghi"
|
257 | },
|
258 | {
|
259 | _links: {
|
260 | self: {
|
261 | href: "/items/jkl"
|
262 | }
|
263 | },
|
264 | _id: "jkl",
|
265 | title: "jkl"
|
266 | }
|
267 | ]
|
268 | }
|
269 |
|
270 | ```
|
271 |
|
272 | So in the end the combination of **bissle** and a HAL plugin results in a REST/HAL compliant and paginated response.
|
273 |
|
274 | ## Testing
|
275 | First you have to install all dependencies:
|
276 | ```
|
277 | $ npm install
|
278 | ```
|
279 |
|
280 | To execute all unit tests once, use:
|
281 | ```
|
282 | $ npm test
|
283 | ```
|
284 |
|
285 | or to run tests based on file watcher, use:
|
286 | ```
|
287 | $ npm start
|
288 | ```
|
289 |
|
290 | To get information about the test coverage, use:
|
291 | ```
|
292 | $ npm run coverage
|
293 | ```
|
294 |
|
295 | ## Contribution
|
296 | Fork this repository and push in your ideas.
|
297 |
|
298 | Do not forget to add corresponding tests to keep up 100% test coverage.
|
299 |
|
300 | ## License
|
301 | The MIT License
|
302 |
|
303 | Copyright (c) 2016-2017 Felix Heck
|
304 |
|
305 | Permission is hereby granted, free of charge, to any person obtaining a copy
|
306 | of this software and associated documentation files (the "Software"), to deal
|
307 | in the Software without restriction, including without limitation the rights
|
308 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
309 | copies of the Software, and to permit persons to whom the Software is
|
310 | furnished to do so, subject to the following conditions:
|
311 |
|
312 | The above copyright notice and this permission notice shall be included in
|
313 | all copies or substantial portions of the Software.
|
314 |
|
315 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
316 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
317 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
318 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
319 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
320 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
321 | THE SOFTWARE.
|