import type { APIGatewayEvent, Context, DynamoDBStreamEvent, SQSEvent } from 'aws-lambda' import Request from '../API/Request.js' import Response, { ResponseErrorType } from '../API/Response.js' import { DatabaseManager } from '../Database/DatabaseManager.js' import { DatabaseTransaction } from '../Database/DatabaseTransaction.js' import type { DatabaseImplType, DatabaseTransactionType, DatabaseType, DbConfig, } from '../Database/types.js' import Globals from '../Globals.js' import Logger, { LoggerConfig } from '../Logger/Logger.js' import Publisher, { PublisherConfig } from '../Publisher/Publisher.js' /** * Defines a type for executing a transaction and returning a promise with the response. * @param {TransactionType} transaction - The transaction to execute. * @returns A promise that resolves to the response of the transaction. */ export type TransactionExecution = ( transaction: TransactionType ) => Promise | Response | MiscRespType> /** * Represents the configuration options for a transaction. * @typedef {Object} TransactionConfig * @property {boolean} [throwOnErrors] - Whether to throw an error if there are any errors during the transaction. * @property {boolean} [syncReturn] - Whether to return the result of the transaction synchronously. * @property {boolean} [skipCleanTmp] - Whether to skip cleaning the temporary folder, in EventProcessor. * @property {LoggerConfig} [logger] - The configuration options for the logger. * @property {PublisherConfig} [publisher] - The configuration options for the publisher. */ export type TransactionConfig = { throwOnErrors?: boolean syncReturn?: boolean skipCleanTmp?: boolean // logger?: LoggerConfig publisher?: PublisherConfig } /** * Represents a simple string dictionary with string values */ export type StringMap = { [key: string]: string | null } /** * Represents a transaction object that handles the execution of a request and manages the response. * @template InputType - The type of the input data for the transaction. * @template ResponseInnerType - The type of the inner response data for the transaction. * @template MiscRespType - The type of miscellaneous response data for the transaction. */ export default class Transaction< InputType = never, ResponseInnerType = null, MiscRespType = never, PathParamsType = StringMap, QueryParamsType = StringMap, > { /** * The instance of the DatabaseManager class used for managing the database. */ private databaseManager: DatabaseManager = DatabaseManager.INSTANCE /** * An array of database transactions. * @type {DatabaseTransaction[]} */ private transactions: DatabaseTransaction[] = [] /** * Represents an event object. * @private * @type {any} */ private event: any /** * The context object for the current instance. */ private context: Context /** * The response object that can hold different types of responses. * @type {Response | MiscRespType | null} */ private response!: Response | MiscRespType /** * A private boolean variable indicating whether the return value of a synchronous operation * should be synchronized with the calling thread. */ private syncReturn: boolean /** * A boolean flag indicating whether retroactive errors are enabled or not. * @private */ private retrowErrors: boolean /** * A logger object used for logging messages, errors, and other information. * @readonly */ public readonly logger: Logger /** * The request object for making a request of type InputType. * @readonly */ public readonly request: Request /** * The publisher of the content. */ public readonly publisher: Publisher /** * A function that acts as a response proxy for a given response object. * @param {Response} response - The response object to proxy. * @returns A promise that resolves to void. */ public responseProxy: ((response: Response) => Promise) | null /** * Constructs a new instance of the Transaction class. * @param {APIGatewayEvent | SQSEvent | DynamoDBStreamEvent} event - The event object passed to the Lambda function. * @param {Context} context - The context object passed to the Lambda function. * @param {TransactionConfig} [config] - Optional configuration object for the transaction. * @returns None */ constructor( event: APIGatewayEvent | SQSEvent | DynamoDBStreamEvent, context: Context, config?: TransactionConfig ) { const transactionId = context.awsRequestId ? context.awsRequestId : (event).requestContext ? (event).requestContext.requestId : 'unknown' // transaction ctx this.event = event this.context = context // when set, this will be called with the response context right before calling the context suceed/fail - useful for writing the resp for example. this.responseProxy = null // transaction flags this.syncReturn = !!config?.syncReturn this.retrowErrors = !!config?.throwOnErrors /* retrow internal errors */ // components const isHealthCheck = Request.isHealthCheckPath((event).path || '') this.logger = new Logger( { ...config?.logger, silent: isHealthCheck || !!config?.logger?.silent }, transactionId ) this.request = new Request( this.event, this.context, this.logger ) this.publisher = new Publisher(config?.publisher) } /** * Executes a transaction using the provided execution function and returns a promise * that resolves to the response or miscellaneous response. * @param {TransactionExecution, ResponseInnerType, MiscRespType>} executionFunc - The execution function to be executed. * @returns {Promise | MiscRespType>} - A promise that resolves to the response or miscellaneous response. */ public async execute( executionFunc: TransactionExecution< Transaction, ResponseInnerType, MiscRespType > ): Promise | MiscRespType | null> { await this.executeLoggerFlush(async () => { await this.executeDBTransactions(async () => { return await this.iexecute(executionFunc) }) }) // return raw response if sync return is requested if (this.syncReturn) return this.response // allow request to async succeed through lambda context return null } /** * Executes a transaction using the provided execution function and handles the response. * @param {TransactionExecution, ResponseInnerType, MiscRespType>} executionFunc - The function to execute the transaction. * @returns {Promise} - A promise that resolves to a boolean indicating whether the execution failed or not. */ private async iexecute( executionFunc: TransactionExecution< Transaction, ResponseInnerType, MiscRespType > ): Promise { let executionFailed = true //failed til we say no! //safe execution handler try { //Execute this.logger.debug('Starting main request code') this.response = await executionFunc(this) //Answer client if (this.response && this.response instanceof Response) { await this.response.build(this.context, this, this.syncReturn) executionFailed = !!(this.response.getBody() && this.response.getBody()['rollback']) } else if (this.syncReturn && this.response) { this.logger.log('Sync return with different response object') this.logger.debug(this.response) executionFailed = false } else { this.response = this.getErrorResponse( Globals.ErrorResponseInvalidServerResponse, Globals.ErrorCode_APIError ) await this.response.build(this.context, this, this.syncReturn) this.logger.error('Invalid response object from main request code.') } } catch (e) { /*EXECUTION FAIL*/ this.logger.error('Exception when executing main request code.') this.logger.exception(e) //retrow? if (this.retrowErrors) throw e //envelope exception? this.response = this.getErrorResponse( Globals.ErrorResponseUnhandledError, Globals.ErrorCode_APIError ) await this.response.build(this.context, this, this.syncReturn) } return executionFailed } /** * Retrieves a database transaction based on the provided database configuration. * @param {DbConfig} config - The configuration object for the database. * @returns {Promise>} A promise that resolves to the database transaction. */ public async getDbTransaction( config: DbConfig ): Promise> { const db = this.getDatabase(config) const dbTrans = await db.transaction() this.transactions.push(dbTrans) return dbTrans as any } /** * Retrieves a database instance based on the provided configuration. * @param {DbConfig} config - The configuration object specifying the type of database. * @returns {DatabaseImplType} A database instance based on the provided configuration. */ public getDatabase( config: DbConfig ): DatabaseImplType { return this.databaseManager.create(config) } /** * Get the remaining time in milliseconds for the current execution context. * @returns {number} The remaining time in milliseconds, or -1 if the time is not available. */ public getRemainingTimeInMillis() { return this.context?.getRemainingTimeInMillis?.() || -1 } /* * Executes a series of database transactions in a safe manner. * @param {Function} safeExecution - The function that contains the database transactions to be executed. * @returns None * @throws {Error} - If an exception occurs during the execution of the transactions and `retrowErrors` is true. */ private async executeDBTransactions(safeExecution: () => Promise): Promise { try { // Execute const execFailed = await safeExecution() for (const transaction of [...this.transactions].reverse()) { try { await transaction[execFailed ? 'closeFailure' : 'closeSuccess']() } catch (e) { // TODO: should we keep committing transactions even if one fails? this.logger.error('Exception when closing DB transactions after success.') this.logger.exception(e) } } } catch (e) { /* this part of the code handle exceptions at transaction level, so probably a bug but we still handle such */ for (const transaction of [...this.transactions].reverse()) { try { await transaction.closeFailure() } catch (e) { this.logger.error('Exception when closing DB transactions after failure.') this.logger.exception(e) } } this.logger.error('Exception when executing DB transactions.') this.logger.log((e as Error).stack) //retrow? if (this.retrowErrors) throw e } } /** * Executes a logger flush operation with error handling and logging. * @param {Function} safeExecution - The function to execute safely. * @returns None * @throws {Error} - If `retrowErrors` is true and an error occurs during execution. */ private async executeLoggerFlush(safeExecution): Promise { try { await safeExecution() } catch (e) { this.logger.error('Exception when flushing logs.') this.logger.exception(e) //retrow? if (this.retrowErrors) throw e } finally { this.logger.debug('Transaction ended') } } /** * Returns an error response with the specified error message and error code. * @param {string} error - The error message. * @param {string} code - The error code. * @returns {Response} - The error response. */ private getErrorResponse(error: string, code: string): Response { return Response.BadRequestResponseWithRollback(error, code) } }