@loopback/booter-lb3app
Version:
A booter component for LoopBack 3 applications to expose their REST API via LoopBack 4
166 lines (141 loc) • 5.24 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
// Node module: @loopback/booter-lb3app
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {BootBindings, Booter} from '@loopback/boot';
import {CoreBindings, inject} from '@loopback/core';
import {
ExpressRequestHandler,
OpenApiSpec,
rebaseOpenApiSpec,
RestApplication,
} from '@loopback/rest';
import debugFactory from 'debug';
import {once} from 'events';
import {Application as ExpressApplication} from 'express';
import path from 'path';
const {generateSwaggerSpec} = require('loopback-swagger');
const swagger2openapi = require('swagger2openapi');
const debug = debugFactory('loopback:boot:lb3app');
const DefaultOptions: Lb3AppBooterOptions = {
// from "/dist/application.ts" to "/lb3app"
path: '../lb3app/server/server',
mode: 'fullApp',
restApiRoot: '/api',
};
export class Lb3AppBooter implements Booter {
options: Lb3AppBooterOptions;
appPath: string;
constructor(
(CoreBindings.APPLICATION_INSTANCE)
public app: RestApplication,
(BootBindings.PROJECT_ROOT)
public projectRoot: string,
(`${BootBindings.BOOT_OPTIONS}#lb3app`)
options: Partial<Lb3AppBooterOptions> = {},
) {
this.options = Object.assign({}, DefaultOptions, options);
if (typeof app.mountExpressRouter !== 'function') {
throw new Error(
'Lb3AppBooter requires RestApplication with mountExpressRouter API',
);
}
}
async configure?(): Promise<void> {
this.appPath = path.join(this.projectRoot, this.options.path);
}
async load?(): Promise<void> {
const lb3App = await this.loadAndBootTheApp();
const spec = await this.buildOpenApiSpec(lb3App);
if (this.options.mode === 'fullApp') {
this.mountFullApp(lb3App, spec);
} else {
this.mountRoutesOnly(lb3App, spec);
}
const dataSources = lb3App.dataSources;
if (dataSources) {
const visitedDs = new Set();
for (const k in dataSources) {
const ds = dataSources[k];
if (visitedDs.has(ds)) continue;
this.app.bind(`lb3-datasources.${k}`).to(ds).tag('lb3-datasource');
visitedDs.add(ds);
}
}
const models = lb3App.models;
if (models) {
const visitedModels = new Set();
for (const k in models) {
const model = models[k];
if (visitedModels.has(model)) continue;
this.app.bind(`lb3-models.${k}`).to(model).tag('lb3-model');
visitedModels.add(model);
}
}
// TODO(bajtos) Listen for the following events to update the OpenAPI spec:
// - modelRemoted
// - modelDeleted
// - remoteMethodAdded
// - remoteMethodDisabled
// Note: LB4 does not support live spec updates yet. See
// https://github.com/loopbackio/loopback-next/issues/2394 for details.
}
private async loadAndBootTheApp() {
debug('Loading LB3 app from', this.appPath);
const lb3App = require(this.appPath);
debug(
'If your LB3 app does not boot correctly then make sure it is using loopback-boot version 3.2.1 or higher.',
);
if (lb3App.booting) {
debug(' waiting for boot process to finish');
// Wait until the legacy LB3 app boots
await once(lb3App, 'booted');
}
return lb3App as Lb3Application;
}
private async buildOpenApiSpec(lb3App: Lb3Application) {
const swaggerSpec = generateSwaggerSpec(lb3App, {
generateOperationScopedModels: true,
});
// remove any properties that have values that are functions before
// converting, as `convertObj` can't handle function values
const fixedSwaggerSpec = JSON.parse(JSON.stringify(swaggerSpec));
const result = await swagger2openapi.convertObj(fixedSwaggerSpec, {
// swagger2openapi options
});
let spec = result.openapi as OpenApiSpec;
if (typeof this.options.specTransformer === 'function') {
spec = this.options.specTransformer(spec);
}
return spec;
}
private mountFullApp(lb3App: Lb3Application, spec: OpenApiSpec) {
const restApiRoot = lb3App.get('restApiRoot') || '/';
debug('Mounting the entire LB3 app at %s', restApiRoot);
const specInRoot = rebaseOpenApiSpec(spec, restApiRoot);
this.app.mountExpressRouter('/', lb3App, specInRoot);
}
private mountRoutesOnly(lb3App: Lb3Application, spec: OpenApiSpec) {
const restApiRoot = this.options.restApiRoot;
debug('Mounting LB3 REST router at %s', restApiRoot);
this.app.mountExpressRouter(
restApiRoot,
// TODO(bajtos) reload the handler when a model/method was added/removed
// See https://github.com/loopbackio/loopback-next/issues/2394 for details.
lb3App.handler('rest'),
spec,
);
}
}
export interface Lb3AppBooterOptions {
path: string;
mode: 'fullApp' | 'restRouter';
restApiRoot: string;
specTransformer?: (spec: OpenApiSpec) => OpenApiSpec;
}
interface Lb3Application extends ExpressApplication {
handler(name: 'rest'): ExpressRequestHandler;
dataSources?: Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
models?: Record<string, any>;
}