diff --git a/.travis.yml b/.travis.yml index 06984d8..7fa3ad8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,21 @@ language: node_js node_js: - - "8" - - "10" - "12" - "14" - "node" +env: + - HAPI_VERSION="19" + - HAPI_VERSION="20" + +install: + - "npm install" + - "npm install @hapi/hapi@$HAPI_VERSION" + after_script: "npm run coveralls" + +jobs: + fast_finish: true + allow_failures: + - node_js: "node" diff --git a/API.md b/API.md index 71a3993..6628cf5 100644 --- a/API.md +++ b/API.md @@ -1,19 +1,9 @@ # API ## `Toys` -A container for a group of toys, each toy being a hapi utility. - -### `new Toys(server)` -Creates an instance of toys specific to a hapi server `server`; - -> **A note on usage** -> -> Each instance method, e.g. `toys.reacher()`, may also be used statically, e.g. `Toys.reacher()`. The only difference between the static and instance methods is that some of the static methods have or require `server` arguments that are not necessary when working with an instance. -> -> The docs below will document the static version of each toy, then note the signature of the instance version. +A collection of toys, each toy being a hapi utility. ### `Toys.withRouteDefaults(defaults)` -> As instance, `toys.withRouteDefaults(defaults)` 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. @@ -36,7 +26,6 @@ server.route( ``` ### `Toys.pre(prereqs)` -> As instance, `toys.pre(prereqs)` 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. @@ -91,7 +80,6 @@ server.route({ ``` ### `Toys.ext(method, [options])` -> As instance, `toys.ext(method, [options])` 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. @@ -119,7 +107,6 @@ server.route({ ``` ### `Toys.EXTENSION(method, [options])` -> As instance, `toys.EXTENSION(method, [options])` 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()). @@ -149,7 +136,6 @@ server.ext([ ``` ### `Toys.reacher(chain, [options])` -> As instance, `toys.reacher(chain, [options])` Returns a function `function(obj)` that will return [`Hoek.reach(obj, chain, options)`](https://github.com/hapijs/hoek/blob/master/API.md#reachobj-chain-options). Unlike `Hoek.reach()`, this function is designed to be performant in hot code paths such as route handlers. See [`Hoek.reach()`](https://github.com/hapijs/hoek/blob/master/API.md#reachobj-chain-options) for a description of `options`. @@ -176,7 +162,6 @@ server.route({ ``` ### `Toys.transformer(transform, [options])` -> As instance, `toys.transformer(transform, [options])` Returns a function `function(obj)` that will return [`Hoek.transform(obj, transform, options)`](https://github.com/hapijs/hoek/blob/master/API.md#transformobj-transform-options). Unlike `Hoek.transform()`, this function is designed to be performant in hot code paths such as route handlers. See [`Hoek.reach()`](https://github.com/hapijs/hoek/blob/master/API.md#reachobj-chain-options) for a description of `options`. @@ -205,7 +190,6 @@ server.route({ ``` ### `Toys.auth.strategy(server, name, authenticate)` -> As instance, `toys.auth.strategy(name, authenticate)` 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. @@ -234,7 +218,6 @@ server.route({ ``` ### `Toys.noop` -> As instance, `toys.noop` 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. @@ -247,7 +230,6 @@ await server.register([ ``` ### `await Toys.event(emitter, eventName, [options])` -> As instance, `await toys.event(emitter, eventName, [options])` Waits for `emitter` to emit an event named `eventName` and returns the first value passed to the event's listener. When `options.multiple` is `true` it instead returns an array of all values passed to the listener. Throws if an event named `'error'` is emitted unless `options.error` is `false`. This can be useful when waiting for an event in a handler, extension, or server method, which all require an `async` function when returning a value asynchronously. @@ -275,7 +257,6 @@ server.route({ ``` ### `await Toys.stream(stream)` -> As instance, `await toys.stream(stream)` Waits for a readable `stream` to end, a writable `stream` to finish, or a duplex `stream` to both end and finish. Throws an error if `stream` emits an `'error'` event. This can be useful when waiting for a stream to process in a handler, extension, or server method, which all require an `async` function when returning a value asynchronously. @@ -313,7 +294,6 @@ server.method({ ``` ### `Toys.options(obj)` -> As instance, `toys.options([obj])` 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`. @@ -343,7 +323,6 @@ module.exports = { ``` ### `Toys.header(response, name, value, [options])` -> As instance, `toys.header(response, name, value, [options])` 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`. @@ -356,41 +335,33 @@ Designed to behave identically to hapi's [`response.header(name, value, [options - `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`. ### `Toys.getHeaders(response)` -> As instance, `toys.getHeaders(response)` 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. ### `Toys.code(response, statusCode)` -> As instance, `toys.code(response, statusCode)` 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`. ### `Toys.getCode(response)` -> As instance, `toys.getCode(response)` 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. ### `Toys.realm(obj)` -> As instance, `toys.realm([obj])` 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`. ### `Toys.rootRealm(realm)` -> As instance, `toys.rootRealm()` 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. ### `Toys.state(realm, pluginName)` -> As instance, `toys.state(pluginName)` 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`. ### `Toys.rootState(realm, pluginName)` -> As instance, `toys.rootState(pluginName)` 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. ### `Toys.forEachAncestorRealm(realm, fn)` -> As instance, `toys.forEachAncestorRealm(fn)` 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`. diff --git a/README.md b/README.md index a602b9c..7665e4b 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,10 @@ Lead Maintainer - [Devin Ivy](https://github.com/devinivy) ## Usage > See also the [API Reference](API.md) +> +> For support below node v12 or hapi v19 see toys v2. -Toys is a collection of utilities made to reduce common boilerplate in **hapi v17+** projects, aid usage of events and streams in `async` functions (e.g. handlers and server methods), and provide versions of widely-used utilities from [Hoek](https://github.com/hapijs/hoek) optimized to perform well in hot code paths such as route handlers. +Toys is a collection of utilities made to reduce common boilerplate in **hapi v19+** projects, aid usage of events and streams in `async` functions (e.g. handlers and server methods), and provide versions of widely-used utilities from [Hoek](https://github.com/hapijs/hoek) optimized to perform well in hot code paths such as route handlers. Below is an example featuring [`Toys.auth.strategy()`](API.md#toysauthstrategyserver-name-authenticate), [`Toys.reacher()`](API.md#toysreacherchain-options), and [`Toys.withRouteDefaults()`](API.md#toyswithroutedefaultsdefaults). The [API Reference](API.md) is also filled with examples. diff --git a/lib/index.js b/lib/index.js index f6fe95b..4d5856e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,549 +4,387 @@ const Hoek = require('@hapi/hoek'); const internals = {}; -module.exports = class Toys { +exports.withRouteDefaults = (defaults) => { - constructor(server) { + return (options) => { - this.server = server; - } - - static withRouteDefaults(defaults) { - - return (options) => { - - if (Array.isArray(options)) { - return options.map((opt) => internals.applyRouteDefaults(defaults, opt)); - } - - return internals.applyRouteDefaults(defaults, options); - }; - } - - withRouteDefaults(defaults) { - - return this.constructor.withRouteDefaults(defaults); - } - - static pre(prereqSets) { - - const method = (prereq) => { - - return (typeof prereq === 'function') ? { method: prereq } : prereq; - }; - - const toPres = (prereqs) => { - - if (typeof prereqs === 'function') { - return prereqs; - } - - return Object.keys(prereqs).reduce((collect, assign) => { - - const prereq = prereqs[assign]; - - return collect.concat({ - assign, - ...method(prereq) - }); - }, []); - }; - - if (Array.isArray(prereqSets)) { - return prereqSets.map(toPres); + if (Array.isArray(options)) { + return options.map((opt) => internals.applyRouteDefaults(defaults, opt)); } - return toPres(prereqSets); - } - - pre(prereqSets) { - - return this.constructor.pre(prereqSets); - } - - static ext(method, options) { - - return internals.ext(null, method, options); - } - - ext(method, options) { + return internals.applyRouteDefaults(defaults, options); + }; +}; - return this.constructor.ext(method, options); - } +exports.pre = (prereqSets) => { - static onRequest(method, options) { + const method = (prereq) => { - return internals.ext('onRequest', method, options); - } + return (typeof prereq === 'function') ? { method: prereq } : prereq; + }; - onRequest(method, options) { + const toPres = (prereqs) => { - return this.constructor.onRequest(method, options); - } + if (typeof prereqs === 'function') { + return prereqs; + } - static onPreAuth(method, options) { + return Object.keys(prereqs).reduce((collect, assign) => { - return internals.ext('onPreAuth', method, options); - } + const prereq = prereqs[assign]; - onPreAuth(method, options) { + return collect.concat({ + assign, + ...method(prereq) + }); + }, []); + }; - return this.constructor.onPreAuth(method, options); + if (Array.isArray(prereqSets)) { + return prereqSets.map(toPres); } - static onCredentials(method, options) { + return toPres(prereqSets); +}; - return internals.ext('onCredentials', method, options); - } +exports.ext = (method, options) => { - onCredentials(method, options) { + return internals.ext(null, method, options); +}; - return this.constructor.onCredentials(method, options); - } +exports.onRequest = (method, options) => { - static onPostAuth(method, options) { + return internals.ext('onRequest', method, options); +}; - return internals.ext('onPostAuth', method, options); - } +exports.onPreAuth = (method, options) => { - onPostAuth(method, options) { + return internals.ext('onPreAuth', method, options); +}; - return this.constructor.onPostAuth(method, options); - } +exports.onCredentials = (method, options) => { - static onPreHandler(method, options) { + return internals.ext('onCredentials', method, options); +}; - return internals.ext('onPreHandler', method, options); - } +exports.onPostAuth = (method, options) => { - onPreHandler(method, options) { + return internals.ext('onPostAuth', method, options); +}; - return this.constructor.onPreHandler(method, options); - } +exports.onPreHandler = (method, options) => { - static onPostHandler(method, options) { + return internals.ext('onPreHandler', method, options); +}; - return internals.ext('onPostHandler', method, options); - } +exports.onPostHandler = (method, options) => { - onPostHandler(method, options) { + return internals.ext('onPostHandler', method, options); +}; - return this.constructor.onPostHandler(method, options); - } +exports.onPreResponse = (method, options) => { - static onPreResponse(method, options) { + return internals.ext('onPreResponse', method, options); +}; - return internals.ext('onPreResponse', method, options); - } +exports.onPreStart = (method, options) => { - onPreResponse(method, options) { + return internals.ext('onPreStart', method, options); +}; - return this.constructor.onPreResponse(method, options); - } +exports.onPostStart = (method, options) => { - static onPreStart(method, options) { + return internals.ext('onPostStart', method, options); +}; - return internals.ext('onPreStart', method, options); - } +exports.onPreStop = (method, options) => { - onPreStart(method, options) { + return internals.ext('onPreStop', method, options); +}; - return this.constructor.onPreStart(method, options); - } +exports.onPostStop = (method, options) => { - static onPostStart(method, options) { + return internals.ext('onPostStop', method, options); +}; - return internals.ext('onPostStart', method, options); - } +exports.reacher = (chain, options) => { - onPostStart(method, options) { + if (chain === false || + chain === null || + typeof chain === 'undefined') { - return this.constructor.onPostStart(method, options); + return (obj) => obj; } - static onPreStop(method, options) { + options = options || {}; - return internals.ext('onPreStop', method, options); + if (typeof options === 'string') { + options = { separator: options }; } - onPreStop(method, options) { + const path = chain.split(options.separator || '.'); - return this.constructor.onPreStop(method, options); - } + return (obj) => { - static onPostStop(method, options) { + let ref = obj; - return internals.ext('onPostStop', method, options); - } + for (let i = 0; i < path.length; ++i) { + let key = path[i]; - onPostStop(method, options) { + if (key[0] === '-' && Array.isArray(ref)) { + key = key.slice(1, key.length); + key = ref.length - key; + } - return this.constructor.onPostStop(method, options); - } + if (!ref || + !((typeof ref === 'object' || typeof ref === 'function') && key in ref) || + (typeof ref !== 'object' && options.functions === false)) { // Only object and function can have properties - static reacher(chain, options) { + Hoek.assert(!options.strict || i + 1 === path.length, `Missing segment ${key} in reach path ${chain}`); + Hoek.assert(typeof ref === 'object' || options.functions === true || typeof ref !== 'function', `Invalid segment ${key} in reach path ${chain}`); - if (chain === false || - chain === null || - typeof chain === 'undefined') { + ref = options.default; - return (obj) => obj; - } - - options = options || {}; + break; + } - if (typeof options === 'string') { - options = { separator: options }; + ref = ref[key]; } - const path = chain.split(options.separator || '.'); - - return (obj) => { - - let ref = obj; - - for (let i = 0; i < path.length; ++i) { - let key = path[i]; - - if (key[0] === '-' && Array.isArray(ref)) { - key = key.slice(1, key.length); - key = ref.length - key; - } - - if (!ref || - !((typeof ref === 'object' || typeof ref === 'function') && key in ref) || - (typeof ref !== 'object' && options.functions === false)) { // Only object and function can have properties - - Hoek.assert(!options.strict || i + 1 === path.length, `Missing segment ${key} in reach path ${chain}`); - Hoek.assert(typeof ref === 'object' || options.functions === true || typeof ref !== 'function', `Invalid segment ${key} in reach path ${chain}`); - - ref = options.default; + return ref; + }; +}; - break; - } +exports.transformer = (transform, options) => { - ref = ref[key]; - } + const separator = (typeof options === 'object' && options !== null) ? (options.separator || '.') : '.'; + const transformSteps = []; + const keys = Object.keys(transform); - return ref; - }; - } + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + const sourcePath = transform[key]; - reacher(chain, options) { + Hoek.assert(typeof sourcePath === 'string', 'All mappings must be strings'); - return this.constructor.reacher(chain, options); + transformSteps.push({ + path: key.split(separator), + reacher: this.reacher(sourcePath, options) + }); } - static transformer(transform, options) { - - const separator = (typeof options === 'object' && options !== null) ? (options.separator || '.') : '.'; - const transformSteps = []; - const keys = Object.keys(transform); - - for (let i = 0; i < keys.length; ++i) { - const key = keys[i]; - const sourcePath = transform[key]; - - Hoek.assert(typeof sourcePath === 'string', 'All mappings must be strings'); - - transformSteps.push({ - path: key.split(separator), - reacher: this.reacher(sourcePath, options) - }); - } - - const transformFn = (source) => { + const transformFn = (source) => { - Hoek.assert(source === null || source === undefined || typeof source === 'object' || Array.isArray(source), 'Invalid source object: must be null, undefined, an object, or an array'); + Hoek.assert(source === null || source === undefined || typeof source === 'object' || Array.isArray(source), 'Invalid source object: must be null, undefined, an object, or an array'); - if (Array.isArray(source)) { + if (Array.isArray(source)) { - const results = []; - for (let i = 0; i < source.length; ++i) { - results.push(transformFn(source[i])); - } - - return results; + const results = []; + for (let i = 0; i < source.length; ++i) { + results.push(transformFn(source[i])); } - const result = {}; + return results; + } - for (let i = 0; i < transformSteps.length; ++i) { - const step = transformSteps[i]; - const path = step.path; - const reacher = step.reacher; + const result = {}; - let segment; - let res = result; + for (let i = 0; i < transformSteps.length; ++i) { + const step = transformSteps[i]; + const path = step.path; + const reacher = step.reacher; - for (let j = 0; j < path.length - 1; ++j) { + let segment; + let res = result; - segment = path[j]; + for (let j = 0; j < path.length - 1; ++j) { - if (!res[segment]) { - res[segment] = {}; - } + segment = path[j]; - res = res[segment]; + if (!res[segment]) { + res[segment] = {}; } - segment = path[path.length - 1]; - res[segment] = reacher(source); + res = res[segment]; } - return result; - }; - - return transformFn; - } - - transformer(transform, options) { - - return this.constructor.transformer(transform, options); - } + segment = path[path.length - 1]; + res[segment] = reacher(source); + } - static get auth() { + return result; + }; - return { - strategy: internals.strategy - }; - } + return transformFn; +}; - get auth() { +exports.auth = {}; - return { - strategy: this.constructor.auth.strategy.bind(this, this.server) - }; - } +exports.auth.strategy = (server, name, authenticate) => { - static get noop() { + server.auth.scheme(name, () => ({ authenticate })); + server.auth.strategy(name, name); +}; - return { - name: 'toys-noop', - multiple: true, - register: Hoek.ignore - }; - } +exports.noop = { + name: 'toys-noop', + multiple: true, + register: Hoek.ignore +}; - get noop() { +exports.event = async (emitter, name, options) => { - return this.constructor.noop; - } - - static async event(emitter, name, options) { + return await internals.event(emitter, name, options); +}; - return await internals.event(emitter, name, options); - } +exports.stream = async (stream) => { - async event(emitter, name, options) { + Hoek.assert(stream.readable || stream.writable, 'Stream must be readable or writable'); - return await this.constructor.event(emitter, name, options); + if (!stream.writable) { // Only readable + await internals.event(stream, 'end'); + return; } - - static async stream(stream) { - - Hoek.assert(stream.readable || stream.writable, 'Stream must be readable or writable'); - - if (!stream.writable) { // Only readable - await internals.event(stream, 'end'); - return; - } - else if (!stream.readable) { // Only writable - await internals.event(stream, 'finish'); - return; - } - - await Promise.all([ - internals.event(stream, 'end'), - internals.event(stream, 'finish') - ]); + else if (!stream.readable) { // Only writable + await internals.event(stream, 'finish'); + return; } - async stream(stream) { - - return await this.constructor.stream(stream); - } + await Promise.all([ + internals.event(stream, 'end'), + internals.event(stream, 'finish') + ]); +}; - static forEachAncestorRealm(realm, fn) { +exports.forEachAncestorRealm = (realm, fn) => { - do { - fn(realm); - realm = realm.parent; - } - while (realm); + do { + fn(realm); + realm = realm.parent; } + while (realm); +}; - forEachAncestorRealm(fn) { +exports.rootRealm = (realm) => { - return this.constructor.forEachAncestorRealm(this.server.realm, fn); + while (realm.parent) { + realm = realm.parent; } - static rootRealm(realm) { + return realm; +}; - while (realm.parent) { - realm = realm.parent; - } +exports.state = (realm, name) => { - return realm; - } + return internals.state(realm, name); +}; - rootRealm() { +exports.rootState = (realm, name) => { - return this.constructor.rootRealm(this.server.realm); + while (realm.parent) { + realm = realm.parent; } - static state(realm, name) { - - return internals.state(realm, name); - } + return internals.state(realm, name); +}; - state(name) { +exports.realm = (obj) => { - return this.constructor.state(this.server.realm, name); + if (internals.isRealm(obj && obj.realm)) { + // Server, route, response toolkit + return obj.realm; } - - static rootState(realm, name) { - - while (realm.parent) { - realm = realm.parent; - } - - return internals.state(realm, name); + else if (internals.isRealm(obj && obj.route && obj.route.realm)) { + // Request + return obj.route.realm; } - rootState(name) { + Hoek.assert(internals.isRealm(obj), 'Must pass a server, request, route, response toolkit, or realm'); - return this.constructor.rootState(this.server.realm, name); - } - - static realm(obj) { + // Realm + return obj; +}; - if (internals.isRealm(obj && obj.realm)) { - // Server, route, response toolkit - return obj.realm; - } - else if (internals.isRealm(obj && obj.route && obj.route.realm)) { - // Request - return obj.route.realm; - } +exports.options = (obj) => { - Hoek.assert(internals.isRealm(obj), 'Must pass a server, request, route, response toolkit, or realm'); + return this.realm(obj).pluginOptions; +}; - // Realm - return obj; - } +exports.header = (response, key, value, options = {}) => { - realm(obj = this.server) { + Hoek.assert(response && (response.isBoom || typeof response.header === 'function'), 'The passed response must be a boom error or hapi response object.'); - return this.constructor.realm(obj); + if (!response.isBoom) { + return response.header(key, value, options); } - static options(obj) { - - return this.realm(obj).pluginOptions; - } + key = key.toLowerCase(); + const { headers } = response.output; - options(obj = this.server) { + const append = options.append || false; + const separator = options.separator || ','; + const override = options.override !== false; + const duplicate = options.duplicate !== false; - return this.constructor.options(obj); + if ((!append && override) || !headers[key]) { + headers[key] = value; } - - static header(response, key, value, options = {}) { - - Hoek.assert(response && (response.isBoom || typeof response.header === 'function'), 'The passed response must be a boom error or hapi response object.'); - - if (!response.isBoom) { - return response.header(key, value, options); + else if (override) { + if (key === 'set-cookie') { + headers[key] = [].concat(headers[key], value); } - - key = key.toLowerCase(); - const { headers } = response.output; - - const append = options.append || false; - const separator = options.separator || ','; - const override = options.override !== false; - const duplicate = options.duplicate !== false; - - if ((!append && override) || !headers[key]) { - headers[key] = value; - } - else if (override) { - if (key === 'set-cookie') { - headers[key] = [].concat(headers[key], value); - } - else { - const existing = headers[key]; - if (!duplicate) { - const values = existing.split(separator); - for (const v of values) { - if (v === value) { - return response; - } + else { + const existing = headers[key]; + if (!duplicate) { + const values = existing.split(separator); + for (const v of values) { + if (v === value) { + return response; } } - - headers[key] = existing + separator + value; } - } - return response; - } - - header(response, key, value, options) { - - return this.constructor.header(response, key, value, options); + headers[key] = existing + separator + value; + } } - static getHeaders(response) { - - Hoek.assert(response && (response.isBoom || typeof response.header === 'function'), 'The passed response must be a boom error or hapi response object.'); - - if (!response.isBoom) { - return response.headers; - } + return response; +}; - return response.output.headers; - } +exports.getHeaders = (response) => { - getHeaders(response) { + Hoek.assert(response && (response.isBoom || typeof response.header === 'function'), 'The passed response must be a boom error or hapi response object.'); - return this.constructor.getHeaders(response); + if (!response.isBoom) { + return response.headers; } - static code(response, statusCode) { - - Hoek.assert(response && (response.isBoom || typeof response.code === 'function'), 'The passed response must be a boom error or hapi response object.'); + return response.output.headers; +}; - if (!response.isBoom) { - return response.code(statusCode); - } +exports.code = (response, statusCode) => { - response.output.statusCode = statusCode; - response.reformat(); + Hoek.assert(response && (response.isBoom || typeof response.code === 'function'), 'The passed response must be a boom error or hapi response object.'); - return response; + if (!response.isBoom) { + return response.code(statusCode); } - code(response, statusCode) { + response.output.statusCode = statusCode; + response.reformat(); - return this.constructor.code(response, statusCode); - } - - static getCode(response) { + return response; +}; - Hoek.assert(response && (response.isBoom || typeof response.code === 'function'), 'The passed response must be a boom error or hapi response object.'); +exports.getCode = (response) => { - if (!response.isBoom) { - return response.statusCode; - } + Hoek.assert(response && (response.isBoom || typeof response.code === 'function'), 'The passed response must be a boom error or hapi response object.'); - return response.output.statusCode; + if (!response.isBoom) { + return response.statusCode; } - getCode(response) { - - return this.constructor.getCode(response); - } + return response.output.statusCode; }; // Looks like a realm @@ -614,12 +452,6 @@ internals.event = (emitter, name, options = {}) => { }); }; -internals.strategy = (server, name, authenticate) => { - - server.auth.scheme(name, () => ({ authenticate })); - server.auth.strategy(name, name); -}; - internals.ext = (type, method, options) => { const extConfig = { method }; diff --git a/package.json b/package.json index 0462da0..2e03007 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "test" }, "scripts": { - "test": "lab -a @hapi/code -t 100 -L -I \"FinalizationRegistry,WeakRef\"", + "test": "lab -a @hapi/code -t 100 -L", "coveralls": "lab -r lcov | coveralls" }, "repository": { @@ -28,15 +28,13 @@ }, "homepage": "https://github.com/hapipal/toys#readme", "dependencies": { - "@hapi/hoek": "8.x.x" + "@hapi/hoek": "9.x.x" }, "devDependencies": { - "@hapi/boom": "7.x.x", - "@hapi/code": "6.x.x", - "@hapi/hapi": "18.x.x", - "@hapi/hapi-20": "npm:@hapi/hapi@20", - "@hapi/lab": "20.x.x", - "@hapi/somever": "2.x.x", + "@hapi/boom": "9.x.x", + "@hapi/code": "8.x.x", + "@hapi/hapi": "20.x.x", + "@hapi/lab": "23.x.x", "coveralls": "3.x.x" } } diff --git a/test/index.js b/test/index.js index 7c3e875..4a7d548 100644 --- a/test/index.js +++ b/test/index.js @@ -6,13 +6,11 @@ const EventEmitter = require('events'); const Stream = require('stream'); const Lab = require('@hapi/lab'); const Code = require('@hapi/code'); -const Somever = require('@hapi/somever'); +const Hapi = require('@hapi/hapi'); const Boom = require('@hapi/boom'); const Hoek = require('@hapi/hoek'); const Toys = require('..'); -const Hapi = Somever.match(process.version, '>=12') ? require('@hapi/hapi-20') : require('@hapi/hapi'); - // Test shortcuts const lab = exports.lab = Lab.script(); @@ -203,7 +201,7 @@ describe('Toys', () => { expect(result).to.equal({ config: { - anything: { x:1, y: 2 }, + anything: { x: 1, y: 2 }, validate: { query: {}, payload: {}, @@ -217,7 +215,7 @@ describe('Toys', () => { } }, options: { - anything: { x:1, y: 2 }, + anything: { x: 1, y: 2 }, validate: { query: {}, payload: {}, @@ -232,29 +230,6 @@ describe('Toys', () => { } }); }); - - it('works as an instance method.', () => { - - const obj = { - a: null, - c: { - e: [4] - }, - f: 0, - g: { - h: 5 - } - }; - - const toys = new Toys(); - const result = toys.withRouteDefaults(defaults)(obj); - - expect(result.c.e).to.equal([4]); - expect(result.a).to.equal(1); - expect(result.b).to.equal(2); - expect(result.f).to.equal(0); - expect(result.g).to.equal({ h: 5 }); - }); }); describe('reacher()', () => { @@ -386,13 +361,6 @@ describe('Toys', () => { expect(reacher(obj)).to.equal(1); expect(reacher(anotherObj)).to.equal('x'); }); - - it('works as an instance method.', () => { - - const toys = new Toys(); - - expect(toys.reacher('a/b/c/d', '/')(obj)).to.equal(1); - }); }); describe('transformer()', () => { @@ -631,26 +599,6 @@ describe('Toys', () => { province: null }); }); - - it('works as an instance method.', () => { - - const toys = new Toys(); - const transform = toys.transformer({ - lineOne: 'address.one', - lineTwo: 'address.two', - title: 'title', - region: 'state', - province: 'zip.province' - }); - - expect(transform(source)).to.equal({ - lineOne: '123 main street', - lineTwo: 'PO Box 1234', - title: 'Warehouse', - region: 'CA', - province: null - }); - }); }); describe('auth.strategy()', () => { @@ -694,32 +642,6 @@ describe('Toys', () => { expect(res2.result).to.equal({ user: 'bill' }); }); - - it('works as an instance method.', async () => { - - const server = Hapi.server(); - const toys = new Toys(server); - - toys.auth.strategy('test-auth', (request, h) => { - - return h.authenticated({ credentials: { user: 'bill' } }); - }); - - server.route([ - { - method: 'get', - path: '/', - config: { - auth: 'test-auth', - handler: (request) => request.auth.credentials - } - } - ]); - - const res = await server.inject('/'); - - expect(res.result).to.equal({ user: 'bill' }); - }); }); describe('pre()', () => { @@ -793,28 +715,6 @@ describe('Toys', () => { expect(pre5).to.shallow.equal(prereqs[3]); }); - - it('works as an instance method.', () => { - - const prereqs = { - assign1: () => null, - assign2: { method: () => null, failAction: 'log' } - }; - - const toys = new Toys(); - const [pre1, pre2, ...others] = toys.pre(prereqs); - - expect(others).to.have.length(0); - - expect(pre1).to.only.contain(['assign', 'method']); - expect(pre1.assign).to.equal('assign1'); - expect(pre1.method).to.shallow.equal(prereqs.assign1); - - expect(pre2).to.only.contain(['assign', 'method', 'failAction']); - expect(pre2.assign).to.equal('assign2'); - expect(pre2.method).to.shallow.equal(prereqs.assign2.method); - expect(pre2.failAction).to.equal('log'); - }); }); // Test the request extension helpers @@ -863,21 +763,6 @@ describe('Toys', () => { expect(extension.method).to.shallow.equal(fn); expect(extension.options).to.shallow.equal(opts); }); - - it('works as an instance method.', () => { - - const fn = function () {}; - const opts = { before: 'loveboat' }; - const toys = new Toys(); - const extension = toys[ext](fn, opts); - - const keys = isExt ? ['method', 'options'] : ['type', 'method', 'options']; - - expect(Object.keys(extension)).to.only.contain(keys); - isExt || expect(extension.type).to.equal(ext); - expect(extension.method).to.shallow.equal(fn); - expect(extension.options).to.shallow.equal(opts); - }); }); }); @@ -897,22 +782,6 @@ describe('Toys', () => { expect(server.registrations).to.only.contain('toys-noop'); }); - - it('works as an instance property.', async () => { - - const server = Hapi.server(); - const toys = new Toys(); - - expect(toys.noop.register).to.shallow.equal(Hoek.ignore); - - await server.register(toys.noop); - - expect(server.registrations).to.only.contain('toys-noop'); - - await server.register(toys.noop); - - expect(server.registrations).to.only.contain('toys-noop'); - }); }); describe('async event()', () => { @@ -1027,30 +896,6 @@ describe('Toys', () => { expect(emitter.listenerCount('my-event')).to.equal(0); expect(emitter.listenerCount('error')).to.equal(0); }); - - it('works as an instance method.', async () => { - - const toys = new Toys(); - const emitter = new EventEmitter(); - - let emitted = false; - - emitter.once('my-event', () => { - - emitted = true; - }); - - setTimeout(() => emitter.emit('my-event', 'value')); - - const myEvent = toys.event(emitter, 'my-event'); - - expect(emitted).to.equal(false); - - const value = await myEvent; - - expect(value).to.equal('value'); - expect(emitted).to.equal(true); - }); }); describe('async stream()', () => { @@ -1213,42 +1058,6 @@ describe('Toys', () => { expect(ended).to.equal(false); expect(data).to.equal(['0', '1', '2', '3', '4']); }); - - it('works as an instance method.', async () => { - - let i = 0; - - const counter = new Stream.Readable({ - read() { - - if (i >= 10) { - return this.push(null); - } - - const count = `${i++}`; - process.nextTick(() => this.push(count)); - } - }); - - let ended = false; - counter.once('end', () => { - - ended = true; - }); - - const data = []; - counter.on('data', (count) => data.push(count.toString())); - - expect(ended).to.equal(false); - expect(data).to.equal([]); - - const toys = new Toys(); - const value = await toys.stream(counter); - - expect(value).to.not.exist(); - expect(ended).to.equal(true); - expect(data).to.equal(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); - }); }); describe('options()', () => { @@ -1369,26 +1178,6 @@ describe('Toys', () => { expect(() => Toys.options({})).to.throw('Must pass a server, request, route, response toolkit, or realm'); expect(() => Toys.options(null)).to.throw('Must pass a server, request, route, response toolkit, or realm'); }); - - it('works as an instance method, defaulting to get this.server\'s plugin options.', async () => { - - const server = Hapi.server(); - const toys1 = new Toys(server); - - expect(toys1.options(server.realm)).to.shallow.equal(server.realm.pluginOptions); - expect(toys1.options()).to.shallow.equal(server.realm.pluginOptions); - - await server.register({ - name: 'plugin', - register(srv) { - - const toys2 = new Toys(srv); - - expect(toys2.options(srv.realm)).to.shallow.equal(srv.realm.pluginOptions); - expect(toys2.options()).to.shallow.equal(srv.realm.pluginOptions); - } - }); - }); }); describe('realm()', () => { @@ -1509,26 +1298,6 @@ describe('Toys', () => { expect(() => Toys.realm({})).to.throw('Must pass a server, request, route, response toolkit, or realm'); expect(() => Toys.realm(null)).to.throw('Must pass a server, request, route, response toolkit, or realm'); }); - - it('works as an instance method, defaulting to get this.server\'s realm.', async () => { - - const server = Hapi.server(); - const toys1 = new Toys(server); - - expect(toys1.realm(server.realm)).to.shallow.equal(server.realm); - expect(toys1.realm()).to.shallow.equal(server.realm); - - await server.register({ - name: 'plugin', - register(srv) { - - const toys2 = new Toys(srv); - - expect(toys2.realm(srv.realm)).to.shallow.equal(srv.realm); - expect(toys2.realm()).to.shallow.equal(srv.realm); - } - }); - }); }); describe('rootRealm()', () => { @@ -1553,29 +1322,6 @@ describe('Toys', () => { } }); }); - - it('works as an instance method, returning this.server\'s root realm.', async () => { - - const server = Hapi.server(); - const toys = new Toys(server); - - expect(toys.rootRealm()).to.shallow.equal(server.realm); - - await server.register({ - name: 'plugin-a', - async register(srvB) { - - await srvB.register({ - name: 'plugin-a1', - register(srvA1) { - - const toysA1 = new Toys(srvA1); - expect(toysA1.rootRealm()).to.shallow.equal(server.realm); - } - }); - } - }); - }); }); describe('state()', () => { @@ -1603,32 +1349,6 @@ describe('Toys', () => { } }); }); - - it('works as instance method, using this.server\'s realm.', async () => { - - const server = Hapi.server(); - const toys = new Toys(server); - const state = () => toys.state('root'); - - expect(server.realm.plugins.root).to.not.exist(); - expect(state()).to.shallow.equal(state()); - expect(state()).to.shallow.equal(server.realm.plugins.root); - expect(state()).to.equal({}); - - await server.register({ - name: 'plugin-a', - register(srv) { - - const toysA = new Toys(srv); - const stateA = () => toysA.state('plugin-a'); - - expect(srv.realm.plugins['plugin-a']).to.not.exist(); - expect(stateA()).to.shallow.equal(stateA()); - expect(stateA()).to.shallow.equal(srv.realm.plugins['plugin-a']); - expect(stateA()).to.equal({}); - } - }); - }); }); describe('rootState()', () => { @@ -1662,38 +1382,6 @@ describe('Toys', () => { } }); }); - - it('works as an instance method, using this.server\'s realm.', async () => { - - const server = Hapi.server(); - const toys = new Toys(server); - const state = () => toys.rootState('root'); - - expect(server.realm.plugins.root).to.not.exist(); - expect(state()).to.shallow.equal(state()); - expect(state()).to.shallow.equal(server.realm.plugins.root); - expect(state()).to.equal({}); - - await server.register({ - name: 'plugin-a', - async register(srvB) { - - await srvB.register({ - name: 'plugin-a1', - register(srvA1) { - - const toysA1 = new Toys(srvA1); - const stateA1 = () => toysA1.rootState('plugin-a1'); - - expect(server.realm.plugins['plugin-a1']).to.not.exist(); - expect(stateA1()).to.shallow.equal(stateA1()); - expect(stateA1()).to.shallow.equal(server.realm.plugins['plugin-a1']); - expect(stateA1()).to.equal({}); - } - }); - } - }); - }); }); describe('forEachAncestorRealm()', () => { @@ -1727,38 +1415,6 @@ describe('Toys', () => { } }); }); - - it('works as an instance method, using this.server\'s realm.', async () => { - - const server = Hapi.server(); - - await server.register({ - name: 'plugin-a', - register() {} - }); - - await server.register({ - name: 'plugin-b', - async register(srvB) { - - await srvB.register({ - name: 'plugin-b1', - register(srvB1) { - - const toysB1 = new Toys(srvB1); - - const realms = []; - toysB1.forEachAncestorRealm((realm) => realms.push(realm)); - - expect(realms).to.have.length(3); - expect(realms[0]).to.shallow.equal(srvB1.realm); - expect(realms[1]).to.shallow.equal(srvB.realm); - expect(realms[2]).to.shallow.equal(server.realm); - } - }); - } - }); - }); }); describe('header()', () => { @@ -1893,26 +1549,6 @@ describe('Toys', () => { expect(errorHeaders).to.contain({ a: 'x,y', b: 'x;y' }); expect(nonErrorHeaders).to.contain({ a: 'x,y', b: 'x;y' }); }); - - it('works as an instance method.', async () => { - - const server = testHeadersWith((request, h) => { - - const toys = new Toys(); - - toys.header(request.response, 'a', 'x'); - toys.header(request.response, 'b', 'x'); - toys.header(request.response, 'b', 'y', { append: true }); - - return h.continue; - }); - - const { headers: errorHeaders } = await server.inject('/error'); - const { headers: nonErrorHeaders } = await server.inject('/non-error'); - - expect(errorHeaders).to.contain({ a: 'x', b: 'x,y' }); - expect(nonErrorHeaders).to.contain({ a: 'x', b: 'x,y' }); - }); }); describe('getHeaders()', () => { @@ -1991,41 +1627,6 @@ describe('Toys', () => { expect(headers).to.equal({ a: 'x', b: 'y' }); expect(res.headers).to.contain({ a: 'x', b: 'y' }); }); - - it('works as an instance method.', async () => { - - const server = Hapi.server(); - - let headers; - - server.route({ - method: 'get', - path: '/non-error', - options: { - handler: () => ({ success: true }), - ext: { - onPreResponse: { - method: (request, h) => { - - const toys = new Toys(); - - request.response.header('a', 'x'); - request.response.header('b', 'y'); - - headers = toys.getHeaders(request.response); - - return h.continue; - } - } - } - } - }); - - const res = await server.inject('/non-error'); - - expect(headers).to.contain({ a: 'x', b: 'y' }); - expect(res.headers).to.contain({ a: 'x', b: 'y' }); - }); }); describe('code()', () => { @@ -2091,32 +1692,6 @@ describe('Toys', () => { expect(nonErrorRes.result).to.equal({ success: true }); }); - - it('works as an instance method.', async () => { - - const server = testCodeWith((request, h) => { - - const toys = new Toys(); - - toys.code(request.response, 403); - - return h.continue; - }); - - const errorRes = await server.inject('/error'); - const nonErrorRes = await server.inject('/non-error'); - - expect(errorRes.statusCode).to.equal(403); - expect(nonErrorRes.statusCode).to.equal(403); - - expect(errorRes.result).to.equal({ - statusCode: 403, - error: 'Forbidden', - message: 'Original message' - }); - - expect(nonErrorRes.result).to.equal({ success: true }); - }); }); describe('getCode()', () => { @@ -2195,39 +1770,5 @@ describe('Toys', () => { expect(code).to.equal(403); expect(res.statusCode).to.equal(403); }); - - it('works as an instance method.', async () => { - - const server = Hapi.server(); - - let code; - - server.route({ - method: 'get', - path: '/non-error', - options: { - handler: () => ({ success: true }), - ext: { - onPreResponse: { - method: (request, h) => { - - const toys = new Toys(); - - request.response.code(202); - - code = toys.getCode(request.response); - - return h.continue; - } - } - } - } - }); - - const res = await server.inject('/non-error'); - - expect(code).to.equal(202); - expect(res.statusCode).to.equal(202); - }); }); });