diff --git a/packages/cactus-core/package.json b/packages/cactus-core/package.json index d7b112747a..628383037b 100644 --- a/packages/cactus-core/package.json +++ b/packages/cactus-core/package.json @@ -55,6 +55,7 @@ "express": "4.17.3", "express-jwt-authz": "2.4.1", "express-openapi-validator": "5.0.4", + "safe-stable-stringify": "2.4.3", "typescript-optional": "2.0.1" }, "devDependencies": { diff --git a/packages/cactus-core/src/main/typescript/public-api.ts b/packages/cactus-core/src/main/typescript/public-api.ts index b922fe3d24..3871206ef8 100755 --- a/packages/cactus-core/src/main/typescript/public-api.ts +++ b/packages/cactus-core/src/main/typescript/public-api.ts @@ -14,3 +14,7 @@ export { consensusHasTransactionFinality } from "./consensus-has-transaction-fin export { IInstallOpenapiValidationMiddlewareRequest } from "./web-services/install-open-api-validator-middleware"; export { installOpenapiValidationMiddleware } from "./web-services/install-open-api-validator-middleware"; +export { + GetOpenApiSpecV1EndpointBase, + IGetOpenApiSpecV1EndpointBaseOptions, +} from "./web-services/get-open-api-spec-v1-endpoint-base"; diff --git a/packages/cactus-core/src/main/typescript/web-services/get-open-api-spec-v1-endpoint-base.ts b/packages/cactus-core/src/main/typescript/web-services/get-open-api-spec-v1-endpoint-base.ts new file mode 100644 index 0000000000..40b645b0bd --- /dev/null +++ b/packages/cactus-core/src/main/typescript/web-services/get-open-api-spec-v1-endpoint-base.ts @@ -0,0 +1,196 @@ +import type { Express, Request, Response } from "express"; +import { RuntimeError } from "run-time-error"; +import { stringify } from "safe-stable-stringify"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, +} from "@hyperledger/cactus-core-api"; + +import { PluginRegistry } from "../plugin-registry"; + +import { registerWebServiceEndpoint } from "./register-web-service-endpoint"; + +export interface IGetOpenApiSpecV1EndpointBaseOptions { + logLevel?: LogLevelDesc; + pluginRegistry: PluginRegistry; + oasPath: P; + oas: S; + path: string; + verbLowerCase: string; + operationId: string; +} + +/** + * A generic base class that plugins can re-use to implement their own endpoints + * which are returning their own OpenAPI specification documents with much less + * boilerplate than otherwise would be needed. + * + * As an example, you can implement a sub-class like this: + * + * ```typescript + * import { + * GetOpenApiSpecV1EndpointBase, + * IGetOpenApiSpecV1EndpointBaseOptions, + * } from "@hyperledger/cactus-core"; + * + * import { Checks, LogLevelDesc } from "@hyperledger/cactus-common"; + * import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api"; + * + * import OAS from "../../json/openapi.json"; + * + * export const OasPathGetOpenApiSpecV1 = + * OAS.paths[ + * "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" + * ]; + * + * export type OasPathTypeGetOpenApiSpecV1 = typeof OasPathGetOpenApiSpecV1; + * + * export interface IGetOpenApiSpecV1EndpointOptions + * extends IGetOpenApiSpecV1EndpointBaseOptions< + * typeof OAS, + * OasPathTypeGetOpenApiSpecV1 + * > { + * readonly logLevel?: LogLevelDesc; + * } + * + * export class GetOpenApiSpecV1Endpoint + * extends GetOpenApiSpecV1EndpointBase + * implements IWebServiceEndpoint + * { + * public get className(): string { + * return GetOpenApiSpecV1Endpoint.CLASS_NAME; + * } + * + * constructor(public readonly options: IGetOpenApiSpecV1EndpointOptions) { + * super(options); + * const fnTag = `${this.className}#constructor()`; + * Checks.truthy(options, `${fnTag} arg options`); + * } + * } + * + * ``` + * + * The above code will also need you to update your openapi.json spec file by + * adding a new endpoint matching it (if you skip this step the compiler should + * complain about missing paths) + * + * ```json + * "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec": { + * "get": { + * "x-hyperledger-cactus": { + * "http": { + * "verbLowerCase": "get", + * "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" + * } + * }, + * "operationId": "getOpenApiSpecV1", + * "summary": "Retrieves the .json file that contains the OpenAPI specification for the plugin.", + * "parameters": [], + * "responses": { + * "200": { + * "description": "OK", + * "content": { + * "application/json": { + * "schema": { + * "type": "string" + * } + * } + * } + * } + * } + * } + * }, + * ``` + */ +export class GetOpenApiSpecV1EndpointBase implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GetOpenApiSpecV1EndpointBase"; + + protected readonly log: Logger; + + public get className(): string { + return GetOpenApiSpecV1EndpointBase.CLASS_NAME; + } + + constructor( + public readonly opts: IGetOpenApiSpecV1EndpointBaseOptions, + ) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(opts, `${fnTag} arg options`); + Checks.truthy(opts.pluginRegistry, `${fnTag} arg options.pluginRegistry`); + + const level = this.opts.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public get oasPath(): P { + return this.opts.oasPath; + } + + public getPath(): string { + return this.opts.path; + } + + public getVerbLowerCase(): string { + return this.opts.verbLowerCase; + } + + public getOperationId(): string { + return this.opts.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = `${this.className}#handleRequest()`; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + const reqMeta = `${verbUpper} ${this.getPath()}`; + this.log.debug(reqMeta); + + try { + const { oas } = this.opts; + res.status(200); + res.json(oas); + } catch (ex: unknown) { + const eMsg = `${fnTag} failed to serve request: ${reqMeta}`; + this.log.debug(eMsg, ex); + + const cause = ex instanceof Error ? ex : stringify(ex); + const error = new RuntimeError(eMsg, cause); + + res.status(500).json({ + message: "Internal Server Error", + error, + }); + } + } +} diff --git a/yarn.lock b/yarn.lock index 9a1a721061..5da1530083 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6411,6 +6411,7 @@ __metadata: express: 4.17.3 express-jwt-authz: 2.4.1 express-openapi-validator: 5.0.4 + safe-stable-stringify: 2.4.3 typescript-optional: 2.0.1 uuid: 8.3.2 languageName: unknown