import type { Context, DynamoDBBatchResponse, DynamoDBRecord, DynamoDBStreamEvent, } 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' /** * Interface representing a DynamoDB record with marshalled data. * Extends the DynamoDBRecord interface. * @property {object} Keys - The keys of the record. * @property {object} OldImage - The old image of the record. * @property {object} NewImage - The new image of the record. */ export interface DynamoDBMarshalledRecord extends DynamoDBRecord { marshalled: { Keys?: object OldImage?: object NewImage?: object } } /** * Defines a type for executing a transaction on DynamoDB. * @param {Transaction} transaction - The transaction to execute. * @param {DynamoDBMarshalledRecord} recordContent - The content of the DynamoDB record. * @returns A promise that resolves to a response or a DynamoDB batch response. */ export type DynamoTransactionExecution = ( transaction: Transaction< null, ResponseInnerType | ResponseErrorType, DynamoDBBatchResponse | null >, recordContent: DynamoDBMarshalledRecord ) => Promise | DynamoDBBatchResponse | null> /** * Represents a DynamoDB transaction handler that processes events from a DynamoDB stream. * @template ResponseInnerType - The inner type of the response. */ export default class DynamoTransaction { /** * A boolean flag indicating whether failures are allowed. */ private readonly allowFailure: boolean /** * Readonly property that holds the transaction configuration. */ private readonly config: TransactionConfig /** * The context object that provides information about the current execution context. * @type {Context} */ private readonly context: Context /** * Represents an event from a DynamoDB stream. * @type {DynamoDBStreamEvent} */ private readonly event: DynamoDBStreamEvent /** * Constructor for a TransactionHandler object. * @param {DynamoDBStreamEvent} event - The DynamoDB stream event that triggered the transaction. * @param {Context} context - The AWS Lambda context object. * @param {TransactionConfig} [config] - Optional configuration for the transaction. * @param {boolean} [allowFailure] - Flag to indicate whether to allow transaction failure. * @returns None */ constructor( event: DynamoDBStreamEvent, context: Context, config?: TransactionConfig, allowFailure?: boolean ) { this.event = event this.context = context this.config = config || {} this.allowFailure = !!allowFailure } /** * Processes the event execution and returns a response based on the outcome. * @param {DynamoTransactionExecution} execution - The execution object to process. * @returns {Promise | null | DynamoDBBatchResponse>} A promise that resolves to a response object or null. * @throws {Error} If the response code is not within the success range and failure is not allowed. */ public async processEvent( execution: DynamoTransactionExecution ): Promise | null | DynamoDBBatchResponse> { const resp = await this.processRawEvent(execution) 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 a transaction on each record in the event. * @param {DynamoTransactionExecution} execution - The transaction execution function. * @returns {Promise | null | DynamoDBBatchResponse>} A promise that resolves to a response object, null, or a DynamoDB batch response. */ private async processRawEvent( execution: DynamoTransactionExecution ): Promise | null | DynamoDBBatchResponse> { // safe check for empty events? if (this.event.Records && this.event.Records.length > 0) { // Regular cleanup of tmp disk to avoid tmp overflow on reused lambdas if (!this.config?.skipCleanTmp) await Utils.cleanTemporaryFolder() // init transaction for all records return await new Transaction( this.event, this.context, { ...this.config, syncReturn: true, } ).execute(async transaction => { // for each available event const failureIDs: Array = [] for (const eventRecordIdx in this.event.Records) { const eventRecord = this.event.Records[eventRecordIdx] const record = { ...eventRecord, marshalled: { ...(eventRecord.dynamodb?.Keys ? { Keys: Utils.ddbUnmarshall(eventRecord.dynamodb?.Keys) } : {}), ...(eventRecord.dynamodb?.OldImage ? { OldImage: Utils.ddbUnmarshall(eventRecord.dynamodb?.OldImage) } : {}), ...(eventRecord.dynamodb?.NewImage ? { NewImage: Utils.ddbUnmarshall(eventRecord.dynamodb?.NewImage) } : {}), }, } // Call execution with marshalled item const resp = await execution(transaction, record) // 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(eventRecord.eventID!) 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 ) } }