From 28f0f86739ad9d82fbed1588bd7c7b365eb65bb5 Mon Sep 17 00:00:00 2001 From: Wasiu Bakare Date: Sat, 24 Aug 2024 20:23:30 +0100 Subject: [PATCH] feat: api status updates --- src/controllers/api-status.controller.ts | 13 ++++++ src/controllers/index.ts | 1 + src/index.ts | 6 ++- src/middleware/asyncHandler.ts | 14 +++++++ src/models/api-model.ts | 44 +++++++++++++++++++++ src/routes/api-status.ts | 8 ++++ src/routes/index.ts | 1 + src/services/api-status.services.ts | 50 ++++++++++++++++++++++++ src/services/index.ts | 1 + src/utils/sendJsonResponse.ts | 34 ++++++++++++++++ 10 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api-status.controller.ts create mode 100644 src/middleware/asyncHandler.ts create mode 100644 src/models/api-model.ts create mode 100644 src/routes/api-status.ts create mode 100644 src/services/api-status.services.ts create mode 100644 src/utils/sendJsonResponse.ts diff --git a/src/controllers/api-status.controller.ts b/src/controllers/api-status.controller.ts new file mode 100644 index 00000000..78a1c90b --- /dev/null +++ b/src/controllers/api-status.controller.ts @@ -0,0 +1,13 @@ +import { Request, Response } from "express"; +import { asyncHandler } from "../middleware/asyncHandler"; +import { parseJsonResponse } from "../services/api-status.services"; +import { sendJsonResponse } from "../utils/sendJsonResponse"; + +export const createApiStatus = asyncHandler( + async (req: Request, res: Response) => { + const resultJson = req.body; + + await parseJsonResponse(resultJson); + sendJsonResponse(res, 201, "API status updated successfully"); + }, +); diff --git a/src/controllers/index.ts b/src/controllers/index.ts index b76b730f..9fca29be 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -22,3 +22,4 @@ export * from "./billingController"; export * from "./SqueezeController"; export * from "./NotificationController"; export * from "./billingplanController"; +export * from "./api-status.controller"; diff --git a/src/index.ts b/src/index.ts index 233600ae..04512d89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import AppDataSource from "./data-source"; import { errorHandler, routeNotFound } from "./middleware"; import { adminRouter, + apiStatusRouter, authRoute, billingPlanRouter, billingRouter, @@ -59,8 +60,8 @@ server.use( ); server.use(Limiter); -server.use(express.json()); -server.use(express.urlencoded({ extended: true })); +server.use(express.json({ limit: "10mb" })); +server.use(express.urlencoded({ limit: "10mb", extended: true })); server.use(passport.initialize()); server.get("/", (req: Request, res: Response) => { @@ -101,6 +102,7 @@ server.use("/api/v1", billingPlanRouter); server.use("/api/v1", newsLetterSubscriptionRoute); server.use("/api/v1", squeezeRoute); server.use("/api/v1", planRouter); +server.use("/api/v1", apiStatusRouter); server.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); server.use("/openapi.json", (_req: Request, res: Response) => { diff --git a/src/middleware/asyncHandler.ts b/src/middleware/asyncHandler.ts new file mode 100644 index 00000000..de6d8198 --- /dev/null +++ b/src/middleware/asyncHandler.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from "express"; + +/** + * Async handler to wrap the API routes, this allows for async error handling. + * @param fn Function to call for the API endpoint + * @returns Promise with a catch statement + */ +const asyncHandler = + (fn: (req: Request, res: Response, next: NextFunction) => void) => + (req: Request, res: Response, next: NextFunction) => { + return Promise.resolve(fn(req, res, next)).catch(next); + }; + +export { asyncHandler }; diff --git a/src/models/api-model.ts b/src/models/api-model.ts new file mode 100644 index 00000000..4adfefbc --- /dev/null +++ b/src/models/api-model.ts @@ -0,0 +1,44 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from "typeorm"; + +export enum API_STATUS { + OPERATIONAL = "operational", + DEGRADED = "degraded", + DOWN = "down", +} + +@Entity({ name: "api_status" }) +export class ApiStatus { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ name: "api_group" }) + api_group: string; + + @Column({ name: "api_name" }) + api_name: string; + + @Column({ name: "status", type: "enum", enum: API_STATUS }) + status: API_STATUS; + + @Column("text", { nullable: true }) + details: string; + + @Column({ name: "response_time", type: "int", nullable: true }) + response_time: string; + + @CreateDateColumn({ name: "created_at" }) + created_at: Date; + + @UpdateDateColumn({ name: "updated_at" }) + updated_at: Date; + + @DeleteDateColumn({ nullable: true }) + deleted_at: Date; +} diff --git a/src/routes/api-status.ts b/src/routes/api-status.ts new file mode 100644 index 00000000..c699ff13 --- /dev/null +++ b/src/routes/api-status.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import { createApiStatus } from "../controllers/api-status.controller"; + +const apiStatusRouter = Router(); + +apiStatusRouter.post("/api-status", createApiStatus); + +export { apiStatusRouter }; diff --git a/src/routes/index.ts b/src/routes/index.ts index 95ee8377..f310278e 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -23,3 +23,4 @@ export * from "./squeeze"; export * from "./newsLetterSubscription"; export * from "./notification"; export * from "./billingplan"; +export * from "./api-status"; diff --git a/src/services/api-status.services.ts b/src/services/api-status.services.ts new file mode 100644 index 00000000..feba7f27 --- /dev/null +++ b/src/services/api-status.services.ts @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import AppDataSource from "../data-source"; +import { API_STATUS, ApiStatus } from "../models/api-model"; + +const apiStatusRepository = AppDataSource.getRepository(ApiStatus); +const MAX_ALLOWED_RESPONSE_TIME = 2000; + +const determineStatus = ( + statusCode: number, + responseTime: number, +): API_STATUS => { + if (statusCode >= 200 && statusCode < 300) { + if (responseTime && responseTime > MAX_ALLOWED_RESPONSE_TIME) { + return API_STATUS.DEGRADED; + } + return API_STATUS.OPERATIONAL; + } else if (statusCode >= 500) { + return API_STATUS.DOWN; + } + return API_STATUS.DEGRADED; +}; + +const parseJsonResponse = async (resultJson: any): Promise => { + const apiGroups = resultJson.collection.item; + + for (const apiGroup of apiGroups) { + for (const api of apiGroup.item) { + let status = API_STATUS.DEGRADED; + let responseTime = null; + + if (api.response && api.response.length > 0) { + const response = api.response[0]; + responseTime = response.responseTime || null; + status = determineStatus(response.code, responseTime); + } + + const apiStatus = apiStatusRepository.create({ + api_group: apiGroup.name, + api_name: api.name, + status, + response_time: responseTime, + details: responseTime ? `Response time: ${responseTime}ms` : null, + }); + + await apiStatusRepository.save(apiStatus); + } + } +}; + +export { parseJsonResponse }; diff --git a/src/services/index.ts b/src/services/index.ts index 5e885d16..fc77872b 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -15,3 +15,4 @@ export * from "./billing-plans.services"; export * from "./squeezeService"; export * from "./blogComment.services"; export * from "./notification.services"; +export * from "./api-status.services"; diff --git a/src/utils/sendJsonResponse.ts b/src/utils/sendJsonResponse.ts new file mode 100644 index 00000000..5b8e92da --- /dev/null +++ b/src/utils/sendJsonResponse.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Response } from "express"; + +/** + * Sends a JSON response with a standard structure. + * + * @param res - The Express response object. + * @param statusCode - The HTTP status code to send. + * @param message - The message to include in the response. + * @param data - The data to include in the response. Can be any type. + * @param accessToken - Optional access token to include in the response. + */ +const sendJsonResponse = ( + res: Response, + statusCode: number, + message: string, + data?: any, + accessToken?: string, +) => { + const responsePayload: any = { + status: "success", + message, + status_code: statusCode, + data, + }; + + if (accessToken) { + responsePayload.access_token = accessToken; + } + + res.status(statusCode).json(responsePayload); +}; + +export { sendJsonResponse };