├── .gitignore ├── .npmignore ├── .travis.yml ├── API.md ├── LICENSE ├── README.md ├── lib ├── index.d.ts └── index.js ├── package.json └── test ├── esm.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | import: 3 | - source: hapipal/ci-config-travis:node_js.yml@node-v16-min 4 | - source: hapipal/ci-config-travis:hapi_all.yml@node-v16-min 5 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | The hapi utility toy chest 4 | 5 | > **Note** 6 | > 7 | > Toys is intended for use with hapi v20+ and nodejs v16+ (_see v3 for lower support_). 8 | 9 | ## `Toys` 10 | ### `Toys.withRouteDefaults(defaults)` 11 | 12 | Returns a function with signature `function(route)` that will apply `defaults` as defaults to the `route` [route configuration](https://github.com/hapijs/hapi/blob/master/API.md#server.route()) object. It will shallow-merge any route `validate` and `bind` options to avoid inadvertently applying defaults to a Joi schema or other unfamiliar object. If `route` is an array of routes, it will apply the defaults to each route in the array. 13 | 14 | ```js 15 | const defaultToGet = Toys.withRouteDefaults({ method: 'get' }); 16 | 17 | server.route( 18 | defaultToGet([ 19 | { 20 | path: '/', 21 | handler: () => 'I was gotten' 22 | }, 23 | { 24 | method: 'post', 25 | path: '/', 26 | handler: () => 'I was posted' 27 | } 28 | ]) 29 | ); 30 | ``` 31 | 32 | ### `Toys.pre(prereqs)` 33 | 34 | Returns a hapi [route prerequisite configuration](https://github.com/hapijs/hapi/blob/master/API.md#route.options.pre), mapping each key of `prereqs` to the `assign` value of a route prerequisite. When the key's corresponding value is a function, that function is used as the `method` of the prerequisite. When the key's corresponding value is an object, that object's keys and values are included in the prerequisite. When `prereqs` is a function, that function is simply passed-through. When `prereqs` is an array, the array's values are simply mapped as described above. 35 | 36 | This is intended to be a useful shorthand for writing route prerequisites, as demonstrated below. 37 | 38 | ```js 39 | server.route({ 40 | method: 'get', 41 | path: '/user/{id}', 42 | options: { 43 | pre: Toys.pre([ 44 | { 45 | user: async ({ params }) => await getUserById(params.id) 46 | }, 47 | ({ pre }) => ensureUserIsPublic(pre.user), 48 | { 49 | groups: async ({ params, pre }) => await getUserGroups(params.id, pre.user.roles), 50 | posts: async ({ params, pre }) => await getUserPosts(params.id, pre.user.roles) 51 | } 52 | ]), 53 | handler: ({ pre }) => ({ 54 | ...pre.user, 55 | groups: pre.groups, 56 | posts: pre.posts 57 | }) 58 | } 59 | }); 60 | 61 | // pre value is expanded as shown below 62 | 63 | /* 64 | pre: [ 65 | [ 66 | { 67 | assign: 'user', 68 | method: async ({ params }) => await getUserById(params.id) 69 | } 70 | ], 71 | ({ pre }) => ensureUserIsPublic(pre.user), 72 | [ 73 | { 74 | assign: 'groups', 75 | method: async ({ params, pre }) => await getUserGroups(params.id, pre.user.roles) 76 | }, 77 | { 78 | assign: 'posts', 79 | method: async ({ params, pre }) => await getUserPosts(params.id, pre.user.roles) 80 | } 81 | ] 82 | ] 83 | */ 84 | ``` 85 | 86 | ### `Toys.ext(method, [options])` 87 | 88 | Returns a hapi [extension config](https://github.com/hapijs/hapi/blob/master/API.md#server.ext()) `{ method, options }` without the `type` field. The config only has `options` set when provided as an argument. This is intended to be used with the route `ext` config. 89 | 90 | ```js 91 | server.route({ 92 | method: 'get', 93 | path: '/', 94 | options: { 95 | handler: (request) => { 96 | 97 | return { ok: true }; 98 | }, 99 | ext: { 100 | onPostAuth: Toys.ext((request, h) => { 101 | 102 | if (!request.headers['special-header']) { 103 | throw Boom.unauthorized(); 104 | } 105 | 106 | return h.continue; 107 | }) 108 | } 109 | } 110 | }); 111 | ``` 112 | 113 | ### `Toys.EXTENSION(method, [options])` 114 | 115 | Returns a hapi [extension config](https://github.com/hapijs/hapi/blob/master/API.md#server.ext()) `{ type, method, options}` with the `type` field set to `EXTENSION`, where `EXTENSION` is any of `onRequest`, `onPreAuth`, `onPostAuth`, `onCredentials`, `onPreHandler`, `onPostHandler`, `onPreResponse`, `onPreStart`, `onPostStart`, `onPreStop`, or `onPostStop`. The config only has `options` set when provided as an argument. This is intended to be used with [`server.ext()`](https://github.com/hapijs/hapi/blob/master/API.md#server.ext()). 116 | 117 | ```js 118 | server.ext([ 119 | Toys.onPreAuth((request, h) => { 120 | 121 | if (!request.query.specialParam) { 122 | throw Boom.unauthorized(); 123 | } 124 | 125 | return h.continue; 126 | }), 127 | Toys.onPreResponse((request, h) => { 128 | 129 | if (!request.response.isBoom && 130 | request.query.specialParam === 'secret') { 131 | 132 | request.log(['my-plugin'], 'Someone knew a secret'); 133 | } 134 | 135 | return h.continue; 136 | }, { 137 | sandbox: 'plugin' 138 | }) 139 | ]); 140 | ``` 141 | 142 | ### `Toys.auth.strategy(server, name, authenticate)` 143 | 144 | Adds an auth scheme and strategy with name `name` to `server`. Its implementation is given by `authenticate` as described in [`server.auth.scheme()`](https://github.com/hapijs/hapi/blob/master/API.md#server.auth.scheme()). This is intended to make it simple to create a barebones auth strategy without having to create a reusable auth scheme; it is often useful for testing and simple auth implementations. 145 | 146 | ```js 147 | Toys.auth.strategy(server, 'simple-bearer', async (request, h) => { 148 | 149 | const token = (request.headers.authorization || '').replace('Bearer ', ''); 150 | 151 | if (!token) { 152 | throw Boom.unauthorized(null, 'Bearer'); 153 | } 154 | 155 | const credentials = await lookupSession(token); 156 | 157 | return h.authenticated({ credentials }); 158 | }); 159 | 160 | server.route({ 161 | method: 'get', 162 | path: '/user', 163 | options: { 164 | auth: 'simple-bearer', 165 | handler: (request) => request.auth.credentials.user 166 | } 167 | }); 168 | ``` 169 | 170 | ### `Toys.noop` 171 | 172 | This is a plugin named `toys-noop` that does nothing and can be registered multiple times. This can be useful when conditionally registering a plugin in a list or [glue](https://github.com/hapijs/glue) manifest. 173 | 174 | ```js 175 | await server.register([ 176 | require('./my-plugin-a'), 177 | require('./my-plugin-b'), 178 | (process.env.NODE_ENV === 'production') ? Toys.noop : require('lout') 179 | ]); 180 | ``` 181 | 182 | ### `Toys.options(obj)` 183 | 184 | Given `obj` as a server, request, route, response toolkit, or realm, returns the relevant plugin options. If `obj` is none of the above then this method will throw an error. When used as an instance `obj` defaults to `toys.server`. 185 | 186 | ```js 187 | // Here is a route configuration in its own file. 188 | // 189 | // The route is added to the server somewhere else, but we still 190 | // need that server's plugin options for use in the handler. 191 | 192 | module.exports = { 193 | method: 'post', 194 | path: '/user/{id}/resend-verification-email', 195 | handler: async (request) => { 196 | 197 | // fromAddress configured at plugin registration time, e.g. no-reply@toys.biz 198 | const { fromAddress } = Toys.options(request); 199 | const user = await server.methods.getUser(request.params.id); 200 | 201 | await server.methods.sendVerificationEmail({ 202 | to: user.email, 203 | from: fromAddress 204 | }); 205 | 206 | return { success: true }; 207 | } 208 | }; 209 | ``` 210 | 211 | ### `Toys.header(response, name, value, [options])` 212 | 213 | Designed to behave identically to hapi's [`response.header(name, value, [options])`](https://hapi.dev/api/#response.header()), but provide a unified interface for setting HTTP headers between both hapi [response objects](https://hapi.dev/api/#response-object) and [boom](https://hapi.dev/family/boom) errors. This is useful in request extensions, when you don't know if [`request.response`](https://hapi.dev/api/#request.response) is a hapi response object or a boom error. Returns `response`. 214 | 215 | - `name` - the header name. 216 | - `value` - the header value. 217 | - `options` - (optional) object where: 218 | - `append` - if `true`, the value is appended to any existing header value using `separator`. Defaults to `false`. 219 | - `separator` - string used as separator when appending to an existing value. Defaults to `','`. 220 | - `override` - if `false`, the header value is not set if an existing value present. Defaults to `true`. 221 | - `duplicate` - if `false`, the header value is not modified if the provided value is already included. Does not apply when `append` is `false` or if the `name` is `'set-cookie'`. Defaults to `true`. 222 | 223 | ### `Toys.getHeaders(response)` 224 | 225 | Returns `response`'s current HTTP headers, where `response` may be a hapi [response object](https://hapi.dev/api/#response-object) or a [boom](https://hapi.dev/family/boom) error. 226 | 227 | ### `Toys.code(response, statusCode)` 228 | 229 | Designed to behave identically to hapi's [`response.code(statusCode)`](https://hapi.dev/api/#response.code()), but provide a unified interface for setting the HTTP status code between both hapi [response objects](https://hapi.dev/api/#response-object) and [boom](https://hapi.dev/family/boom) errors. This is useful in request extensions, when you don't know if [`request.response`](https://hapi.dev/api/#request.response) is a hapi response object or a boom error. Returns `response`. 230 | 231 | ### `Toys.getCode(response)` 232 | 233 | Returns `response`'s current HTTP status code, where `response` may be a hapi [response object](https://hapi.dev/api/#response-object) or a [boom](https://hapi.dev/family/boom) error. 234 | 235 | ### `Toys.realm(obj)` 236 | 237 | Given `obj` as a server, request, route, response toolkit, or realm, returns the relevant realm. If `obj` is none of the above then this method will throw an error. When used as an instance `obj` defaults to `toys.server`. 238 | 239 | ### `Toys.rootRealm(realm)` 240 | 241 | Given a `realm` this method follows the `realm.parent` chain and returns the topmost realm, known as the "root realm." When used as an instance, returns `toys.server.realm`'s root realm. 242 | 243 | ### `Toys.state(realm, pluginName)` 244 | 245 | Returns the plugin state for `pluginName` within `realm` (`realm.plugins[pluginName]`), and initializes it to an empty object if it is not already set. When used as an instance, returns the plugin state within `toys.server.realm`. 246 | 247 | ### `Toys.rootState(realm, pluginName)` 248 | 249 | Returns the plugin state for `pluginName` within `realm`'s [root realm](#toysrootrealmrealm), and initializes it to an empty object if it is not already set. When used as an instance, returns the plugin state within `toys.server.realm`'s root realm. 250 | 251 | ### `Toys.forEachAncestorRealm(realm, fn)` 252 | 253 | Walks up the `realm.parent` chain and calls `fn(realm)` for each realm, starting with the passed `realm`. When used as an instance, this method starts with `toys.server.realm`. 254 | 255 | ### `Toys.asyncStorage(identifier)` 256 | 257 | Returns async local storage store associated with `identifier`, as set-up using [`Toys.withAsyncStorage()`](#toyswithasyncstorageidentifier-store-fn). When there is no active store, returns `undefined`. 258 | 259 | ### `Toys.withAsyncStorage(identifier, store, fn)` 260 | 261 | Runs and returns the result of `fn` with an active async local storage `store` identified by `identifier`. Intended to be used with [`Toys.asyncStorage()`](#toysasyncstorageidentifier). Note that string identifiers beginning with `'@hapipal'` are reserved. 262 | 263 | ```js 264 | const multiplyBy = async (x) => { 265 | 266 | await Hoek.wait(10); // Wait 10ms 267 | 268 | return x * (Toys.asyncStorage('y') || 0); 269 | }; 270 | 271 | // The result is 4 * 3 = 12 272 | const result = await Toys.withAsyncStorage('y', 3, async () => { 273 | 274 | return await multiplyBy(4); 275 | }); 276 | ``` 277 | 278 | ### `Toys.asyncStorageInternals()` 279 | 280 | Returns a `Map` which maps identifiers utilized by [`Toys.withAsyncStorage()`](#toyswithasyncstorageidentifier-store-fn) to the underlying instances of [`AsyncLocalStorage`](https://nodejs.org/api/async_hooks.html#async_hooks_class_asynclocalstorage). 281 | 282 | ### `Toys.patchJoiSchema(schema)` 283 | 284 | Converts a Joi creation schema into a patch schema, ignoring defaults and making all keys optional. 285 | 286 | ```js 287 | // result.name is no longer required 288 | const result = Toys.patchJoiSchema(Joi.object().keys({ 289 | name: Joi.string().required() 290 | })); 291 | ``` 292 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2022 Devin Ivy and project contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------- 23 | 24 | The Toys.reacher() and Toys.transformer() functions are based on code from hoek, 25 | and the Toys.header() function is based on code from hapi. Your use of 26 | the source code for these functions is additionally subject to the terms and 27 | conditions of the following license. 28 | 29 | -------------------------------------------------- 30 | 31 | Copyright (c) 2011-2016, Project contributors 32 | Copyright (c) 2011-2014, Walmart 33 | Copyright (c) 2011, Yahoo Inc. 34 | All rights reserved. 35 | 36 | Redistribution and use in source and binary forms, with or without 37 | modification, are permitted provided that the following conditions are met: 38 | * Redistributions of source code must retain the above copyright 39 | notice, this list of conditions and the following disclaimer. 40 | * Redistributions in binary form must reproduce the above copyright 41 | notice, this list of conditions and the following disclaimer in the 42 | documentation and/or other materials provided with the distribution. 43 | * The names of any contributors may not be used to endorse or promote 44 | products derived from this software without specific prior written 45 | permission. 46 | 47 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 48 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 49 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 50 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY 51 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 52 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 53 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 54 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 55 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 56 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 57 | 58 | * * * 59 | 60 | The complete list of contributors can be found at: https://github.com/hapijs/hapi/graphs/contributors 61 | Portions of this project were initially based on the Yahoo! Inc. Postmile project, 62 | published at https://github.com/yahoo/postmile. 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toys 2 | 3 | The hapi utility toy chest 4 | 5 | [![Build Status](https://app.travis-ci.com/hapipal/toys.svg?branch=main)](https://app.travis-ci.com/hapipal/toys) [![Coverage Status](https://coveralls.io/repos/hapipal/toys/badge.svg?branch=main&service=github)](https://coveralls.io/github/hapipal/toys?branch=main) 6 | 7 | Lead Maintainer - [Devin Ivy](https://github.com/devinivy) 8 | 9 | ## Installation 10 | ```sh 11 | npm install @hapipal/toys 12 | ``` 13 | 14 | ## Usage 15 | > See also the [API Reference](API.md) 16 | > 17 | > Toys is intended for use with hapi v20+ and nodejs v16+ (_see v3 for lower support_). 18 | 19 | Toys is a collection of utilities made to reduce common boilerplate in **hapi v20+** projects. 20 | 21 | Below is an example featuring [`Toys.auth.strategy()`](API.md#toysauthstrategyserver-name-authenticate) and [`Toys.withRouteDefaults()`](API.md#toyswithroutedefaultsdefaults). The [API Reference](API.md) is also filled with examples. 22 | 23 | ```js 24 | const Hapi = require('@hapi/hapi'); 25 | const Boom = require('@hapi/boom'); 26 | const Toys = require('@hapipal/toys'); 27 | 28 | (async () => { 29 | 30 | const server = Hapi.server(); 31 | 32 | // Make a one-off auth strategy for testing 33 | Toys.auth.strategy(server, 'name-from-param', (request, h) => { 34 | 35 | // Yes, perhaps not the most secure 36 | const { username } = request.params; 37 | 38 | if (!username) { 39 | throw Boom.unauthorized(null, 'Custom'); 40 | } 41 | 42 | return h.authenticated({ credentials: { user: { name: username } } }); 43 | }); 44 | 45 | // Default all route methods to "get", unless otherwise specified 46 | const defaultToGet = Toys.withRouteDefaults({ method: 'get' }); 47 | 48 | server.route( 49 | defaultToGet([ 50 | { 51 | method: 'post', 52 | path: '/', 53 | handler: (request) => { 54 | 55 | return { posted: true }; 56 | } 57 | }, 58 | { // Look ma, my method is defaulting to "get"! 59 | path: '/as/{username}', 60 | options: { 61 | auth: 'name-from-param', // Here's our simple auth strategy 62 | handler: (request) => { 63 | 64 | const username = request.auth.credentials?.user?.name; 65 | 66 | return { username }; 67 | } 68 | } 69 | } 70 | ]) 71 | ); 72 | 73 | await server.start(); 74 | 75 | console.log(`Now, go forth and ${server.info.uri}/as/your-name`); 76 | })(); 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Lifecycle, 3 | Plugin, 4 | Request, 5 | ResponseObject, 6 | ResponseObjectHeaderOptions, 7 | ResponseToolkit, 8 | RouteExtObject, 9 | RouteOptionsPreArray, 10 | Server, 11 | ServerAuthScheme, 12 | ServerExtEventsObject, 13 | ServerExtOptions, 14 | ServerRealm, 15 | ServerRoute, 16 | } from '@hapi/hapi'; 17 | import { Boom } from '@hapi/boom'; 18 | import { EventEmitter } from 'events'; 19 | import { Stream, FinishedOptions } from 'stream'; 20 | import { AsyncLocalStorage } from 'async_hooks'; 21 | 22 | export function ext(method: Lifecycle.Method, options?: ServerExtOptions): RouteExtObject; 23 | export function onRequest(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 24 | export function onPreAuth(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 25 | export function onCredentials(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 26 | export function onPostAuth(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 27 | export function onPreHandler(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 28 | export function onPostHandler(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 29 | export function onPreResponse(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 30 | export function onPostResponse(method: Lifecycle.Method, options?: ServerExtOptions): ServerExtEventsObject; 31 | 32 | export type AssignRouteDefaults = (routes: Partial | Array>) => ServerRoute | ServerRoute[]; 33 | export function withRouteDefaults(defaults: Partial): AssignRouteDefaults; 34 | 35 | export interface ToysPreShorthand { 36 | [assignKey: string]: Lifecycle.Method; 37 | } 38 | export type ToysPreArg = (ToysPreShorthand | Lifecycle.Method); 39 | export function pre(options: ToysPreArg | ToysPreArg[]): RouteOptionsPreArray; 40 | 41 | export interface ReacherOptions { 42 | separator?: string | undefined; 43 | default?: any; 44 | strict?: boolean | undefined; 45 | functions?: boolean | undefined; 46 | iterables?: boolean | undefined; 47 | } 48 | export type PerformReach = (input?: object) => any; 49 | export function reacher(chain: string | Array<(string | number)>, options?: ReacherOptions): PerformReach; 50 | 51 | export type PerformTransform = (input: object) => object; 52 | export interface Transformer { 53 | [fromPath: string]: string; 54 | } 55 | export function transformer(transform: Transformer, options?: ReacherOptions): PerformTransform; 56 | 57 | export const noop: Plugin; 58 | 59 | export function header(response: ResponseObject | Boom, name: string, value: string, options?: ResponseObjectHeaderOptions): void; 60 | export function getHeaders(response: ResponseObject | Boom): { [header: string]: string }; 61 | 62 | export function code(response: ResponseObject | Boom, code: number): void; 63 | export function getCode(response: ResponseObject | Boom): number; 64 | 65 | export namespace auth { 66 | type ServerAuthSchemeAuthenticate = (request: Request, h: ResponseToolkit) => Lifecycle.ReturnValue; 67 | function strategy(server: Server, name: string, authenticate: ServerAuthSchemeAuthenticate): ServerAuthScheme; 68 | } 69 | 70 | export interface EventOptions { 71 | multiple?: boolean | undefined; 72 | error?: boolean | undefined; 73 | } 74 | // has to resolve an `any` because return type is dependent on the event that is emitted 75 | export function event(emitter: EventEmitter, eventName: string, options?: EventOptions): Promise; 76 | 77 | export interface StreamOptions { 78 | cleanup?: boolean | undefined; 79 | } 80 | export function stream(stream: Stream, options?: StreamOptions & FinishedOptions): Promise; 81 | 82 | export type TypesWithRealmsAndOptions = (Server | Request | ResponseToolkit | ServerRealm | ServerRoute); 83 | export function options(obj: TypesWithRealmsAndOptions): object; 84 | export function realm(obj: TypesWithRealmsAndOptions): object; 85 | 86 | export function rootRealm(realm: ServerRealm): ServerRealm; 87 | 88 | export function state(realm: ServerRealm, pluginName: string): object; 89 | export function rootState(realm: ServerRealm, pluginName: string): object; 90 | 91 | export type AncestorRealmIterator = (realm: ServerRealm) => void; 92 | export function forEachAncestorRealm(realm: ServerRealm, fn: AncestorRealmIterator): void; 93 | 94 | export function asyncStorage(identifier: string): any; 95 | export function withAsyncStorage(identifier: string, value: T, fn: () => void): Promise; 96 | 97 | export function asyncStorageInternals(): Map; 98 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { AsyncLocalStorage } = require('async_hooks'); 4 | const Hoek = require('@hapi/hoek'); 5 | 6 | const internals = {}; 7 | 8 | exports.withRouteDefaults = (defaults) => { 9 | 10 | return (options) => { 11 | 12 | if (Array.isArray(options)) { 13 | return options.map((opt) => internals.applyRouteDefaults(defaults, opt)); 14 | } 15 | 16 | return internals.applyRouteDefaults(defaults, options); 17 | }; 18 | }; 19 | 20 | exports.pre = (prereqSets) => { 21 | 22 | const method = (prereq) => { 23 | 24 | return (typeof prereq === 'function') ? { method: prereq } : prereq; 25 | }; 26 | 27 | const toPres = (prereqs) => { 28 | 29 | if (typeof prereqs === 'function') { 30 | return prereqs; 31 | } 32 | 33 | return Object.keys(prereqs).reduce((collect, assign) => { 34 | 35 | const prereq = prereqs[assign]; 36 | 37 | return collect.concat({ 38 | assign, 39 | ...method(prereq) 40 | }); 41 | }, []); 42 | }; 43 | 44 | if (Array.isArray(prereqSets)) { 45 | return prereqSets.map(toPres); 46 | } 47 | 48 | return toPres(prereqSets); 49 | }; 50 | 51 | exports.ext = (method, options) => { 52 | 53 | return internals.ext(null, method, options); 54 | }; 55 | 56 | exports.onRequest = (method, options) => { 57 | 58 | return internals.ext('onRequest', method, options); 59 | }; 60 | 61 | exports.onPreAuth = (method, options) => { 62 | 63 | return internals.ext('onPreAuth', method, options); 64 | }; 65 | 66 | exports.onCredentials = (method, options) => { 67 | 68 | return internals.ext('onCredentials', method, options); 69 | }; 70 | 71 | exports.onPostAuth = (method, options) => { 72 | 73 | return internals.ext('onPostAuth', method, options); 74 | }; 75 | 76 | exports.onPreHandler = (method, options) => { 77 | 78 | return internals.ext('onPreHandler', method, options); 79 | }; 80 | 81 | exports.onPostHandler = (method, options) => { 82 | 83 | return internals.ext('onPostHandler', method, options); 84 | }; 85 | 86 | exports.onPreResponse = (method, options) => { 87 | 88 | return internals.ext('onPreResponse', method, options); 89 | }; 90 | 91 | exports.onPreStart = (method, options) => { 92 | 93 | return internals.ext('onPreStart', method, options); 94 | }; 95 | 96 | exports.onPostStart = (method, options) => { 97 | 98 | return internals.ext('onPostStart', method, options); 99 | }; 100 | 101 | exports.onPreStop = (method, options) => { 102 | 103 | return internals.ext('onPreStop', method, options); 104 | }; 105 | 106 | exports.onPostStop = (method, options) => { 107 | 108 | return internals.ext('onPostStop', method, options); 109 | }; 110 | 111 | exports.auth = {}; 112 | 113 | exports.auth.strategy = (server, name, authenticate) => { 114 | 115 | server.auth.scheme(name, () => ({ authenticate })); 116 | server.auth.strategy(name, name); 117 | }; 118 | 119 | exports.noop = { 120 | name: 'toys-noop', 121 | multiple: true, 122 | register: Hoek.ignore 123 | }; 124 | 125 | exports.forEachAncestorRealm = (realm, fn) => { 126 | 127 | do { 128 | fn(realm); 129 | realm = realm.parent; 130 | } 131 | while (realm); 132 | }; 133 | 134 | exports.rootRealm = (realm) => { 135 | 136 | while (realm.parent) { 137 | realm = realm.parent; 138 | } 139 | 140 | return realm; 141 | }; 142 | 143 | exports.state = (realm, name) => { 144 | 145 | return internals.state(realm, name); 146 | }; 147 | 148 | exports.rootState = (realm, name) => { 149 | 150 | while (realm.parent) { 151 | realm = realm.parent; 152 | } 153 | 154 | return internals.state(realm, name); 155 | }; 156 | 157 | exports.realm = (obj) => { 158 | 159 | if (internals.isRealm(obj && obj.realm)) { 160 | // Server, route, response toolkit 161 | return obj.realm; 162 | } 163 | else if (internals.isRealm(obj && obj.route && obj.route.realm)) { 164 | // Request 165 | return obj.route.realm; 166 | } 167 | 168 | Hoek.assert(internals.isRealm(obj), 'Must pass a server, request, route, response toolkit, or realm'); 169 | 170 | // Realm 171 | return obj; 172 | }; 173 | 174 | exports.options = (obj) => { 175 | 176 | return this.realm(obj).pluginOptions; 177 | }; 178 | 179 | exports.header = (response, key, value, options = {}) => { 180 | 181 | Hoek.assert(response && (response.isBoom || typeof response.header === 'function'), 'The passed response must be a boom error or hapi response object.'); 182 | 183 | if (!response.isBoom) { 184 | return response.header(key, value, options); 185 | } 186 | 187 | key = key.toLowerCase(); 188 | const { headers } = response.output; 189 | 190 | const append = options.append || false; 191 | const separator = options.separator || ','; 192 | const override = options.override !== false; 193 | const duplicate = options.duplicate !== false; 194 | 195 | if ((!append && override) || !headers[key]) { 196 | headers[key] = value; 197 | } 198 | else if (override) { 199 | if (key === 'set-cookie') { 200 | headers[key] = [].concat(headers[key], value); 201 | } 202 | else { 203 | const existing = headers[key]; 204 | if (!duplicate) { 205 | const values = existing.split(separator); 206 | for (const v of values) { 207 | if (v === value) { 208 | return response; 209 | } 210 | } 211 | } 212 | 213 | headers[key] = existing + separator + value; 214 | } 215 | } 216 | 217 | return response; 218 | }; 219 | 220 | exports.getHeaders = (response) => { 221 | 222 | Hoek.assert(response && (response.isBoom || typeof response.header === 'function'), 'The passed response must be a boom error or hapi response object.'); 223 | 224 | if (!response.isBoom) { 225 | return response.headers; 226 | } 227 | 228 | return response.output.headers; 229 | }; 230 | 231 | exports.code = (response, statusCode) => { 232 | 233 | Hoek.assert(response && (response.isBoom || typeof response.code === 'function'), 'The passed response must be a boom error or hapi response object.'); 234 | 235 | if (!response.isBoom) { 236 | return response.code(statusCode); 237 | } 238 | 239 | response.output.statusCode = statusCode; 240 | response.reformat(); 241 | 242 | return response; 243 | }; 244 | 245 | exports.getCode = (response) => { 246 | 247 | Hoek.assert(response && (response.isBoom || typeof response.code === 'function'), 'The passed response must be a boom error or hapi response object.'); 248 | 249 | if (!response.isBoom) { 250 | return response.statusCode; 251 | } 252 | 253 | return response.output.statusCode; 254 | }; 255 | 256 | exports.asyncStorage = (identifier) => { 257 | 258 | if (!internals.asyncStorageInternals.has(identifier)) { 259 | return; 260 | } 261 | 262 | return internals.asyncStorageInternals.get(identifier).getStore(); 263 | }; 264 | 265 | exports.withAsyncStorage = (identifier, store, fn) => { 266 | 267 | Hoek.assert(typeof exports.asyncStorage(identifier) === 'undefined', `There is already an active async store for identifier "${String(identifier)}".`); 268 | 269 | if (!internals.asyncStorageInternals.has(identifier)) { 270 | internals.asyncStorageInternals.set(identifier, new AsyncLocalStorage()); 271 | } 272 | 273 | return internals.asyncStorageInternals.get(identifier).run(store, fn); 274 | }; 275 | 276 | exports.asyncStorageInternals = () => internals.asyncStorageInternals; 277 | 278 | exports.patchJoiSchema = (schema) => { 279 | 280 | const keys = Object.keys(schema.describe().keys || {}); 281 | 282 | // Make all keys optional, do not enforce defaults 283 | 284 | if (keys.length) { 285 | schema = schema.fork(keys, (s) => s.optional()); 286 | } 287 | 288 | return schema.prefs({ noDefaults: true }); 289 | }; 290 | 291 | // Looks like a realm 292 | internals.isRealm = (obj) => obj && obj.hasOwnProperty('pluginOptions') && obj.hasOwnProperty('modifiers'); 293 | 294 | internals.applyRouteDefaults = (defaults, options) => { 295 | 296 | return Hoek.applyToDefaults(defaults, options, { 297 | shallow: [ 298 | 'options.bind', 299 | 'config.bind', 300 | 'options.validate.headers', 301 | 'config.validate.headers', 302 | 'options.validate.payload', 303 | 'config.validate.payload', 304 | 'options.validate.params', 305 | 'config.validate.params', 306 | 'options.validate.query', 307 | 'config.validate.query', 308 | 'options.response.schema', 309 | 'config.response.schema', 310 | 'options.validate.validator', 311 | 'config.validate.validator' 312 | ] 313 | }); 314 | }; 315 | 316 | internals.state = (realm, name) => { 317 | 318 | const state = realm.plugins[name] = realm.plugins[name] || {}; 319 | return state; 320 | }; 321 | 322 | internals.ext = (type, method, options) => { 323 | 324 | const extConfig = { method }; 325 | 326 | if (type) { 327 | extConfig.type = type; 328 | } 329 | 330 | if (options) { 331 | extConfig.options = options; 332 | } 333 | 334 | return extConfig; 335 | }; 336 | 337 | internals.asyncStorageInternals = new Map(); 338 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hapipal/toys", 3 | "version": "4.0.0", 4 | "description": "The hapi utility toy chest", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "engines": { 8 | "node": ">=16" 9 | }, 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "test": "lab -a @hapi/code -t 100 -L", 15 | "coveralls": "lab -r lcov | coveralls" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/hapipal/toys.git" 20 | }, 21 | "keywords": [ 22 | "hapi", 23 | "utility", 24 | "performance", 25 | "convenience", 26 | "tools" 27 | ], 28 | "author": "Devin Ivy ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/hapipal/toys/issues" 32 | }, 33 | "homepage": "https://github.com/hapipal/toys#readme", 34 | "dependencies": { 35 | "@hapi/hoek": "^11.0.2" 36 | }, 37 | "peerDependencies": { 38 | "@hapi/hapi": ">=20 <22" 39 | }, 40 | "peerDependenciesMeta": { 41 | "@hapi/hapi": { 42 | "optional": true 43 | } 44 | }, 45 | "devDependencies": { 46 | "@hapi/boom": "^10.0.0", 47 | "@hapi/code": "^9.0.2", 48 | "@hapi/hapi": "^21.2.1", 49 | "@hapi/lab": "^25.1.0", 50 | "coveralls": "^3.0.0", 51 | "joi": "^17.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/esm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Lab = require('@hapi/lab'); 5 | 6 | 7 | const { before, describe, it } = exports.lab = Lab.script(); 8 | const expect = Code.expect; 9 | 10 | 11 | describe('import()', () => { 12 | 13 | let Toys; 14 | 15 | before(async () => { 16 | 17 | Toys = await import('../lib/index.js'); 18 | }); 19 | 20 | it('exposes all methods and classes as named imports', () => { 21 | 22 | expect(Object.keys(Toys)).to.equal([ 23 | 'asyncStorage', 24 | 'asyncStorageInternals', 25 | 'auth', 26 | 'code', 27 | 'default', 28 | 'ext', 29 | 'forEachAncestorRealm', 30 | 'getCode', 31 | 'getHeaders', 32 | 'header', 33 | 'noop', 34 | 'onCredentials', 35 | 'onPostAuth', 36 | 'onPostHandler', 37 | 'onPostStart', 38 | 'onPostStop', 39 | 'onPreAuth', 40 | 'onPreHandler', 41 | 'onPreResponse', 42 | 'onPreStart', 43 | 'onPreStop', 44 | 'onRequest', 45 | 'options', 46 | 'patchJoiSchema', 47 | 'pre', 48 | 'realm', 49 | 'rootRealm', 50 | 'rootState', 51 | 'state', 52 | 'withAsyncStorage', 53 | 'withRouteDefaults' 54 | ]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Lab = require('@hapi/lab'); 6 | const Code = require('@hapi/code'); 7 | const Hapi = require('@hapi/hapi'); 8 | const Boom = require('@hapi/boom'); 9 | const Hoek = require('@hapi/hoek'); 10 | const Joi = require('joi'); 11 | const Toys = require('..'); 12 | 13 | // Test shortcuts 14 | 15 | const lab = exports.lab = Lab.script(); 16 | const describe = lab.describe; 17 | const it = lab.it; 18 | const expect = Code.expect; 19 | 20 | describe('Toys', () => { 21 | 22 | describe('withRouteDefaults()', () => { 23 | 24 | const defaults = { 25 | a: 1, 26 | b: 2, 27 | c: { 28 | d: 3, 29 | e: [5, 6] 30 | }, 31 | f: 6, 32 | g: 'test' 33 | }; 34 | 35 | it('throws when target is null.', () => { 36 | 37 | expect(() => { 38 | 39 | Toys.withRouteDefaults(null)({}); 40 | }).to.throw('Invalid defaults value: must be an object'); 41 | }); 42 | 43 | it('returns null if options is false.', () => { 44 | 45 | const result = Toys.withRouteDefaults(defaults)(false); 46 | expect(result).to.equal(null); 47 | }); 48 | 49 | it('returns null if options is null.', () => { 50 | 51 | const result = Toys.withRouteDefaults(defaults)(null); 52 | expect(result).to.equal(null); 53 | }); 54 | 55 | it('returns null if options is undefined.', () => { 56 | 57 | const result = Toys.withRouteDefaults(defaults)(undefined); 58 | expect(result).to.equal(null); 59 | }); 60 | 61 | it('returns a copy of defaults if options is true.', () => { 62 | 63 | const result = Toys.withRouteDefaults(defaults)(true); 64 | expect(result).to.equal(defaults); 65 | }); 66 | 67 | it('applies object to defaults.', () => { 68 | 69 | const obj = { 70 | a: null, 71 | c: { 72 | e: [4] 73 | }, 74 | f: 0, 75 | g: { 76 | h: 5 77 | } 78 | }; 79 | 80 | const result = Toys.withRouteDefaults(defaults)(obj); 81 | expect(result.c.e).to.equal([4]); 82 | expect(result.a).to.equal(1); 83 | expect(result.b).to.equal(2); 84 | expect(result.f).to.equal(0); 85 | expect(result.g).to.equal({ h: 5 }); 86 | }); 87 | 88 | it('maps array objects over defaults.', () => { 89 | 90 | const obj = { 91 | a: null, 92 | c: { 93 | e: [4] 94 | }, 95 | f: 0, 96 | g: { 97 | h: 5 98 | } 99 | }; 100 | 101 | const results = Toys.withRouteDefaults(defaults)([obj, obj, obj]); 102 | 103 | expect(results).to.have.length(3); 104 | 105 | results.forEach((result) => { 106 | 107 | expect(result.c.e).to.equal([4]); 108 | expect(result.a).to.equal(1); 109 | expect(result.b).to.equal(2); 110 | expect(result.f).to.equal(0); 111 | expect(result.g).to.equal({ h: 5 }); 112 | }); 113 | }); 114 | 115 | it('applies object to defaults multiple times.', () => { 116 | 117 | const obj = { 118 | a: null, 119 | c: { 120 | e: [4] 121 | }, 122 | f: 0, 123 | g: { 124 | h: 5 125 | } 126 | }; 127 | 128 | const withRouteDefaults = Toys.withRouteDefaults(defaults); 129 | const once = withRouteDefaults(obj); 130 | const twice = withRouteDefaults(obj); 131 | 132 | expect(once).to.equal(twice); 133 | }); 134 | 135 | it('shallow copies route-specific properties.', () => { 136 | 137 | const shallowDefaults = { 138 | config: { 139 | anything: { x: 1 }, 140 | validate: { 141 | query: { defaults: true }, 142 | payload: { defaults: true }, 143 | headers: { defaults: true }, 144 | params: { defaults: true }, 145 | validator: { defaults: null } 146 | }, 147 | bind: { defaults: true }, 148 | response: { 149 | schema: { defaults: true } 150 | } 151 | }, 152 | options: { 153 | anything: { x: 1 }, 154 | validate: { 155 | query: { defaults: true }, 156 | payload: { defaults: true }, 157 | headers: { defaults: true }, 158 | params: { defaults: true }, 159 | validator: { defaults: null } 160 | }, 161 | bind: { defaults: true }, 162 | response: { 163 | schema: { defaults: true } 164 | } 165 | } 166 | }; 167 | 168 | const route = { 169 | config: { 170 | anything: { y: 2 }, 171 | validate: { 172 | query: {}, 173 | payload: {}, 174 | headers: {}, 175 | params: {}, 176 | validator: {} 177 | }, 178 | bind: {}, 179 | response: { 180 | schema: {} 181 | } 182 | }, 183 | options: { 184 | anything: { y: 2 }, 185 | validate: { 186 | query: {}, 187 | payload: {}, 188 | headers: {}, 189 | params: {}, 190 | validator: {} 191 | }, 192 | bind: {}, 193 | response: { 194 | schema: {} 195 | } 196 | } 197 | }; 198 | 199 | const result = Toys.withRouteDefaults(shallowDefaults)(route); 200 | 201 | expect(result).to.equal({ 202 | config: { 203 | anything: { x: 1, y: 2 }, 204 | validate: { 205 | query: {}, 206 | payload: {}, 207 | headers: {}, 208 | params: {}, 209 | validator: {} 210 | }, 211 | bind: {}, 212 | response: { 213 | schema: {} 214 | } 215 | }, 216 | options: { 217 | anything: { x: 1, y: 2 }, 218 | validate: { 219 | query: {}, 220 | payload: {}, 221 | headers: {}, 222 | params: {}, 223 | validator: {} 224 | }, 225 | bind: {}, 226 | response: { 227 | schema: {} 228 | } 229 | } 230 | }); 231 | }); 232 | }); 233 | 234 | describe('auth.strategy()', () => { 235 | 236 | it('creates a one-off auth strategy with duplicate scheme name.', async () => { 237 | 238 | const server = Hapi.server(); 239 | 240 | Toys.auth.strategy(server, 'test-auth', (request, h) => { 241 | 242 | return h.authenticated({ credentials: { user: 'bill' } }); 243 | }); 244 | 245 | // Reuse the scheme implicitly created above 246 | server.auth.strategy('test-auth-again', 'test-auth'); 247 | 248 | server.route([ 249 | { 250 | method: 'get', 251 | path: '/', 252 | config: { 253 | auth: 'test-auth', 254 | handler: (request) => request.auth.credentials 255 | } 256 | }, 257 | { 258 | method: 'get', 259 | path: '/again', 260 | config: { 261 | auth: 'test-auth-again', 262 | handler: (request) => request.auth.credentials 263 | } 264 | } 265 | ]); 266 | 267 | const res1 = await server.inject('/'); 268 | 269 | expect(res1.result).to.equal({ user: 'bill' }); 270 | 271 | const res2 = await server.inject('/again'); 272 | 273 | expect(res2.result).to.equal({ user: 'bill' }); 274 | }); 275 | }); 276 | 277 | describe('pre()', () => { 278 | 279 | it('creates valid hapi route prerequisites with a function or config.', () => { 280 | 281 | const prereqs = { 282 | assign1: () => null, 283 | assign2: { method: () => null, failAction: 'log' } 284 | }; 285 | 286 | const [pre1, pre2, ...others] = Toys.pre(prereqs); 287 | 288 | expect(others).to.have.length(0); 289 | 290 | expect(pre1).to.only.contain(['assign', 'method']); 291 | expect(pre1.assign).to.equal('assign1'); 292 | expect(pre1.method).to.shallow.equal(prereqs.assign1); 293 | 294 | expect(pre2).to.only.contain(['assign', 'method', 'failAction']); 295 | expect(pre2.assign).to.equal('assign2'); 296 | expect(pre2.method).to.shallow.equal(prereqs.assign2.method); 297 | expect(pre2.failAction).to.equal('log'); 298 | 299 | const method = () => null; 300 | 301 | expect(Toys.pre(method)).to.shallow.equal(method); 302 | }); 303 | 304 | it('creates valid hapi route prerequisites with an array.', () => { 305 | 306 | const prereqs = [ 307 | { assign1: () => null }, 308 | { assign2: { method: () => null, failAction: 'log' } }, 309 | { 310 | assign3: () => null, 311 | assign4: () => null 312 | }, 313 | () => null 314 | ]; 315 | 316 | const [ 317 | [pre1, ...others1], 318 | [pre2, ...others2], 319 | [pre3, pre4, ...others3], 320 | pre5, 321 | ...others 322 | ] = Toys.pre(prereqs); 323 | 324 | expect(others).to.have.length(0); 325 | expect(others1).to.have.length(0); 326 | expect(others2).to.have.length(0); 327 | expect(others3).to.have.length(0); 328 | 329 | expect(pre1).to.only.contain(['assign', 'method']); 330 | expect(pre1.assign).to.equal('assign1'); 331 | expect(pre1.method).to.shallow.equal(prereqs[0].assign1); 332 | 333 | expect(pre2).to.only.contain(['assign', 'method', 'failAction']); 334 | expect(pre2.assign).to.equal('assign2'); 335 | expect(pre2.method).to.shallow.equal(prereqs[1].assign2.method); 336 | expect(pre2.failAction).to.equal('log'); 337 | 338 | expect(pre3).to.only.contain(['assign', 'method']); 339 | expect(pre3.assign).to.equal('assign3'); 340 | expect(pre3.method).to.shallow.equal(prereqs[2].assign3); 341 | 342 | expect(pre4).to.only.contain(['assign', 'method']); 343 | expect(pre4.assign).to.equal('assign4'); 344 | expect(pre4.method).to.shallow.equal(prereqs[2].assign4); 345 | 346 | expect(pre5).to.shallow.equal(prereqs[3]); 347 | }); 348 | }); 349 | 350 | // Test the request extension helpers 351 | 352 | [ 353 | 'ext', 354 | 'onPreStart', 355 | 'onPostStart', 356 | 'onPreStop', 357 | 'onPostStop', 358 | 'onRequest', 359 | 'onPreAuth', 360 | 'onPostAuth', 361 | 'onCredentials', 362 | 'onPreHandler', 363 | 'onPostHandler', 364 | 'onPreResponse' 365 | ].forEach((ext) => { 366 | 367 | const isExt = (ext === 'ext'); 368 | 369 | describe(`${ext}()`, () => { 370 | 371 | it('creates a valid hapi request extension without options.', () => { 372 | 373 | const fn = function () {}; 374 | const extension = Toys[ext](fn); 375 | 376 | const keys = isExt ? ['method'] : ['type', 'method']; 377 | 378 | expect(Object.keys(extension)).to.only.contain(keys); 379 | isExt || expect(extension.type).to.equal(ext); 380 | expect(extension.method).to.shallow.equal(fn); 381 | }); 382 | 383 | it('creates a valid hapi request extension with options.', () => { 384 | 385 | const fn = function () {}; 386 | const opts = { before: 'loveboat' }; 387 | const extension = Toys[ext](fn, opts); 388 | 389 | const keys = isExt ? ['method', 'options'] : ['type', 'method', 'options']; 390 | 391 | expect(Object.keys(extension)).to.only.contain(keys); 392 | isExt || expect(extension.type).to.equal(ext); 393 | expect(extension.method).to.shallow.equal(fn); 394 | expect(extension.options).to.shallow.equal(opts); 395 | }); 396 | }); 397 | }); 398 | 399 | describe('noop', () => { 400 | 401 | it('is a hapi plugin that does nothing and can be registered multiple times.', async () => { 402 | 403 | const server = Hapi.server(); 404 | 405 | expect(Toys.noop.register).to.shallow.equal(Hoek.ignore); 406 | 407 | await server.register(Toys.noop); 408 | 409 | expect(server.registrations).to.only.contain('toys-noop'); 410 | 411 | await server.register(Toys.noop); 412 | 413 | expect(server.registrations).to.only.contain('toys-noop'); 414 | }); 415 | }); 416 | 417 | describe('options()', () => { 418 | 419 | it('gets plugin options from a server.', async () => { 420 | 421 | const server = Hapi.server(); 422 | 423 | expect(Toys.options(server)).to.shallow.equal(server.realm.pluginOptions); 424 | 425 | await server.register({ 426 | name: 'plugin', 427 | register(srv) { 428 | 429 | expect(Toys.options(srv)).to.shallow.equal(srv.realm.pluginOptions); 430 | } 431 | }); 432 | }); 433 | 434 | it('gets plugin options from a request.', async () => { 435 | 436 | const server = Hapi.server(); 437 | 438 | await server.register({ 439 | name: 'plugin', 440 | register(srv) { 441 | 442 | srv.route({ 443 | method: 'get', 444 | path: '/', 445 | handler(request) { 446 | 447 | expect(Toys.options(request)).to.shallow.equal(request.route.realm.pluginOptions); 448 | expect(Toys.options(request)).to.shallow.equal(srv.realm.pluginOptions); 449 | 450 | return { ok: true }; 451 | } 452 | }); 453 | } 454 | }); 455 | 456 | const { result } = await server.inject('/'); 457 | 458 | expect(result).to.equal({ ok: true }); 459 | }); 460 | 461 | it('gets plugin options from a route.', async () => { 462 | 463 | const server = Hapi.server(); 464 | 465 | await server.register({ 466 | name: 'plugin', 467 | register(srv) { 468 | 469 | srv.route({ 470 | method: 'get', 471 | path: '/', 472 | handler(request) { 473 | 474 | expect(Toys.options(request.route)).to.shallow.equal(request.route.realm.pluginOptions); 475 | expect(Toys.options(request.route)).to.shallow.equal(srv.realm.pluginOptions); 476 | 477 | return { ok: true }; 478 | } 479 | }); 480 | } 481 | }); 482 | 483 | const { result } = await server.inject('/'); 484 | 485 | expect(result).to.equal({ ok: true }); 486 | }); 487 | 488 | it('gets plugin options from a response toolkit.', async () => { 489 | 490 | const server = Hapi.server(); 491 | 492 | await server.register({ 493 | name: 'plugin', 494 | register(srv) { 495 | 496 | srv.route({ 497 | method: 'get', 498 | path: '/', 499 | handler(request, h) { 500 | 501 | expect(Toys.options(h)).to.shallow.equal(h.realm.pluginOptions); 502 | expect(Toys.options(h)).to.shallow.equal(srv.realm.pluginOptions); 503 | 504 | return { ok: true }; 505 | } 506 | }); 507 | } 508 | }); 509 | 510 | const { result } = await server.inject('/'); 511 | 512 | expect(result).to.equal({ ok: true }); 513 | }); 514 | 515 | it('gets plugin options from a realm.', async () => { 516 | 517 | const server = Hapi.server(); 518 | 519 | expect(Toys.options(server.realm)).to.shallow.equal(server.realm.pluginOptions); 520 | 521 | await server.register({ 522 | name: 'plugin', 523 | register(srv) { 524 | 525 | expect(Toys.options(srv.realm)).to.shallow.equal(srv.realm.pluginOptions); 526 | } 527 | }); 528 | }); 529 | 530 | it('throws when passed an unfamiliar object.', () => { 531 | 532 | expect(() => Toys.options({})).to.throw('Must pass a server, request, route, response toolkit, or realm'); 533 | expect(() => Toys.options(null)).to.throw('Must pass a server, request, route, response toolkit, or realm'); 534 | }); 535 | }); 536 | 537 | describe('realm()', () => { 538 | 539 | it('gets realm from a server.', async () => { 540 | 541 | const server = Hapi.server(); 542 | 543 | expect(Toys.realm(server)).to.shallow.equal(server.realm); 544 | 545 | await server.register({ 546 | name: 'plugin', 547 | register(srv) { 548 | 549 | expect(Toys.realm(srv)).to.shallow.equal(srv.realm); 550 | } 551 | }); 552 | }); 553 | 554 | it('gets realm from a request.', async () => { 555 | 556 | const server = Hapi.server(); 557 | 558 | await server.register({ 559 | name: 'plugin', 560 | register(srv) { 561 | 562 | srv.route({ 563 | method: 'get', 564 | path: '/', 565 | handler(request) { 566 | 567 | expect(Toys.realm(request)).to.shallow.equal(request.route.realm); 568 | expect(Toys.realm(request)).to.shallow.equal(srv.realm); 569 | 570 | return { ok: true }; 571 | } 572 | }); 573 | } 574 | }); 575 | 576 | const { result } = await server.inject('/'); 577 | 578 | expect(result).to.equal({ ok: true }); 579 | }); 580 | 581 | it('gets realm from a route.', async () => { 582 | 583 | const server = Hapi.server(); 584 | 585 | await server.register({ 586 | name: 'plugin', 587 | register(srv) { 588 | 589 | srv.route({ 590 | method: 'get', 591 | path: '/', 592 | handler(request) { 593 | 594 | expect(Toys.realm(request.route)).to.shallow.equal(request.route.realm); 595 | expect(Toys.realm(request.route)).to.shallow.equal(srv.realm); 596 | 597 | return { ok: true }; 598 | } 599 | }); 600 | } 601 | }); 602 | 603 | const { result } = await server.inject('/'); 604 | 605 | expect(result).to.equal({ ok: true }); 606 | }); 607 | 608 | it('gets realm from a response toolkit.', async () => { 609 | 610 | const server = Hapi.server(); 611 | 612 | await server.register({ 613 | name: 'plugin', 614 | register(srv) { 615 | 616 | srv.route({ 617 | method: 'get', 618 | path: '/', 619 | handler(request, h) { 620 | 621 | expect(Toys.realm(h)).to.shallow.equal(h.realm); 622 | expect(Toys.realm(h)).to.shallow.equal(srv.realm); 623 | 624 | return { ok: true }; 625 | } 626 | }); 627 | } 628 | }); 629 | 630 | const { result } = await server.inject('/'); 631 | 632 | expect(result).to.equal({ ok: true }); 633 | }); 634 | 635 | it('gets realm from a realm.', async () => { 636 | 637 | const server = Hapi.server(); 638 | 639 | expect(Toys.realm(server.realm)).to.shallow.equal(server.realm); 640 | 641 | await server.register({ 642 | name: 'plugin', 643 | register(srv) { 644 | 645 | expect(Toys.realm(srv.realm)).to.shallow.equal(srv.realm); 646 | } 647 | }); 648 | }); 649 | 650 | it('throws when passed an unfamiliar object.', () => { 651 | 652 | expect(() => Toys.realm({})).to.throw('Must pass a server, request, route, response toolkit, or realm'); 653 | expect(() => Toys.realm(null)).to.throw('Must pass a server, request, route, response toolkit, or realm'); 654 | }); 655 | }); 656 | 657 | describe('rootRealm()', () => { 658 | 659 | it('given a realm, returns the root server\'s realm.', async () => { 660 | 661 | const server = Hapi.server(); 662 | 663 | expect(Toys.rootRealm(server.realm)).to.shallow.equal(server.realm); 664 | 665 | await server.register({ 666 | name: 'plugin-a', 667 | async register(srvB) { 668 | 669 | await srvB.register({ 670 | name: 'plugin-a1', 671 | register(srvA1) { 672 | 673 | expect(Toys.rootRealm(srvA1.realm)).to.shallow.equal(server.realm); 674 | } 675 | }); 676 | } 677 | }); 678 | }); 679 | }); 680 | 681 | describe('state()', () => { 682 | 683 | it('returns/initializes plugin state given a realm and plugin name.', async () => { 684 | 685 | const server = Hapi.server(); 686 | const state = () => Toys.state(server.realm, 'root'); 687 | 688 | expect(server.realm.plugins.root).to.not.exist(); 689 | expect(state()).to.shallow.equal(state()); 690 | expect(state()).to.shallow.equal(server.realm.plugins.root); 691 | expect(state()).to.equal({}); 692 | 693 | await server.register({ 694 | name: 'plugin-a', 695 | register(srv) { 696 | 697 | const stateA = () => Toys.state(srv.realm, 'plugin-a'); 698 | 699 | expect(srv.realm.plugins['plugin-a']).to.not.exist(); 700 | expect(stateA()).to.shallow.equal(stateA()); 701 | expect(stateA()).to.shallow.equal(srv.realm.plugins['plugin-a']); 702 | expect(stateA()).to.equal({}); 703 | } 704 | }); 705 | }); 706 | }); 707 | 708 | describe('rootState()', () => { 709 | 710 | it('given a realm, returns the root server\'s realm.', async () => { 711 | 712 | const server = Hapi.server(); 713 | const state = () => Toys.rootState(server.realm, 'root'); 714 | 715 | expect(server.realm.plugins.root).to.not.exist(); 716 | expect(state()).to.shallow.equal(state()); 717 | expect(state()).to.shallow.equal(server.realm.plugins.root); 718 | expect(state()).to.equal({}); 719 | 720 | await server.register({ 721 | name: 'plugin-a', 722 | async register(srvB) { 723 | 724 | await srvB.register({ 725 | name: 'plugin-a1', 726 | register(srvA1) { 727 | 728 | const stateA1 = () => Toys.rootState(srvA1.realm, 'plugin-a1'); 729 | 730 | expect(server.realm.plugins['plugin-a1']).to.not.exist(); 731 | expect(stateA1()).to.shallow.equal(stateA1()); 732 | expect(stateA1()).to.shallow.equal(server.realm.plugins['plugin-a1']); 733 | expect(stateA1()).to.equal({}); 734 | } 735 | }); 736 | } 737 | }); 738 | }); 739 | }); 740 | 741 | describe('forEachAncestorRealm()', () => { 742 | 743 | it('calls a function for each ancestor realm up to the root realm', async () => { 744 | 745 | const server = Hapi.server(); 746 | 747 | await server.register({ 748 | name: 'plugin-a', 749 | register() {} 750 | }); 751 | 752 | await server.register({ 753 | name: 'plugin-b', 754 | async register(srvB) { 755 | 756 | await srvB.register({ 757 | name: 'plugin-b1', 758 | register(srvB1) { 759 | 760 | const realms = []; 761 | Toys.forEachAncestorRealm(srvB1.realm, (realm) => realms.push(realm)); 762 | 763 | expect(realms).to.have.length(3); 764 | expect(realms[0]).to.shallow.equal(srvB1.realm); 765 | expect(realms[1]).to.shallow.equal(srvB.realm); 766 | expect(realms[2]).to.shallow.equal(server.realm); 767 | } 768 | }); 769 | } 770 | }); 771 | }); 772 | }); 773 | 774 | describe('header()', () => { 775 | 776 | const testHeadersWith = (method) => { 777 | 778 | const server = Hapi.server(); 779 | 780 | server.route({ 781 | method: 'get', 782 | path: '/non-error', 783 | options: { 784 | handler: () => ({ success: true }), 785 | ext: { 786 | onPreResponse: { method } 787 | } 788 | } 789 | }); 790 | 791 | server.route({ 792 | method: 'get', 793 | path: '/error', 794 | options: { 795 | handler: () => { 796 | 797 | throw Boom.unauthorized('Original message'); 798 | }, 799 | ext: { 800 | onPreResponse: { method } 801 | } 802 | } 803 | }); 804 | 805 | return server; 806 | }; 807 | 808 | it('throws when passed a non-response.', () => { 809 | 810 | expect(() => Toys.header(null)).to.throw('The passed response must be a boom error or hapi response object.'); 811 | expect(() => Toys.header({})).to.throw('The passed response must be a boom error or hapi response object.'); 812 | }); 813 | 814 | it('sets headers without any options.', async () => { 815 | 816 | const server = testHeadersWith((request, h) => { 817 | 818 | Toys.header(request.response, 'a', 'x'); 819 | Toys.header(request.response, 'b', 'x'); 820 | Toys.header(request.response, 'b', 'y'); 821 | 822 | return h.continue; 823 | }); 824 | 825 | const { headers: errorHeaders } = await server.inject('/error'); 826 | const { headers: nonErrorHeaders } = await server.inject('/non-error'); 827 | 828 | expect(errorHeaders).to.contain({ a: 'x', b: 'y' }); 829 | expect(nonErrorHeaders).to.contain({ a: 'x', b: 'y' }); 830 | }); 831 | 832 | it('does not set existing header when override is false.', async () => { 833 | 834 | const server = testHeadersWith((request, h) => { 835 | 836 | Toys.header(request.response, 'a', 'x', { override: false }); 837 | Toys.header(request.response, 'a', 'y', { override: false }); 838 | 839 | return h.continue; 840 | }); 841 | 842 | const { headers: errorHeaders } = await server.inject('/error'); 843 | const { headers: nonErrorHeaders } = await server.inject('/non-error'); 844 | 845 | expect(errorHeaders).to.contain({ a: 'x' }); 846 | expect(nonErrorHeaders).to.contain({ a: 'x' }); 847 | }); 848 | 849 | it('appends to existing headers with separator.', async () => { 850 | 851 | const server = testHeadersWith((request, h) => { 852 | 853 | Toys.header(request.response, 'a', 'x', { append: true }); 854 | Toys.header(request.response, 'A', 'y', { append: true }); 855 | Toys.header(request.response, 'b', 'x', { append: true, separator: ';' }); 856 | Toys.header(request.response, 'B', 'y', { append: true, separator: ';' }); 857 | 858 | return h.continue; 859 | }); 860 | 861 | const { headers: errorHeaders } = await server.inject('/error'); 862 | const { headers: nonErrorHeaders } = await server.inject('/non-error'); 863 | 864 | expect(errorHeaders).to.contain({ a: 'x,y', b: 'x;y' }); 865 | expect(nonErrorHeaders).to.contain({ a: 'x,y', b: 'x;y' }); 866 | }); 867 | 868 | it('handles special case for appending set-cookie.', async () => { 869 | 870 | const server = testHeadersWith((request, h) => { 871 | 872 | Toys.header(request.response, 'set-cookie', 'a=x', { append: true }); 873 | Toys.header(request.response, 'set-cookie', 'b=x', { append: true }); 874 | Toys.header(request.response, 'Set-Cookie', 'b=y', { append: true }); 875 | 876 | return h.continue; 877 | }); 878 | 879 | const { headers: errorHeaders } = await server.inject('/error'); 880 | const { headers: nonErrorHeaders } = await server.inject('/non-error'); 881 | 882 | expect(nonErrorHeaders).to.contain({ 'set-cookie': ['a=x', 'b=x', 'b=y'] }); 883 | expect(errorHeaders).to.contain({ 'set-cookie': ['a=x', 'b=x', 'b=y'] }); 884 | }); 885 | 886 | it('prevents duplicates when appending when duplicate is false.', async () => { 887 | 888 | const server = testHeadersWith((request, h) => { 889 | 890 | Toys.header(request.response, 'a', 'x', { append: true }); 891 | Toys.header(request.response, 'A', 'y', { append: true }); 892 | Toys.header(request.response, 'a', 'y', { append: true, duplicate: false }); 893 | Toys.header(request.response, 'b', 'x', { append: true, separator: ';' }); 894 | Toys.header(request.response, 'B', 'y', { append: true, separator: ';' }); 895 | Toys.header(request.response, 'b', 'y', { append: true, separator: ';', duplicate: false }); 896 | 897 | return h.continue; 898 | }); 899 | 900 | const { headers: errorHeaders } = await server.inject('/error'); 901 | const { headers: nonErrorHeaders } = await server.inject('/non-error'); 902 | 903 | expect(errorHeaders).to.contain({ a: 'x,y', b: 'x;y' }); 904 | expect(nonErrorHeaders).to.contain({ a: 'x,y', b: 'x;y' }); 905 | }); 906 | }); 907 | 908 | describe('getHeaders()', () => { 909 | 910 | it('throws when passed a non-response.', () => { 911 | 912 | expect(() => Toys.getHeaders(null)).to.throw('The passed response must be a boom error or hapi response object.'); 913 | expect(() => Toys.getHeaders({})).to.throw('The passed response must be a boom error or hapi response object.'); 914 | }); 915 | 916 | it('gets headers values from a non-error response.', async () => { 917 | 918 | const server = Hapi.server(); 919 | 920 | let headers; 921 | 922 | server.route({ 923 | method: 'get', 924 | path: '/non-error', 925 | options: { 926 | handler: () => ({ success: true }), 927 | ext: { 928 | onPreResponse: { 929 | method: (request, h) => { 930 | 931 | request.response.header('a', 'x'); 932 | request.response.header('b', 'y'); 933 | 934 | headers = Toys.getHeaders(request.response); 935 | 936 | return h.continue; 937 | } 938 | } 939 | } 940 | } 941 | }); 942 | 943 | const res = await server.inject('/non-error'); 944 | 945 | expect(headers).to.contain({ a: 'x', b: 'y' }); 946 | expect(res.headers).to.contain({ a: 'x', b: 'y' }); 947 | }); 948 | 949 | it('gets headers values from an error response.', async () => { 950 | 951 | const server = Hapi.server(); 952 | 953 | let headers; 954 | 955 | server.route({ 956 | method: 'get', 957 | path: '/error', 958 | options: { 959 | handler: () => { 960 | 961 | throw Boom.unauthorized('Original message'); 962 | }, 963 | ext: { 964 | onPreResponse: { 965 | method: (request, h) => { 966 | 967 | request.response.output.headers.a = 'x'; 968 | request.response.output.headers.b = 'y'; 969 | 970 | headers = Toys.getHeaders(request.response); 971 | 972 | return h.continue; 973 | } 974 | } 975 | } 976 | } 977 | }); 978 | 979 | const res = await server.inject('/error'); 980 | 981 | expect(headers).to.equal({ a: 'x', b: 'y' }); 982 | expect(res.headers).to.contain({ a: 'x', b: 'y' }); 983 | }); 984 | }); 985 | 986 | describe('code()', () => { 987 | 988 | const testCodeWith = (method) => { 989 | 990 | const server = Hapi.server(); 991 | 992 | server.route({ 993 | method: 'get', 994 | path: '/non-error', 995 | options: { 996 | handler: () => ({ success: true }), 997 | ext: { 998 | onPreResponse: { method } 999 | } 1000 | } 1001 | }); 1002 | 1003 | server.route({ 1004 | method: 'get', 1005 | path: '/error', 1006 | options: { 1007 | handler: () => { 1008 | 1009 | throw Boom.unauthorized('Original message'); 1010 | }, 1011 | ext: { 1012 | onPreResponse: { method } 1013 | } 1014 | } 1015 | }); 1016 | 1017 | return server; 1018 | }; 1019 | 1020 | it('throws when passed a non-response.', () => { 1021 | 1022 | expect(() => Toys.code(null)).to.throw('The passed response must be a boom error or hapi response object.'); 1023 | expect(() => Toys.code({})).to.throw('The passed response must be a boom error or hapi response object.'); 1024 | }); 1025 | 1026 | it('sets status code.', async () => { 1027 | 1028 | const server = testCodeWith((request, h) => { 1029 | 1030 | Toys.code(request.response, 403); 1031 | 1032 | return h.continue; 1033 | }); 1034 | 1035 | const errorRes = await server.inject('/error'); 1036 | const nonErrorRes = await server.inject('/non-error'); 1037 | 1038 | expect(errorRes.statusCode).to.equal(403); 1039 | expect(nonErrorRes.statusCode).to.equal(403); 1040 | 1041 | expect(errorRes.result).to.equal({ 1042 | statusCode: 403, 1043 | error: 'Forbidden', 1044 | message: 'Original message' 1045 | }); 1046 | 1047 | expect(nonErrorRes.result).to.equal({ success: true }); 1048 | }); 1049 | }); 1050 | 1051 | describe('getCode()', () => { 1052 | 1053 | 1054 | it('throws when passed a non-response.', () => { 1055 | 1056 | expect(() => Toys.getCode(null)).to.throw('The passed response must be a boom error or hapi response object.'); 1057 | expect(() => Toys.getCode({})).to.throw('The passed response must be a boom error or hapi response object.'); 1058 | }); 1059 | 1060 | it('gets status code from a non-error response.', async () => { 1061 | 1062 | const server = Hapi.server(); 1063 | 1064 | let code; 1065 | 1066 | server.route({ 1067 | method: 'get', 1068 | path: '/non-error', 1069 | options: { 1070 | handler: () => ({ success: true }), 1071 | ext: { 1072 | onPreResponse: { 1073 | method: (request, h) => { 1074 | 1075 | request.response.code(202); 1076 | 1077 | code = Toys.getCode(request.response); 1078 | 1079 | return h.continue; 1080 | } 1081 | } 1082 | } 1083 | } 1084 | }); 1085 | 1086 | const res = await server.inject('/non-error'); 1087 | 1088 | expect(code).to.equal(202); 1089 | expect(res.statusCode).to.equal(202); 1090 | }); 1091 | 1092 | it('gets status code from an error response.', async () => { 1093 | 1094 | const server = Hapi.server(); 1095 | 1096 | let code; 1097 | 1098 | server.route({ 1099 | method: 'get', 1100 | path: '/error', 1101 | options: { 1102 | handler: () => { 1103 | 1104 | throw Boom.unauthorized(); 1105 | }, 1106 | ext: { 1107 | onPreResponse: { 1108 | method: (request, h) => { 1109 | 1110 | request.response.output.statusCode = 403; 1111 | request.response.reformat(); 1112 | 1113 | code = Toys.getCode(request.response); 1114 | 1115 | return h.continue; 1116 | } 1117 | } 1118 | } 1119 | } 1120 | }); 1121 | 1122 | const res = await server.inject('/error'); 1123 | 1124 | expect(code).to.equal(403); 1125 | expect(res.statusCode).to.equal(403); 1126 | }); 1127 | }); 1128 | 1129 | describe('asyncStorage(), withAsyncStorage(), and asyncStorageInternals()', () => { 1130 | 1131 | it('set-up async local storage based on a string identifier.', async () => { 1132 | 1133 | const multiplyBy = async (x, ms) => { 1134 | 1135 | await Hoek.wait(ms); 1136 | 1137 | return x * (Toys.asyncStorage('y') || 0); 1138 | }; 1139 | 1140 | const [a, b, c, d] = await Promise.all([ 1141 | Toys.withAsyncStorage('y', 2, async () => await multiplyBy(4, 10)), 1142 | multiplyBy(4, 20), 1143 | Toys.withAsyncStorage('y', 4, async () => await multiplyBy(4, 30)), 1144 | Toys.withAsyncStorage('y', 5, async () => await multiplyBy(4, 40)) 1145 | ]); 1146 | 1147 | expect(a).to.equal(2 * 4); 1148 | expect(b).to.equal(0 * 4); 1149 | expect(c).to.equal(4 * 4); 1150 | expect(d).to.equal(5 * 4); 1151 | }); 1152 | 1153 | it('set-up async local storage based on a symbol identifier.', async () => { 1154 | 1155 | const kY = Symbol('y'); 1156 | 1157 | const multiplyBy = async (x, ms) => { 1158 | 1159 | await Hoek.wait(ms); 1160 | 1161 | return x * (Toys.asyncStorage(kY) || 0); 1162 | }; 1163 | 1164 | const [a, b, c, d] = await Promise.all([ 1165 | Toys.withAsyncStorage(kY, 2, async () => await multiplyBy(4, 10)), 1166 | multiplyBy(4, 20), 1167 | Toys.withAsyncStorage(kY, 4, async () => await multiplyBy(4, 30)), 1168 | Toys.withAsyncStorage(kY, 5, async () => await multiplyBy(4, 40)) 1169 | ]); 1170 | 1171 | expect(a).to.equal(2 * 4); 1172 | expect(b).to.equal(0 * 4); 1173 | expect(c).to.equal(4 * 4); 1174 | expect(d).to.equal(5 * 4); 1175 | }); 1176 | 1177 | it('do not allow conflicting async local storage identifiers on the same stack.', async () => { 1178 | 1179 | const multiplyBy = async (x) => { 1180 | 1181 | await Hoek.wait(0); 1182 | 1183 | return x * (Toys.asyncStorage('y') || 0); 1184 | }; 1185 | 1186 | const getResult = async () => { 1187 | 1188 | return await Toys.withAsyncStorage('y', 3, async () => { 1189 | 1190 | const a = await multiplyBy(4); 1191 | 1192 | return await Toys.withAsyncStorage('y', a, async () => { 1193 | 1194 | return await multiplyBy(5); 1195 | }); 1196 | }); 1197 | }; 1198 | 1199 | await expect(getResult()).to.reject('There is already an active async store for identifier "y".'); 1200 | }); 1201 | 1202 | it('generate errors for Symbols properly.', async () => { 1203 | 1204 | const kY = Symbol('y'); 1205 | 1206 | const multiplyBy = async (x) => { 1207 | 1208 | await Hoek.wait(0); 1209 | 1210 | return x * (Toys.asyncStorage(kY) || 0); 1211 | }; 1212 | 1213 | const getResult = async () => { 1214 | 1215 | return await Toys.withAsyncStorage(kY, 3, async () => { 1216 | 1217 | const a = await multiplyBy(4); 1218 | 1219 | return await Toys.withAsyncStorage(kY, a, async () => { 1220 | 1221 | return await multiplyBy(5); 1222 | }); 1223 | }); 1224 | }; 1225 | 1226 | await expect(getResult()).to.reject('There is already an active async store for identifier "Symbol(y)".'); 1227 | }); 1228 | 1229 | it('expose async local storage instances.', async () => { 1230 | 1231 | const multiplyBy = async (x) => { 1232 | 1233 | await Hoek.wait(0); 1234 | 1235 | Toys.asyncStorageInternals().get('y').disable(); 1236 | 1237 | return x * (Toys.asyncStorage('y') || 0); 1238 | }; 1239 | 1240 | const a = await Toys.withAsyncStorage('y', 3, async () => await multiplyBy(4)); 1241 | 1242 | expect(a).to.equal(0); 1243 | expect(Toys.asyncStorageInternals()).to.be.an.instanceof(Map); 1244 | }); 1245 | }); 1246 | 1247 | describe('patchJoiSchema()', () => { 1248 | 1249 | it('returns patched schema for joi object', () => { 1250 | 1251 | const schema = Joi.object(); 1252 | 1253 | const patchedSchema = Toys.patchJoiSchema(schema); 1254 | 1255 | expect(patchedSchema).to.be.an.object(); 1256 | }); 1257 | 1258 | it('patched joi schema contains optional keys', () => { 1259 | 1260 | const schema = Joi.object().keys({ 1261 | a: Joi.string().required(), 1262 | b: Joi.date().required() 1263 | }); 1264 | 1265 | const patchedSchema = Toys.patchJoiSchema(schema); 1266 | const described = patchedSchema.describe(); 1267 | 1268 | expect(described.preferences).to.equal({ noDefaults: true }); 1269 | expect(described.keys.a.flags).to.equal({ presence: 'optional' }); 1270 | expect(described.keys.b.flags).to.equal({ presence: 'optional' }); 1271 | }); 1272 | 1273 | it('patched joi schema does not enforce defaults', () => { 1274 | 1275 | const schema = Joi.object().keys({ 1276 | x: Joi.number().integer().default(10) 1277 | }); 1278 | 1279 | const patchedSchema = Toys.patchJoiSchema(schema); 1280 | const described = patchedSchema.describe(); 1281 | 1282 | expect(described.keys.x.flags).to.equal({ default: 10, presence: 'optional' }); 1283 | expect(patchedSchema.validate({ x: undefined })).to.equal({ value: { x: undefined } }); 1284 | }); 1285 | }); 1286 | }); 1287 | --------------------------------------------------------------------------------