UNPKG

@flowcore/generator-library-template

Version:

A Yeoman generator for adding flowcore library components to a typescript service or application

205 lines (188 loc) 6.06 kB
/** * @module ExceptionGuard * This module provides functionality for handling and guarding against exceptions in an API context. */ import { ApiException, ApiInternalServerErrorException, ApiNotFoundException, ApiUnauthorizedException, ApiUnprocessableContentException, } from "@/lib/api-exceptions" import type { Logger } from "@/lib/logger" import { Type } from "@sinclair/typebox" import { Value } from "@sinclair/typebox/value" import type { StatusMap } from "elysia" import type { ElysiaCookie } from "elysia/cookies" import type { HTTPHeaders } from "elysia/types" /** Represents an error during JSON parsing */ type SafeJsonParseError = [Error, undefined] /** Represents a successful JSON parse */ type SafeJsonParseSuccess = [undefined, Error] /** Represents the result of a safe JSON parse operation */ type SafeJsonParseResult = SafeJsonParseError | SafeJsonParseSuccess /** * Safely parses a JSON string * @param obj - The string to parse * @returns A tuple containing either an Error and undefined, or undefined and the parsed object */ function safeParseJson(obj: string): SafeJsonParseResult { try { const parsed = JSON.parse(obj) return [undefined, parsed] } catch (error) { return [error as Error, undefined] } } /** Schema for validation errors */ export const ValidationErrorSchema = Type.Object({ on: Type.String(), errors: Type.Array( Type.Object({ path: Type.String(), message: Type.String(), }), ), }) /** Possible error codes */ type ErrorCode = | "VALIDATION" | "UNKNOWN" | "NOT_FOUND" | "PARSE" | "INTERNAL_SERVER_ERROR" | "INVALID_COOKIE_SIGNATURE" | "UNAUTHORIZED" /** State to be set in response */ type SetState = { headers: HTTPHeaders status?: number | keyof StatusMap redirect?: string cookie?: Record<string, ElysiaCookie> } /** Result of an error specifier */ type ErrorSpecifierResult<T extends ApiException> = | { match: true result: T } | { match: false result?: never } /** Function to specify how to handle a specific type of ApiException */ type ErrorSpecifier<T extends ApiException> = (exception: ApiException) => ErrorSpecifierResult<T> /** Options for the ExceptionGuard */ interface ExceptionGuardOptions { logAllErrors?: boolean } /** * Builder class for creating an exception guard */ export class ExceptionGuardBuilder { private logger: Logger = console private errorSpecifiers: ErrorSpecifier<ApiException>[] = [] private options: ExceptionGuardOptions = {} /** * Sets the logger for the exception guard * @param logger - The logger to use */ withLogger(logger: Logger) { this.logger = logger return this } /** * Sets the options for the exception guard * @param options - The options to set */ withOptions(options: ExceptionGuardOptions) { this.options = options return this } /** * Adds an error specifier to the exception guard * @param specifier - The error specifier to add */ withErrorSpecifier<T extends ApiException>(specifier: ErrorSpecifier<T>) { this.errorSpecifiers.push(specifier) return this } /** * Builds and returns the exception guard function */ build() { return (code: ErrorCode, set: SetState, error: Error, request: Request) => { if (code === "VALIDATION") { let on: string | undefined let validationErrors: Record<string, string> | undefined const [_err, parsedMessage] = safeParseJson(error.message) if (Value.Check(ValidationErrorSchema, parsedMessage)) { on = parsedMessage.on validationErrors = {} for (const validationError of parsedMessage.errors) { validationErrors[validationError.path] = validationError.message } } // biome-ignore lint/style/noParameterAssign: intentional error = new ApiUnprocessableContentException("Unprocessable Content", on, validationErrors) } else if (code === "NOT_FOUND" && !(error instanceof ApiNotFoundException)) { // biome-ignore lint/style/noParameterAssign: intentional error = new ApiNotFoundException("Not Found") } else if (code === "UNAUTHORIZED" && !(error instanceof ApiUnauthorizedException)) { // biome-ignore lint/style/noParameterAssign: intentional error = new ApiUnauthorizedException() } const apiError = error instanceof ApiException ? error : new ApiInternalServerErrorException() const errorLogContext = { method: request.method, pathname: new URL(request.url).pathname, } if (error !== apiError) { this.logger.error(error, errorLogContext) } set.status = apiError.status for (const specifier of this.errorSpecifiers) { const { match, result } = specifier(apiError) if (match) { set.status = result.status if (this.options.logAllErrors) { this.logger.error(result, errorLogContext) } return result } } if (error === apiError && this.options.logAllErrors) { this.logger.error(error, errorLogContext) } return { status: apiError.status, code: apiError.code, message: apiError.status === 500 ? "Internal server error" : apiError.message, } } } } /** * Creates a new ExceptionGuardBuilder with a default error specifier for ApiUnprocessableContentException * @returns An ExceptionGuardBuilder instance */ export function createExceptionGuard() { return new ExceptionGuardBuilder().withErrorSpecifier<ApiUnprocessableContentException>((apiError) => { if (apiError instanceof ApiUnprocessableContentException) { return { match: true, result: { ...apiError, status: apiError.status, code: apiError.code, message: apiError.message, on: apiError.on, errors: apiError.errors, }, } } return { match: false, } }) }