Skip to content

Commit

Permalink
refactor(core,core-api): use fastify, drop express
Browse files Browse the repository at this point in the history
Primary Changes
---------------
1. Migrated files of (cactus-core and core-api) using
   express to fastify

Fixes: hyperledger-cacti#3598
Signed-off-by: aldousalvarez <aldousss.alvarez@gmail.com>
  • Loading branch information
aldousalvarez committed Feb 26, 2025
1 parent ca70682 commit bc8800f
Show file tree
Hide file tree
Showing 14 changed files with 830 additions and 10 deletions.
4 changes: 3 additions & 1 deletion packages/cactus-core-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@
"ajv": "8.17.1",
"ajv-draft-04": "1.0.0",
"ajv-formats": "3.0.1",
"axios": "1.7.9",
"axios": "1.7.7",
"fastify": "4.22.2",
"google-protobuf": "3.21.4"
},
"devDependencies": {
"@bufbuild/protobuf": "1.10.0",
"@connectrpc/connect": "1.4.0",
"@fastify/jwt": "9.0.3",
"@grpc/proto-loader": "0.7.13",
"@types/express": "5.0.0",
"@types/google-protobuf": "3.15.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { FastifyRequest, FastifyReply } from "fastify";

export type IFastifyRequestHandler = (
req: FastifyRequest,
reply: FastifyReply,
) => Promise<void>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { FastifyInstance } from "fastify"; // Import Fastify type
import { IWebServiceEndpointFastify } from "./i-web-service-endpoint-fastify";
import { ICactusPlugin } from "../i-cactus-plugin";
import type { Server as SocketIoServer } from "socket.io";

export interface IPluginWebServiceFastify extends ICactusPlugin {
getOrCreateWebServices(): Promise<IWebServiceEndpointFastify[]>;

registerWebServices(
fastifyApp: FastifyInstance, // Updated to FastifyInstance
wsApi: SocketIoServer,
): Promise<IWebServiceEndpointFastify[]>;

shutdown(): Promise<void>;
getOpenApiSpec(): unknown;
}

export function isIPluginWebServiceFastify(
x: unknown,
): x is IPluginWebServiceFastify {
return (
!!x &&
typeof (x as IPluginWebServiceFastify).registerWebServices === "function" &&
typeof (x as IPluginWebServiceFastify).getOrCreateWebServices ===
"function" &&
typeof (x as IPluginWebServiceFastify).getPackageName === "function" &&
typeof (x as IPluginWebServiceFastify).getInstanceId === "function" &&
typeof (x as IPluginWebServiceFastify).shutdown === "function" &&
typeof (x as IPluginWebServiceFastify).getOpenApiSpec === "function"
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { IAsyncProvider } from "@hyperledger/cactus-common";
import { IEndpointAuthzOptions } from "./i-endpoint-authz-options";

/**
* Interface for implementing API endpoints that can be dynamically registered
* at runtime in a Fastify web application.
*
* This interface outlines the necessary methods for a Cactus API endpoint,
* which is typically part of a plugin. It facilitates dynamic routing and
* ensures consistent endpoint registration.
*/
export interface IWebServiceEndpointFastify {
/**
* Registers this endpoint with a Fastify application.
*
* @param fastifyApp - The Fastify application instance to register the endpoint with.
* @returns A promise that resolves to the endpoint instance after registration.
*/
registerFastify(
fastifyApp: FastifyInstance,
): Promise<IWebServiceEndpointFastify>;

/**
* Gets the HTTP verb for this endpoint in lowercase.
*
* @example "get", "post", "put", "delete"
* @returns The lowercase HTTP verb.
*/
getVerbLowerCase(): string;

/**
* Gets the HTTP path for this endpoint.
*
* @returns The path string where the endpoint will be accessible.
*/
getPath(): string;

/**
* Provides the Fastify-compatible request handler for this endpoint.
*
* The handler can be directly registered using Fastify's route configuration methods.
*
* @returns The request handler function.
*/
getFastifyHandler(): (
request: FastifyRequest,
reply: FastifyReply,
) => Promise<void>;

/**
* Provides an asynchronous provider for authorization options.
*
* This allows dynamic determination of authorization configurations,
* such as role-based access control (RBAC) or token requirements.
*
* @returns An async provider for authorization options.
*/
getAuthorizationOptionsProvider(): IAsyncProvider<IEndpointAuthzOptions>;

/**
* Optionally provides a reference to the Fastify reply object.
*
* This can be used for scenarios requiring custom reply handling
* after the handler function completes.
*
* @returns The Fastify reply object, if applicable.
*/
}
7 changes: 7 additions & 0 deletions packages/cactus-core-api/src/main/typescript/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ export { IPluginConsortium } from "./plugin/consortium/i-plugin-consortium";
export { IPluginKeychain } from "./plugin/keychain/i-plugin-keychain";
export { isIPluginKeychain } from "./plugin/keychain/is-i-plugin-keychain";
export { IExpressRequestHandler } from "./plugin/web-service/i-express-request-handler";
export { IFastifyRequestHandler } from "./plugin/web-service/i-fastify-request-handler";

export {
IPluginWebService,
isIPluginWebService,
} from "./plugin/web-service/i-plugin-web-service";

export {
IPluginWebServiceFastify,
isIPluginWebServiceFastify,
} from "./plugin/web-service/i-plugin-web-service-fastify";

export { IWebServiceEndpoint } from "./plugin/web-service/i-web-service-endpoint";
export { IWebServiceEndpointFastify } from "./plugin/web-service/i-web-service-endpoint-fastify";
export { PluginFactory } from "./plugin/plugin-factory";

export {
Expand Down
6 changes: 6 additions & 0 deletions packages/cactus-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,20 @@
"webpack:dev:web": "webpack --env=dev --target=web --config ../../webpack.config.js"
},
"dependencies": {
"@fastify/auth": "5.0.2",
"@fastify/jwt": "9.0.3",
"@hyperledger/cactus-common": "2.1.0",
"@hyperledger/cactus-core-api": "2.1.0",
"body-parser": "1.20.3",
"express": "4.21.2",
"express-jwt-authz": "2.4.1",
"express-openapi-validator": "5.2.0",
"fastify": "4.22.2",
"fastify-guard": "3.0.1",
"fastify-openapi-glue": "4.8.0",
"http-errors": "2.0.0",
"http-errors-enhanced-cjs": "2.0.1",
"jose": "5.10.0",
"run-time-error-cjs": "1.4.0",
"safe-stable-stringify": "2.4.3",
"typescript-optional": "2.0.1"
Expand Down
4 changes: 4 additions & 0 deletions packages/cactus-core/src/main/typescript/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ export { stringifyBigIntReplacer } from "./web-services/stringify-big-int-replac

export { IConfigureExpressAppContext } from "./web-services/configure-express-app-base";
export { configureExpressAppBase } from "./web-services/configure-express-app-base";
export { IConfigureFastifyAppContext } from "./web-services/configure-fastify-app-base";
export { configureFastifyAppBase } from "./web-services/configure-fastify-app-base";

export { CACTI_CORE_CONFIGURE_EXPRESS_APP_BASE_MARKER } from "./web-services/configure-express-app-base";
export { CACTI_CORE_CONFIGURE_FASTIFY_APP_BASE_MARKER } from "./web-services/configure-fastify-app-base";
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FastifyInstance } from "fastify";
import {
Checks,
LogLevelDesc,
LoggerProvider,
} from "@hyperledger/cactus-common";
import { stringifyBigIntReplacer } from "./stringify-big-int-replacer";

export const CACTI_CORE_CONFIGURE_FASTIFY_APP_BASE_MARKER =
"CACTI_CORE_CONFIGURE_FASTIFY_APP_BASE_MARKER";

export interface IConfigureFastifyAppContext {
readonly logLevel?: LogLevelDesc;
readonly app: FastifyInstance;
}

export async function configureFastifyAppBase(
ctx: IConfigureFastifyAppContext,
): Promise<void> {
const fn = "configureFastifyAppBase()";
Checks.truthy(ctx, `${fn} arg1 ctx`);
Checks.truthy(ctx.app, `${fn} arg1 ctx.app`);

const logLevel: LogLevelDesc = ctx.logLevel || "WARN";
const log = LoggerProvider.getOrCreate({ level: logLevel, label: fn });

log.debug("ENTRY");

if (ctx.app.hasDecorator(CACTI_CORE_CONFIGURE_FASTIFY_APP_BASE_MARKER)) {
throw new Error("Fastify instance has already been configured.");
}

log.debug("Fastify JSON parsing enabled by default");

ctx.app.addHook("onSend", async (request, reply, payload) => {
if (typeof payload === "string") {
try {
return JSON.stringify(JSON.parse(payload), stringifyBigIntReplacer);
} catch (err) {
return payload;
}
}
return payload;
});

ctx.app.decorate(CACTI_CORE_CONFIGURE_FASTIFY_APP_BASE_MARKER, true);

log.debug("EXIT");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
FastifyReply,
FastifyRequest,
FastifyInstance,
RouteOptions,
} from "fastify";
import {
Logger,
LoggerProvider,
IAsyncProvider,
} from "@hyperledger/cactus-common";

import {
IWebServiceEndpointFastify,
IEndpointAuthzOptions,
} from "@hyperledger/cactus-core-api";

export class GetOpenApiSpecV1EndpointBase
implements IWebServiceEndpointFastify
{
public static readonly CLASS_NAME = "GetOpenApiSpecV1EndpointBase<S, P>";

protected readonly log: Logger;

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

constructor(public readonly opts: { path: string; verbLowerCase: string }) {
const level = "INFO";
const label = this.className;
this.log = LoggerProvider.getOrCreate({ level, label });
}

public getPath(): string {
return this.opts.path;
}

public getVerbLowerCase(): string {
return this.opts.verbLowerCase;
}

public async registerFastify(
fastify: FastifyInstance,
): Promise<IWebServiceEndpointFastify> {
fastify.route({
method: this.getVerbLowerCase().toUpperCase() as RouteOptions["method"],
url: this.getPath(),
handler: this.getFastifyHandler(),
});
return this;
}

getAuthorizationOptionsProvider(): IAsyncProvider<IEndpointAuthzOptions> {
return {
get: async () => ({
isProtected: true,
requiredRoles: [],
}),
};
}

public getFastifyHandler(): (
request: FastifyRequest,
reply: FastifyReply,
) => Promise<void> {
return async (
request: FastifyRequest,
reply: FastifyReply,
): Promise<void> => {
await this.handleRequest(request, reply);
};
}

private async handleRequest(
request: FastifyRequest,
reply: FastifyReply,
): Promise<void> {
try {
reply.status(200).send({ success: true });
} catch (error) {
reply.status(500).send({ error: "Internal Server Error" });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { FastifyReply } from "fastify";
import createHttpError from "http-errors";

import {
identifierByCodes,
INTERNAL_SERVER_ERROR,
} from "http-errors-enhanced-cjs";

import {
Logger,
createRuntimeErrorWithCause,
safeStringifyException,
} from "@hyperledger/cactus-common";

/**
* An interface describing the object containing the contextual information needed by the
* `#handleRestEndpointException()` method to perform its duties.
*
* @param ctx - An object containing options for handling the REST endpoint exception.
* @param ctx.errorMsg - The error message to log (if there will be error logging e.g. HTTP 500)
* @param ctx.log - The logger instance used for logging errors and/or debug messages.
* @param ctx.error - The error object representing the exception that is being handled.
* @param ctx.reply - The Fastify reply object to send the HTTP response.
*/
export interface IHandleRestEndpointExceptionOptions {
readonly errorMsg: string;
readonly log: Logger;
readonly error: unknown;
readonly reply: FastifyReply;
}

/**
* Handles exceptions thrown during REST endpoint processing and sends an appropriate HTTP response.
*
* If the exception is an instance of `HttpError` from the `http-errors` library,
* it logs the error at the debug level and sends a JSON response with the error details
* and the corresponding HTTP status code.
*
* If the exception is not an instance of `HttpError`, it logs the error at the error level,
* creates a runtime error with the original error as the cause, and sends a JSON response
* with a generic "Internal Server Error" message and a 500 HTTP status code.
*
* @param ctx - An object containing options for handling the REST endpoint exception.
*/
export async function handleRestEndpointException(
ctx: Readonly<IHandleRestEndpointExceptionOptions>,
): Promise<void> {
const errorAsSanitizedJson = safeStringifyException(ctx.error);

if (createHttpError.isHttpError(ctx.error)) {
ctx.reply.status(ctx.error.statusCode);

if (ctx.error.statusCode >= INTERNAL_SERVER_ERROR) {
ctx.log.debug(ctx.errorMsg, errorAsSanitizedJson);
} else {
ctx.log.error(ctx.errorMsg, errorAsSanitizedJson);
}

if (ctx.error.expose) {
ctx.reply.send({
message: identifierByCodes[ctx.error.statusCode],
error: errorAsSanitizedJson,
});
} else {
ctx.reply.send({
message: identifierByCodes[ctx.error.statusCode],
});
}
} else {
ctx.log.error(ctx.errorMsg, errorAsSanitizedJson);

const rex = createRuntimeErrorWithCause(ctx.errorMsg, ctx.error);
const sanitizedJsonRex = safeStringifyException(rex);

ctx.reply.status(INTERNAL_SERVER_ERROR).send({
message: identifierByCodes[INTERNAL_SERVER_ERROR],
error: sanitizedJsonRex,
});
}
}
Loading

0 comments on commit bc8800f

Please sign in to comment.