From 383f8528d88989b44c9763fc883c3d9ac74da21e Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Thu, 4 Apr 2024 11:24:46 -0700 Subject: [PATCH] feat(core): add configureExpressAppBase() utility function 1. The idea here is to re-use the common basic tasks of configuring an express instance similar to how the API server does it but without having the chicken-egg problem of circular dependencies between the API server and the plugins. 2. More detailed discussion can be seen in this other pull request in the comments: https://github.com/hyperledger/cacti/pull/3169 Signed-off-by: Peter Somogyvari --- packages/cactus-core/package.json | 2 + .../src/main/typescript/public-api.ts | 6 ++ .../configure-express-app-base.ts | 91 +++++++++++++++++++ .../stringify-big-int-replacer.ts | 13 +++ .../configure-express-app-base.test.ts | 18 ++++ yarn.lock | 2 + 6 files changed, 132 insertions(+) create mode 100644 packages/cactus-core/src/main/typescript/web-services/configure-express-app-base.ts create mode 100644 packages/cactus-core/src/main/typescript/web-services/stringify-big-int-replacer.ts create mode 100644 packages/cactus-core/src/test/typescript/unit/web-services/configure-express-app-base.test.ts diff --git a/packages/cactus-core/package.json b/packages/cactus-core/package.json index 3e2dea8a26..3900846b19 100644 --- a/packages/cactus-core/package.json +++ b/packages/cactus-core/package.json @@ -52,6 +52,7 @@ "dependencies": { "@hyperledger/cactus-common": "2.0.0-alpha.2", "@hyperledger/cactus-core-api": "2.0.0-alpha.2", + "body-parser": "1.20.2", "express": "4.19.2", "express-jwt-authz": "2.4.1", "express-openapi-validator": "5.0.4", @@ -62,6 +63,7 @@ "typescript-optional": "2.0.1" }, "devDependencies": { + "@types/body-parser": "1.19.4", "@types/express": "4.17.19", "@types/http-errors": "2.0.2", "node-mocks-http": "1.14.0", diff --git a/packages/cactus-core/src/main/typescript/public-api.ts b/packages/cactus-core/src/main/typescript/public-api.ts index 6e748c5696..c0a833fb24 100755 --- a/packages/cactus-core/src/main/typescript/public-api.ts +++ b/packages/cactus-core/src/main/typescript/public-api.ts @@ -23,3 +23,9 @@ export { IHandleRestEndpointExceptionOptions, handleRestEndpointException, } from "./web-services/handle-rest-endpoint-exception"; + +export { stringifyBigIntReplacer } from "./web-services/stringify-big-int-replacer"; + +export { IConfigureExpressAppContext } from "./web-services/configure-express-app-base"; +export { configureExpressAppBase } from "./web-services/configure-express-app-base"; +export { CACTI_CORE_CONFIGURE_EXPRESS_APP_BASE_MARKER } from "./web-services/configure-express-app-base"; diff --git a/packages/cactus-core/src/main/typescript/web-services/configure-express-app-base.ts b/packages/cactus-core/src/main/typescript/web-services/configure-express-app-base.ts new file mode 100644 index 0000000000..79b3a047a3 --- /dev/null +++ b/packages/cactus-core/src/main/typescript/web-services/configure-express-app-base.ts @@ -0,0 +1,91 @@ +import type { Express } from "express"; +import bodyParser, { OptionsJson } from "body-parser"; + +import { + Checks, + LogLevelDesc, + LoggerProvider, +} from "@hyperledger/cactus-common"; + +import { stringifyBigIntReplacer } from "./stringify-big-int-replacer"; + +export const CACTI_CORE_CONFIGURE_EXPRESS_APP_BASE_MARKER = + "CACTI_CORE_CONFIGURE_EXPRESS_APP_BASE_MARKER"; + +/** + * Implementations of this interface are objects who represent a valid execution + * context for the `configureExpressAppBase()` utility function. + * + * @see {configureExpressAppBase} + * @see {ApiServer} + */ +export interface IConfigureExpressAppContext { + readonly logLevel?: LogLevelDesc; + readonly app: Express; + readonly bodyParserJsonOpts?: OptionsJson; +} + +/** + * Configures the base functionalities for an Express.js application. + * + * The main purpose of this function is to have a reusable implementation + * of the base setup logic that the API server does. For test cases of + * plugins we can't directly import the API server because it would cause + * circular dependencies in the mono-repo that usually ends up causing mayhem + * with the build in general and also with the architecture longer term. + * + * So, with this function being here in the core package it makes it easy + * to reuse by both plugin test cases and also the API server itself. + * + * The logic here is kept very small because the order of the ExpressJS + * middleware handler's matters a lot and if you mix up the order then + * new bugs can appear. + * + * @param ctx The context object holding information about the log level + * that the caller wants and the ExpressJS instance itself which is to be + * configured. + * + * @throws {Error} If any of the required context properties are missing. + */ +export async function configureExpressAppBase( + ctx: IConfigureExpressAppContext, +): Promise { + const fn = "configureExpressAppBase()"; + Checks.truthy(ctx, `${fn} arg1 ctx`); + Checks.truthy(ctx.app, `${fn} arg1 ctx.app`); + Checks.truthy(ctx.app.use, `${fn} arg1 ctx.app.use`); + + const logLevel: LogLevelDesc = ctx.logLevel || "WARN"; + + const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: fn, + }); + + log.debug("ENTRY"); + + const didRun = ctx.app.get(CACTI_CORE_CONFIGURE_EXPRESS_APP_BASE_MARKER); + if (didRun) { + const duplicateConfigurationAttemptErrorMsg = + `Already configured this express instance before. Check the ` + + `configuration variable of the ExpressJS instance under the key ` + + `"CACTI_CORE_CONFIGURE_EXPRESS_APP_BASE_MARKER" to determine if an ` + + `instance has already been `; + throw new Error(duplicateConfigurationAttemptErrorMsg); + } + + const bodyParserJsonOpts: OptionsJson = ctx.bodyParserJsonOpts || { + limit: "50mb", + }; + log.debug("body-parser middleware opts: %o", bodyParserJsonOpts); + + const bodyParserMiddleware = bodyParser.json(bodyParserJsonOpts); + ctx.app.use(bodyParserMiddleware); + + // Add custom replacer to handle bigint responses correctly + ctx.app.set("json replacer", stringifyBigIntReplacer); + + ctx.app.set(CACTI_CORE_CONFIGURE_EXPRESS_APP_BASE_MARKER, true); + + log.debug("EXIT"); +} diff --git a/packages/cactus-core/src/main/typescript/web-services/stringify-big-int-replacer.ts b/packages/cactus-core/src/main/typescript/web-services/stringify-big-int-replacer.ts new file mode 100644 index 0000000000..ff77ddbffc --- /dev/null +++ b/packages/cactus-core/src/main/typescript/web-services/stringify-big-int-replacer.ts @@ -0,0 +1,13 @@ +/** + * `JSON.stringify` replacer function to handle BigInt. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json + */ +export function stringifyBigIntReplacer( + _key: string, + value: bigint | unknown, +): string | unknown { + if (typeof value === "bigint") { + return value.toString(); + } + return value; +} diff --git a/packages/cactus-core/src/test/typescript/unit/web-services/configure-express-app-base.test.ts b/packages/cactus-core/src/test/typescript/unit/web-services/configure-express-app-base.test.ts new file mode 100644 index 0000000000..05cdaea4d7 --- /dev/null +++ b/packages/cactus-core/src/test/typescript/unit/web-services/configure-express-app-base.test.ts @@ -0,0 +1,18 @@ +import "jest-extended"; +import express from "express"; + +import { configureExpressAppBase } from "../../../../main/typescript/public-api"; + +describe("configureExpressAppBase()", () => { + test("Crashes if missing Express instance from ctx", async () => { + const invocationPromise = configureExpressAppBase({} as never); + await expect(invocationPromise).toReject(); + }); + + test("Does not crash if parameters were valid", async () => { + const app = express(); + await expect( + async () => await configureExpressAppBase({ app, logLevel: "DEBUG" }), + ).not.toThrow(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 09e8f0b711..cc281b5e54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7916,8 +7916,10 @@ __metadata: dependencies: "@hyperledger/cactus-common": "npm:2.0.0-alpha.2" "@hyperledger/cactus-core-api": "npm:2.0.0-alpha.2" + "@types/body-parser": "npm:1.19.4" "@types/express": "npm:4.17.19" "@types/http-errors": "npm:2.0.2" + body-parser: "npm:1.20.2" express: "npm:4.19.2" express-jwt-authz: "npm:2.4.1" express-openapi-validator: "npm:5.0.4"