Skip to content

Commit

Permalink
feat(cactus-core): add GetOpenApiSpecV1EndpointBase<S, P> class
Browse files Browse the repository at this point in the history
This is the pre-requisite to finishing the task at
#1877

Having this generic endpoint class available will allow us to send in
another commit which then uses it to create the Open API spec endpoints
in all the plugin classes.

Example usage of the generic class looks 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<typeof OAS, OasPathTypeGetOpenApiSpecV1>
  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`);
  }
}
```

And the associated OpenAPI specification's paths entry:

```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"
            }
          }
        }
      }
    }
  }
},
```

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed Sep 17, 2023
1 parent 4ee6919 commit 6d68292
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/cactus-core/package.json
Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions packages/cactus-core/src/main/typescript/public-api.ts
Expand Up @@ -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";
@@ -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<S, P> {
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<typeof OAS, OasPathTypeGetOpenApiSpecV1>
* 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<S, P> implements IWebServiceEndpoint {
public static readonly CLASS_NAME = "GetOpenApiSpecV1EndpointBase<S, P>";

protected readonly log: Logger;

public get className(): string {
return GetOpenApiSpecV1EndpointBase.CLASS_NAME;
}

constructor(
public readonly opts: IGetOpenApiSpecV1EndpointBaseOptions<S, P>,
) {
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<IWebServiceEndpoint> {
await registerWebServiceEndpoint(expressApp, this);
return this;
}

getAuthorizationOptionsProvider(): IAsyncProvider<IEndpointAuthzOptions> {
// TODO: make this an injectable dependency in the constructor
return {
get: async () => ({
isProtected: true,
requiredRoles: [],
}),
};
}

async handleRequest(req: Request, res: Response): Promise<void> {
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,
});
}
}
}
1 change: 1 addition & 0 deletions yarn.lock
Expand Up @@ -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
Expand Down

0 comments on commit 6d68292

Please sign in to comment.