import type { Context, SQSBatchResponse, SQSEvent } from 'aws-lambda' import Transaction, { TransactionConfig } from './Transaction.js' import Response, { ResponseErrorType } from '../API/Response.js' import Globals from '../Globals.js' import Utils from '../Util/Utils.js' /** * Type definition for an event processor execution function. * @param {Transaction} transaction - The transaction object. * @param {string | object} recordContent - The content of the record being processed. * @returns {Promise | SQSBatchResponse>} - A promise that resolves to the response or batch response. */ export type EventProcessorExecution = ( transaction: Transaction, recordContent: string | object ) => Promise | SQSBatchResponse | null> /** * EventProcessor class that processes events from an SQS queue. * @template ResponseInnerType - The type of the inner response object. */ export default class EventProcessor { /** * A boolean flag indicating whether failures are allowed or not. * @readonly */ private readonly allowFailure: boolean /** * The configuration object for the API transaction. */ private readonly config: TransactionConfig /** * The private readonly context property of the class. * @type {Context} */ private readonly context: Context /** * The SQS event object that triggered the Lambda function. */ private readonly event: SQSEvent /** * Constructs a new instance of the class. * @param {SQSEvent} event - The event object representing the incoming SQS message. * @param {Context} context - The context object representing the AWS Lambda execution context. * @param {TransactionConfig} [config] - Optional configuration object for the transaction. * @param {boolean} [allowFailure] - Optional flag indicating whether to allow failure for the transaction. * @returns None */ constructor( event: SQSEvent, context: Context, config?: TransactionConfig, allowFailure?: boolean ) { this.event = event this.context = context this.config = config || {} this.allowFailure = !!allowFailure } /** * Processes an event using the provided execution object and returns a response. * @param {EventProcessorExecution} execution - The execution object containing the event to process. * @param {boolean} [doNotDecodeMessage] - Optional flag indicating whether to decode the message. * @returns {Promise | null | SQSBatchResponse>} - A promise that resolves to the response object, or null if no response is available. * @throws {Error} - Throws an error if the response code is not within the range of 200 to 299 and failure is not allowed. */ public async processEvent( execution: EventProcessorExecution, doNotDecodeMessage?: boolean ): Promise | null | SQSBatchResponse> { const resp = await this.processRawEvent(execution, !!doNotDecodeMessage) if ( !this.allowFailure && resp && resp instanceof Response && !(resp.getCode() >= 200 && resp.getCode() < 300) ) throw new Error(JSON.stringify(resp.getBody() || {})) else if (resp) return resp return null } /** * Processes a raw event by executing the provided execution function and handling any errors or failures. * @param {EventProcessorExecution} execution - The execution function to process the event. * @param {boolean} doNotDecodeMessage - Flag indicating whether to decode the message or not. * @returns {Promise | null | SQSBatchResponse>} - A promise that resolves to a response object, null, or a SQS batch response. */ private async processRawEvent( execution: EventProcessorExecution, doNotDecodeMessage: boolean ): Promise | null | SQSBatchResponse> { if (this.event.Records && this.event.Records.length > 0) { //safe check for empty events? //init transaction for all records if (!this.config?.skipCleanTmp) await Utils.cleanTemporaryFolder() return await new Transaction(this.event, this.context, { ...this.config, syncReturn: true, }).execute(async transaction => { //Map records with decoded message when required const decodedRecords: string[] | object[] = this.event.Records.map(eventRecord => doNotDecodeMessage ? eventRecord.body : JSON.parse(eventRecord.body) ) //for each available event const failureIDs: Array = [] for (const eventRecordIdx in decodedRecords) { const eventRecord = decodedRecords[eventRecordIdx] const message = this.event.Records[eventRecordIdx] //Call execution const resp = await execution(transaction, eventRecord) //check for failure if ( !resp || (resp instanceof Response && !(resp?.getCode() >= 200 && resp?.getCode() < 300)) ) { //response with failures or fail hard at first if (this.allowFailure) failureIDs.push(message.messageId) else return resp } } //not errored and loop ended - succeeded (might have failures) if (this.allowFailure) return { batchItemFailures: failureIDs.map(id => ({ itemIdentifier: id })), } return Response.SuccessResponse(null) }) } else return Response.BadRequestResponse( Globals.ErrorResponseNoRecords, Globals.ErrorCode_NoRecords ) //no event to be processed? } }