import type { Context } from 'aws-lambda' import Transaction from '../BaseEvent/Transaction.js' import Globals from '../Globals.js' /** * CustomError class that extends the built-in Error class. * @class * @extends Error * @param {any} body - The body of the error, which can be an object or a string. * @constructor * @property {string} name - The name of the error. */ class CustomError extends Error { constructor(body: any) { super(body.message || 'Unknown error!') this.name = body.error || 'UnknownError' } } /** * Represents an error response from an API. * @typedef {Object} ResponseErrorType * @property {string} err - The error message. * @property {string} [errCode] - The error code, if available. */ export type ResponseErrorType = { err: string errCode?: string } /** * Represents a response object with various methods for building and manipulating the response. * @template BodyType - The type of the response body. */ export default class Response { /** * The private property that stores the status code. * @type {number} * @private */ private statusCode: number /** * Private property representing the body of an object. * @type {any} * @private */ private body: any /** * Indicates whether the object is currently piping out. * @type {boolean} */ private isPipingOut: boolean /** * Private property that stores the headers as an object. */ private headers: object /** * Determines whether streaming is enabled or not. * @returns {boolean} - True if streaming is enabled, false otherwise. */ public readonly shouldStream: boolean /** * Indicates whether the request body should be treated as raw data. * @type {boolean} */ public readonly rawBody: boolean /** * A boolean flag indicating whether to throw an error when encountering errors. * If set to true, any errors encountered will result in an exception being thrown. * If set to false, errors will be logged but the program will continue execution. */ public readonly throwOnErrors: boolean /** * Indicates whether the transaction ID is disabled. * @type {boolean} */ public readonly disableTransactionID: boolean /** * Constructs a new Response object with the given status code, body, and optional behavior. * @param {number} statusCode - The HTTP status code of the response. * @param {BodyType} body - The body of the response. * @param {Object} [optBehaviour] - Optional behavior configuration for the response. * @param {boolean} [optBehaviour.shouldStream] - Indicates whether the response should be streamed. * @param {boolean} [optBehaviour.rawBody] - Indicates whether the response body should be treated as raw data. * @param {boolean} [optBehaviour.throwOnErrors] - Indicates whether errors should be thrown for non-successful status codes. * @param {boolean} [optBehaviour */ constructor( statusCode: number, body: BodyType, optBehaviour?: | { shouldStream?: boolean rawBody?: boolean throwOnErrors?: boolean disableTransactionID?: boolean } | undefined ) { // response this.statusCode = statusCode this.body = body this.headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, 'Content-Type': 'application/json', } // behaviour this.isPipingOut = false //internal -- flag to indicate streaming out has started and avoid double stream call // options this.shouldStream = !!optBehaviour?.shouldStream // this.rawBody = !!optBehaviour?.rawBody this.throwOnErrors = !!optBehaviour?.throwOnErrors this.disableTransactionID = !!optBehaviour?.disableTransactionID } /** * Get the status code of the response. * @returns {number} The status code. */ public getCode(): number { return this.statusCode } /** * Get the body of the object. * @returns {BodyType} The body of the object. */ public getBody(): BodyType { return this.body } /** * Appends a key-value pair into the body object. * @param {string} key - The key to append. * @param {any} value - The value to append. * @returns None */ public appendIntoBody(key: string, value: any): void { this.body[key] = value } /** * Appends a header to the existing headers object. * @param {string} key - The key of the header. * @param {any} value - The value of the header. * @returns None */ public appendHeader(key: string, value: any): void { this.headers[key] = value } /** * Builds the response for the given context and transaction. * @param {Context} context - The context object. * @param {Transaction} transaction - The transaction object. * @param {boolean} optDoNotCallContext - Optional flag to indicate whether to call the context or not. * @returns {Promise} - A promise that resolves when the response is built. */ public async build( context: Context, transaction: Transaction, optDoNotCallContext: boolean ): Promise { //Stream support if (this.isPipingOut) return if (this.shouldStream) return this.pipe(context) //append default fields if (transaction.request.getRequestID() && this.body && !this.disableTransactionID) this.appendIntoBody('transactionID', transaction.request.getRequestID()) //append transaction ID //Raw response support if (this.rawBody) return this.rawContext(context, transaction) //build response const b = { statusCode: this.statusCode, headers: this.headers, ...(this.body ? { body: JSON.stringify(this.body) } : {}), } //log response and respond to context transaction.logger.info(b) //Check for transaction response proxy if (transaction.responseProxy) await transaction.responseProxy(this) //Batch does not succeed directly just on upper transaction (which will should be a batch) if (!optDoNotCallContext) context.succeed(b) } /** * Private method that pipes the response to the given context. * @param {Context} context - The context object provided by AWS Lambda. * @returns None */ private pipe(context: Context): void { //Check if not streaming this.isPipingOut = true //build response const b = { statusCode: this.statusCode, body: this.body, headers: this.headers, } //log response and respond to context context.succeed(b) } /** * Private method that handles the raw context of a transaction. * @param {Context} context - The context object. * @param {Transaction} transaction - The transaction object. * @returns None */ private rawContext(context: Context, transaction: Transaction): void { //log response and respond to context transaction.logger.info(this.body) if (this.getCode() <= 200 && this.getCode() <= 299) context.succeed(this.body) else { if (!this.throwOnErrors) context.fail(new CustomError(this.body)) else throw new CustomError(this.body) } } /** * Generates a response object for a missing path parameter error. * @param {string} paramName - The name of the missing path parameter. * @returns {Response} - The response object with error details. */ public static MissingParamResponse(paramName: string): Response { console.warn(`Invalid request - Path parameter ${paramName} is missing.`) return new Response(400, { err: `Invalid request. Path parameter ${paramName} is missing.`, errCode: Globals.ErrorCode_MissingParam, }) } /** * Creates a response object for a missing query parameter error. * @param {string} paramName - The name of the missing query parameter. * @returns {Response} - The response object with error details. */ public static MissingQueryResponse(paramName: string): Response { console.warn(`Invalid request - Query parameter ${paramName} is missing.`) return new Response(400, { err: `Invalid request. Query parameter ${paramName} is missing.`, errCode: Globals.ErrorCode_MissingParam, }) } /** * Creates a BadRequestResponse object with the given parameters. * @param {string} [msg] - The error message. * @param {string} [errCode] - The error code. * @param {any} [optBody] - Optional additional body data. * @returns {Response} - The BadRequestResponse object. */ public static BadRequestResponse( msg?: string, errCode?: string, optBody?: any ): Response { console.warn(`Bad request - ${msg}`) return new Response(400, { err: msg, ...(errCode ? { errCode: errCode } : {}), ...(optBody || {}), }) } /** * Creates a BadRequestResponse object with rollback option. * @param {string} msg - The error message. * @param {string} [errCode] - The error code. * @param {any} [optBody] - Optional body to include in the response. * @returns {Response} - The BadRequestResponse object. */ public static BadRequestResponseWithRollback( msg: string, errCode?: string, optBody?: any ): Response { console.warn(`Bad request - ${msg}`) return new Response(400, { err: msg, rollback: true, ...(errCode ? { errCode: errCode } : {}), ...(optBody || {}), }) } /** * Creates an unauthorized response with the given error message and error code. * @param {string} msg - The error message. * @param {string} [errCode] - The error code (optional). * @returns {Response} - The unauthorized response. */ public static UnauthorizedResponse(msg: string, errCode?: string): Response { console.warn(`Denying request - ${msg}`) return new Response(401, { err: msg, ...(errCode ? { errCode: errCode } : {}), }) } /** * Creates a success response object with the given body. * @param {BodyType} body - The body of the response. * @returns {Response} - The success response object. */ public static SuccessResponse(body: BodyType): Response { return new Response(200, (body ? body : {}) as BodyType) } /** * Creates a redirect response with the specified URL. * @param {string} url - The URL to redirect to. * @returns {Response} - The redirect response. */ public static RedirectResponse(url: string): Response { const resp = new Response(302, null) resp.appendHeader('Location', url) return resp } /** * Creates a success response with no content. * @returns {Response} A response object with a status code of 204 and no content. */ public static SuccessNoContentResponse(): Response { return new Response(204, null) } /** * Creates a success response object with a streaming body and specified content type. * @param {any} stream - The stream object to be used as the response body. * @param {string} contentType - The content type of the response. * @returns {Response} - The success response object. */ public static SuccessStreamResponse(stream: any, contentType: string): Response { const resp = new Response(200, stream, { shouldStream: true, }) resp.appendHeader('Connection', 'keep-alive') if (contentType) resp.appendHeader('Content-Type', contentType) return resp } /** * Creates a simple HTTP response with the given body and optional status code. * @param {BodyType} body - The body of the response. * @param {number} [optionalCode] - The optional status code of the response. Defaults to 200. * @returns {Response} - The created response object. */ public static SimpleResponse( body: BodyType, optionalCode?: number ): Response { const resp = new Response(optionalCode || 200, body) return resp } }