├── .travis.yml ├── images └── bassmaster.png ├── CONTRIBUTING.md ├── .gitignore ├── CHANGELOG.md ├── package.json ├── lib ├── index.js └── batch.js ├── LICENSE ├── examples └── batch.js ├── test ├── plugin.js ├── internals.js └── batch.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | node_js: 6 | - 8 7 | 8 | -------------------------------------------------------------------------------- /images/bassmaster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outmoded/bassmaster/HEAD/images/bassmaster.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please view our [hapijs contributing guide](https://github.com/hapijs/hapi/blob/master/.github/CONTRIBUTING.md). 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | npm-debug.log 4 | dump.rdb 5 | node_modules 6 | results.tap 7 | results.xml 8 | npm-shrinkwrap.json 9 | .DS_Store 10 | */.DS_Store 11 | */*/.DS_Store 12 | ._* 13 | */._* 14 | */*/._* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Breaking changes are documented using GitHub issues, see [issues labeled "release notes"](https://github.com/hapijs/bassmaster/issues?q=is%3Aissue+label%3A%22release+notes%22). 2 | 3 | If you want changes of a specific minor or patch release, you can browse the [GitHub milestones](https://github.com/hapijs/bassmaster/milestones?state=closed&direction=asc&sort=due_date). 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bassmaster", 3 | "description": "Batch processing plugin for hapi", 4 | "repository": "git://github.com/hapijs/bassmaster", 5 | "version": "3.2.0", 6 | "main": "lib/index.js", 7 | "keywords": [ 8 | "hapi", 9 | "plugin", 10 | "batch" 11 | ], 12 | "engines": { 13 | "node": ">=4.4.5" 14 | }, 15 | "dependencies": { 16 | "async": "2.x.x", 17 | "boom": "7.x.x", 18 | "hoek": "^5.0.4", 19 | "joi": "^13.7.0", 20 | "traverse": "0.6.x" 21 | }, 22 | "devDependencies": { 23 | "code": "5.x.x", 24 | "hapi": "^17.6.0", 25 | "lab": "15.x.x" 26 | }, 27 | "scripts": { 28 | "test": "lab -r console -t 100 -a code -L", 29 | "test-cov-html": "lab -r html -o coverage.html -a code -L" 30 | }, 31 | "license": "BSD-3-Clause" 32 | } 33 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Hoek = require('hoek'); 6 | const Batch = require('./batch'); 7 | const Pkg = require('../package.json'); 8 | const { name } = Pkg; 9 | 10 | // Declare internals 11 | 12 | const internals = { 13 | defaults: { 14 | batchEndpoint: '/batch', 15 | description: 'Batch endpoint', 16 | notes: 'A batch endpoint that makes it easy to combine multiple requests to other endpoints in a single call.', 17 | tags: ['bassmaster'] 18 | } 19 | }; 20 | 21 | const register = function (server, options) { 22 | 23 | const settings = Hoek.applyToDefaults(internals.defaults, options); 24 | 25 | server.route({ 26 | method: 'POST', 27 | path: settings.batchEndpoint, 28 | config: Batch.config(settings) 29 | }); 30 | }; 31 | 32 | exports.plugin = { register, name, Pkg }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014, Walmart and other contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of any contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | * * * 27 | 28 | The complete list of contributors can be found at: https://github.com/hapijs/bassmaster/graphs/contributors 29 | -------------------------------------------------------------------------------- /examples/batch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Hapi = require('hapi'); 6 | 7 | 8 | // Declare internals 9 | 10 | const internals = {}; 11 | 12 | 13 | /** 14 | * To Test: 15 | * 16 | * Run the server and try a batch request like the following: 17 | * 18 | * POST /batch 19 | * { "requests": [{ "method": "get", "path": "/profile" }, { "method": "get", "path": "/item" }, { "method": "get", "path": "/item/$1.id" }] 20 | * 21 | * or a GET request to http://localhost:8080/request will perform the above request for you 22 | */ 23 | 24 | 25 | internals.profile = function (request, reply) { 26 | 27 | return reply({ 28 | 'id': 'fa0dbda9b1b', 29 | 'name': 'John Doe' 30 | }); 31 | }; 32 | 33 | 34 | internals.activeItem = function (request, reply) { 35 | 36 | return reply({ 37 | 'id': '55cf687663', 38 | 'name': 'Active Item' 39 | }); 40 | }; 41 | 42 | 43 | internals.item = function (request, reply) { 44 | 45 | return reply({ 46 | 'id': request.params.id, 47 | 'name': 'Item' 48 | }); 49 | }; 50 | 51 | 52 | internals.requestBatch = function (request, reply) { 53 | 54 | internals.http.inject({ 55 | method: 'POST', 56 | url: '/batch', 57 | payload: '{ "requests": [{ "method": "get", "path": "/profile" }, { "method": "get", "path": "/item" }, { "method": "get", "path": "/item/$1.id" }] }' 58 | }, (res) => { 59 | 60 | reply(res.result); 61 | }); 62 | }; 63 | 64 | 65 | internals.main = function () { 66 | 67 | internals.http = new Hapi.Server(); 68 | internals.http.connection({ port: 8080 }); 69 | 70 | internals.http.route([ 71 | { method: 'GET', path: '/profile', handler: internals.profile }, 72 | { method: 'GET', path: '/item', handler: internals.activeItem }, 73 | { method: 'GET', path: '/item/{id}', handler: internals.item }, 74 | { method: 'GET', path: '/request', handler: internals.requestBatch } 75 | ]); 76 | 77 | internals.http.register(require('../'), (err) => { 78 | 79 | if (err) { 80 | console.log(err); 81 | } 82 | else { 83 | internals.http.start(() => console.log('Server started.')); 84 | } 85 | }); 86 | }; 87 | 88 | 89 | internals.main(); 90 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Bassmaster = require('../'); 7 | const Lab = require('lab'); 8 | const Hapi = require('hapi'); 9 | 10 | 11 | // Declare internals 12 | 13 | const internals = {}; 14 | 15 | 16 | // Test shortcuts 17 | 18 | const lab = exports.lab = Lab.script(); 19 | const describe = lab.describe; 20 | const it = lab.it; 21 | const { expect, fail } = Code; 22 | 23 | 24 | describe('bassmaster', () => { 25 | 26 | it('can be added as a plugin to hapi', async () => { 27 | 28 | const server = new Hapi.Server(); 29 | 30 | try { 31 | await server.register(Bassmaster); 32 | } 33 | catch (e) { 34 | fail('Plugin failed to register'); 35 | } 36 | 37 | expect(true).to.be.true(); 38 | }); 39 | 40 | it('can be given a custom route url', async () => { 41 | 42 | const server = new Hapi.Server(); 43 | 44 | try { 45 | await server.register({ plugin: Bassmaster, options: { batchEndpoint: '/custom' } }); 46 | } 47 | catch (e) { 48 | fail('Plugin failed to register'); 49 | } 50 | 51 | const path = server.table()[0].path; 52 | expect(path).to.equal('/custom'); 53 | }); 54 | 55 | it('can be given a custom description', async () => { 56 | 57 | const server = new Hapi.Server(); 58 | try { 59 | await server.register({ plugin: Bassmaster, options: { description: 'customDescription' } }); 60 | } 61 | catch (e) { 62 | fail('Plugin failed to register'); 63 | } 64 | 65 | const description = server.table()[0].settings.description; 66 | expect(description).to.equal('customDescription'); 67 | }); 68 | 69 | it('can be given an authentication strategy', async () => { 70 | 71 | const server = new Hapi.Server(); 72 | const mockScheme = { 73 | authenticate: () => { 74 | 75 | return null; 76 | }, 77 | payload: () => { 78 | 79 | return null; 80 | }, 81 | response: () => { 82 | 83 | return null; 84 | } 85 | }; 86 | server.auth.scheme('mockScheme', () => { 87 | 88 | return mockScheme; 89 | }); 90 | server.auth.strategy('mockStrategy', 'mockScheme'); 91 | 92 | try { 93 | await server.register({ plugin: Bassmaster, options: { auth: 'mockStrategy' } }); 94 | } 95 | catch (e) { 96 | fail('Plugin failed to register'); 97 | } 98 | 99 | const auth = server.table()[0].settings.auth.strategies[0]; 100 | expect(auth).to.equal('mockStrategy'); 101 | }); 102 | 103 | it('can be given custom tags', async () => { 104 | 105 | const server = new Hapi.Server(); 106 | try { 107 | await server.register({ plugin: Bassmaster, options: { tags: ['custom', 'tags'] } }); 108 | } 109 | catch (e) { 110 | fail('Plugin failed to register'); 111 | } 112 | 113 | const tags = server.table()[0].settings.tags; 114 | expect(tags).to.equal(['custom', 'tags']); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![bassmaster Logo](https://raw.github.com/spumko/bassmaster/master/images/bassmaster.png) 2 | 3 | Bassmaster makes it easy to combine requests into a single one. It also supports pipelining, allowing you to take the result of one query in the batch request and use it in a subsequent one. The batch endpoint only responds to POST requests. 4 | 5 | [![Build Status](https://secure.travis-ci.org/hapijs/bassmaster.png)](http://travis-ci.org/hapijs/bassmaster) 6 | 7 | [![NPM](https://nodei.co/npm/bassmaster.png?downloads=true&stars=true)](https://nodei.co/npm/bassmaster/) 8 | 9 | Lead Maintainer: [Christopher De Cairos](https://github.com/cadecairos) 10 | 11 | 12 | ## Getting Started 13 | Install **bassmaster** by either running `npm install bassmaster` in your sites working directory or add 'bassmaster' to the dependencies section of the 'package.json' file and run `npm install`. 14 | 15 | ### Available options 16 | At this time the options object supports the following configuration: 17 | - `batchEndpoint` - the path where batch requests will be served from. Default is '/batch'. 18 | - `description` - route description used for generating documentation. Default is 'Batch endpoint' 19 | - `notes` - route notes used for generating documentation. Default is 'A batch endpoint which makes it easy to combine multiple requests to other endpoints in a single call.' 20 | - `tags` - route tags used for generating documentation. Default is ['bassmaster'] 21 | - `auth` - If you need the batch route to have authentication 22 | 23 | As an example to help explain the use of the endpoint, assume that the server has a route at '/currentuser' and '/users/{id}/profile/'. 24 | You can make a POST request to the batch endpoint with the following body and it will return an array with the current user and their profile. 25 | Pipelining uses [Hoek.reach](https://www.npmjs.com/package/hoek#reach-obj-chain-options) to retrieve values from request results. 26 | 27 | ```json 28 | { "requests": [ 29 | {"method": "get", "path": "/currentuser"}, 30 | {"method": "get", "path": "/users/$0.id/profile"} 31 | ] } 32 | ``` 33 | 34 | The response body to the batch endpoint is an ordered array of the response to each request. Therefore, if you make a request to the batch endpoint that looks like 35 | 36 | ```json 37 | { "requests": [ 38 | {"method": "get", "path": "/users/1"}, 39 | {"method": "get", "path": "/users/2"} 40 | ] } 41 | ``` 42 | 43 | The response will look like the following, where the first item in the response array is the result of the request from the first item in the request array. 44 | 45 | ```json 46 | [{"userId": "1", "username": "bob"}, {"userId": "2", "username": "billy" }] 47 | ``` 48 | 49 | When making a POST request as part of the batch assign the _'payload'_ property with the contents of the payload to send. 50 | 51 | Optionally you can assign the query as a third property rather than placing it directly into the path. The query property accepts an object that will be formatted into a querystring. 52 | 53 | ```json 54 | { "requests": [ 55 | { "method": "get", "path": "/users/1", "query": { "id": "23", "user": "John" } } 56 | ] } 57 | ``` 58 | 59 | If an error occurs as a result of one the requests to an endpoint it will be included in the response in the same location in the array as the request causing the issue. The error object will include an error property that you can interrogate. At this time the response is a 200 even when a request in the batch returns a different code. 60 | -------------------------------------------------------------------------------- /test/internals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hapi = require('hapi'); 4 | const Bassmaster = require('../'); 5 | 6 | const profileHandler = function (request, h) { 7 | 8 | const id = request.query.id || 'fa0dbda9b1b'; 9 | 10 | return h.response({ 11 | id, 12 | 'name': 'John Doe' 13 | }); 14 | }; 15 | 16 | const activeItemHandler = function (request, h) { 17 | 18 | return h.response({ 19 | 'id': '55cf687663', 20 | 'name': 'Active Item' 21 | }); 22 | }; 23 | 24 | const itemHandler = function (request, h) { 25 | 26 | return h.response({ 27 | 'id': request.params.id, 28 | 'name': 'Item' 29 | }); 30 | }; 31 | 32 | const deepItemHandler = function (request, h) { 33 | 34 | return h.response({ 35 | 'id': '55cf687663', 36 | 'name': 'Deep Item', 37 | 'inner': { 38 | 'name': 'Level 1', 39 | 'inner': { 40 | 'name': 'Level 2', 41 | 'inner': { 42 | 'name': 'Level 3', 43 | 'array': [ 44 | { 45 | 'name': 'Array Item 0' 46 | }, 47 | { 48 | 'name': 'Array Item 1' 49 | } 50 | ] 51 | } 52 | } 53 | } 54 | }); 55 | }; 56 | 57 | const item2Handler = function (request, h) { 58 | 59 | return h.response({ 60 | 'id': request.params.id || 'mystery-guest', 61 | 'name': 'Item' 62 | }); 63 | }; 64 | 65 | const arrayHandler = function (request, h) { 66 | 67 | return h.response({ 68 | 'id': '55cf687663', 69 | 'name': 'Dress', 70 | 'items': [{ 'color': 'blackandblue' }, { 'color': 'whiteandgold' }] 71 | }); 72 | }; 73 | 74 | const zeroIntegerHandler = function (request, h) { 75 | 76 | return h.response({ 77 | 'id': 0, 78 | 'name': 'Zero Item' 79 | }); 80 | }; 81 | 82 | const integerHandler = function (request, h) { 83 | 84 | return h.response({ 85 | 'id': 123, 86 | 'name': 'Integer Item' 87 | }); 88 | }; 89 | 90 | const integerItemHandler = function (request, h) { 91 | 92 | return h.response({ 93 | 'id': request.params.id, 94 | 'name': 'Integer' 95 | }); 96 | }; 97 | 98 | const stringItemHandler = function (request, h) { 99 | 100 | return h.response('{' + 101 | '"id": "55cf687663",' + 102 | '"name": "String Item"' + 103 | '}'); 104 | }; 105 | 106 | const badCharHandler = function (request, h) { 107 | 108 | return h.response({ 109 | 'id': 'test', 110 | 'null': null, 111 | 'invalidChar': '#' 112 | }); 113 | }; 114 | 115 | const badValueHandler = function (request, h) { 116 | 117 | return h.response(null); 118 | }; 119 | 120 | const redirectHandler = function (request, h) { 121 | 122 | return h.redirect('/profile'); 123 | }; 124 | 125 | const interestingIdsHandler = function (request, h) { 126 | 127 | return h.response({ 128 | 'idWithDash': '55cf-687663-55cf687663', 129 | 'idLikeFilename': '55cf687663.png', 130 | 'idLikeFileNameWithDash': '55cf-687663-55cf687663.png' 131 | }); 132 | }; 133 | 134 | const fetch1 = function (request, h) { 135 | 136 | return 'Hello'; 137 | }; 138 | 139 | const fetch2 = function (request, h) { 140 | 141 | return request.pre.m1 + request.pre.m3 + request.pre.m4; 142 | }; 143 | 144 | const fetch3 = function (request, h) { 145 | 146 | return ' '; 147 | }; 148 | 149 | const fetch4 = function (request, h) { 150 | 151 | return 'World'; 152 | }; 153 | 154 | const fetch5 = function (request, h) { 155 | 156 | return `${request.pre.m2}!`; 157 | }; 158 | 159 | const getFetch = function (request, h) { 160 | 161 | return `${request.pre.m5}\n`; 162 | }; 163 | 164 | const errorHandler = function (request, h) { 165 | 166 | return new Error('myerror'); 167 | }; 168 | 169 | const echoHandler = function (request, h) { 170 | 171 | return request.payload; 172 | }; 173 | 174 | const returnInputtedIntegerHandler = function (request, h) { 175 | 176 | return request.payload.id; 177 | }; 178 | 179 | const getFalseHandler = function (request, h) { 180 | 181 | return false; 182 | }; 183 | 184 | const returnInputtedBooleanHandler = function (request, h) { 185 | 186 | return request.payload.bool; 187 | }; 188 | 189 | const returnPathParamHandler = function (request, h) { 190 | 191 | return request.params.pathParamInteger; 192 | }; 193 | 194 | const returnInputtedStringHandler = function (request, h) { 195 | 196 | return { 197 | id: request.params.id, 198 | paramString: request.params.paramString, 199 | queryString: request.query.queryString, 200 | payloadString: request.payload.payloadString 201 | }; 202 | }; 203 | 204 | module.exports.setupServer = async function () { 205 | 206 | const server = new Hapi.Server(); 207 | server.route([ 208 | { method: 'POST', path: '/echo', handler: echoHandler }, 209 | { method: 'PUT', path: '/echo', handler: echoHandler }, 210 | { method: 'GET', path: '/profile', handler: profileHandler }, 211 | { method: 'GET', path: '/item', handler: activeItemHandler }, 212 | { method: 'GET', path: '/deepItem', handler: deepItemHandler }, 213 | { method: 'GET', path: '/array', handler: arrayHandler }, 214 | { method: 'GET', path: '/item/{id}', handler: itemHandler }, 215 | { method: 'GET', path: '/item2/{id?}', handler: item2Handler }, 216 | { method: 'GET', path: '/zero', handler: zeroIntegerHandler }, 217 | { method: 'GET', path: '/int', handler: integerHandler }, 218 | { method: 'GET', path: '/int/{id}', handler: integerItemHandler }, 219 | { method: 'GET', path: '/string', handler: stringItemHandler }, 220 | { method: 'GET', path: '/interestingIds', handler: interestingIdsHandler }, 221 | { method: 'GET', path: '/error', handler: errorHandler }, 222 | { method: 'GET', path: '/badchar', handler: badCharHandler }, 223 | { method: 'GET', path: '/badvalue', handler: badValueHandler }, 224 | { 225 | method: 'GET', 226 | path: '/fetch', 227 | handler: getFetch, 228 | config: { 229 | pre: [ 230 | { method: fetch1, assign: 'm1', mode: 'parallel' }, 231 | { method: fetch2, assign: 'm2' }, 232 | { method: fetch3, assign: 'm3', mode: 'parallel' }, 233 | { method: fetch4, assign: 'm4', mode: 'parallel' }, 234 | { method: fetch5, assign: 'm5' } 235 | ] 236 | } 237 | }, 238 | { method: 'GET', path: '/redirect', handler: redirectHandler }, 239 | { method: 'POST', path: '/returnInputtedInteger', handler: returnInputtedIntegerHandler }, 240 | { method: 'GET', path: '/returnPathParamInteger/{pathParamInteger}', handler: returnPathParamHandler }, 241 | { method: 'GET', path: '/getFalse', handler: getFalseHandler }, 242 | { method: 'POST', path: '/returnInputtedBoolean', handler: returnInputtedBooleanHandler }, 243 | { method: 'POST', path: '/returnInputtedString/{id}/{paramString}', handler: returnInputtedStringHandler } 244 | ]); 245 | 246 | await server.register(Bassmaster); 247 | 248 | return server; 249 | }; 250 | 251 | module.exports.makeRequest = async function (server, payload) { 252 | 253 | return (await server.inject({ 254 | method: 'post', 255 | url: '/batch', 256 | payload 257 | })).result; 258 | }; 259 | -------------------------------------------------------------------------------- /lib/batch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Url = require('url'); 6 | const Async = require('async'); 7 | const Boom = require('boom'); 8 | const Traverse = require('traverse'); 9 | const Hoek = require('hoek'); 10 | const Joi = require('joi'); 11 | const { promisify } = require('util'); 12 | 13 | const asyncSeries = promisify(Async.series); 14 | const asyncParallel = promisify(Async.parallel); 15 | 16 | // Declare internals 17 | 18 | const internals = {}; 19 | 20 | module.exports.config = function (settings) { 21 | 22 | return { 23 | handler: async function (request, h) { 24 | 25 | const requests = []; 26 | const parsables = { 27 | payloads: [], 28 | queries: [] 29 | }; 30 | const resultsData = { 31 | results: [], 32 | resultsMap: [] 33 | }; 34 | 35 | let errorMessage = null; 36 | 37 | request.payload.requests.every((req, idx) => { 38 | 39 | const requestParts = []; 40 | const result = req.path.replace(internals.requestRegex, (match, p1, p2, p3) => { 41 | 42 | if (!p1) { 43 | requestParts.push({ type: 'text', value: p3 }); 44 | return ''; 45 | } 46 | 47 | if (p1 < idx) { 48 | requestParts.push({ type: 'ref', index: p1, value: p3 }); 49 | return ''; 50 | } 51 | 52 | errorMessage = `Request reference is beyond array size: ${idx}`; 53 | return match; 54 | }); 55 | 56 | // Make sure entire string was processed (empty) 57 | 58 | if (result === '') { 59 | requests.push(requestParts); 60 | } 61 | else { 62 | errorMessage = errorMessage || `Invalid request format in item: ${idx}`; 63 | return false; 64 | } 65 | 66 | const queryParts = internals.parse(req.query); 67 | const payloadParts = internals.parse(req.payload); 68 | 69 | parsables.queries.push(queryParts || []); 70 | parsables.payloads.push(payloadParts || []); 71 | return true; 72 | }); 73 | 74 | if (errorMessage !== null) { 75 | throw Boom.badRequest(errorMessage); 76 | } 77 | 78 | try { 79 | await internals.process(request, requests, parsables, resultsData); 80 | } 81 | catch (err) { 82 | // console.log("ERROR ", err); 83 | throw Boom.badRequest(err); 84 | } 85 | 86 | return h.response(resultsData.results).code(200); 87 | }, 88 | notes: settings.notes, 89 | description: settings.description, 90 | validate: { 91 | payload: Joi.object({ 92 | requests: Joi.array().items(Joi.object({ 93 | method: Joi.string().required(), 94 | path: Joi.string().required(), 95 | query: [Joi.object().unknown().allow(null),Joi.string()], 96 | payload: [Joi.object().unknown().allow(null),Joi.string()] 97 | }).label('BatchRequest')).min(1).required() 98 | }).required().label('BatchRequestPayload') 99 | }, 100 | auth: settings.auth, 101 | tags: settings.tags 102 | }; 103 | }; 104 | 105 | internals.process = async function (request, requests, parsables, resultsData) { 106 | 107 | const fnsParallel = []; 108 | const fnsSerial = []; 109 | 110 | requests.forEach((requestParts, idx) => { 111 | 112 | const parsableParts = { 113 | payloadParts: parsables.payloads[idx], 114 | queryParts: parsables.queries[idx] 115 | }; 116 | if (internals.hasRefPart(requestParts) 117 | || parsableParts.payloadParts.length 118 | || parsableParts.queryParts.length) { 119 | return fnsSerial.push( 120 | async () => await internals.batch(request, resultsData, idx, requestParts, parsableParts) 121 | ); 122 | } 123 | 124 | fnsParallel.push( 125 | async () => await internals.batch(request, resultsData, idx, requestParts) 126 | ); 127 | }); 128 | 129 | 130 | return await asyncSeries([ 131 | async () => await asyncParallel(fnsParallel), 132 | async () => await asyncSeries(fnsSerial) 133 | ]); 134 | }; 135 | 136 | internals.hasRefPart = function (parts) { 137 | 138 | return parts.some((part) => part.type === 'ref'); 139 | }; 140 | 141 | internals.buildPath = function (resultsData, pos, parts) { 142 | 143 | let path = ''; 144 | let error = null; 145 | let ref; 146 | let value; 147 | let part; 148 | const partsLength = parts.length; 149 | 150 | for (let i = 0; i < partsLength; ++i) { 151 | path += '/'; 152 | part = parts[i]; 153 | 154 | if (part.type !== 'ref') { 155 | path += part.value; 156 | continue; 157 | } 158 | 159 | ref = resultsData.resultsMap[part.index]; 160 | 161 | if (!ref) { 162 | error = new Error('Missing reference response'); 163 | break; 164 | } 165 | 166 | value = part.value ? Hoek.reach(ref, part.value) : ref; 167 | 168 | if (value === null || value === undefined) { 169 | error = new Error('Reference not found'); 170 | break; 171 | } 172 | 173 | if (!/^[\w\d-\.]+$/.test(value)) { 174 | error = new Error('Reference value includes illegal characters'); 175 | break; 176 | } 177 | 178 | path += value; 179 | 180 | } 181 | 182 | return error ? error : path; 183 | }; 184 | 185 | internals.pipelinableParsableRegex = /^\$(\d+)(?:\.([^\s\$]*))?/; 186 | 187 | internals.requestRegex = /(?:\/)(?:\$(\d+))?(\.)?([^\/\$]*)/g; 188 | 189 | internals.parse = function (obj) { 190 | 191 | const parsableParts = []; 192 | 193 | if (!obj) { 194 | return null; 195 | } 196 | 197 | Traverse(obj).forEach(function (value) { 198 | 199 | if (typeof value !== 'string') { 200 | return; 201 | } 202 | 203 | const match = internals.pipelinableParsableRegex.exec(value); 204 | 205 | if (!match) { 206 | return; 207 | } 208 | 209 | parsableParts.push({ 210 | path: this.path, 211 | resultIndex: match[1], 212 | resultPath: match[2] 213 | }); 214 | }); 215 | 216 | return parsableParts; 217 | }; 218 | 219 | internals.evalResults = function (results, index, path) { 220 | 221 | const result = results[index]; 222 | 223 | if (!path) { 224 | return result; 225 | } 226 | 227 | const evalResults = Hoek.reach(result, path); 228 | 229 | return evalResults || evalResults === 0 || evalResults === false ? evalResults : {}; 230 | }; 231 | 232 | internals.buildParsable = function (parsable, resultsData, parts) { 233 | 234 | const partsLength = parts.length; 235 | let result; 236 | let part; 237 | for ( let i = 0; i < partsLength; ++i) { 238 | 239 | part = parts[i]; 240 | result = internals.evalResults(resultsData.resultsMap, part.resultIndex, part.resultPath); 241 | 242 | if (!part.path.length) { 243 | parsable = result; 244 | } 245 | else { 246 | Traverse(parsable).set(part.path, result); 247 | } 248 | } 249 | 250 | return parsable; 251 | }; 252 | 253 | internals.batch = async function (batchRequest, resultsData, pos, requestParts, parsableParts = {}) { 254 | 255 | const path = internals.buildPath(resultsData, pos, requestParts); 256 | 257 | if (path instanceof Error) { 258 | resultsData.results[pos] = path; 259 | throw path; 260 | } 261 | 262 | // Make request 263 | const request = batchRequest.payload.requests[pos]; 264 | 265 | request.path = path; 266 | 267 | if (parsableParts.payloadParts && parsableParts.payloadParts.length) { 268 | 269 | // Make payload 270 | request.payload = internals.buildParsable( 271 | request.payload, 272 | resultsData, 273 | parsableParts.payloadParts 274 | ); 275 | } 276 | 277 | if (parsableParts.queryParts && parsableParts.queryParts.length) { 278 | 279 | // Make queryParts 280 | request.query = internals.buildParsable( 281 | request.query, 282 | resultsData, 283 | parsableParts.queryParts 284 | ); 285 | } 286 | 287 | let data = await internals.dispatch(batchRequest, request); 288 | 289 | // If redirection 290 | if (data.statusCode >= 300 && data.statusCode < 400) { 291 | request.path = data.headers.location; 292 | data = await internals.dispatch(batchRequest, request); 293 | } 294 | 295 | const result = internals.parseResult(data.result); 296 | resultsData.results[pos] = result; 297 | resultsData.resultsMap[pos] = result; 298 | return result; 299 | }; 300 | 301 | internals.parseResult = function (result){ 302 | 303 | if (typeof (result) === 'string'){ 304 | try { 305 | return JSON.parse(result); 306 | } 307 | catch (e) { 308 | return result; 309 | } 310 | } 311 | else { 312 | return result; 313 | } 314 | }; 315 | 316 | internals.dispatch = async function (batchRequest, request) { 317 | 318 | let path; 319 | 320 | if (request.query) { 321 | path = Url.format({ 322 | pathname: request.path, 323 | query: request.query 324 | }); 325 | } 326 | else { 327 | path = request.path; 328 | } 329 | 330 | const body = (request.payload !== null && request.payload !== undefined ? JSON.stringify(request.payload) : null); 331 | const injectOptions = { 332 | url: path, 333 | method: request.method, 334 | headers: batchRequest.headers, 335 | payload: body 336 | }; 337 | 338 | return await batchRequest.server.inject(injectOptions); 339 | }; 340 | -------------------------------------------------------------------------------- /test/batch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Lab = require('lab'); 7 | const Internals = require('./internals.js'); 8 | 9 | // Test shortcuts 10 | 11 | const lab = exports.lab = Lab.script(); 12 | const { describe, it, before } = lab; 13 | const { expect } = Code; 14 | 15 | let server = null; 16 | 17 | describe('Batch', () => { 18 | 19 | before( async () => { 20 | 21 | server = await Internals.setupServer(); 22 | }); 23 | 24 | it('shows single response when making request for single endpoint', async () => { 25 | 26 | const res = await Internals.makeRequest(server, '{ "requests": [{ "method": "get", "path": "/profile" }] }'); 27 | 28 | expect(res[0].id).to.equal('fa0dbda9b1b'); 29 | expect(res[0].name).to.equal('John Doe'); 30 | expect(res.length).to.equal(1); 31 | }); 32 | 33 | it('supports redirect', async () => { 34 | 35 | const res = await Internals.makeRequest(server, '{ "requests": [{ "method": "get", "path": "/redirect" }] }'); 36 | expect(res[0].id).to.equal('fa0dbda9b1b'); 37 | expect(res[0].name).to.equal('John Doe'); 38 | expect(res.length).to.equal(1); 39 | }); 40 | 41 | it('supports query string in the request', async () => { 42 | 43 | const res = await Internals.makeRequest(server, '{ "requests": [{ "method": "get", "path": "/profile?id=someid" }] }'); 44 | 45 | expect(res[0].id).to.equal('someid'); 46 | expect(res[0].name).to.equal('John Doe'); 47 | expect(res.length).to.equal(1); 48 | }); 49 | 50 | it('supports non alphanum characters in the request', async () => { 51 | 52 | const res = await Internals.makeRequest(server, '{ "requests": [{ "method": "get", "path": "/item/item-_^~&-end" }] }'); 53 | 54 | expect(res[0].id).to.equal('item-_^~&-end'); 55 | expect(res[0].name).to.equal('Item'); 56 | expect(res.length).to.equal(1); 57 | }); 58 | 59 | it('shows two ordered responses when requesting two endpoints', async () => { 60 | 61 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/profile"}, {"method": "get", "path": "/item"}] }'); 62 | 63 | expect(res[0].id).to.equal('fa0dbda9b1b'); 64 | expect(res[0].name).to.equal('John Doe'); 65 | expect(res.length).to.equal(2); 66 | expect(res[1].id).to.equal('55cf687663'); 67 | expect(res[1].name).to.equal('Active Item'); 68 | }); 69 | 70 | it('shows two ordered responses when requesting two endpoints (with optional path param)', async () => { 71 | 72 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/item2/john"}, {"method": "get", "path": "/item2/"}] }'); 73 | 74 | expect(res.length).to.equal(2); 75 | expect(res[0].id).to.equal('john'); 76 | expect(res[1].id).to.equal('mystery-guest'); 77 | }); 78 | 79 | it('handles a large number of batch requests in parallel', async () => { 80 | 81 | const requestBody = '{ "requests": [{"method": "get", "path": "/profile"},' + 82 | '{"method": "get", "path": "/item"},' + 83 | '{"method": "get", "path": "/profile"},' + 84 | '{"method": "get", "path": "/item"},' + 85 | '{"method": "get", "path": "/profile"},' + 86 | '{"method": "get", "path": "/item"},' + 87 | '{"method": "get", "path": "/profile"},' + 88 | '{"method": "get", "path": "/item"},' + 89 | '{"method": "get", "path": "/profile"},' + 90 | '{"method": "get", "path": "/item"},' + 91 | '{"method": "get", "path": "/profile"},' + 92 | '{"method": "get", "path": "/item"},' + 93 | '{"method": "get", "path": "/profile"},' + 94 | '{"method": "get", "path": "/item"},' + 95 | '{"method": "get", "path": "/profile"},' + 96 | '{"method": "get", "path": "/item"},' + 97 | '{"method": "get", "path": "/profile"},' + 98 | '{"method": "get", "path": "/item"},' + 99 | '{"method": "get", "path": "/profile"},' + 100 | '{"method": "get", "path": "/item"},' + 101 | '{"method": "get", "path": "/profile"},' + 102 | '{"method": "get", "path": "/item"},' + 103 | '{"method": "get", "path": "/profile"},' + 104 | '{"method": "get", "path": "/item"},' + 105 | '{"method": "get", "path": "/profile"},' + 106 | '{"method": "get", "path": "/item"},' + 107 | '{"method": "get", "path": "/profile"},' + 108 | '{"method": "get", "path": "/item"},' + 109 | '{"method": "get", "path": "/profile"},' + 110 | '{"method": "get", "path": "/item"},' + 111 | '{"method": "get", "path": "/profile"},' + 112 | '{"method": "get", "path": "/item"},' + 113 | '{"method": "get", "path": "/profile"},' + 114 | '{"method": "get", "path": "/item"},' + 115 | '{"method": "get", "path": "/profile"},' + 116 | '{"method": "get", "path": "/item"},' + 117 | '{"method": "get", "path": "/profile"},' + 118 | '{"method": "get", "path": "/item"},' + 119 | '{"method": "get", "path": "/profile"},' + 120 | '{"method": "get", "path": "/item"},' + 121 | '{"method": "get", "path": "/profile"},' + 122 | '{"method": "get", "path": "/item"},' + 123 | '{"method": "get", "path": "/profile"},' + 124 | '{"method": "get", "path": "/item"},' + 125 | '{"method": "get", "path": "/profile"},' + 126 | '{"method": "get", "path": "/item"},' + 127 | '{"method": "get", "path": "/profile"},' + 128 | '{"method": "get", "path": "/item"},' + 129 | '{"method": "get", "path": "/profile"},' + 130 | '{"method": "get", "path": "/item"},' + 131 | '{"method": "get", "path": "/profile"},' + 132 | '{"method": "get", "path": "/item"},' + 133 | '{"method": "get", "path": "/profile"},' + 134 | '{"method": "get", "path": "/item"},' + 135 | '{"method": "get", "path": "/profile"},' + 136 | '{"method": "get", "path": "/item"},' + 137 | '{"method": "get", "path": "/profile"},' + 138 | '{"method": "get", "path": "/item"},' + 139 | '{"method": "get", "path": "/profile"},' + 140 | '{"method": "get", "path": "/item"},' + 141 | '{"method": "get", "path": "/profile"},' + 142 | '{"method": "get", "path": "/item"},' + 143 | '{"method": "get", "path": "/profile"},' + 144 | '{"method": "get", "path": "/item"},' + 145 | '{"method": "get", "path": "/profile"},' + 146 | '{"method": "get", "path": "/item"},' + 147 | '{"method": "get", "path": "/profile"},' + 148 | '{"method": "get", "path": "/item"},' + 149 | '{"method": "get", "path": "/profile"},' + 150 | '{"method": "get", "path": "/item"},' + 151 | '{"method": "get", "path": "/profile"},' + 152 | '{"method": "get", "path": "/item"},' + 153 | '{"method": "get", "path": "/profile"},' + 154 | '{"method": "get", "path": "/item"},' + 155 | '{"method": "get", "path": "/profile"},' + 156 | '{"method": "get", "path": "/item"},' + 157 | '{"method": "get", "path": "/profile"},' + 158 | '{"method": "get", "path": "/profile"},' + 159 | '{"method": "get", "path": "/profile"},' + 160 | '{"method": "get", "path": "/fetch"}' + 161 | '] }'; 162 | 163 | const res = await Internals.makeRequest(server, requestBody); 164 | 165 | expect(res[0].id).to.equal('fa0dbda9b1b'); 166 | expect(res[0].name).to.equal('John Doe'); 167 | expect(res.length).to.equal(80); 168 | expect(res[1].id).to.equal('55cf687663'); 169 | expect(res[1].name).to.equal('Active Item'); 170 | }); 171 | 172 | it('supports piping a response into the next request', async () => { 173 | 174 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/item"}, {"method": "get", "path": "/item/$0.id"}] }'); 175 | 176 | expect(res.length).to.equal(2); 177 | expect(res[0].id).to.equal('55cf687663'); 178 | expect(res[0].name).to.equal('Active Item'); 179 | expect(res[1].id).to.equal('55cf687663'); 180 | expect(res[1].name).to.equal('Item'); 181 | }); 182 | 183 | it('supports piping Id\'s with "-" (like a uuid) into the next request', async () => { 184 | 185 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/interestingIds"}, {"method": "get", "path": "/item/$0.idWithDash"}] }'); 186 | 187 | expect(res.length).to.equal(2); 188 | expect(res[0].idWithDash).to.equal('55cf-687663-55cf687663'); 189 | expect(res[1].id).to.equal('55cf-687663-55cf687663'); 190 | }); 191 | 192 | it('supports piping interesting Ids with "." (like a filename) into the next request', async () => { 193 | 194 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/interestingIds"}, {"method": "get", "path": "/item/$0.idLikeFilename"}] }'); 195 | 196 | expect(res.length).to.equal(2); 197 | expect(res[0].idLikeFilename).to.equal('55cf687663.png'); 198 | expect(res[1].id).to.equal('55cf687663.png'); 199 | }); 200 | 201 | it('supports piping interesting Ids with "-" and "." (like a filename) into the next request', async () => { 202 | 203 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/interestingIds"}, {"method": "get", "path": "/item/$0.idLikeFileNameWithDash"}] }'); 204 | 205 | expect(res.length).to.equal(2); 206 | expect(res[0].idLikeFileNameWithDash).to.equal('55cf-687663-55cf687663.png'); 207 | expect(res[1].id).to.equal('55cf-687663-55cf687663.png'); 208 | }); 209 | 210 | it('supports piping a deep response into the next request', async () => { 211 | 212 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/deepItem"}, {"method": "post", "path": "/echo", "payload": "$0.inner.name"}, {"method": "post", "path": "/echo", "payload": "$0.inner.inner.name"}, {"method": "post", "path": "/echo", "payload": "$0.inner.inner.inner.name"}] }'); 213 | 214 | expect(res.length).to.equal(4); 215 | expect(res[0].id).to.equal('55cf687663'); 216 | expect(res[0].name).to.equal('Deep Item'); 217 | expect(res[1]).to.equal('Level 1'); 218 | expect(res[2]).to.equal('Level 2'); 219 | expect(res[3]).to.equal('Level 3'); 220 | }); 221 | 222 | it('supports piping a deep response into an array in the next request', async () => { 223 | 224 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/deepItem"}, {"method": "post", "path": "/echo", "payload": "$0.inner.inner.inner.array.0.name"}] }'); 225 | 226 | expect(res.length).to.equal(2); 227 | expect(res[0].id).to.equal('55cf687663'); 228 | expect(res[0].name).to.equal('Deep Item'); 229 | expect(res[1]).to.equal('Array Item 0'); 230 | }); 231 | 232 | it('supports piping integer response into the next request', async () => { 233 | 234 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/int"}, {"method": "get", "path": "/int/$0.id"}] }'); 235 | 236 | expect(res.length).to.equal(2); 237 | expect(res[0].id).to.equal(123); 238 | expect(res[0].name).to.equal('Integer Item'); 239 | expect(res[1].id).to.equal('123'); 240 | expect(res[1].name).to.equal('Integer'); 241 | }); 242 | 243 | it('supports the return of strings instead of json', async () => { 244 | 245 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/string"}, {"method": "get", "path": "/item/$0.id"}] }'); 246 | 247 | expect(res.length).to.equal(2); 248 | expect(res[0].id).to.equal('55cf687663'); 249 | expect(res[0].name).to.equal('String Item'); 250 | expect(res[1].id).to.equal('55cf687663'); 251 | expect(res[1].name).to.equal('Item'); 252 | }); 253 | 254 | it('supports piping a zero integer response into the next request', async () => { 255 | 256 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/zero"}, {"method": "get", "path": "/int/$0.id"}] }'); 257 | 258 | expect(res.length).to.equal(2); 259 | expect(res[0].id).to.equal(0); 260 | expect(res[0].name).to.equal('Zero Item'); 261 | expect(res[1].id).to.equal('0'); 262 | expect(res[1].name).to.equal('Integer'); 263 | }); 264 | 265 | it('supports posting multiple requests', async () => { 266 | 267 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "post", "path": "/echo", "payload":{"a":1}}, {"method": "post", "path": "/echo", "payload":{"a":2}}] }'); 268 | 269 | expect(res.length).to.equal(2); 270 | expect(res[0]).to.equal({ a: 1 }); 271 | expect(res[1]).to.equal({ a: 2 }); 272 | }); 273 | 274 | it('supports sending multiple PUTs requests', async () => { 275 | 276 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "put", "path": "/echo", "payload":{"a":1}}, {"method": "put", "path": "/echo", "payload":{"a":2}}] }'); 277 | 278 | expect(res.length).to.equal(2); 279 | expect(res[0]).to.equal({ a: 1 }); 280 | expect(res[1]).to.equal({ a: 2 }); 281 | }); 282 | 283 | it('supports piping a response from post into the next get request', async () => { 284 | 285 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "post", "path": "/echo", "payload": {"id":"55cf687663"}}, {"method": "get", "path": "/item/$0.id"}] }'); 286 | 287 | expect(res.length).to.equal(2); 288 | expect(res[0].id).to.equal('55cf687663'); 289 | expect(res[1].id).to.equal('55cf687663'); 290 | expect(res[1].name).to.equal('Item'); 291 | }); 292 | 293 | it('supports piping a nested response value from post into the next get request', async () => { 294 | 295 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "post", "path": "/echo", "payload": { "data": {"id":"44cf687663"}}}, {"method": "get", "path": "/item/$0.data.id"}] }'); 296 | 297 | expect(res.length).to.equal(2); 298 | expect(res[0].data.id).to.equal('44cf687663'); 299 | expect(res[1].id).to.equal('44cf687663'); 300 | expect(res[1].name).to.equal('Item'); 301 | }); 302 | 303 | it('handles null payloads gracefully', async () => { 304 | 305 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "post", "path": "/echo", "payload":{"a":1}}, {"method": "post", "path": "/echo", "payload":null}] }'); 306 | 307 | expect(res.length).to.equal(2); 308 | expect(res[0]).to.equal({ a: 1 }); 309 | expect(res[1]).to.equal(null); 310 | }); 311 | 312 | it('includes errors when they occur in the request', async () => { 313 | 314 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/item"}, {"method": "get", "path": "/nothere"}] }'); 315 | 316 | expect(res.length).to.equal(2); 317 | expect(res[0].id).to.equal('55cf687663'); 318 | expect(res[0].name).to.equal('Active Item'); 319 | expect(res[1].error).to.exist(); 320 | }); 321 | 322 | it('bad requests return the correct error', async () => { 323 | 324 | const res = await Internals.makeRequest(server, '{ "blah": "test" }'); 325 | 326 | expect(res.statusCode).to.equal(400); 327 | }); 328 | 329 | 330 | it('handles empty payload', async () => { 331 | 332 | const res = await Internals.makeRequest(server, null); 333 | 334 | expect(res.statusCode).to.equal(400); 335 | }); 336 | 337 | it('handles payload request not array', async () => { 338 | 339 | const res = await Internals.makeRequest(server, '{ "requests": {"method": "get", "path": "/$1"} }'); 340 | 341 | expect(res.statusCode).to.equal(400); 342 | }); 343 | 344 | it('handles bad paths in requests array', async () => { 345 | 346 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/$1"}] }'); 347 | 348 | expect(res.statusCode).to.equal(400); 349 | }); 350 | 351 | it('handles errors in the requested handlers', async () => { 352 | 353 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/error"}] }'); 354 | 355 | expect(res[0].statusCode).to.equal(500); 356 | }); 357 | 358 | it('an out of bounds reference returns an error', async () => { 359 | 360 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/item"}, {"method": "get", "path": "/item/$1.id"}] }'); 361 | 362 | expect(res.error).to.equal('Bad Request'); 363 | }); 364 | 365 | it('a non-existant reference returns an error', async () => { 366 | 367 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/item"}, {"method": "get", "path": "/item/$0.nothere"}] }'); 368 | 369 | expect(res.error).to.equal('Bad Request'); 370 | }); 371 | 372 | it('a non-existant & nested reference returns an error', async () => { 373 | 374 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "post", "path": "/echo", "payload": { "data": {"id":"44cf687663"}}}, {"method": "get", "path": "/item/$0.data.not.here"}] }'); 375 | 376 | expect(res.error).to.equal('Bad Request'); 377 | }); 378 | 379 | it('handles a bad character in the reference value', async () => { 380 | 381 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/badchar"}, {"method": "get", "path": "/item/$0.invalidChar"}] }'); 382 | 383 | expect(res.error).to.equal('Bad Request'); 384 | }); 385 | 386 | it('handles a null value in the reference value', async () => { 387 | 388 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/badchar"}, {"method": "get", "path": "/item/$0.null"}] }'); 389 | 390 | expect(res.error).to.equal('Bad Request'); 391 | }); 392 | 393 | it('cannot use invalid character to request reference', async () => { 394 | 395 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/badvalue"}, {"method": "get", "path": "/item/$:.name"}] }'); 396 | 397 | expect(res.error).to.equal('Bad Request'); 398 | }); 399 | 400 | it('handles missing reference', async () => { 401 | 402 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/badvalue"}, {"method": "get", "path": "/item/$0.name"}] }'); 403 | 404 | expect(res.error).to.equal('Bad Request'); 405 | }); 406 | 407 | it('handles error when getting reference value', async () => { 408 | 409 | const res = await Internals.makeRequest(server, '{ "requests": [{"method": "get", "path": "/item"}, {"method": "get", "path": "/item/$0.1"}] }'); 410 | 411 | expect(res.error).to.equal('Bad Request'); 412 | }); 413 | 414 | it('supports an optional query object', async () => { 415 | 416 | const res = await Internals.makeRequest(server, '{ "requests": [{ "method": "get", "path": "/profile", "query": { "id": "someid" } }] }'); 417 | 418 | expect(res[0].id).to.equal('someid'); 419 | expect(res[0].name).to.equal('John Doe'); 420 | expect(res.length).to.equal(1); 421 | }); 422 | 423 | it('supports alphanum characters in the query', async () => { 424 | 425 | const res = await Internals.makeRequest(server, '{ "requests": [{ "method": "get", "path": "/profile", "query": { "id": "item-_^~&-end" } }] }'); 426 | 427 | expect(res[0].id).to.equal('item-_^~&-end'); 428 | expect(res[0].name).to.equal('John Doe'); 429 | expect(res.length).to.equal(1); 430 | }); 431 | 432 | it('handles null queries gracefully', async () => { 433 | 434 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "post", "path": "/echo", "query": null}] }'); 435 | 436 | expect(res.length).to.equal(1); 437 | expect(res[0]).to.equal(null); 438 | }); 439 | 440 | it('supports piping a whole payload to the next request', async () => { 441 | 442 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/item"}, {"method": "post", "path": "/echo", "payload":"$0"} ] }'); 443 | 444 | expect(res.length).to.equal(2); 445 | expect(res[0].id).to.equal('55cf687663'); 446 | expect(res[0].name).to.equal('Active Item'); 447 | expect(res[1].id).to.equal('55cf687663'); 448 | expect(res[1].name).to.equal('Active Item'); 449 | }); 450 | 451 | it('supports piping a partial payload to the next request', async () => { 452 | 453 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/item"}, {"method": "post", "path": "/echo", "payload":"$0.name"} ] }'); 454 | 455 | expect(res.length).to.equal(2); 456 | expect(res[0].id).to.equal('55cf687663'); 457 | expect(res[0].name).to.equal('Active Item'); 458 | expect(res[1]).to.equal('Active Item'); 459 | }); 460 | 461 | it('supports piping a partial payload from a nested array to the next request', async () => { 462 | 463 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/array"}, {"method": "post", "path": "/echo", "payload":"$0.items.1"} ] }'); 464 | 465 | expect(res.length).to.equal(2); 466 | expect(res[0].id).to.equal('55cf687663'); 467 | expect(res[0].name).to.equal('Dress'); 468 | expect(res[1].color).to.equal('whiteandgold'); 469 | }); 470 | 471 | it('returns an empty object when a non-existent path is set at the root of the payload', async () => { 472 | 473 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/item"}, {"method": "post", "path": "/echo", "payload":"$0.foo"} ] }'); 474 | 475 | expect(res.length).to.equal(2); 476 | expect(res[0].id).to.equal('55cf687663'); 477 | expect(res[0].name).to.equal('Active Item'); 478 | expect(res[1]).to.be.empty(); 479 | }); 480 | 481 | it('sets a nested reference in the payload', async () => { 482 | 483 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/item"}, {"method": "post", "path": "/echo", "payload":{"name2": "$0.name"}} ] }'); 484 | 485 | expect(res.length).to.equal(2); 486 | expect(res[0].id).to.equal('55cf687663'); 487 | expect(res[0].name).to.equal('Active Item'); 488 | expect(res[1].name2).to.equal('Active Item'); 489 | }); 490 | 491 | it('returns an empty object when a nonexistent path is set in the payload', async () => { 492 | 493 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/item"}, {"method": "post", "path": "/echo", "payload":{"foo": "$0.foo"}} ] }'); 494 | 495 | expect(res.length).to.equal(2); 496 | expect(res[0].id).to.equal('55cf687663'); 497 | expect(res[0].name).to.equal('Active Item'); 498 | expect(res[1].foo).to.be.empty(); 499 | 500 | }); 501 | 502 | it('Now substitutes even `0` in serialized requests', async () => { 503 | 504 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/zero"}, {"method": "post", "path": "/returnInputtedInteger", "payload": {"id": "$0.id"}} ] }'); 505 | 506 | expect(res[0].id).to.equal(0); 507 | expect(res[1]).to.equal(0); 508 | }); 509 | 510 | it('Now substitutes even `false` in serialized requests', async () => { 511 | 512 | const res = await Internals.makeRequest(server, '{ "requests": [ {"method": "get", "path": "/getFalse"}, {"method": "post", "path": "/returnInputtedBoolean", "payload": {"bool": "$0"}} ] }'); 513 | 514 | expect(res[0]).to.equal(false); 515 | expect(res[1]).to.equal(false); 516 | }); 517 | 518 | it('Checks if pipeline requests works for a request depending on other request with index in non-single digit', async () => { 519 | 520 | const res = await Internals.makeRequest(server, JSON.stringify({ 521 | requests: [ 522 | { 523 | method: 'GET', 524 | path: '/item/0' 525 | }, 526 | { 527 | method: 'GET', 528 | path: '/item/1' 529 | }, 530 | { 531 | method: 'GET', 532 | path: '/item/2' 533 | }, 534 | { 535 | method: 'GET', 536 | path: '/item/3' 537 | }, 538 | { 539 | method: 'GET', 540 | path: '/item/4' 541 | }, 542 | { 543 | method: 'GET', 544 | path: '/item/5' 545 | }, 546 | { 547 | method: 'GET', 548 | path: '/item/6' 549 | }, 550 | { 551 | method: 'GET', 552 | path: '/item/7' 553 | }, 554 | { 555 | method: 'GET', 556 | path: '/item/8' 557 | }, 558 | { 559 | method: 'GET', 560 | path: '/item/9' 561 | }, 562 | { 563 | method: 'GET', 564 | path: '/item/10' 565 | }, 566 | { 567 | method: 'GET', 568 | path: '/item/$10.id' 569 | } 570 | ] 571 | })); 572 | 573 | expect(res[0].id).to.equal('0'); 574 | expect(res[1].id).to.equal('1'); 575 | expect(res[10].id).to.equal('10'); 576 | expect(res[11].id).to.equal('10'); 577 | expect(res[11].name).to.equal('Item'); 578 | }); 579 | 580 | it('substitutes index in url without any resultPath in url path parameters', async () => { 581 | 582 | const res = await Internals.makeRequest(server, JSON.stringify({ 583 | requests: [ 584 | { 585 | method: 'POST', 586 | path: '/returnInputtedInteger', 587 | payload: { 588 | id: 10041995 589 | } 590 | }, 591 | { 592 | method: 'GET', 593 | path: '/returnPathParamInteger/$0' 594 | } 595 | ] 596 | })); 597 | 598 | expect(res[0]).to.equal(10041995); 599 | expect(res[1]).to.equal(10041995); 600 | }); 601 | 602 | it('Query parameters and payload both now supports pipelined requests', async () => { 603 | 604 | const res = await Internals.makeRequest(server, JSON.stringify({ 605 | requests: [ 606 | { 607 | method: 'GET', 608 | path: '/item' 609 | }, 610 | { 611 | method: 'GET', 612 | path: '/profile', 613 | query: { 614 | id: '$0.id' 615 | } 616 | }, 617 | { 618 | method: 'GET', 619 | path: '/item/$1.id' 620 | }, 621 | { 622 | method: 'POST', 623 | path: '/echo', 624 | payload: { 625 | id: '$2.id', 626 | name: '$2.name' 627 | } 628 | }, 629 | { 630 | method: 'POST', 631 | path: '/returnInputtedString/$3.id/$3.name', 632 | query: { 633 | queryString: '$3.name' 634 | }, 635 | payload: { 636 | payloadString: '$3.name' 637 | } 638 | }, 639 | { 640 | method: 'GET', 641 | path: '/profile', 642 | query: { 643 | id: '$4.id' 644 | } 645 | } 646 | ] 647 | })); 648 | 649 | expect(res[0]).to.equal({ id: '55cf687663', name: 'Active Item' }); 650 | expect(res[1]).to.equal({ id: '55cf687663', name: 'John Doe' }); 651 | expect(res[2]).to.equal({ id: '55cf687663', name: 'Item' }); 652 | expect(res[3]).to.equal({ id: '55cf687663', name: 'Item' }); 653 | expect(res[4]).to.equal({ id: '55cf687663', paramString: 'Item', queryString: 'Item', payloadString: 'Item' }); 654 | expect(res[5]).to.equal({ id: '55cf687663', name: 'John Doe' }); 655 | }); 656 | }); 657 | --------------------------------------------------------------------------------