Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core,core-api): use fastify, drop express #3788

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cactus-core-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"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": {
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
5 changes: 5 additions & 0 deletions packages/cactus-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,17 @@
"dependencies": {
"@hyperledger/cactus-common": "2.1.0",
"@hyperledger/cactus-core-api": "2.1.0",
"ajv": "8.17.1",
"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-openapi-glue": "4.8.0",
"http-errors": "2.0.0",
"http-errors-enhanced-cjs": "2.0.1",
"jose": "5.10.0",
"openapi-types": "12.1.3",
"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
Loading