Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core-api): decouple web service install & registration #771 #772

Merged
merged 1 commit into from
Apr 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Optional } from "typescript-optional";
import { Express } from "express";

import {
Logger,
Checks,
Expand All @@ -18,6 +20,7 @@ import {
import { DefaultApi as BesuApi } from "@hyperledger/cactus-plugin-ledger-connector-besu";
import { InsertBambooHarvestEndpoint } from "./web-services/insert-bamboo-harvest-endpoint";
import { DefaultApi as FabricApi } from "@hyperledger/cactus-plugin-ledger-connector-fabric";

import { ListBambooHarvestEndpoint } from "./web-services/list-bamboo-harvest-endpoint";
import { ISupplyChainContractDeploymentInfo } from "../i-supply-chain-contract-deployment-info";
import { InsertBookshelfEndpoint } from "./web-services/insert-bookshelf-endpoint";
Expand Down Expand Up @@ -51,6 +54,8 @@ export class SupplyChainCactusPlugin
private readonly log: Logger;
private readonly instanceId: string;

private endpoints: IWebServiceEndpoint[] | undefined;

public get className(): string {
return SupplyChainCactusPlugin.CLASS_NAME;
}
Expand All @@ -73,9 +78,16 @@ export class SupplyChainCactusPlugin
this.instanceId = options.instanceId;
}

public async installWebServices(
expressApp: any,
): Promise<IWebServiceEndpoint[]> {
async registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
const webServices = await this.getOrCreateWebServices();
webServices.forEach((ws) => ws.registerExpress(app));
return webServices;
}

public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
if (Array.isArray(this.endpoints)) {
return this.endpoints;
}
const insertBambooHarvest = new InsertBambooHarvestEndpoint({
contractAddress: this.options.contracts.bambooHarvestRepository.address,
contractAbi: this.options.contracts.bambooHarvestRepository.abi,
Expand All @@ -84,15 +96,13 @@ export class SupplyChainCactusPlugin
.web3SigningCredential as Web3SigningCredential,
logLevel: this.options.logLevel,
});
insertBambooHarvest.registerExpress(expressApp);

const listBambooHarvest = new ListBambooHarvestEndpoint({
contractAddress: this.options.contracts.bambooHarvestRepository.address,
contractAbi: this.options.contracts.bambooHarvestRepository.abi,
apiClient: this.options.quorumApiClient,
logLevel: this.options.logLevel,
});
listBambooHarvest.registerExpress(expressApp);

const insertBookshelf = new InsertBookshelfEndpoint({
contractAddress: this.options.contracts.bookshelfRepository.address,
Expand All @@ -102,36 +112,33 @@ export class SupplyChainCactusPlugin
.web3SigningCredential as Web3SigningCredential,
logLevel: this.options.logLevel,
});
insertBookshelf.registerExpress(expressApp);

const listBookshelf = new ListBookshelfEndpoint({
contractAddress: this.options.contracts.bookshelfRepository.address,
contractAbi: this.options.contracts.bookshelfRepository.abi,
besuApi: this.options.besuApiClient,
logLevel: this.options.logLevel,
});
listBookshelf.registerExpress(expressApp);

const insertShipment = new InsertShipmentEndpoint({
logLevel: this.options.logLevel,
fabricApi: this.options.fabricApiClient,
});
insertShipment.registerExpress(expressApp);

const listShipment = new ListShipmentEndpoint({
logLevel: this.options.logLevel,
fabricApi: this.options.fabricApiClient,
});

listShipment.registerExpress(expressApp);
return [
this.endpoints = [
insertBambooHarvest,
listBambooHarvest,
insertBookshelf,
listBookshelf,
insertShipment,
listShipment,
];
return this.endpoints;
}

public getHttpServer(): Optional<any> {
Expand Down
9 changes: 9 additions & 0 deletions packages/cactus-cmd-api-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/cactus-cmd-api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"@types/node-forge": "0.9.3",
"@types/npm": "2.0.31",
"@types/semver": "7.3.1",
"@types/uuid": "7.0.2"
"@types/uuid": "7.0.2",
"@types/xml2js": "0.4.8"
}
}
46 changes: 28 additions & 18 deletions packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,20 +372,12 @@ export class ApiServer {
return addressInfo;
}

async startApiServer(): Promise<AddressInfo> {
const app: Application = express();
app.use(compression());

const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv;
const allowedDomains = apiCorsDomainCsv.split(",");
const corsMiddleware = this.createCorsMiddleware(allowedDomains);
app.use(corsMiddleware);

app.use(bodyParser.json({ limit: "50mb" }));

const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);

/**
* Installs the own endpoints of the API server such as the ones providing
* healthcheck and monitoring information.
* @param app
*/
async getOrCreateWebServices(app: express.Application): Promise<void> {
const healthcheckHandler = (req: Request, res: Response) => {
res.json({
success: true,
Expand Down Expand Up @@ -420,16 +412,34 @@ export class ApiServer {
httpPathPrometheus,
prometheusExporterHandler,
);
}

const registry = await this.getOrInitPluginRegistry();
async startApiServer(): Promise<AddressInfo> {
const pluginRegistry = await this.getOrInitPluginRegistry();

const app: Application = express();
app.use(compression());

const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv;
const allowedDomains = apiCorsDomainCsv.split(",");
const corsMiddleware = this.createCorsMiddleware(allowedDomains);
app.use(corsMiddleware);

app.use(bodyParser.json({ limit: "50mb" }));

const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);

this.getOrCreateWebServices(app); // The API server's own endpoints

this.log.info(`Starting to install web services...`);

const webServicesInstalled = registry
const webServicesInstalled = pluginRegistry
.getPlugins()
.filter((pluginInstance) => isIPluginWebService(pluginInstance))
.map((pluginInstance: ICactusPlugin) => {
return (pluginInstance as IPluginWebService).installWebServices(app);
.map(async (plugin: ICactusPlugin) => {
await (plugin as IPluginWebService).getOrCreateWebServices();
return (plugin as IPluginWebService).registerWebServices(app);
});

const endpoints2D = await Promise.all(webServicesInstalled);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { IWebServiceEndpoint } from "./i-web-service-endpoint";
import { ICactusPlugin } from "../i-cactus-plugin";
import { Server } from "http";
import { Server as SecureServer } from "https";
import { Optional } from "typescript-optional";
import { Application } from "express";
import { IWebServiceEndpoint } from "./i-web-service-endpoint";
import { ICactusPlugin } from "../i-cactus-plugin";

export interface IPluginWebService extends ICactusPlugin {
installWebServices(expressApp: any): Promise<IWebServiceEndpoint[]>;
getOrCreateWebServices(): Promise<IWebServiceEndpoint[]>;
registerWebServices(expressApp: Application): Promise<IWebServiceEndpoint[]>;
getHttpServer(): Optional<Server | SecureServer>;
shutdown(): Promise<void>;
}
Expand All @@ -15,7 +17,9 @@ export function isIPluginWebService(
): pluginInstance is IPluginWebService {
return (
pluginInstance &&
typeof (pluginInstance as IPluginWebService).installWebServices ===
typeof (pluginInstance as IPluginWebService).registerWebServices ===
"function" &&
typeof (pluginInstance as IPluginWebService).getOrCreateWebServices ===
"function" &&
typeof (pluginInstance as IPluginWebService).getHttpServer === "function" &&
typeof (pluginInstance as IPluginWebService).getPackageName ===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class PluginConsortiumManual
public prometheusExporter: PrometheusExporter;
private readonly log: Logger;
private readonly instanceId: string;
private endpoints: IWebServiceEndpoint[] | undefined;
private httpServer: Server | SecureServer | null = null;

constructor(public readonly options: IPluginConsortiumManualOptions) {
Expand Down Expand Up @@ -126,16 +127,11 @@ export class PluginConsortiumManual
}
}

public async installWebServices(
expressApp: Express,
public async registerWebServices(
app: Express,
): Promise<IWebServiceEndpoint[]> {
const { log } = this;

log.info(`Installing web services for plugin ${this.getPackageName()}...`);
const webApp: Express = this.options.webAppOptions ? express() : expressApp;
const webApp: Express = this.options.webAppOptions ? express() : app;

// presence of webAppOptions implies that caller wants the plugin to configure it's own express instance on a custom
// host/port to listen on
if (this.options.webAppOptions) {
this.log.info(`Creating dedicated HTTP server...`);
const { port, hostname } = this.options.webAppOptions;
Expand All @@ -157,6 +153,22 @@ export class PluginConsortiumManual
this.log.info(`Creation of HTTP server OK`, { address });
}

const webServices = await this.getOrCreateWebServices();
webServices.forEach((ws) => ws.registerExpress(webApp));
return webServices;
}

public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
const { log } = this;
const pkgName = this.getPackageName();

if (this.endpoints) {
return this.endpoints;
}
log.info(`Creating web services for plugin ${pkgName}...`);
// presence of webAppOptions implies that caller wants the plugin to configure it's own express instance on a custom
// host/port to listen on

const { consortiumDatabase, keyPairPem } = this.options;
const consortiumRepo = new ConsortiumRepository({
db: consortiumDatabase,
Expand All @@ -166,32 +178,30 @@ export class PluginConsortiumManual
{
const options = { keyPairPem, consortiumRepo };
const endpoint = new GetConsortiumEndpointV1(options);
const path = endpoint.getPath();
webApp.get(path, endpoint.getExpressRequestHandler());
endpoints.push(endpoint);
this.log.info(`Registered GetConsortiumEndpointV1 at ${path}`);
const path = endpoint.getPath();
this.log.info(`Instantiated GetConsortiumEndpointV1 at ${path}`);
}
{
const options = { keyPairPem, consortiumRepo, plugin: this };
const endpoint = new GetNodeJwsEndpoint(options);
const path = endpoint.getPath();
webApp.get(path, endpoint.getExpressRequestHandler());
endpoints.push(endpoint);
this.log.info(`Registered GetNodeJwsEndpoint at ${path}`);
this.log.info(`Instantiated GetNodeJwsEndpoint at ${path}`);
}
{
const opts: IGetPrometheusExporterMetricsEndpointV1Options = {
plugin: this,
logLevel: this.options.logLevel,
};
const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts);
endpoint.registerExpress(expressApp);
const path = endpoint.getPath();
endpoints.push(endpoint);
this.log.info(`Instantiated GetNodeJwsEndpoint at ${path}`);
}
this.endpoints = endpoints;

log.info(`Installed web svcs for plugin ${this.getPackageName()} OK`, {
endpoints,
});
log.info(`Instantiated web svcs for plugin ${pkgName} OK`, { endpoints });
return endpoints;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ test("Can provide JWS", async (t: Test) => {
);
const apiClient = new ConsortiumManualApi({ basePath: apiHost });

await pluginConsortiumManual.installWebServices(expressApp);
await pluginConsortiumManual.getOrCreateWebServices();
await pluginConsortiumManual.registerWebServices(expressApp);

const epOpts: IGetNodeJwsEndpointOptions = {
plugin: pluginConsortiumManual,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class PluginKeychainMemory {
return res;
}

public async installWebServices(
public async getOrCreateWebServices(
expressApp: Express,
): Promise<IWebServiceEndpoint[]> {
const { log } = this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ test("PluginKeychainMemory", (t1: Test) => {
);
const apiClient = new KeychainMemoryApi({ basePath: apiHost });

await plugin.installWebServices(expressApp);
await plugin.getOrCreateWebServices(expressApp);

t.equal(plugin.getKeychainId(), options.keychainId, "Keychain ID set OK");
t.equal(plugin.getInstanceId(), options.instanceId, "Instance ID set OK");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Server } from "http";
import { Server as SecureServer } from "https";

import { Express } from "express";
import { Optional } from "typescript-optional";

import {
Expand Down Expand Up @@ -79,10 +80,15 @@ export class PluginKeychainVaultRemoteAdapter
*
* @param _expressApp
*/
public async installWebServices(): Promise<IWebServiceEndpoint[]> {
public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
return [];
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
public registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
return this.getOrCreateWebServices();
}

public getHttpServer(): Optional<Server | SecureServer> {
return Optional.empty();
}
Expand Down
Loading