Skip to content

Commit

Permalink
Merge pull request #10 from hapipal/namespaces-and-sandbox
Browse files Browse the repository at this point in the history
Namespaces and sandboxing
  • Loading branch information
devinivy authored Mar 2, 2020
2 parents 6a409f1 + e72aea9 commit 8be281b
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 41 deletions.
70 changes: 57 additions & 13 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Schmervice may be registered multiple times– it should be registered in any pl

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 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)).
- 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)).

```js
server.registerService(
Expand All @@ -19,7 +19,7 @@ Registers a service with `server` (which may be a plugin's server or root server
);
```

- 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)).
- 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)).

```js
server.registerService((server, options) => ({
Expand All @@ -28,7 +28,7 @@ Registers a service with `server` (which may be a plugin's server or root server
}));
```

- 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)).
- 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)).

```js
server.registerService({
Expand All @@ -52,22 +52,26 @@ Registers a service with `server` (which may be a plugin's server or root server
]);
```

#### `server.services([all])`
Returns an object containing each service instance keyed by their [instance names](#service-naming). 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). When `all` is passed as `true` then every service registered with the hapi server– across all plugins– will be returned.
#### `server.services([namespace])`
Returns an object containing each service instance keyed by their [instance names](#service-naming-and-sandboxing).

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.
### Request decorations
#### `request.services([all])`
See [`server.services()`](#serverservicesall), where `server` is the one in which the `request`'s route was declared (i.e. based upon `request.route.realm`).
#### `request.services([namespace])`
See [`server.services()`](#serverservicesnamespace), where `server` is the one in which the `request`'s route was declared (i.e. based upon `request.route.realm`).

### Response toolkit decorations
#### `h.services([all])`
See [`server.services()`](#serverservicesall), where `server` is the one in which the corresponding route or server extension was declared (i.e. based upon `h.realm`).
#### `h.services([namespace])`
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`).

## Service naming and sandboxing

## Service naming
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.

The name of a service is primarily used to determine the key on the result of [`server.services()`](#serverservicesall) 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`](#schmervicename) and [`Schmervice.withName()`](#schmervicewithnamename-options-servicefactory) can be useful.

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-servicefactory) can be useful.
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.

### `Schmervice.name`

Expand All @@ -83,10 +87,33 @@ server.registerService({
const { myServiceName } = server.services();
```

### `Schmervice.withName(name, serviceFactory)`
### `Schmervice.sandbox`

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.

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)`).

```js
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);
```

### `Schmervice.withName(name, [options], serviceFactory)`

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.
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.

```js
server.registerService(
Schmervice.withName('myServiceName', () => ({
Expand All @@ -111,6 +138,23 @@ server.registerService(Schmervice.withName('emailService', transport));
const { emailService } = server.services();
```

An example usage of `options.sandbox`:

```js
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);
```

## `Schmervice.Service`
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.

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2017-2019 Devin Ivy
Copyright (c) 2017-2020 Devin Ivy

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
82 changes: 68 additions & 14 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,44 +109,66 @@ exports.plugin = {

exports.name = Symbol('serviceName');

exports.withName = (name, factory) => {
exports.sandbox = Symbol('serviceSandbox');

exports.withName = (name, options, factory) => {

if (typeof factory === 'undefined') {
factory = options;
options = {};
}

if (typeof factory === 'function' && !internals.isClass(factory)) {
return (...args) => {

const service = factory(...args);

if (typeof service.then === 'function') {
return service.then((x) => internals.withNameObject(name, x));
return service.then((x) => internals.withNameObject(name, options, x));
}

return internals.withNameObject(name, service);
return internals.withNameObject(name, options, service);
};
}

return internals.withNameObject(name, factory);
return internals.withNameObject(name, options, factory);
};

internals.withNameObject = (name, obj) => {
internals.withNameObject = (name, { sandbox }, obj) => {

Hoek.assert(!obj[exports.name], 'Cannot apply a name to a service that already has one.');

obj[exports.name] = name;

if (typeof sandbox !== 'undefined') {
Hoek.assert(typeof obj[exports.sandbox] === 'undefined', 'Cannot apply a sandbox setting to a service that already has one.');
obj[exports.sandbox] = sandbox;
}

return obj;
};

internals.boundInstance = Symbol('boundInstance');

internals.services = (getRealm) => {

return function (all) {
return function (namespace) {

const realm = getRealm(this);

return all ?
internals.rootState(realm) :
internals.state(realm);
if (!namespace) {
return internals.state(realm).services;
}

if (typeof namespace === 'string') {
const namespaceSet = internals.rootState(realm).namespaces[namespace];
Hoek.assert(namespaceSet, `The plugin namespace ${namespace} does not exist.`);
Hoek.assert(namespaceSet.size === 1, `The plugin namespace ${namespace} is not unique: is that plugin registered multiple times?`);
const [namespaceRealm] = [...namespaceSet];
return internals.state(namespaceRealm).services;
}

return internals.rootState(realm).services;
};
};

Expand All @@ -156,19 +178,32 @@ internals.registerService = function (services) {

services.forEach((factory) => {

const { name, instanceName, service } = internals.serviceFactory(factory, this, this.realm.pluginOptions);
const { name, instanceName, service, sandbox } = internals.serviceFactory(factory, this, this.realm.pluginOptions);
const rootState = internals.rootState(this.realm);

Hoek.assert(!rootState[instanceName], `A service named ${name} has already been registered.`);
Hoek.assert(sandbox || !rootState.services[instanceName], `A service named ${name} has already been registered.`);

rootState.namespaces[this.realm.plugin] = rootState.namespaces[this.realm.plugin] || new Set();
rootState.namespaces[this.realm.plugin].add(this.realm);

if (sandbox) {
return internals.addServiceToRealm(this.realm, service, instanceName);
}

internals.forEachAncestorRealm(this.realm, (realm) => {

const state = internals.state(realm);
state[instanceName] = service;
internals.addServiceToRealm(realm, service, instanceName);
});
});
};

internals.addServiceToRealm = (realm, service, name) => {

const state = internals.state(realm);
Hoek.assert(!state.services[name], `A service named ${name} has already been registered in plugin namespace ${realm.plugin}.`);
state.services[name] = service;
};

internals.forEachAncestorRealm = (realm, fn) => {

do {
Expand All @@ -189,7 +224,11 @@ internals.rootState = (realm) => {

internals.state = (realm) => {

const state = realm.plugins.schmervice = realm.plugins.schmervice || {};
const state = realm.plugins.schmervice = realm.plugins.schmervice || {
services: {},
namespaces: {}
};

return state;
};

Expand All @@ -205,6 +244,7 @@ internals.serviceFactory = (factory, server, options) => {
return {
name,
instanceName: factory[exports.name] ? name : internals.instanceName(name),
sandbox: internals.sandbox(factory[exports.sandbox]),
service: new factory(server, options)
};
}
Expand All @@ -218,6 +258,7 @@ internals.serviceFactory = (factory, server, options) => {
return {
name,
instanceName: service[exports.name] ? name : internals.instanceName(name),
sandbox: internals.sandbox(service[exports.sandbox]),
service
};
};
Expand All @@ -229,4 +270,17 @@ internals.instanceName = (name) => {
.replace(/^./, (m) => m.toLowerCase());
};

internals.sandbox = (value) => {

if (value === 'plugin') {
return true;
}

if (value === 'server') {
return false;
}

return value;
};

internals.isClass = (func) => (/^\s*class\s/).test(func.toString());
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@hapi/hapi-19": "npm:@hapi/hapi@19",
"@hapi/lab": "20.x.x",
"@hapi/somever": "2.x.x",
"ahem": "1.x.x",
"coveralls": "3.x.x"
}
}
Loading

0 comments on commit 8be281b

Please sign in to comment.