A service layer for hapi
Note
Schmervice is intended for use with hapi v20+ and nodejs v16+ (see v2 for lower support).
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()
.
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:
-
A service class. Services are instanced immediately when they are registered, with
server
and corresponding pluginoptions
passed to the constructor. The service class should be named via its natural classname
or theSchmervice.name
symbol (see more under service naming).server.registerService( class MyServiceName { constructor(server, options) {} someMethod() {} } );
-
A factory function returning a service object. The factory function is called immediately to create the service, with
server
and corresponding pluginoptions
passed as arguments. The service object should be named using either aname
property or theSchmervice.name
symbol (see more under service naming).server.registerService((server, options) => ({ name: 'myServiceName', someMethod: () => {} }));
-
A service object. The service object should be named using either a
name
property or theSchmervice.name
symbol (see more under service naming).server.registerService({ name: 'myServiceName', someMethod: () => {} });
-
An array containing any of the above.
server.registerService([ class MyServiceName { constructor(server, options) {} someMethod() {} }, { name: 'myOtherServiceName', someOtherMethod: () => {} } ]);
Returns an object containing each service instance keyed by their instance names.
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. 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.
See server.services()
, where server
is the one in which the request
's route was declared (i.e. based upon request.route.realm
).
See server.services()
, where server
is the one in which the corresponding route or server extension was declared (i.e. based upon h.realm
).
The name of a service is primarily used to determine the key on the result of server.services()
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.
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
and Schmervice.withName()
can be useful.
Sandboxing is a concept that determines whether a given service is available in the "plugin namespace" accessed using server.services()
. 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
and Schmervice.withName()
's options come into play.
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.
server.registerService({
[Schmervice.name]: 'myServiceName',
someMethod: () => {}
});
// ...
const { myServiceName } = server.services();
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()
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.
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)
).
server.registerService({
[Schmervice.name]: 'privateService',
[Schmervice.sandbox]: true,
someMethod: () => {}
});
// ...
// Can access the service in the same plugin that registered it
const { privateService } = server.services();
// But cannot access it in other namespaces, e.g. the root namespace, because it is sandboxed
const { privateService: doesNotExist } = server.services(true);
This is a helper that assigns name
to the service instance or object produced by serviceFactory
by setting the service's Schmervice.name
. 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.
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
. If the resulting service class or object already has a Schmervice.sandbox
then this helper will fail.
server.registerService(
Schmervice.withName('myServiceName', () => ({
someMethod: () => {}
}))
);
// ...
const { myServiceName } = server.services();
This is also the preferred way to name a service object from some other library, since it prevents property conflicts.
const Nodemailer = require('nodemailer');
const transport = Nodemailer.createTransport();
server.registerService(Schmervice.withName('emailService', transport));
// ...
const { emailService } = server.services();
An example usage of options.sandbox
:
server.registerService(
Schmervice.withName('privateService', { sandbox: true }, {
someMethod: () => {}
})
);
// ...
// Can access the service in the same plugin that registered it
const { privateService } = server.services();
// But cannot access it in other namespaces, e.g. the root namespace, because it is sandboxed
const { privateService: doesNotExist } = server.services(true);
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 if desired.
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 register(server, options)
. Note: creating a service instance may have side-effects on the server
, e.g. adding server extensions– keep reading for details.
The server
passed to the constructor. Should be a hapi plugin's server or root server.
The hapi plugin options
passed to the constructor.
The context of service.server
set using 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.
Returns a new service instance where all methods are bound to the service instance allowing you to deconstruct methods without losing the this
context.
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 added when the service is instanced).
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 added when the service is instanced).
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,
- An object
{ cache, generateKey }
as detailed in the server method options documentation. - An object containing the
cache
options as detailed in the server method options documentation.
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 {}
.
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()
when the service is instanced).