diff --git a/packages/oc/src/registry/domain/http-server/express-adapter.ts b/packages/oc/src/registry/domain/http-server/express-adapter.ts index 27cb941c2..39fc3fbdc 100644 --- a/packages/oc/src/registry/domain/http-server/express-adapter.ts +++ b/packages/oc/src/registry/domain/http-server/express-adapter.ts @@ -41,9 +41,16 @@ function normaliseParams(raw: Record): Record { } export default function createExpressAdapter( - port?: number | string + options?: unknown ): HttpServerAdapter { - return new ExpressHttpServerAdapter(port); + const adapterOptions = options as + | { port?: number | string } + | number + | string + | undefined; + return new ExpressHttpServerAdapter( + typeof adapterOptions === 'object' ? adapterOptions.port : adapterOptions + ); } class ExpressHttpServerAdapter implements HttpServerAdapter { diff --git a/packages/oc/src/registry/domain/options-sanitiser.ts b/packages/oc/src/registry/domain/options-sanitiser.ts index 44a20f966..f32b1a8ec 100644 --- a/packages/oc/src/registry/domain/options-sanitiser.ts +++ b/packages/oc/src/registry/domain/options-sanitiser.ts @@ -3,6 +3,7 @@ import { compileSync } from 'oc-client-browser'; import settings from '../../resources/settings'; import type { Config } from '../../types'; import * as auth from './authentication'; +import createExpressAdapter from './http-server/express-adapter'; const DEFAULT_NODE_KEEPALIVE_MS = 5000; @@ -197,6 +198,21 @@ export default function optionsSanitiser(input: RegistryOptions): Config { options.keepAliveTimeout = options.keepAliveTimeout || DEFAULT_NODE_KEEPALIVE_MS; + if (!options.server) { + options.server = { + adapter: createExpressAdapter, + options: { port: options.port } + }; + } + + if (!options.server.adapter) { + options.server.adapter = createExpressAdapter; + } + + if (typeof options.server.options === 'undefined') { + options.server.options = { port: options.port }; + } + if (options.s3) { options.storage = { adapter: require('oc-s3-storage-adapter'), diff --git a/packages/oc/src/registry/domain/server-adapter.ts b/packages/oc/src/registry/domain/server-adapter.ts new file mode 100644 index 000000000..653e0b825 --- /dev/null +++ b/packages/oc/src/registry/domain/server-adapter.ts @@ -0,0 +1,29 @@ +import type { HttpServerAdapter } from './http-server/types'; + +type HttpServerAdapterFactory = (options?: T) => HttpServerAdapter; + +function isHttpServerAdapter(adapter: unknown): adapter is HttpServerAdapter { + return ( + !!adapter && + typeof adapter === 'object' && + typeof (adapter as HttpServerAdapter).native === 'function' && + typeof (adapter as HttpServerAdapter).listen === 'function' && + typeof (adapter as HttpServerAdapter).httpServer === 'function' + ); +} + +export default function getHttpServerAdapter( + adapter: HttpServerAdapter | HttpServerAdapterFactory, + options?: T +): HttpServerAdapter { + if (isHttpServerAdapter(adapter)) { + return adapter; + } + + const instance = adapter(options); + if (!isHttpServerAdapter(instance)) { + throw new Error('Invalid HTTP server adapter'); + } + + return instance; +} diff --git a/packages/oc/src/registry/index.ts b/packages/oc/src/registry/index.ts index 06466caab..2f04efe84 100644 --- a/packages/oc/src/registry/index.ts +++ b/packages/oc/src/registry/index.ts @@ -5,10 +5,10 @@ import type express from 'express'; import type { Plugin } from '../types'; import appStart from './app-start'; import eventsHandler from './domain/events-handler'; -import createExpressAdapter from './domain/http-server/express-adapter'; import sanitiseOptions, { RegistryOptions } from './domain/options-sanitiser'; import * as pluginsInitialiser from './domain/plugins-initialiser'; import Repository from './domain/repository'; +import getHttpServerAdapter from './domain/server-adapter'; import * as validator from './domain/validators'; import * as middleware from './middleware'; import { create as createRouter } from './router'; @@ -24,7 +24,10 @@ export default function registry(inputOptions: RegistryOptions) { const options = sanitiseOptions(inputOptions); const plugins: Plugin[] = []; - const adapter = middleware.bind(createExpressAdapter(options.port), options); + const adapter = middleware.bind( + getHttpServerAdapter(options.server.adapter, options.server.options), + options + ); const app = adapter.native() as express.Express; const repository = Repository(options); diff --git a/packages/oc/src/types.ts b/packages/oc/src/types.ts index 934a6103c..ab4a96ce6 100644 --- a/packages/oc/src/types.ts +++ b/packages/oc/src/types.ts @@ -2,6 +2,7 @@ import type { NextFunction, Request, Response } from 'express'; import type { MetadataStore as MetadataStoreType } from 'oc-metadata-adapters-utils'; import type { StorageAdapter } from 'oc-storage-adapters-utils'; import type { PackageJson } from 'type-fest'; +import type { HttpServerAdapter } from './registry/domain/http-server/types'; export type { ComponentRow, MetadataStore } from 'oc-metadata-adapters-utils'; @@ -426,6 +427,13 @@ export interface Config { adapter: (options: T) => StorageAdapter; options: T & { componentsDir: string }; }; + /** + * Low-level HTTP server adapter used by the registry. + */ + server: { + adapter: (options?: unknown) => HttpServerAdapter; + options?: unknown; + }; /** * Directory used by the registry for temporary files. */ diff --git a/packages/oc/test/unit/registry-domain-options-sanitiser.js b/packages/oc/test/unit/registry-domain-options-sanitiser.js index e6676ab07..fd692810d 100644 --- a/packages/oc/test/unit/registry-domain-options-sanitiser.js +++ b/packages/oc/test/unit/registry-domain-options-sanitiser.js @@ -20,6 +20,52 @@ describe('registry : domain : options-sanitiser', () => { expect(sanitise(options)[property]).to.eql(value); }); } + + it('should default to the Express HTTP server adapter', () => { + expect(sanitise(options).server.adapter).to.equal( + require('../../dist/registry/domain/http-server/express-adapter').default + ); + expect(sanitise(options).server.options).to.eql({ port: 3000 }); + }); + }); + + describe('server adapter configuration', () => { + describe('when no server adapter is provided', () => { + const options = { server: {}, port: 1234, baseUrl: 'dummy' }; + + it('should default to the Express HTTP server adapter', () => { + expect(sanitise(options).server.adapter).to.equal( + require('../../dist/registry/domain/http-server/express-adapter') + .default + ); + }); + }); + + describe('when no server options are provided', () => { + const serverAdapter = () => ({}); + const options = { + server: { adapter: serverAdapter }, + port: 1234, + baseUrl: 'dummy' + }; + + it('should pass the port to the server options', () => { + expect(sanitise(options).server.options).to.eql({ port: 1234 }); + }); + }); + + describe('when server options are provided', () => { + const serverAdapter = () => ({}); + const options = { + server: { adapter: serverAdapter, options: { custom: true } }, + port: 1234, + baseUrl: 'dummy' + }; + + it('should leave them untouched', () => { + expect(sanitise(options).server.options).to.eql({ custom: true }); + }); + }); }); describe('when verbosity is provided', () => { diff --git a/packages/oc/test/unit/registry-domain-server-adapter.js b/packages/oc/test/unit/registry-domain-server-adapter.js new file mode 100644 index 000000000..6f53e1943 --- /dev/null +++ b/packages/oc/test/unit/registry-domain-server-adapter.js @@ -0,0 +1,40 @@ +const expect = require('chai').expect; +const sinon = require('sinon'); + +describe('registry : domain : server-adapter', () => { + const getHttpServerAdapter = + require('../../dist/registry/domain/server-adapter').default; + + const createAdapter = () => ({ + native: sinon.stub(), + listen: sinon.stub(), + httpServer: sinon.stub() + }); + + describe('when given an adapter factory', () => { + it('should return the created adapter', () => { + const adapter = createAdapter(); + const factory = sinon.stub().returns(adapter); + const options = { custom: true }; + + expect(getHttpServerAdapter(factory, options)).to.equal(adapter); + expect(factory.calledWith(options)).to.be.true; + }); + }); + + describe('when given an adapter instance', () => { + it('should return the adapter untouched', () => { + const adapter = createAdapter(); + + expect(getHttpServerAdapter(adapter)).to.equal(adapter); + }); + }); + + describe('when the factory returns an invalid adapter', () => { + it('should throw', () => { + expect(() => getHttpServerAdapter(() => ({}))).to.throw( + 'Invalid HTTP server adapter' + ); + }); + }); +}); diff --git a/packages/oc/test/unit/registry.js b/packages/oc/test/unit/registry.js index 13396b1af..39e92bae6 100644 --- a/packages/oc/test/unit/registry.js +++ b/packages/oc/test/unit/registry.js @@ -15,10 +15,12 @@ describe('registry', () => { close: sinon.stub() }); + const serverAdapterFactory = sinon.stub(); + const deps = { './app-start': sinon.stub(), './domain/events-handler': { fire: sinon.stub() }, - './domain/http-server/express-adapter': sinon.stub().callsFake(() => { + './domain/server-adapter': sinon.stub().callsFake(() => { adapter = createAdapter(); return adapter; }), @@ -65,20 +67,29 @@ describe('registry', () => { deps['./domain/validators'].validateRegistryConfiguration.returns({ isValid: true }); - deps['./domain/options-sanitiser'].returns({ port: 3000 }); + deps['./domain/options-sanitiser'].returns({ + port: 3000, + server: { adapter: serverAdapterFactory, options: { port: 3000 } } + }); registry = Registry({}); }); it('should instantiate the HTTP server adapter', () => { - expect(deps['./domain/http-server/express-adapter'].calledWith(3000)).to - .be.true; + expect( + deps['./domain/server-adapter'].calledWith(serverAdapterFactory, { + port: 3000 + }) + ).to.be.true; }); it('should bind the middleware', () => { const bind = deps['./middleware'].bind; expect(bind.called).to.be.true; expect(bind.lastCall.args[0]).to.equal(adapter); - expect(bind.lastCall.args[1]).to.eql({ port: 3000 }); + expect(bind.lastCall.args[1]).to.eql({ + port: 3000, + server: { adapter: serverAdapterFactory, options: { port: 3000 } } + }); }); it('should instanciate the repository', () => { @@ -247,7 +258,10 @@ describe('registry', () => { deps['./domain/validators'].validateRegistryConfiguration.returns({ isValid: true }); - deps['./domain/options-sanitiser'].returns({ port: 3000 }); + deps['./domain/options-sanitiser'].returns({ + port: 3000, + server: { adapter: serverAdapterFactory, options: { port: 3000 } } + }); }); it('should close the repository when the server is not listening', (done) => {