Skip to content

Commit b50e148

Browse files
petermetzkikoncuo
authored andcommitted
feat(core-api): decouple web service install & registration #771
Primary change: ============== Divide the endpoint installation method into two smaller ones where the first one only instantiates the endpoints and the other one "registers" them as-in it hooks the endpoint up onto the ExpressJS web app object so that it will actually be responded to as HTTP requests come in. Why though? We'll need this for the authorization MVP which will need to be able to introspect the endpoints **prior** to them getting registered so that it can determine authz specific parameters like "is this endpoint secured or non-secure?" or what scopes are necessary for this endpoint? Etc. Additional changes: ================== Everything else that was needed to make the project compile again and the tests passnig (which is a lot because this is a very central, heavily used interface that we just modified). Fixes #771 Signed-off-by: Peter Somogyvari <[email protected]>
1 parent 77ac399 commit b50e148

File tree

21 files changed

+179
-95
lines changed

21 files changed

+179
-95
lines changed

examples/cactus-example-supply-chain-business-logic-plugin/src/main/typescript/business-logic-plugin/supply-chain-cactus-plugin.ts

+17-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Optional } from "typescript-optional";
2+
import { Express } from "express";
3+
24
import {
35
Logger,
46
Checks,
@@ -18,6 +20,7 @@ import {
1820
import { DefaultApi as BesuApi } from "@hyperledger/cactus-plugin-ledger-connector-besu";
1921
import { InsertBambooHarvestEndpoint } from "./web-services/insert-bamboo-harvest-endpoint";
2022
import { DefaultApi as FabricApi } from "@hyperledger/cactus-plugin-ledger-connector-fabric";
23+
2124
import { ListBambooHarvestEndpoint } from "./web-services/list-bamboo-harvest-endpoint";
2225
import { ISupplyChainContractDeploymentInfo } from "../i-supply-chain-contract-deployment-info";
2326
import { InsertBookshelfEndpoint } from "./web-services/insert-bookshelf-endpoint";
@@ -51,6 +54,8 @@ export class SupplyChainCactusPlugin
5154
private readonly log: Logger;
5255
private readonly instanceId: string;
5356

57+
private endpoints: IWebServiceEndpoint[] | undefined;
58+
5459
public get className(): string {
5560
return SupplyChainCactusPlugin.CLASS_NAME;
5661
}
@@ -73,9 +78,16 @@ export class SupplyChainCactusPlugin
7378
this.instanceId = options.instanceId;
7479
}
7580

76-
public async installWebServices(
77-
expressApp: any,
78-
): Promise<IWebServiceEndpoint[]> {
81+
async registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
82+
const webServices = await this.getOrCreateWebServices();
83+
webServices.forEach((ws) => ws.registerExpress(app));
84+
return webServices;
85+
}
86+
87+
public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
88+
if (Array.isArray(this.endpoints)) {
89+
return this.endpoints;
90+
}
7991
const insertBambooHarvest = new InsertBambooHarvestEndpoint({
8092
contractAddress: this.options.contracts.bambooHarvestRepository.address,
8193
contractAbi: this.options.contracts.bambooHarvestRepository.abi,
@@ -84,15 +96,13 @@ export class SupplyChainCactusPlugin
8496
.web3SigningCredential as Web3SigningCredential,
8597
logLevel: this.options.logLevel,
8698
});
87-
insertBambooHarvest.registerExpress(expressApp);
8899

89100
const listBambooHarvest = new ListBambooHarvestEndpoint({
90101
contractAddress: this.options.contracts.bambooHarvestRepository.address,
91102
contractAbi: this.options.contracts.bambooHarvestRepository.abi,
92103
apiClient: this.options.quorumApiClient,
93104
logLevel: this.options.logLevel,
94105
});
95-
listBambooHarvest.registerExpress(expressApp);
96106

97107
const insertBookshelf = new InsertBookshelfEndpoint({
98108
contractAddress: this.options.contracts.bookshelfRepository.address,
@@ -102,36 +112,33 @@ export class SupplyChainCactusPlugin
102112
.web3SigningCredential as Web3SigningCredential,
103113
logLevel: this.options.logLevel,
104114
});
105-
insertBookshelf.registerExpress(expressApp);
106115

107116
const listBookshelf = new ListBookshelfEndpoint({
108117
contractAddress: this.options.contracts.bookshelfRepository.address,
109118
contractAbi: this.options.contracts.bookshelfRepository.abi,
110119
besuApi: this.options.besuApiClient,
111120
logLevel: this.options.logLevel,
112121
});
113-
listBookshelf.registerExpress(expressApp);
114122

115123
const insertShipment = new InsertShipmentEndpoint({
116124
logLevel: this.options.logLevel,
117125
fabricApi: this.options.fabricApiClient,
118126
});
119-
insertShipment.registerExpress(expressApp);
120127

121128
const listShipment = new ListShipmentEndpoint({
122129
logLevel: this.options.logLevel,
123130
fabricApi: this.options.fabricApiClient,
124131
});
125132

126-
listShipment.registerExpress(expressApp);
127-
return [
133+
this.endpoints = [
128134
insertBambooHarvest,
129135
listBambooHarvest,
130136
insertBookshelf,
131137
listBookshelf,
132138
insertShipment,
133139
listShipment,
134140
];
141+
return this.endpoints;
135142
}
136143

137144
public getHttpServer(): Optional<any> {

packages/cactus-cmd-api-server/package-lock.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cactus-cmd-api-server/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"@types/node-forge": "0.9.3",
119119
"@types/npm": "2.0.31",
120120
"@types/semver": "7.3.1",
121-
"@types/uuid": "7.0.2"
121+
"@types/uuid": "7.0.2",
122+
"@types/xml2js": "0.4.8"
122123
}
123124
}

packages/cactus-cmd-api-server/src/main/typescript/api-server.ts

+28-18
Original file line numberDiff line numberDiff line change
@@ -372,20 +372,12 @@ export class ApiServer {
372372
return addressInfo;
373373
}
374374

375-
async startApiServer(): Promise<AddressInfo> {
376-
const app: Application = express();
377-
app.use(compression());
378-
379-
const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv;
380-
const allowedDomains = apiCorsDomainCsv.split(",");
381-
const corsMiddleware = this.createCorsMiddleware(allowedDomains);
382-
app.use(corsMiddleware);
383-
384-
app.use(bodyParser.json({ limit: "50mb" }));
385-
386-
const openApiValidator = this.createOpenApiValidator();
387-
await openApiValidator.install(app);
388-
375+
/**
376+
* Installs the own endpoints of the API server such as the ones providing
377+
* healthcheck and monitoring information.
378+
* @param app
379+
*/
380+
async getOrCreateWebServices(app: express.Application): Promise<void> {
389381
const healthcheckHandler = (req: Request, res: Response) => {
390382
res.json({
391383
success: true,
@@ -420,16 +412,34 @@ export class ApiServer {
420412
httpPathPrometheus,
421413
prometheusExporterHandler,
422414
);
415+
}
423416

424-
const registry = await this.getOrInitPluginRegistry();
417+
async startApiServer(): Promise<AddressInfo> {
418+
const pluginRegistry = await this.getOrInitPluginRegistry();
419+
420+
const app: Application = express();
421+
app.use(compression());
422+
423+
const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv;
424+
const allowedDomains = apiCorsDomainCsv.split(",");
425+
const corsMiddleware = this.createCorsMiddleware(allowedDomains);
426+
app.use(corsMiddleware);
427+
428+
app.use(bodyParser.json({ limit: "50mb" }));
429+
430+
const openApiValidator = this.createOpenApiValidator();
431+
await openApiValidator.install(app);
432+
433+
this.getOrCreateWebServices(app); // The API server's own endpoints
425434

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

428-
const webServicesInstalled = registry
437+
const webServicesInstalled = pluginRegistry
429438
.getPlugins()
430439
.filter((pluginInstance) => isIPluginWebService(pluginInstance))
431-
.map((pluginInstance: ICactusPlugin) => {
432-
return (pluginInstance as IPluginWebService).installWebServices(app);
440+
.map(async (plugin: ICactusPlugin) => {
441+
await (plugin as IPluginWebService).getOrCreateWebServices();
442+
return (plugin as IPluginWebService).registerWebServices(app);
433443
});
434444

435445
const endpoints2D = await Promise.all(webServicesInstalled);

packages/cactus-core-api/src/main/typescript/plugin/web-service/i-plugin-web-service.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { IWebServiceEndpoint } from "./i-web-service-endpoint";
2-
import { ICactusPlugin } from "../i-cactus-plugin";
31
import { Server } from "http";
42
import { Server as SecureServer } from "https";
53
import { Optional } from "typescript-optional";
4+
import { Application } from "express";
5+
import { IWebServiceEndpoint } from "./i-web-service-endpoint";
6+
import { ICactusPlugin } from "../i-cactus-plugin";
67

78
export interface IPluginWebService extends ICactusPlugin {
8-
installWebServices(expressApp: any): Promise<IWebServiceEndpoint[]>;
9+
getOrCreateWebServices(): Promise<IWebServiceEndpoint[]>;
10+
registerWebServices(expressApp: Application): Promise<IWebServiceEndpoint[]>;
911
getHttpServer(): Optional<Server | SecureServer>;
1012
shutdown(): Promise<void>;
1113
}
@@ -15,7 +17,9 @@ export function isIPluginWebService(
1517
): pluginInstance is IPluginWebService {
1618
return (
1719
pluginInstance &&
18-
typeof (pluginInstance as IPluginWebService).installWebServices ===
20+
typeof (pluginInstance as IPluginWebService).registerWebServices ===
21+
"function" &&
22+
typeof (pluginInstance as IPluginWebService).getOrCreateWebServices ===
1923
"function" &&
2024
typeof (pluginInstance as IPluginWebService).getHttpServer === "function" &&
2125
typeof (pluginInstance as IPluginWebService).getPackageName ===

packages/cactus-plugin-consortium-manual/src/main/typescript/plugin-consortium-manual.ts

+27-17
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class PluginConsortiumManual
5050
public prometheusExporter: PrometheusExporter;
5151
private readonly log: Logger;
5252
private readonly instanceId: string;
53+
private endpoints: IWebServiceEndpoint[] | undefined;
5354
private httpServer: Server | SecureServer | null = null;
5455

5556
constructor(public readonly options: IPluginConsortiumManualOptions) {
@@ -126,16 +127,11 @@ export class PluginConsortiumManual
126127
}
127128
}
128129

129-
public async installWebServices(
130-
expressApp: Express,
130+
public async registerWebServices(
131+
app: Express,
131132
): Promise<IWebServiceEndpoint[]> {
132-
const { log } = this;
133-
134-
log.info(`Installing web services for plugin ${this.getPackageName()}...`);
135-
const webApp: Express = this.options.webAppOptions ? express() : expressApp;
133+
const webApp: Express = this.options.webAppOptions ? express() : app;
136134

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

156+
const webServices = await this.getOrCreateWebServices();
157+
webServices.forEach((ws) => ws.registerExpress(webApp));
158+
return webServices;
159+
}
160+
161+
public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
162+
const { log } = this;
163+
const pkgName = this.getPackageName();
164+
165+
if (this.endpoints) {
166+
return this.endpoints;
167+
}
168+
log.info(`Creating web services for plugin ${pkgName}...`);
169+
// presence of webAppOptions implies that caller wants the plugin to configure it's own express instance on a custom
170+
// host/port to listen on
171+
160172
const { consortiumDatabase, keyPairPem } = this.options;
161173
const consortiumRepo = new ConsortiumRepository({
162174
db: consortiumDatabase,
@@ -166,32 +178,30 @@ export class PluginConsortiumManual
166178
{
167179
const options = { keyPairPem, consortiumRepo };
168180
const endpoint = new GetConsortiumEndpointV1(options);
169-
const path = endpoint.getPath();
170-
webApp.get(path, endpoint.getExpressRequestHandler());
171181
endpoints.push(endpoint);
172-
this.log.info(`Registered GetConsortiumEndpointV1 at ${path}`);
182+
const path = endpoint.getPath();
183+
this.log.info(`Instantiated GetConsortiumEndpointV1 at ${path}`);
173184
}
174185
{
175186
const options = { keyPairPem, consortiumRepo, plugin: this };
176187
const endpoint = new GetNodeJwsEndpoint(options);
177188
const path = endpoint.getPath();
178-
webApp.get(path, endpoint.getExpressRequestHandler());
179189
endpoints.push(endpoint);
180-
this.log.info(`Registered GetNodeJwsEndpoint at ${path}`);
190+
this.log.info(`Instantiated GetNodeJwsEndpoint at ${path}`);
181191
}
182192
{
183193
const opts: IGetPrometheusExporterMetricsEndpointV1Options = {
184194
plugin: this,
185195
logLevel: this.options.logLevel,
186196
};
187197
const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts);
188-
endpoint.registerExpress(expressApp);
198+
const path = endpoint.getPath();
189199
endpoints.push(endpoint);
200+
this.log.info(`Instantiated GetNodeJwsEndpoint at ${path}`);
190201
}
202+
this.endpoints = endpoints;
191203

192-
log.info(`Installed web svcs for plugin ${this.getPackageName()} OK`, {
193-
endpoints,
194-
});
204+
log.info(`Instantiated web svcs for plugin ${pkgName} OK`, { endpoints });
195205
return endpoints;
196206
}
197207

packages/cactus-plugin-consortium-manual/src/test/typescript/unit/consortium/get-node-jws-endpoint-v1.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ test("Can provide JWS", async (t: Test) => {
7272
);
7373
const apiClient = new ConsortiumManualApi({ basePath: apiHost });
7474

75-
await pluginConsortiumManual.installWebServices(expressApp);
75+
await pluginConsortiumManual.getOrCreateWebServices();
76+
await pluginConsortiumManual.registerWebServices(expressApp);
7677

7778
const epOpts: IGetNodeJwsEndpointOptions = {
7879
plugin: pluginConsortiumManual,

packages/cactus-plugin-keychain-memory/src/main/typescript/plugin-keychain-memory.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class PluginKeychainMemory {
7777
return res;
7878
}
7979

80-
public async installWebServices(
80+
public async getOrCreateWebServices(
8181
expressApp: Express,
8282
): Promise<IWebServiceEndpoint[]> {
8383
const { log } = this;

packages/cactus-plugin-keychain-memory/src/test/typescript/unit/plugin-keychain-memory.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ test("PluginKeychainMemory", (t1: Test) => {
8282
);
8383
const apiClient = new KeychainMemoryApi({ basePath: apiHost });
8484

85-
await plugin.installWebServices(expressApp);
85+
await plugin.getOrCreateWebServices(expressApp);
8686

8787
t.equal(plugin.getKeychainId(), options.keychainId, "Keychain ID set OK");
8888
t.equal(plugin.getInstanceId(), options.instanceId, "Instance ID set OK");

packages/cactus-plugin-keychain-vault/src/main/typescript/plugin-keychain-vault-remote-adapter.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Server } from "http";
22
import { Server as SecureServer } from "https";
33

4+
import { Express } from "express";
45
import { Optional } from "typescript-optional";
56

67
import {
@@ -79,10 +80,15 @@ export class PluginKeychainVaultRemoteAdapter
7980
*
8081
* @param _expressApp
8182
*/
82-
public async installWebServices(): Promise<IWebServiceEndpoint[]> {
83+
public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
8384
return [];
8485
}
8586

87+
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
88+
public registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
89+
return this.getOrCreateWebServices();
90+
}
91+
8692
public getHttpServer(): Optional<Server | SecureServer> {
8793
return Optional.empty();
8894
}

0 commit comments

Comments
 (0)