├── .gitignore ├── .npmignore ├── .travis.yml ├── API.md ├── LICENSE ├── README.md ├── lib ├── index.d.ts └── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | import: 2 | - source: hapipal/ci-config-travis:node_js.yml@node-v16-min 3 | - source: hapipal/ci-config-travis:hapi_all.yml@node-v16-min -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | A service layer for hapi 4 | 5 | > **Note** 6 | > 7 | > Schmervice is intended for use with hapi v20+ and nodejs v16+ (_see v2 for lower support_). 8 | 9 | ## The hapi plugin 10 | ### Registration 11 | Schmervice may be registered multiple times– it should be registered in any plugin that would like to use any of its features. It takes no plugin registration options and is entirely configured per-plugin using [`server.registerService()`](#serverregisterserviceservicefactory). 12 | 13 | ### Server decorations 14 | #### `server.registerService(serviceFactory)` 15 | 16 | Registers a service with `server` (which may be a plugin's server or root server). The passed `serviceFactory` used to define the service object may be any of the following: 17 | 18 | - A service class. Services are instanced immediately when they are registered, with `server` and corresponding plugin `options` passed to the constructor. The service class should be named via its natural class `name` or the [`Schmervice.name`](#schmervicename) symbol (see more under [service naming](#service-naming-and-sandboxing)). 19 | 20 | ```js 21 | server.registerService( 22 | class MyServiceName { 23 | constructor(server, options) {} 24 | someMethod() {} 25 | } 26 | ); 27 | ``` 28 | 29 | - A factory function returning a service object. The factory function is called immediately to create the service, with `server` and corresponding plugin `options` passed as arguments. The service object should be named using either a `name` property or the [`Schmervice.name`](#schmervicename) symbol (see more under [service naming](#service-naming-and-sandboxing)). 30 | 31 | ```js 32 | server.registerService((server, options) => ({ 33 | name: 'myServiceName', 34 | someMethod: () => {} 35 | })); 36 | ``` 37 | 38 | - A service object. The service object should be named using either a `name` property or the [`Schmervice.name`](#schmervicename) symbol (see more under [service naming](#service-naming-and-sandboxing)). 39 | 40 | ```js 41 | server.registerService({ 42 | name: 'myServiceName', 43 | someMethod: () => {} 44 | }); 45 | ``` 46 | 47 | - An array containing any of the above. 48 | 49 | ```js 50 | server.registerService([ 51 | class MyServiceName { 52 | constructor(server, options) {} 53 | someMethod() {} 54 | }, 55 | { 56 | name: 'myOtherServiceName', 57 | someOtherMethod: () => {} 58 | } 59 | ]); 60 | ``` 61 | 62 | #### `server.services([namespace])` 63 | Returns an object containing each service instance keyed by their [instance names](#service-naming-and-sandboxing). 64 | 65 | The services that are available on this object are only those registered by `server` or any plugins for which `server` is an ancestor (e.g. if `server` has registered a plugin that registers services) that are also not [sandboxed](#service-naming-and-sandboxing). By passing a `namespace` you can obtain the services from the perspective of a different plugin. When `namespace` is a string, you receive services that are visibile within the plugin named `namespace`. And when `namespace` is `true`, you receive services that are visibile to the root server: every service registered with the hapi server– across all plugins– that isn't sandboxed. 66 | 67 | ### Request decorations 68 | #### `request.services([namespace])` 69 | See [`server.services()`](#serverservicesnamespace), where `server` is the one in which the `request`'s route was declared (i.e. based upon `request.route.realm`). 70 | 71 | ### Response toolkit decorations 72 | #### `h.services([namespace])` 73 | See [`server.services()`](#serverservicesnamespace), where `server` is the one in which the corresponding route or server extension was declared (i.e. based upon `h.realm`). 74 | 75 | ## Service naming and sandboxing 76 | 77 | The name of a service is primarily used to determine the key on the result of [`server.services()`](#serverservicesnamespace) where the service may be accessed. In the case of service classes, the name is derived from the class's natural `name` (e.g. `class ThisIsTheClassName {}`) by default. In the case of service objects, including those returned from a function, the name is derived from the object's `name` property by default. In both cases the name is converted to camel-case. 78 | 79 | Sometimes you don't want the name to be based on these properties or you don't want their values camel-cased, which is where [`Schmervice.name`](#schmervicename) and [`Schmervice.withName()`](#schmervicewithnamename-options-servicefactory) can be useful. 80 | 81 | Sandboxing is a concept that determines whether a given service is available in the "plugin namespace" accessed using [`server.services()`](#serverservicesnamespace). By default when you register a service, it is available in the current plugin, and all of that plugin's ancestors up to and including the root server. A sandboxed service, on the other hand, is only available in the plugin/namespace in which it is registered, which is where [`Schmervice.sandbox`](#schmervicesandbox) and [`Schmervice.withName()`](#schmervicewithnamename-options-servicefactory)'s options come into play. 82 | 83 | ### `Schmervice.name` 84 | 85 | This is a symbol that can be added as a property to either a service class or service object. Its value should be a string, and this value will be taken literally as the service's name without any camel-casing. A service class or object's `Schmervice.name` property is always preferred to its natural class name or `name` property, so this property can be used as an override. 86 | 87 | ```js 88 | server.registerService({ 89 | [Schmervice.name]: 'myServiceName', 90 | someMethod: () => {} 91 | }); 92 | 93 | // ... 94 | const { myServiceName } = server.services(); 95 | ``` 96 | 97 | ### `Schmervice.sandbox` 98 | 99 | This is a symbol that can be added as a property to either a service class or service object. When the value of this property is `true` or `'plugin'`, then the service is not available to [`server.services()`](#serverservicesnamespace) for any namespace aside from that of the plugin that registered the service. This effectively makes the service "private" within the plugin that it is registered. 100 | 101 | The default behavior, which can also be declared explicitly by setting this property to `false` or `'server'`, makes the service available within the current plugin's namespace, and all of the namespaces of that plugin's ancestors up to and including the root server (i.e. the namespace accessed by `server.services(true)`). 102 | 103 | ```js 104 | server.registerService({ 105 | [Schmervice.name]: 'privateService', 106 | [Schmervice.sandbox]: true, 107 | someMethod: () => {} 108 | }); 109 | 110 | // ... 111 | // Can access the service in the same plugin that registered it 112 | const { privateService } = server.services(); 113 | 114 | // But cannot access it in other namespaces, e.g. the root namespace, because it is sandboxed 115 | const { privateService: doesNotExist } = server.services(true); 116 | ``` 117 | 118 | ### `Schmervice.withName(name, [options], serviceFactory)` 119 | 120 | This is a helper that assigns `name` to the service instance or object produced by `serviceFactory` by setting the service's [`Schmervice.name`](#schmervicename). When `serviceFactory` is a service class or object, `Schmervice.withName()` returns the same service class or object mutated with `Schmervice.name` set accordingly. When `serviceFactory` is a function, this helper returns a new function that behaves identically but adds the `Schmervice.name` property to its result. If the resulting service class or object already has a `Schmervice.name` then this helper will fail. 121 | 122 | Following a similar logic and behavior to the above: when `options` is present, this helper also assigns `options.sandbox` to the service instance or object produced by `serviceFactory` by setting the service's [`Schmervice.sandbox`](#schmervicesandbox). If the resulting service class or object already has a `Schmervice.sandbox` then this helper will fail. 123 | 124 | ```js 125 | server.registerService( 126 | Schmervice.withName('myServiceName', () => ({ 127 | someMethod: () => {} 128 | })) 129 | ); 130 | 131 | // ... 132 | const { myServiceName } = server.services(); 133 | ``` 134 | 135 | This is also the preferred way to name a service object from some other library, since it prevents property conflicts. 136 | 137 | ```js 138 | const Nodemailer = require('nodemailer'); 139 | 140 | const transport = Nodemailer.createTransport(); 141 | 142 | server.registerService(Schmervice.withName('emailService', transport)); 143 | 144 | // ... 145 | const { emailService } = server.services(); 146 | ``` 147 | 148 | An example usage of `options.sandbox`: 149 | 150 | ```js 151 | server.registerService( 152 | Schmervice.withName('privateService', { sandbox: true }, { 153 | someMethod: () => {} 154 | }) 155 | ); 156 | 157 | // ... 158 | // Can access the service in the same plugin that registered it 159 | const { privateService } = server.services(); 160 | 161 | // But cannot access it in other namespaces, e.g. the root namespace, because it is sandboxed 162 | const { privateService: doesNotExist } = server.services(true); 163 | ``` 164 | 165 | ## `Schmervice.Service` 166 | This class is intended to be used as a base class for services registered with schmervice. However, it is completely reasonable to use this class independently of the [schmervice plugin](#the-hapi-plugin) if desired. 167 | 168 | ### `new Service(server, options)` 169 | Constructor to create a new service instance. `server` should be a hapi plugin's server or root server, and `options` should be the corresponding plugin `options`. This is intended to mirror a plugin's [registration function](https://github.com/hapijs/hapi/blob/master/API.md#plugins) `register(server, options)`. Note: creating a service instance may have side-effects on the `server`, e.g. adding server extensions– keep reading for details. 170 | 171 | ### `service.server` 172 | The `server` passed to the constructor. Should be a hapi plugin's server or root server. 173 | 174 | ### `service.options` 175 | The hapi plugin `options` passed to the constructor. 176 | 177 | ### `service.context` 178 | The context of `service.server` set using [`server.bind()`](https://github.com/hapijs/hapi/blob/master/API.md#server.bind()). Will be `null` if no context has been set. This is implemented lazily as a getter based upon `service.server` so that services can be part of the context without introducing any circular dependencies between the two. 179 | 180 | ### `service.bind()` 181 | Returns a new service instance where all methods are bound to the service instance allowing you to deconstruct methods without losing the `this` context. 182 | 183 | ### `async service.initialize()` 184 | This is not implemented on the base service class, but when it is implemented by an extending class then it will be called during `server` initialization (via `onPreStart` [server extension](https://github.com/hapijs/hapi/blob/master/API.md#server.ext()) added when the service is instanced). 185 | 186 | ### `async service.teardown()` 187 | This is not implemented on the base service class, but when it is implemented by an extending class then it will be called during `server` stop (via `onPostStop` [server extension](https://github.com/hapijs/hapi/blob/master/API.md#server.ext()) added when the service is instanced). 188 | 189 | ### `service.caching(options)` 190 | Configures caching for the service's methods, and may be called once. The `options` argument should be an object where each key is the name of one of the service's methods, and each corresponding value is either, 191 | 192 | - An object `{ cache, generateKey }` as detailed in the [server method options](https://github.com/hapijs/hapi/blob/master/API.md#server.method()) documentation. 193 | - An object containing the `cache` options as detailed in the [server method options](https://github.com/hapijs/hapi/blob/master/API.md#server.method()) documentation. 194 | 195 | Note that behind the scenes an actual server method will be created on `service.server` and will replace the respective method on the service instance, which means that any service method configured for caching must be called asynchronously even if its original implementation is synchronous. In order to configure caching, the service class also must have a `name`, e.g. `class MyServiceName extends Schmervice.Service {}`. 196 | 197 | ### `Service.caching` 198 | This is not set on the base service class, but when an extending class has a static `caching` property (or getter) then its value will be used used to configure service method caching (via [`service.caching()`](#servicecachingoptions) when the service is instanced). 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2023 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # schmervice 2 | 3 | A service layer for hapi 4 | 5 | [![Build Status](https://travis-ci.org/hapipal/schmervice.svg?branch=main)](https://travis-ci.org/hapipal/schmervice) [![Coverage Status](https://coveralls.io/repos/hapipal/schmervice/badge.svg?branch=main&service=github)](https://coveralls.io/github/hapipal/schmervice?branch=main) 6 | 7 | Lead Maintainer - [Devin Ivy](https://github.com/devinivy) 8 | 9 | ## Installation 10 | ```sh 11 | npm install @hapipal/schmervice 12 | ``` 13 | 14 | ## Usage 15 | > See also the [API Reference](API.md) 16 | > 17 | > Schmervice is intended for use with hapi v20+ and nodejs v16+ (_see v2 for lower support_). 18 | 19 | Services are a nice way to organize related business logic or transactions into classes. Schmervice is a service layer designed to integrate nicely with hapi. It consists of two parts that can be used together or separately: 20 | 21 | 1. a base `Service` class that integrates your service with hapi by: 22 | - giving it access to the relevant `server` and plugin `options`. 23 | - allowing you to implement `async initialize()` and `async teardown()` methods that should run when the server initializes and stops. 24 | - allowing you to configure certain methods as being cacheable, with all the server method configuration that you're accustomed to. 25 | 26 | 2. a hapi plugin that allows you to register services and access them where it is most convenient, such as in route handlers. This registry respects plugin boundaries and is hierarchical, so unrelated plugins can safely register their own services without affecting each other. 27 | 28 | 29 | ```js 30 | const Schmervice = require('@hapipal/schmervice'); 31 | const Hapi = require('@hapi/hapi'); 32 | 33 | (async () => { 34 | 35 | const server = Hapi.server(); 36 | 37 | await server.register(Schmervice); 38 | 39 | server.registerService( 40 | class MathService extends Schmervice.Service { 41 | 42 | add(x, y) { 43 | 44 | this.server.log(['math-service'], 'Adding'); 45 | 46 | return Number(x) + Number(y); 47 | } 48 | 49 | multiply(x, y) { 50 | 51 | this.server.log(['math-service'], 'Multiplying'); 52 | 53 | return Number(x) * Number(y); 54 | } 55 | } 56 | ); 57 | 58 | server.route({ 59 | method: 'get', 60 | path: '/add/{a}/{b}', 61 | handler: (request) => { 62 | 63 | const { a, b } = request.params; 64 | const { mathService } = request.services(); 65 | 66 | return mathService.add(a, b); 67 | } 68 | }); 69 | 70 | await server.start(); 71 | 72 | console.log(`Start adding at ${server.info.uri}`); 73 | })(); 74 | ``` 75 | 76 | ### Functional style 77 | 78 | Schmervice allows you to write services in a functional style in additional to the class-oriented approach shown above. [`server.registerService()`](API.md#serverregisterserviceservicefactory) can be passed a plain object or a factory function. Just make sure to name your service using the [`Schmervice.name`](API.md#schmervicename) symbol or a `name` property. Here's a functional adaptation of the example above: 79 | 80 | ```js 81 | const Schmervice = require('@hapipal/schmervice'); 82 | const Hapi = require('@hapi/hapi'); 83 | 84 | (async () => { 85 | 86 | const server = Hapi.server(); 87 | 88 | await server.register(Schmervice); 89 | 90 | server.registerService( 91 | (srv) => ({ 92 | [Schmervice.name]: 'mathService', 93 | add: (x, y) => { 94 | 95 | srv.log(['math-service'], 'Adding'); 96 | 97 | return Number(x) + Number(y); 98 | }, 99 | multiply: (x, y) => { 100 | 101 | srv.log(['math-service'], 'Multiplying'); 102 | 103 | return Number(x) * Number(y); 104 | } 105 | }) 106 | ); 107 | 108 | server.route({ 109 | method: 'get', 110 | path: '/add/{a}/{b}', 111 | handler: (request) => { 112 | 113 | const { a, b } = request.params; 114 | const { mathService } = request.services(); 115 | 116 | return mathService.add(a, b); 117 | } 118 | }); 119 | 120 | await server.start(); 121 | 122 | console.log(`Start adding at ${server.info.uri}`); 123 | })(); 124 | ``` 125 | 126 | ### Using existing libraries as services 127 | 128 | It's also possible to use existing libraries as services in your application. Here's an example of how we might utilize [Nodemailer](https://nodemailer.com/) as a service for sending emails. This example features [`Schmervice.withName()`](API.md#schmervicewithname), which is a convenient way to name your service using the [`Schmervice.name`](API.md#schmervicename) symbol, similarly to the example above: 129 | 130 | ```js 131 | const Schmervice = require('@hapipal/schmervice'); 132 | const Nodemailer = require('nodemailer'); 133 | const Hapi = require('@hapi/hapi'); 134 | 135 | (async () => { 136 | 137 | const server = Hapi.server(); 138 | 139 | await server.register(Schmervice); 140 | 141 | server.registerService( 142 | Schmervice.withName('emailService', () => { 143 | 144 | // Sendmail is a simple transport to configure for testing, but if you're 145 | // not seeing the sent emails then make sure to check your spam folder. 146 | 147 | return Nodemailer.createTransport({ 148 | sendmail: true 149 | }); 150 | }) 151 | ); 152 | 153 | server.route({ 154 | method: 'get', 155 | path: '/email/{toAddress}/{message*}', 156 | handler: async (request) => { 157 | 158 | const { toAddress, message } = request.params; 159 | const { emailService } = request.services(); 160 | 161 | await emailService.sendMail({ 162 | from: 'no-reply@yoursite.com', 163 | to: toAddress, 164 | subject: 'A message for you', 165 | text: message 166 | }); 167 | 168 | return { success: true }; 169 | } 170 | }); 171 | 172 | await server.start(); 173 | 174 | console.log(`Start emailing at ${server.info.uri}`); 175 | })(); 176 | ``` 177 | 178 | ## Extras 179 | ##### _What is a service layer?_ 180 | "Service layer" is a very imprecise term because it is utilized in all sorts of different ways by various developers and applications. Our goal here is not to be prescriptive about how you use services. But speaking generally, one might write code in a "service" as a way to group related business logic, data transactions (e.g. saving records to a database), or calls to external APIs. Sometimes the service layer denotes a "headless" interface to all the actions you can take in an application, independent of any transport (such as HTTP), and hiding the app's data layer (or model) from its consumers. 181 | 182 | In our case services make up a general-purpose "layer" or part of your codebase– concretely, they're just classes that are instanced once per server. You can use them however you see fit! 183 | 184 | hapi actually has a feature deeply related to this concept of services: [server methods](https://github.com/hapijs/hapi/blob/master/API.md#server.methods). We love server methods, but also think they work better as a low-level API than being used directly in medium- and large-sized projects. If you're already familiar with hapi server methods, you can think of schmervice as a tool to ergonomically create and use server methods (plus some other bells and whistles). 185 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for @hapipal/schmervice 2.0 2 | // Project: https://github.com/hapipal/schmervice#readme 3 | // Definitions by: Tim Costa 4 | // Danilo Alonso 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | // Minimum TypeScript Version: 4 7 | 8 | import { 9 | Plugin, 10 | Server as HapiServer, 11 | ServerOptionsCache, 12 | ServerMethodOptions 13 | } from '@hapi/hapi'; 14 | 15 | export const name: unique symbol; 16 | export const sandbox: unique symbol; 17 | 18 | export interface ServiceCachingOptions { 19 | [methodNameToCache: string]: ServerOptionsCache | Exclude; 20 | } 21 | 22 | export type ServiceSandbox = boolean | 'plugin' | 'server'; 23 | 24 | export interface ServiceRegistrationObject { 25 | caching?: ServiceCachingOptions | undefined; 26 | name?: string | undefined; 27 | [name]?: string | undefined; 28 | [sandbox]?: ServiceSandbox | undefined; 29 | // any is necessary here as implementation is left to the developers 30 | // without this member the tests fail as the Schmervice.withName factory 31 | // has no members in common with this interface 32 | [serviceMethod: string]: any; 33 | } 34 | 35 | export function ServiceFactory(server: HapiServer, options: object): ServiceRegistrationObject; 36 | 37 | // options is any because it's left to the implementer to define based on usage 38 | export type ServiceOptions = any; 39 | 40 | export class Service { 41 | static caching: ServiceCachingOptions; 42 | static [name]: string; 43 | static [sandbox]: ServiceSandbox; 44 | server: HapiServer; 45 | options: O; 46 | constructor(server: HapiServer, options: O); 47 | // object matches https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/hapi__hapi/index.d.ts#L3104 48 | // null matches else case in schmervice 49 | get context(): object | null; 50 | caching(options: ServiceCachingOptions): void; 51 | bind(): this; 52 | initialize?(): void; 53 | teardown?(): void; 54 | } 55 | 56 | export type RegisterServiceConfiguration = (typeof ServiceFactory | Service | Service[] | ServiceRegistrationObject); 57 | 58 | export const plugin: Plugin>; 59 | 60 | export interface WithNameOptions { 61 | sandbox?: ServiceSandbox | undefined; 62 | } 63 | 64 | // TS takes issue with this function signature (name, [options], serviceFactory) due to a required param 65 | // following an optional param. The best solution short of changing the library appears to be to just 66 | // make options a required parameter that people can set to {} 67 | export function withName(name: string, options: WithNameOptions, serviceFactory: RegisterServiceConfiguration): RegisterServiceConfiguration; 68 | 69 | // allows service definitions to optionally "register" themselves as types that will be returned 70 | // by using typescript declaration merging with interfaces 71 | export interface RegisteredServices { 72 | [key: string]: Service; 73 | } 74 | 75 | 76 | /** 77 | * Server decorator for getting services scoped to the 78 | * current plugin realm using `server.services()`, 79 | * or services registered on the server using `server.services(true)`, 80 | * or services scoped to plugin namespace using `server.services('namespace')`. 81 | * 82 | * 83 | * 84 | * This interface can be overwritten to modify what you want your namespace 85 | * to actually return. For example: 86 | * 87 | * @example 88 | * 89 | * declare module '@hapipal/schmervice' { 90 | * type AuthServices = { 91 | * Members: Service 92 | * Admin: Service 93 | * Mananger: Service 94 | * } 95 | * 96 | * type OathServices = { 97 | * Witness: Service 98 | * Promissory: Service 99 | * CrownCourt: Service 100 | * } 101 | * 102 | * interface SchmerviceDecorator { 103 | * (namespace: 'auth'): AuthServices 104 | * (namespace: 'oath'): OathServices 105 | * } 106 | * } 107 | * 108 | */ 109 | export interface SchmerviceDecorator { 110 | 111 | (all?: boolean): RegisteredServices 112 | (namespace?: string): RegisteredServices 113 | } 114 | 115 | 116 | 117 | declare module '@hapi/hapi' { 118 | 119 | interface Server { 120 | registerService: (config: RegisterServiceConfiguration) => void; 121 | services: SchmerviceDecorator; 122 | } 123 | 124 | interface Request { 125 | services: SchmerviceDecorator; 126 | } 127 | 128 | interface ResponseToolkit { 129 | services: SchmerviceDecorator; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Assert = require('node:assert'); 4 | const Package = require('../package.json'); 5 | 6 | const internals = {}; 7 | 8 | exports.Service = class Service { 9 | 10 | constructor(server, options) { 11 | 12 | this.server = server; 13 | this.options = options; 14 | 15 | if (typeof this.initialize === 'function') { 16 | this.server.ext('onPreStart', this.initialize, { bind: this }); 17 | } 18 | 19 | if (typeof this.teardown === 'function') { 20 | this.server.ext('onPostStop', this.teardown, { bind: this }); 21 | } 22 | 23 | if (this.constructor.caching) { 24 | this.caching(this.constructor.caching); 25 | } 26 | } 27 | 28 | get context() { 29 | 30 | return this.server.realm.settings.bind || null; 31 | } 32 | 33 | caching(options) { 34 | 35 | Assert.ok(this.constructor.name, 'The service class must have a name in order to configure caching.'); 36 | Assert.ok(!this.__caching, 'Caching config can only be specified once.'); 37 | this.__caching = true; 38 | 39 | const instanceName = internals.instanceName(this.constructor.name); 40 | 41 | Object.keys(options).forEach((methodName) => { 42 | 43 | const generateKey = options[methodName].generateKey; 44 | 45 | const cache = options[methodName].cache ? 46 | { ...options[methodName].cache } : 47 | { ...options[methodName] }; 48 | 49 | delete cache.generateKey; 50 | 51 | this.server.method({ 52 | name: `schmervice.${instanceName}.${methodName}`, 53 | method: this[methodName], 54 | options: { 55 | bind: this, 56 | generateKey, 57 | cache 58 | } 59 | }); 60 | 61 | this[methodName] = this.server.methods.schmervice[instanceName][methodName]; 62 | }); 63 | } 64 | 65 | bind() { 66 | 67 | if (!this[internals.boundInstance]) { 68 | 69 | const boundInstance = Object.create(this); 70 | 71 | let chain = boundInstance; 72 | 73 | while (chain !== Object.prototype) { 74 | 75 | for (const key of Reflect.ownKeys(chain)) { 76 | 77 | if (key === 'constructor') { 78 | continue; 79 | } 80 | 81 | const descriptor = Reflect.getOwnPropertyDescriptor(chain, key); 82 | 83 | if (typeof descriptor.value === 'function') { 84 | boundInstance[key] = boundInstance[key].bind(this); 85 | } 86 | } 87 | 88 | chain = Reflect.getPrototypeOf(chain); 89 | } 90 | 91 | this[internals.boundInstance] = boundInstance; 92 | } 93 | 94 | return this[internals.boundInstance]; 95 | } 96 | }; 97 | 98 | exports.plugin = { 99 | pkg: Package, 100 | once: true, 101 | requirements: { 102 | hapi: '>=19' 103 | }, 104 | register(server) { 105 | 106 | server.decorate('server', 'registerService', internals.registerService); 107 | server.decorate('server', 'services', internals.services((srv) => srv.realm)); 108 | server.decorate('request', 'services', internals.services((request) => request.route.realm)); 109 | server.decorate('toolkit', 'services', internals.services((h) => h.realm)); 110 | } 111 | }; 112 | 113 | exports.name = Symbol('serviceName'); 114 | 115 | exports.sandbox = Symbol('serviceSandbox'); 116 | 117 | exports.withName = (name, options, factory) => { 118 | 119 | if (typeof factory === 'undefined') { 120 | factory = options; 121 | options = {}; 122 | } 123 | 124 | if (typeof factory === 'function' && !internals.isClass(factory)) { 125 | return (...args) => { 126 | 127 | const service = factory(...args); 128 | 129 | if (typeof service.then === 'function') { 130 | return service.then((x) => internals.withNameObject(name, options, x)); 131 | } 132 | 133 | return internals.withNameObject(name, options, service); 134 | }; 135 | } 136 | 137 | return internals.withNameObject(name, options, factory); 138 | }; 139 | 140 | internals.withNameObject = (name, { sandbox }, obj) => { 141 | 142 | Assert.ok(!obj[exports.name], 'Cannot apply a name to a service that already has one.'); 143 | 144 | obj[exports.name] = name; 145 | 146 | if (typeof sandbox !== 'undefined') { 147 | Assert.ok(typeof obj[exports.sandbox] === 'undefined', 'Cannot apply a sandbox setting to a service that already has one.'); 148 | obj[exports.sandbox] = sandbox; 149 | } 150 | 151 | return obj; 152 | }; 153 | 154 | internals.boundInstance = Symbol('boundInstance'); 155 | 156 | internals.services = (getRealm) => { 157 | 158 | return function (namespace) { 159 | 160 | const realm = getRealm(this); 161 | 162 | if (!namespace) { 163 | return internals.state(realm).services; 164 | } 165 | 166 | if (typeof namespace === 'string') { 167 | const namespaceSet = internals.rootState(realm).namespaces[namespace]; 168 | Assert.ok(namespaceSet, `The plugin namespace ${namespace} does not exist.`); 169 | Assert.ok(namespaceSet.size === 1, `The plugin namespace ${namespace} is not unique: is that plugin registered multiple times?`); 170 | const [namespaceRealm] = [...namespaceSet]; 171 | return internals.state(namespaceRealm).services; 172 | } 173 | 174 | return internals.rootState(realm).services; 175 | }; 176 | }; 177 | 178 | internals.registerService = function (services) { 179 | 180 | services = [].concat(services); 181 | 182 | services.forEach((factory) => { 183 | 184 | const { name, instanceName, service, sandbox } = internals.serviceFactory(factory, this, this.realm.pluginOptions); 185 | const rootState = internals.rootState(this.realm); 186 | 187 | Assert.ok(sandbox || !rootState.services[instanceName], `A service named ${name} has already been registered.`); 188 | 189 | rootState.namespaces[this.realm.plugin] = rootState.namespaces[this.realm.plugin] || new Set(); 190 | rootState.namespaces[this.realm.plugin].add(this.realm); 191 | 192 | if (sandbox) { 193 | return internals.addServiceToRealm(this.realm, service, instanceName); 194 | } 195 | 196 | internals.forEachAncestorRealm(this.realm, (realm) => { 197 | 198 | internals.addServiceToRealm(realm, service, instanceName); 199 | }); 200 | }); 201 | }; 202 | 203 | internals.addServiceToRealm = (realm, service, name) => { 204 | 205 | const state = internals.state(realm); 206 | Assert.ok(!state.services[name], `A service named ${name} has already been registered in plugin namespace ${realm.plugin}.`); 207 | state.services[name] = service; 208 | }; 209 | 210 | internals.forEachAncestorRealm = (realm, fn) => { 211 | 212 | do { 213 | fn(realm); 214 | realm = realm.parent; 215 | } 216 | while (realm); 217 | }; 218 | 219 | internals.rootState = (realm) => { 220 | 221 | while (realm.parent) { 222 | realm = realm.parent; 223 | } 224 | 225 | return internals.state(realm); 226 | }; 227 | 228 | internals.state = (realm) => { 229 | 230 | const state = realm.plugins.schmervice = realm.plugins.schmervice || { 231 | services: {}, 232 | namespaces: {} 233 | }; 234 | 235 | return state; 236 | }; 237 | 238 | internals.serviceFactory = (factory, server, options) => { 239 | 240 | Assert.ok(factory && (typeof factory === 'object' || typeof factory === 'function')); 241 | 242 | if (typeof factory === 'function' && internals.isClass(factory)) { 243 | 244 | const name = factory[exports.name] || factory.name; 245 | Assert.ok(name && typeof factory.name === 'string', 'The service class must have a name.'); 246 | 247 | return { 248 | name, 249 | instanceName: factory[exports.name] ? name : internals.instanceName(name), 250 | sandbox: internals.sandbox(factory[exports.sandbox]), 251 | service: new factory(server, options) 252 | }; 253 | } 254 | 255 | const service = (typeof factory === 'function') ? factory(server, options) : factory; 256 | Assert.ok(service && typeof service === 'object'); 257 | 258 | const name = service[exports.name] || service.name || service.realm?.plugin; 259 | Assert.ok(name && typeof name === 'string', 'The service must have a name.'); 260 | 261 | return { 262 | name, 263 | instanceName: service[exports.name] ? name : internals.instanceName(name), 264 | sandbox: internals.sandbox(service[exports.sandbox]), 265 | service 266 | }; 267 | }; 268 | 269 | internals.instanceName = (name) => { 270 | 271 | return name 272 | .replace(/[-_ ]+(.?)/g, (ignore, m) => m.toUpperCase()) 273 | .replace(/^./, (m) => m.toLowerCase()); 274 | }; 275 | 276 | internals.sandbox = (value) => { 277 | 278 | if (value === 'plugin') { 279 | return true; 280 | } 281 | 282 | if (value === 'server') { 283 | return false; 284 | } 285 | 286 | return value; 287 | }; 288 | 289 | internals.isClass = (func) => (/^\s*class\s/).test(func.toString()); 290 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hapipal/schmervice", 3 | "version": "3.0.0", 4 | "description": "A service layer for hapi", 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 test/*.js", 15 | "coveralls": "lab -r lcov test/*.js | coveralls" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/hapipal/schmervice.git" 20 | }, 21 | "keywords": [ 22 | "hapi", 23 | "hapijs", 24 | "services", 25 | "architecture", 26 | "caching" 27 | ], 28 | "author": "Devin Ivy ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/hapipal/schmervice/issues" 32 | }, 33 | "homepage": "https://github.com/hapipal/schmervice#readme", 34 | "peerDependencies": { 35 | "@hapi/hapi": ">=20 <22" 36 | }, 37 | "devDependencies": { 38 | "@hapi/code": "^9.0.3", 39 | "@hapi/hapi": "^21.3.0", 40 | "@hapi/lab": "^25.1.2", 41 | "@hapipal/ahem": "^3.0.0", 42 | "coveralls": "^3.1.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Code = require('@hapi/code'); 4 | const Hapi = require('@hapi/hapi'); 5 | const Ahem = require('@hapipal/ahem'); 6 | const Schmervice = require('..'); 7 | const Lab = require('@hapi/lab'); 8 | 9 | const { describe, it } = exports.lab = Lab.script(); 10 | const expect = Code.expect; 11 | 12 | describe('Schmervice', () => { 13 | 14 | describe('Service class', () => { 15 | 16 | const sleep = (ms) => { 17 | 18 | return new Promise((resolve) => setTimeout(resolve, ms)); 19 | }; 20 | 21 | it('sets server and options on the instance.', () => { 22 | 23 | const service = new Schmervice.Service('server', 'options'); 24 | 25 | expect(service.server).to.equal('server'); 26 | expect(service.options).to.equal('options'); 27 | }); 28 | 29 | it('context getter returns server\'s context set with srv.bind().', () => { 30 | 31 | const server = Hapi.server(); 32 | const service = new Schmervice.Service(server, {}); 33 | 34 | expect(service.context).to.equal(null); 35 | 36 | const ctx = {}; 37 | server.bind(ctx); 38 | 39 | expect(service.context).to.shallow.equal(ctx); 40 | }); 41 | 42 | describe('bind() method', () => { 43 | 44 | it('binds functions', () => { 45 | 46 | const ServiceX = class ServiceX extends Schmervice.Service { 47 | org() { 48 | 49 | return this.context.org; 50 | } 51 | }; 52 | 53 | const server = Hapi.server(); 54 | server.bind({ org: 'hapipal' }); 55 | 56 | const serviceX = new ServiceX(server, {}); 57 | const { org } = serviceX.bind(); 58 | 59 | expect(org()).to.equal('hapipal'); 60 | }); 61 | 62 | it('returns a cached bound instance', () => { 63 | 64 | const ServiceX = class ServiceX extends Schmervice.Service {}; 65 | const server = Hapi.server(); 66 | const serviceX = new ServiceX(server, {}); 67 | 68 | expect(serviceX.bind()).to.shallow.equal(serviceX.bind()); 69 | }); 70 | 71 | it('lazily creates a bound instance, without calling constructor or getters.', () => { 72 | 73 | let calledConstructor = 0; 74 | let calledGetter = 0; 75 | const ServiceX = class ServiceX extends Schmervice.Service { 76 | 77 | constructor(...args) { 78 | 79 | super(...args); 80 | 81 | calledConstructor++; 82 | } 83 | 84 | static get someProp() { 85 | 86 | calledGetter++; 87 | } 88 | }; 89 | 90 | const server = Hapi.server(); 91 | const serviceX = new ServiceX(server, {}); 92 | 93 | expect(calledConstructor).to.equal(1); 94 | expect(calledGetter).to.equal(0); 95 | expect(Object.getOwnPropertySymbols(serviceX)).to.have.length(0); 96 | 97 | const boundInstance = serviceX.bind(); 98 | 99 | expect(calledConstructor).to.equal(1); 100 | expect(calledGetter).to.equal(0); 101 | expect(Object.getOwnPropertySymbols(serviceX)).to.have.length(1); 102 | 103 | const [symbol] = Object.getOwnPropertySymbols(serviceX); 104 | 105 | expect(serviceX[symbol]).to.shallow.equal(boundInstance); 106 | }); 107 | 108 | it('binds functions up the prototype chain (#Service.context)', () => { 109 | 110 | const ServiceX = class ServiceX extends Schmervice.Service { 111 | 112 | constructor(...args) { 113 | 114 | super(...args); 115 | 116 | this.a = 'a'; 117 | } 118 | 119 | getB() { 120 | 121 | return this.b; 122 | } 123 | }; 124 | 125 | 126 | const ServiceXX = class ServiceXX extends ServiceX { 127 | 128 | constructor(...args) { 129 | 130 | super(...args); 131 | 132 | this.b = 'b'; 133 | } 134 | 135 | getA() { 136 | 137 | return this.a; 138 | } 139 | }; 140 | 141 | const server = Hapi.server(); 142 | server.bind({ org: 'hapipal' }); 143 | 144 | const serviceXX = new ServiceXX(server, {}); 145 | 146 | // Getters and inheritance are all good 147 | { 148 | const { context, a, b, getA, getB } = serviceXX.bind(); 149 | 150 | expect(context).to.equal({ org: 'hapipal' }); 151 | expect(a).to.equal('a'); 152 | expect(b).to.equal('b'); 153 | expect(getA()).to.equal('a'); 154 | expect(getB()).to.equal('b'); 155 | } 156 | 157 | // Re-binding works equally well 158 | { 159 | const { bind } = serviceXX.bind(); 160 | const { context, a, b, getA, getB } = bind(); 161 | 162 | expect(context).to.equal({ org: 'hapipal' }); 163 | expect(a).to.equal('a'); 164 | expect(b).to.equal('b'); 165 | expect(getA()).to.equal('a'); 166 | expect(getB()).to.equal('b'); 167 | } 168 | }); 169 | 170 | it('binds functions up the prototype chain (#Service.caching)', async () => { 171 | 172 | const ServiceX = class ServiceX extends Schmervice.Service { 173 | 174 | add(a, b) { 175 | 176 | this.called = (this.called || 0) + 1; 177 | 178 | return a + b; 179 | } 180 | }; 181 | 182 | const server = Hapi.server(); 183 | const serviceX = new ServiceX(server, {}); 184 | 185 | const { caching } = serviceX.bind(); 186 | 187 | caching({ 188 | add: { 189 | expiresIn: 2000, 190 | generateTimeout: false 191 | } 192 | }); 193 | 194 | // Replaced with server method 195 | expect(serviceX.add).to.not.shallow.equal(ServiceX.prototype.add); 196 | 197 | expect(await serviceX.add(1, 2)).to.equal(3); 198 | expect(serviceX.called).to.equal(1); 199 | 200 | expect(await serviceX.add(1, 2)).to.equal(3); 201 | expect(serviceX.called).to.equal(2); 202 | 203 | // Let the caching begin 204 | await server.initialize(); 205 | 206 | expect(await serviceX.add(1, 2)).to.equal(3); 207 | expect(serviceX.called).to.equal(3); 208 | 209 | expect(await serviceX.add(1, 2)).to.equal(3); 210 | expect(serviceX.called).to.equal(3); 211 | 212 | expect(await serviceX.add(2, 3)).to.equal(5); 213 | expect(serviceX.called).to.equal(4); 214 | 215 | expect(await serviceX.add(2, 3)).to.equal(5); 216 | expect(serviceX.called).to.equal(4); 217 | }); 218 | }); 219 | 220 | it('runs initialize() onPreStart and teardown() onPostStop.', async () => { 221 | 222 | const ServiceX = class ServiceX extends Schmervice.Service { 223 | 224 | constructor(server, options) { 225 | 226 | super(server, options); 227 | 228 | this.initialized = false; 229 | this.toredown = false; 230 | } 231 | 232 | async initialize() { 233 | 234 | this.initialized = true; 235 | 236 | await Promise.resolve(); 237 | } 238 | 239 | async teardown() { 240 | 241 | this.toredown = true; 242 | 243 | await Promise.resolve(); 244 | } 245 | }; 246 | 247 | const server = Hapi.server(); 248 | const serviceX = new ServiceX(server, {}); 249 | 250 | expect(serviceX.initialized).to.equal(false); 251 | expect(serviceX.toredown).to.equal(false); 252 | 253 | await server.initialize(); 254 | 255 | expect(serviceX.initialized).to.equal(true); 256 | expect(serviceX.toredown).to.equal(false); 257 | 258 | server.ext('onPreStop', () => { 259 | 260 | expect(serviceX.initialized).to.equal(true); 261 | expect(serviceX.toredown).to.equal(false); 262 | }); 263 | 264 | await server.stop(); 265 | 266 | expect(serviceX.initialized).to.equal(true); 267 | expect(serviceX.toredown).to.equal(true); 268 | }); 269 | 270 | it('configures caching via static caching prop.', async () => { 271 | 272 | const ServiceX = class ServiceX extends Schmervice.Service { 273 | add(a, b) { 274 | 275 | this.called = (this.called || 0) + 1; 276 | 277 | return a + b; 278 | } 279 | }; 280 | 281 | ServiceX.caching = { 282 | add: { 283 | expiresIn: 2000, 284 | generateTimeout: false 285 | } 286 | }; 287 | 288 | const server = Hapi.server(); 289 | const serviceX = new ServiceX(server, {}); 290 | 291 | // Replaced with server method 292 | expect(serviceX.add).to.not.shallow.equal(ServiceX.prototype.add); 293 | 294 | expect(await serviceX.add(1, 2)).to.equal(3); 295 | expect(serviceX.called).to.equal(1); 296 | 297 | expect(await serviceX.add(1, 2)).to.equal(3); 298 | expect(serviceX.called).to.equal(2); 299 | 300 | // Let the caching begin 301 | await server.initialize(); 302 | 303 | expect(await serviceX.add(1, 2)).to.equal(3); 304 | expect(serviceX.called).to.equal(3); 305 | 306 | expect(await serviceX.add(1, 2)).to.equal(3); 307 | expect(serviceX.called).to.equal(3); 308 | 309 | expect(await serviceX.add(2, 3)).to.equal(5); 310 | expect(serviceX.called).to.equal(4); 311 | 312 | expect(await serviceX.add(2, 3)).to.equal(5); 313 | expect(serviceX.called).to.equal(4); 314 | }); 315 | 316 | it('configures caching via caching().', async () => { 317 | 318 | const ServiceX = class ServiceX extends Schmervice.Service { 319 | 320 | constructor(server, options) { 321 | 322 | super(server, options); 323 | 324 | this.caching({ 325 | add: { 326 | expiresIn: 2000, 327 | generateTimeout: false 328 | } 329 | }); 330 | } 331 | 332 | add(a, b) { 333 | 334 | this.called = (this.called || 0) + 1; 335 | 336 | return a + b; 337 | } 338 | }; 339 | 340 | const server = Hapi.server(); 341 | const serviceX = new ServiceX(server, {}); 342 | 343 | // Replaced with server method 344 | expect(serviceX.add).to.not.shallow.equal(ServiceX.prototype.add); 345 | 346 | expect(await serviceX.add(1, 2)).to.equal(3); 347 | expect(serviceX.called).to.equal(1); 348 | 349 | expect(await serviceX.add(1, 2)).to.equal(3); 350 | expect(serviceX.called).to.equal(2); 351 | 352 | // Let the caching begin 353 | await server.initialize(); 354 | 355 | expect(await serviceX.add(1, 2)).to.equal(3); 356 | expect(serviceX.called).to.equal(3); 357 | 358 | expect(await serviceX.add(1, 2)).to.equal(3); 359 | expect(serviceX.called).to.equal(3); 360 | 361 | expect(await serviceX.add(2, 3)).to.equal(5); 362 | expect(serviceX.called).to.equal(4); 363 | 364 | expect(await serviceX.add(2, 3)).to.equal(5); 365 | expect(serviceX.called).to.equal(4); 366 | }); 367 | 368 | it('only allows caching to be configured once.', () => { 369 | 370 | const ServiceX = class ServiceX extends Schmervice.Service { 371 | 372 | constructor(server, options) { 373 | 374 | super(server, options); 375 | 376 | this.caching({ 377 | add: { 378 | expiresIn: 2000, 379 | generateTimeout: false 380 | } 381 | }); 382 | } 383 | 384 | add(a, b) { 385 | 386 | this.called = (this.called || 0) + 1; 387 | 388 | return a + b; 389 | } 390 | }; 391 | 392 | ServiceX.caching = { 393 | add: { 394 | expiresIn: 2000, 395 | generateTimeout: false 396 | } 397 | }; 398 | 399 | const server = Hapi.server(); 400 | 401 | expect(() => new ServiceX(server, {})).to.throw('Caching config can only be specified once.'); 402 | }); 403 | 404 | it('accepts caching config in form { cache, generateKey }.', async () => { 405 | 406 | const ServiceX = class ServiceX extends Schmervice.Service { 407 | 408 | async add(a, b, fail) { 409 | 410 | this.called = (this.called || 0) + 1; 411 | 412 | if (fail) { 413 | await sleep(3); // Longer than generateTimeout 414 | } 415 | 416 | return a + b; 417 | } 418 | }; 419 | 420 | ServiceX.caching = { 421 | add: { 422 | cache: { 423 | expiresIn: 100, 424 | generateTimeout: 2 425 | }, 426 | generateKey: (a, b) => { 427 | 428 | // Addition is commutative 429 | return (a < b) ? `${a}:${b}` : `${b}:${a}`; 430 | } 431 | } 432 | }; 433 | 434 | const server = Hapi.server(); 435 | const serviceX = new ServiceX(server, {}); 436 | 437 | await server.initialize(); 438 | 439 | expect(serviceX.called).to.not.exist(); 440 | 441 | // Check { generateKey } 442 | await serviceX.add(1, 2, false); 443 | expect(serviceX.called).to.equal(1); 444 | 445 | await serviceX.add(2, 1, false); 446 | expect(serviceX.called).to.equal(1); 447 | 448 | await serviceX.add(1, 3, false); 449 | expect(serviceX.called).to.equal(2); 450 | 451 | // Check { cache } 452 | await expect(serviceX.add(1, 4, true)).to.reject('Service Unavailable'); 453 | }); 454 | 455 | it('accepts caching config in form { ...cache }.', async () => { 456 | 457 | const ServiceX = class ServiceX extends Schmervice.Service { 458 | 459 | async add(a, b) { 460 | 461 | await sleep(3); // Longer than generateTimeout 462 | 463 | return a + b; 464 | } 465 | }; 466 | 467 | ServiceX.caching = { 468 | add: { 469 | generateTimeout: 2 470 | } 471 | }; 472 | 473 | const server = Hapi.server(); 474 | const serviceX = new ServiceX(server, {}); 475 | 476 | await server.initialize(); 477 | 478 | await expect(serviceX.add(1, 2)).to.reject('Service Unavailable'); 479 | }); 480 | }); 481 | 482 | describe('plugin', () => { 483 | 484 | const getPlugin = async (server, name, others) => { 485 | 486 | const register = () => null; 487 | 488 | return await Ahem.instance(server, { name, register, ...others }, {}, { controlled: false }); 489 | }; 490 | 491 | it('can be registered multiple times.', async () => { 492 | 493 | const server = Hapi.server(); 494 | 495 | expect(server.services).to.not.be.a.function(); 496 | expect(server.registerService).to.not.be.a.function(); 497 | 498 | await server.register(Schmervice); 499 | expect(server.services).to.be.a.function(); 500 | expect(server.registerService).to.be.a.function(); 501 | 502 | await server.register(Schmervice); 503 | expect(server.services).to.be.a.function(); 504 | expect(server.registerService).to.be.a.function(); 505 | }); 506 | 507 | describe('server.registerService() decoration', () => { 508 | 509 | it('registers a single service, passing server and options (class factory).', async () => { 510 | 511 | const server = Hapi.server(); 512 | await server.register(Schmervice); 513 | 514 | const ServiceX = class ServiceX { 515 | constructor(...args) { 516 | 517 | this.args = args; 518 | } 519 | }; 520 | 521 | server.registerService(ServiceX); 522 | 523 | expect(server.services()).to.only.contain(['serviceX']); 524 | 525 | const { serviceX } = server.services(); 526 | 527 | expect(serviceX).to.be.an.instanceof(ServiceX); 528 | expect(serviceX.args).to.have.length(2); 529 | expect(serviceX.args[0]).to.shallow.equal(server); 530 | expect(serviceX.args[1]).to.shallow.equal(server.realm.pluginOptions); 531 | expect(serviceX.args[1]).to.equal({}); 532 | }); 533 | 534 | it('registers a single service, passing server and options (function factory).', async () => { 535 | 536 | const server = Hapi.server(); 537 | await server.register(Schmervice); 538 | 539 | const createServiceX = (...args) => ({ 540 | name: 'ServiceX', 541 | args 542 | }); 543 | 544 | server.registerService(createServiceX); 545 | 546 | expect(server.services()).to.only.contain(['serviceX']); 547 | 548 | const { serviceX } = server.services(); 549 | 550 | expect(serviceX.name).to.equal('ServiceX'); 551 | expect(serviceX.args).to.have.length(2); 552 | expect(serviceX.args[0]).to.shallow.equal(server); 553 | expect(serviceX.args[1]).to.shallow.equal(server.realm.pluginOptions); 554 | expect(serviceX.args[1]).to.equal({}); 555 | }); 556 | 557 | it('registers a single service (object).', async () => { 558 | 559 | const server = Hapi.server(); 560 | await server.register(Schmervice); 561 | 562 | const serviceXObject = { 563 | name: 'ServiceX' 564 | }; 565 | 566 | server.registerService(serviceXObject); 567 | 568 | expect(server.services()).to.only.contain(['serviceX']); 569 | 570 | const { serviceX } = server.services(); 571 | 572 | expect(serviceX.name).to.equal('ServiceX'); 573 | expect(serviceX).to.shallow.equal(serviceXObject); 574 | }); 575 | 576 | it('registers an array of services, passing server and options.', async () => { 577 | 578 | const server = Hapi.server(); 579 | await server.register(Schmervice); 580 | 581 | const ServiceX = class ServiceX { 582 | constructor(...args) { 583 | 584 | this.args = args; 585 | } 586 | }; 587 | 588 | const createServiceY = (...args) => ({ 589 | name: 'ServiceY', 590 | args 591 | }); 592 | 593 | const serviceZObject = { 594 | name: 'ServiceZ' 595 | }; 596 | 597 | server.registerService([ServiceX, createServiceY, serviceZObject]); 598 | 599 | expect(server.services()).to.only.contain(['serviceX', 'serviceY', 'serviceZ']); 600 | 601 | const { serviceX, serviceY, serviceZ } = server.services(); 602 | 603 | expect(serviceX).to.be.an.instanceof(ServiceX); 604 | expect(serviceX.args).to.have.length(2); 605 | expect(serviceX.args[0]).to.shallow.equal(server); 606 | expect(serviceX.args[1]).to.shallow.equal(server.realm.pluginOptions); 607 | expect(serviceX.args[1]).to.equal({}); 608 | 609 | expect(serviceY.name).to.equal('ServiceY'); 610 | expect(serviceY.args).to.have.length(2); 611 | expect(serviceY.args[0]).to.shallow.equal(server); 612 | expect(serviceY.args[1]).to.shallow.equal(server.realm.pluginOptions); 613 | expect(serviceY.args[1]).to.equal({}); 614 | 615 | expect(serviceZ.name).to.equal('ServiceZ'); 616 | expect(serviceZ).to.shallow.equal(serviceZObject); 617 | }); 618 | 619 | it('registers hapi plugin instances, respecting their name.', async () => { 620 | 621 | const server = Hapi.server(); 622 | await server.register(Schmervice); 623 | 624 | const somePlugin = await getPlugin(server, 'some plugin'); 625 | 626 | server.registerService(somePlugin); 627 | 628 | const services = server.services(); 629 | 630 | expect(services).to.only.contain(['somePlugin']); 631 | expect(services.somePlugin).shallow.equal(somePlugin); 632 | }); 633 | 634 | it('names services in camel-case by default.', async () => { 635 | 636 | const server = Hapi.server(); 637 | await server.register(Schmervice); 638 | 639 | server.registerService(class { 640 | static get name() { 641 | 642 | return '-camel_case class_'; 643 | } 644 | }); 645 | 646 | server.registerService({ 647 | name: '-camel_case object_' 648 | }); 649 | 650 | server.registerService(() => ({ 651 | name: '-camel_case function_' 652 | })); 653 | 654 | const services = server.services(); 655 | 656 | expect(services).to.only.contain(['camelCaseClass', 'camelCaseObject', 'camelCaseFunction']); 657 | }); 658 | 659 | it('names services literally when using Schmervice.name symbol.', async () => { 660 | 661 | const server = Hapi.server(); 662 | await server.register(Schmervice); 663 | 664 | server.registerService(class Unused { 665 | static get [Schmervice.name]() { 666 | 667 | return 'raw-name_class'; 668 | } 669 | }); 670 | 671 | server.registerService({ 672 | name: 'Unused', 673 | [Schmervice.name]: 'raw-name_object' 674 | }); 675 | 676 | server.registerService(() => ({ 677 | name: 'Unused', 678 | [Schmervice.name]: 'raw-name_function' 679 | })); 680 | 681 | const services = server.services(); 682 | 683 | expect(services).to.only.contain(['raw-name_class', 'raw-name_function', 'raw-name_object']); 684 | }); 685 | 686 | it('sandboxes services in the current plugin when using Schmervice.sandbox symbol.', async () => { 687 | 688 | const server = Hapi.server(); 689 | await server.register(Schmervice); 690 | 691 | const ServerService = class ServerService {}; 692 | server.registerService(class ServiceA extends ServerService {}); 693 | 694 | const plugin = await getPlugin(server, 'plugin'); 695 | 696 | const PluginService = class PluginService {}; 697 | plugin.registerService(class ServiceA extends PluginService { 698 | static get [Schmervice.sandbox]() { 699 | 700 | return true; 701 | } 702 | }); 703 | 704 | plugin.registerService({ 705 | name: 'serviceB', 706 | [Schmervice.sandbox]: 'plugin' 707 | }); 708 | 709 | plugin.registerService(() => ({ 710 | name: 'serviceC', 711 | [Schmervice.sandbox]: true 712 | })); 713 | 714 | plugin.registerService({ 715 | name: 'serviceD', 716 | [Schmervice.sandbox]: false 717 | }); 718 | 719 | plugin.registerService(() => ({ 720 | name: 'serviceE', 721 | [Schmervice.sandbox]: 'server' 722 | })); 723 | 724 | expect(server.services()).to.only.contain(['serviceA', 'serviceD', 'serviceE']); 725 | expect(plugin.services()).to.only.contain(['serviceA', 'serviceB', 'serviceC', 'serviceD', 'serviceE']); 726 | }); 727 | 728 | it('throws when passed non-services/factories.', async () => { 729 | 730 | const server = Hapi.server(); 731 | await server.register(Schmervice); 732 | 733 | expect(() => server.registerService()).to.throw(); 734 | expect(() => server.registerService('nothing')).to.throw(); 735 | expect(() => server.registerService(() => undefined)).to.throw(); 736 | expect(() => server.registerService(() => 'nothing')).to.throw(); 737 | }); 738 | 739 | it('throws when a service has no name.', async () => { 740 | 741 | const server = Hapi.server(); 742 | await server.register(Schmervice); 743 | 744 | expect(() => server.registerService(class {})).to.throw('The service class must have a name.'); 745 | expect(() => server.registerService(() => ({}))).to.throw('The service must have a name.'); 746 | expect(() => server.registerService({})).to.throw('The service must have a name.'); 747 | }); 748 | 749 | it('throws when two non-sandboxed services with the same name are registered.', async () => { 750 | 751 | const server = Hapi.server(); 752 | await server.register(Schmervice); 753 | 754 | server.registerService(class ServiceX {}); 755 | expect(() => server.registerService(class ServiceX {})).to.throw('A service named ServiceX has already been registered.'); 756 | }); 757 | 758 | it('throws when two sandboxed services with the same name are registered in the same namespace.', async () => { 759 | 760 | const server = Hapi.server(); 761 | await server.register(Schmervice); 762 | 763 | const myPlugin = await getPlugin(server, 'my-plugin'); 764 | 765 | myPlugin.registerService({ 766 | [Schmervice.name]: 'serviceX', 767 | [Schmervice.sandbox]: true 768 | }); 769 | 770 | expect(() => { 771 | 772 | myPlugin.registerService({ 773 | [Schmervice.name]: 'serviceX', 774 | [Schmervice.sandbox]: true 775 | }); 776 | }).to.throw('A service named serviceX has already been registered in plugin namespace my-plugin.'); 777 | }); 778 | 779 | it('throws when a non-sanboxed service shadows a sandboxed service of the same name.', async () => { 780 | 781 | const server = Hapi.server(); 782 | await server.register(Schmervice); 783 | 784 | const myPlugin = await getPlugin(server, 'my-plugin'); 785 | 786 | myPlugin.registerService({ 787 | [Schmervice.name]: 'serviceX', 788 | [Schmervice.sandbox]: true 789 | }); 790 | 791 | const myOtherPlugin = await getPlugin(myPlugin, 'my-other-plugin'); 792 | 793 | expect(() => { 794 | 795 | myOtherPlugin.registerService({ 796 | [Schmervice.name]: 'serviceX', 797 | [Schmervice.sandbox]: false 798 | }); 799 | }).to.throw('A service named serviceX has already been registered in plugin namespace my-plugin.'); 800 | }); 801 | }); 802 | 803 | describe('Schmervice.withName()', () => { 804 | 805 | it('applies a service name to a service or factory.', async () => { 806 | 807 | // Object 808 | 809 | const obj = { name: 'Unused', some: 'prop' }; 810 | 811 | expect(Schmervice.withName('someServiceObject', obj)).to.shallow.equal(obj); 812 | expect(obj).to.equal({ 813 | [Schmervice.name]: 'someServiceObject', 814 | name: 'Unused', 815 | some: 'prop' 816 | }); 817 | 818 | // Class 819 | 820 | const Service = class Service {}; 821 | 822 | expect(Schmervice.withName('someServiceClass', Service)).to.shallow.equal(Service); 823 | expect(Service[Schmervice.name]).to.equal('someServiceClass'); 824 | 825 | // Sync factory 826 | 827 | const factory = (...args) => ({ name: 'Unused', args }); 828 | 829 | expect(Schmervice.withName('someServiceFunction', factory)(1, 2, 3)).to.equal({ 830 | [Schmervice.name]: 'someServiceFunction', 831 | name: 'Unused', 832 | args: [1, 2, 3] 833 | }); 834 | 835 | // Async factory (not supported by server.registerService(), but useful with haute-couture unwrapping) 836 | 837 | const asyncFactory = async (...args) => { 838 | 839 | await new Promise((resolve) => setTimeout(resolve, 1)); 840 | 841 | return { name: 'Unused', args }; 842 | }; 843 | 844 | expect(await Schmervice.withName('someServiceAsyncFunction', asyncFactory)(1, 2, 3)).to.equal({ 845 | [Schmervice.name]: 'someServiceAsyncFunction', 846 | name: 'Unused', 847 | args: [1, 2, 3] 848 | }); 849 | }); 850 | 851 | it('optionally applies sandboxing to a service or factory.', async () => { 852 | 853 | // Object 854 | 855 | const obj = { name: 'Unused', some: 'prop' }; 856 | 857 | expect(Schmervice.withName('someServiceObject', { sandbox: true }, obj)).to.shallow.equal(obj); 858 | expect(obj).to.equal({ 859 | [Schmervice.name]: 'someServiceObject', 860 | [Schmervice.sandbox]: true, 861 | name: 'Unused', 862 | some: 'prop' 863 | }); 864 | 865 | // Class 866 | 867 | const Service = class Service {}; 868 | 869 | expect(Schmervice.withName('someServiceClass', { sandbox: false }, Service)).to.shallow.equal(Service); 870 | expect(Service[Schmervice.name]).to.equal('someServiceClass'); 871 | expect(Service[Schmervice.sandbox]).to.equal(false); 872 | 873 | // Sync factory 874 | 875 | const factory = (...args) => ({ name: 'Unused', args }); 876 | 877 | expect(Schmervice.withName('someServiceFunction', { sandbox: 'plugin' }, factory)(1, 2, 3)).to.equal({ 878 | [Schmervice.name]: 'someServiceFunction', 879 | [Schmervice.sandbox]: 'plugin', 880 | name: 'Unused', 881 | args: [1, 2, 3] 882 | }); 883 | 884 | // Async factory (not supported by server.registerService(), but useful with haute-couture unwrapping) 885 | 886 | const asyncFactory = async (...args) => { 887 | 888 | await new Promise((resolve) => setTimeout(resolve, 1)); 889 | 890 | return { name: 'Unused', args }; 891 | }; 892 | 893 | expect(await Schmervice.withName('someServiceAsyncFunction', { sandbox: 'server' }, asyncFactory)(1, 2, 3)).to.equal({ 894 | [Schmervice.name]: 'someServiceAsyncFunction', 895 | [Schmervice.sandbox]: 'server', 896 | name: 'Unused', 897 | args: [1, 2, 3] 898 | }); 899 | }); 900 | 901 | it('does not apply a service name to object or class that already has one.', async () => { 902 | 903 | // Object 904 | 905 | const obj = { [Schmervice.name]: 'x' }; 906 | 907 | expect(() => Schmervice.withName('someServiceObject', obj)) 908 | .to.throw('Cannot apply a name to a service that already has one.'); 909 | 910 | // Class 911 | 912 | const Service = class Service { 913 | static get [Schmervice.name]() { 914 | 915 | return 'x'; 916 | } 917 | }; 918 | 919 | expect(() => Schmervice.withName('someServiceClass', Service)) 920 | .to.throw('Cannot apply a name to a service that already has one.'); 921 | 922 | // Sync factory 923 | 924 | const factory = () => ({ [Schmervice.name]: 'x' }); 925 | 926 | expect(Schmervice.withName('someServiceFunction', factory)) 927 | .to.throw('Cannot apply a name to a service that already has one.'); 928 | 929 | // Async factory (not supported by server.registerService(), but useful with haute-couture unwrapping) 930 | 931 | const asyncFactory = async () => { 932 | 933 | await new Promise((resolve) => setTimeout(resolve, 1)); 934 | 935 | return { [Schmervice.name]: 'x' }; 936 | }; 937 | 938 | await expect(Schmervice.withName('someServiceAsyncFunction', asyncFactory)()) 939 | .to.reject('Cannot apply a name to a service that already has one.'); 940 | }); 941 | 942 | it('does not apply sandboxing to object or class that already has it set.', async () => { 943 | 944 | // Object 945 | 946 | const obj = { [Schmervice.sandbox]: true }; 947 | 948 | expect(() => Schmervice.withName('someServiceObject', { sandbox: false }, obj)) 949 | .to.throw('Cannot apply a sandbox setting to a service that already has one.'); 950 | 951 | // Class 952 | 953 | const Service = class { 954 | static get [Schmervice.sandbox]() { 955 | 956 | return false; 957 | } 958 | }; 959 | 960 | expect(() => Schmervice.withName('someServiceClass', { sandbox: true }, Service)) 961 | .to.throw('Cannot apply a sandbox setting to a service that already has one.'); 962 | 963 | // Sync factory 964 | 965 | const factory = () => ({ [Schmervice.sandbox]: 'plugin' }); 966 | 967 | expect(Schmervice.withName('someServiceFunction', { sandbox: 'plugin' }, factory)) 968 | .to.throw('Cannot apply a sandbox setting to a service that already has one.'); 969 | 970 | // Async factory (not supported by server.registerService(), but useful with haute-couture unwrapping) 971 | 972 | const asyncFactory = async () => { 973 | 974 | await new Promise((resolve) => setTimeout(resolve, 1)); 975 | 976 | return { [Schmervice.sandbox]: 'server' }; 977 | }; 978 | 979 | await expect(Schmervice.withName('someServiceAsyncFunction', { sandbox: 'plugin' }, asyncFactory)()) 980 | .to.reject('Cannot apply a sandbox setting to a service that already has one.'); 981 | }); 982 | }); 983 | 984 | describe('request.services() decoration', () => { 985 | 986 | it('returns service instances associated with the relevant route\'s realm.', async () => { 987 | 988 | const server = Hapi.server(); 989 | await server.register(Schmervice); 990 | 991 | const ServiceX = class ServiceX {}; 992 | server.registerService(class ServiceY {}); 993 | 994 | let handlerServices; 995 | let extServices; 996 | 997 | const plugin = { 998 | name: 'plugin', 999 | register(srv, options) { 1000 | 1001 | srv.registerService(ServiceX); 1002 | 1003 | srv.route({ 1004 | method: 'get', 1005 | path: '/', 1006 | handler(request) { 1007 | 1008 | handlerServices = request.services(); 1009 | 1010 | return { ok: true }; 1011 | } 1012 | }); 1013 | 1014 | srv.ext('onPreAuth', (request, h) => { 1015 | 1016 | extServices = request.services(); 1017 | 1018 | return h.continue; 1019 | }); 1020 | } 1021 | }; 1022 | 1023 | await server.register(plugin); 1024 | 1025 | await server.inject('/'); 1026 | 1027 | expect(handlerServices).to.shallow.equal(extServices); 1028 | expect(handlerServices).to.only.contain(['serviceX']); 1029 | const { serviceX } = handlerServices; 1030 | expect(serviceX).to.be.an.instanceof(ServiceX); 1031 | }); 1032 | 1033 | it('returns empty object if there are no services associated with relevant route\'s realm.', async () => { 1034 | 1035 | const server = Hapi.server(); 1036 | await server.register(Schmervice); 1037 | 1038 | server.registerService(class ServiceX {}); 1039 | 1040 | let handlerServices; 1041 | let extServices; 1042 | 1043 | const plugin = { 1044 | name: 'plugin', 1045 | register(srv, options) { 1046 | 1047 | srv.route({ 1048 | method: 'get', 1049 | path: '/', 1050 | handler(request) { 1051 | 1052 | handlerServices = request.services(); 1053 | 1054 | return { ok: true }; 1055 | } 1056 | }); 1057 | 1058 | srv.ext('onPreAuth', (request, h) => { 1059 | 1060 | extServices = request.services(); 1061 | 1062 | return h.continue; 1063 | }); 1064 | } 1065 | }; 1066 | 1067 | await server.register(plugin); 1068 | 1069 | await server.inject('/'); 1070 | 1071 | expect(handlerServices).to.equal({}); 1072 | expect(extServices).to.equal({}); 1073 | expect(handlerServices).to.shallow.equal(extServices); 1074 | }); 1075 | 1076 | it('returns service instances associated with the root realm when passed true.', async () => { 1077 | 1078 | const server = Hapi.server(); 1079 | await server.register(Schmervice); 1080 | 1081 | const ServiceX = class ServiceX {}; 1082 | const ServiceY = class ServiceY {}; 1083 | server.registerService(ServiceY); 1084 | 1085 | let handlerServices; 1086 | let extServices; 1087 | 1088 | const plugin = { 1089 | name: 'plugin', 1090 | register(srv, options) { 1091 | 1092 | srv.registerService(ServiceX); 1093 | 1094 | srv.route({ 1095 | method: 'get', 1096 | path: '/', 1097 | handler(request) { 1098 | 1099 | handlerServices = request.services(true); 1100 | 1101 | return { ok: true }; 1102 | } 1103 | }); 1104 | 1105 | srv.ext('onPreAuth', (request, h) => { 1106 | 1107 | extServices = request.services(true); 1108 | 1109 | return h.continue; 1110 | }); 1111 | } 1112 | }; 1113 | 1114 | await server.register(plugin); 1115 | 1116 | await server.inject('/'); 1117 | 1118 | expect(handlerServices).to.shallow.equal(extServices); 1119 | expect(handlerServices).to.only.contain(['serviceX', 'serviceY']); 1120 | const { serviceX, serviceY } = handlerServices; 1121 | expect(serviceX).to.be.an.instanceof(ServiceX); 1122 | expect(serviceY).to.be.an.instanceof(ServiceY); 1123 | }); 1124 | }); 1125 | 1126 | describe('h.services() decoration', () => { 1127 | 1128 | it('returns service instances associated with toolkit\'s realm.', async () => { 1129 | 1130 | const server = Hapi.server(); 1131 | await server.register(Schmervice); 1132 | 1133 | const ServiceX = class ServiceX {}; 1134 | server.registerService(class ServiceY {}); 1135 | 1136 | let handlerServices; 1137 | let extServices; 1138 | 1139 | const plugin = { 1140 | name: 'plugin', 1141 | register(srv, options) { 1142 | 1143 | srv.registerService(ServiceX); 1144 | 1145 | srv.route({ 1146 | method: 'get', 1147 | path: '/', 1148 | handler(request, h) { 1149 | 1150 | handlerServices = h.services(); 1151 | 1152 | return { ok: true }; 1153 | } 1154 | }); 1155 | 1156 | srv.ext('onRequest', (request, h) => { 1157 | 1158 | extServices = h.services(); 1159 | 1160 | return h.continue; 1161 | }); 1162 | } 1163 | }; 1164 | 1165 | await server.register(plugin); 1166 | 1167 | await server.inject('/'); 1168 | 1169 | expect(handlerServices).to.shallow.equal(extServices); 1170 | expect(handlerServices).to.only.contain(['serviceX']); 1171 | const { serviceX } = handlerServices; 1172 | expect(serviceX).to.be.an.instanceof(ServiceX); 1173 | }); 1174 | 1175 | it('returns empty object if there are no services associated with toolkit\'s realm.', async () => { 1176 | 1177 | const server = Hapi.server(); 1178 | await server.register(Schmervice); 1179 | 1180 | server.registerService(class ServiceX {}); 1181 | 1182 | let handlerServices; 1183 | let extServices; 1184 | 1185 | const plugin = { 1186 | name: 'plugin', 1187 | register(srv, options) { 1188 | 1189 | srv.route({ 1190 | method: 'get', 1191 | path: '/', 1192 | handler(request, h) { 1193 | 1194 | handlerServices = h.services(); 1195 | 1196 | return { ok: true }; 1197 | } 1198 | }); 1199 | 1200 | srv.ext('onRequest', (request, h) => { 1201 | 1202 | extServices = h.services(); 1203 | 1204 | return h.continue; 1205 | }); 1206 | } 1207 | }; 1208 | 1209 | await server.register(plugin); 1210 | 1211 | await server.inject('/'); 1212 | 1213 | expect(handlerServices).to.equal({}); 1214 | expect(extServices).to.equal({}); 1215 | expect(handlerServices).to.shallow.equal(extServices); 1216 | }); 1217 | 1218 | it('returns service instances associated with the root realm when passed true.', async () => { 1219 | 1220 | const server = Hapi.server(); 1221 | await server.register(Schmervice); 1222 | 1223 | const ServiceX = class ServiceX {}; 1224 | const ServiceY = class ServiceY {}; 1225 | server.registerService(ServiceY); 1226 | 1227 | let handlerServices; 1228 | let extServices; 1229 | 1230 | const plugin = { 1231 | name: 'plugin', 1232 | register(srv, options) { 1233 | 1234 | srv.registerService(ServiceX); 1235 | 1236 | srv.route({ 1237 | method: 'get', 1238 | path: '/', 1239 | handler(request, h) { 1240 | 1241 | handlerServices = h.services(true); 1242 | 1243 | return { ok: true }; 1244 | } 1245 | }); 1246 | 1247 | srv.ext('onRequest', (request, h) => { 1248 | 1249 | extServices = h.services(true); 1250 | 1251 | return h.continue; 1252 | }); 1253 | } 1254 | }; 1255 | 1256 | await server.register(plugin); 1257 | 1258 | await server.inject('/'); 1259 | 1260 | expect(handlerServices).to.shallow.equal(extServices); 1261 | expect(handlerServices).to.only.contain(['serviceX', 'serviceY']); 1262 | const { serviceX, serviceY } = handlerServices; 1263 | expect(serviceX).to.be.an.instanceof(ServiceX); 1264 | expect(serviceY).to.be.an.instanceof(ServiceY); 1265 | }); 1266 | }); 1267 | 1268 | describe('server.services() decoration', () => { 1269 | 1270 | it('returns service instances associated with server\'s realm.', async () => { 1271 | 1272 | const server = Hapi.server(); 1273 | await server.register(Schmervice); 1274 | 1275 | const ServiceX = class ServiceX {}; 1276 | server.registerService(class ServiceY {}); 1277 | 1278 | let srvServices; 1279 | 1280 | const plugin = { 1281 | name: 'plugin', 1282 | register(srv, options) { 1283 | 1284 | srv.registerService(ServiceX); 1285 | 1286 | srvServices = srv.services(); 1287 | } 1288 | }; 1289 | 1290 | await server.register(plugin); 1291 | 1292 | expect(srvServices).to.only.contain(['serviceX']); 1293 | const { serviceX } = srvServices; 1294 | expect(serviceX).to.be.an.instanceof(ServiceX); 1295 | }); 1296 | 1297 | it('returns empty object if there are no services associated with toolkit\'s realm.', async () => { 1298 | 1299 | const server = Hapi.server(); 1300 | await server.register(Schmervice); 1301 | 1302 | server.registerService(class ServiceX {}); 1303 | 1304 | let srvServices; 1305 | 1306 | const plugin = { 1307 | name: 'plugin', 1308 | register(srv, options) { 1309 | 1310 | srvServices = srv.services(); 1311 | } 1312 | }; 1313 | 1314 | await server.register(plugin); 1315 | 1316 | expect(srvServices).to.equal({}); 1317 | }); 1318 | 1319 | it('returns service instances associated with the root realm when passed true.', async () => { 1320 | 1321 | const server = Hapi.server(); 1322 | await server.register(Schmervice); 1323 | 1324 | const ServiceX = class ServiceX {}; 1325 | const ServiceY = class ServiceY {}; 1326 | server.registerService(ServiceY); 1327 | 1328 | let srvServices; 1329 | 1330 | const plugin = { 1331 | name: 'plugin', 1332 | register(srv, options) { 1333 | 1334 | srv.registerService(ServiceX); 1335 | 1336 | srvServices = srv.services(true); 1337 | } 1338 | }; 1339 | 1340 | await server.register(plugin); 1341 | 1342 | expect(srvServices).to.only.contain(['serviceX', 'serviceY']); 1343 | const { serviceX, serviceY } = srvServices; 1344 | expect(serviceX).to.be.an.instanceof(ServiceX); 1345 | expect(serviceY).to.be.an.instanceof(ServiceY); 1346 | }); 1347 | 1348 | it('returns service instances associated with a plugin namespace when passed a string.', async () => { 1349 | 1350 | const server = Hapi.server(); 1351 | await server.register(Schmervice); 1352 | 1353 | const ServiceX = class ServiceX {}; 1354 | const ServiceY = class ServiceY {}; 1355 | const ServiceZ = class ServiceZ { 1356 | static get [Schmervice.sandbox]() { 1357 | 1358 | return true; 1359 | } 1360 | }; 1361 | const ServiceW = class ServiceW {}; 1362 | 1363 | const pluginA = await getPlugin(server, 'a'); 1364 | const pluginB = await getPlugin(pluginA, 'b'); 1365 | 1366 | server.registerService(ServiceX); 1367 | pluginA.registerService(ServiceY); 1368 | pluginA.registerService(ServiceZ); 1369 | pluginB.registerService(ServiceW); 1370 | 1371 | expect(server.services()).to.shallow.equal(pluginB.services(true)); 1372 | expect(server.services('a')).to.shallow.equal(pluginB.services('a')); 1373 | expect(pluginA.services('b')).to.shallow.equal(pluginB.services()); 1374 | 1375 | expect(server.services()).to.only.contain(['serviceX', 'serviceY', 'serviceW']); 1376 | expect(pluginA.services()).to.only.contain(['serviceY', 'serviceZ', 'serviceW']); 1377 | expect(pluginB.services()).to.only.contain(['serviceW']); 1378 | }); 1379 | 1380 | it('throws when accessing a namespace that doesn\'t exist.', async () => { 1381 | 1382 | const server = Hapi.server(); 1383 | await server.register(Schmervice); 1384 | 1385 | expect(() => server.services('nope')).to.throw('The plugin namespace nope does not exist.'); 1386 | }); 1387 | 1388 | it('throws when accessing a non-unique namespace.', async () => { 1389 | 1390 | const server = Hapi.server(); 1391 | await server.register(Schmervice); 1392 | 1393 | const pluginX1 = await getPlugin(server, 'x', { multiple: true }); 1394 | pluginX1.registerService({ name: 'serviceX' }); 1395 | 1396 | const pluginX2 = await getPlugin(server, 'x', { multiple: true }); 1397 | pluginX2.registerService({ name: 'serviceY' }); 1398 | 1399 | expect(() => server.services('x')).to.throw('The plugin namespace x is not unique: is that plugin registered multiple times?'); 1400 | }); 1401 | }); 1402 | 1403 | describe('service ownership', () => { 1404 | 1405 | it('applies to server\'s realm and its ancestors.', async () => { 1406 | 1407 | const makePlugin = (name, services, plugins) => ({ 1408 | name, 1409 | async register(srv, options) { 1410 | 1411 | await srv.register(plugins); 1412 | srv.registerService(services); 1413 | srv.expose('services', () => srv.services()); 1414 | } 1415 | }); 1416 | 1417 | const ServiceO = class ServiceO {}; 1418 | const ServiceA1 = class ServiceA1 {}; 1419 | const ServiceA1a = class ServiceA1a {}; 1420 | const ServiceA1b = class ServiceA1b {}; 1421 | const ServiceA2 = class ServiceA2 {}; 1422 | const ServiceX1a = class ServiceX1a {}; 1423 | 1424 | const server = Hapi.server(); 1425 | await server.register(Schmervice); 1426 | 1427 | const pluginX1a = makePlugin('pluginX1a', [], []); 1428 | const pluginX1 = makePlugin('pluginX1', [ServiceX1a], [pluginX1a]); 1429 | const pluginX = makePlugin('pluginX', [], [pluginX1]); 1430 | const pluginA1 = makePlugin('pluginA1', [ServiceA1a, ServiceA1b], []); 1431 | const pluginA = makePlugin('pluginA', [ServiceA1, ServiceA2], [pluginA1, pluginX]); 1432 | 1433 | server.registerService(ServiceO); 1434 | 1435 | await server.register(pluginA); 1436 | 1437 | const { 1438 | pluginX1a: X1a, 1439 | pluginX1: X1, 1440 | pluginX: X, 1441 | pluginA1: A1, 1442 | pluginA: A 1443 | } = server.plugins; 1444 | 1445 | expect(X1a.services()).to.equal({}); 1446 | expect(X1.services()).to.only.contain([ 1447 | 'serviceX1a' 1448 | ]); 1449 | expect(X.services()).to.only.contain([ 1450 | 'serviceX1a' 1451 | ]); 1452 | expect(A1.services()).to.only.contain([ 1453 | 'serviceA1a', 1454 | 'serviceA1b' 1455 | ]); 1456 | expect(A.services()).to.only.contain([ 1457 | 'serviceA1', 1458 | 'serviceA1a', 1459 | 'serviceA1b', 1460 | 'serviceA2', 1461 | 'serviceX1a' 1462 | ]); 1463 | expect(server.services()).to.only.contain([ 1464 | 'serviceO', 1465 | 'serviceA1', 1466 | 'serviceA1a', 1467 | 'serviceA1b', 1468 | 'serviceA2', 1469 | 'serviceX1a' 1470 | ]); 1471 | }); 1472 | }); 1473 | }); 1474 | }); 1475 | --------------------------------------------------------------------------------