UNPKG

@axinom/mosaic-transactional-inbox-outbox

Version:

This library encapsulates the Mosaic based transactional inbox and outbox pattern

181 lines (171 loc) 6.79 kB
import { MessagingSettings } from '@axinom/mosaic-message-bus-abstractions'; import { DatabaseClient, MessageError, StoredTransactionalMessage, TransactionalMessageHandler, } from 'pg-transactional-outbox'; import { Dict, InboxOutboxLogger, TypedTransactionalMessage } from '../common'; interface HandlePreprocessor< TMessage, TContext extends Dict<unknown> = Dict<unknown>, > { ( message: TypedTransactionalMessage<TMessage>, envOwnerClient: DatabaseClient, error?: Error, ): Promise<TContext | undefined>; } export abstract class TransactionalInboxMessageHandler< TMessage, TConfig, TContext extends Dict<unknown> = Dict<unknown>, > implements TransactionalMessageHandler { public readonly aggregateType: string; public readonly messageType: string; /** * Create a new Message handler that provides both the functionality to store * an incoming RabbitMQ message in the inbox and provides the actual logic to * execute the business logic that is based on that message. * @param messagingSettings The definitions of the message that is handled. * @param config The service configuration object * @param handlePreprocessor A preprocessor that is called before the `handleMessage` and `handleErrorMessage`methods. It can be used for validations and to provide an optional context. */ constructor( protected messagingSettings: MessagingSettings, protected logger: InboxOutboxLogger, protected config: TConfig, protected handlePreprocessor?: HandlePreprocessor<TMessage, TContext>, ) { this.aggregateType = messagingSettings.aggregateType; this.messageType = messagingSettings.messageType; } /** * Implement to execute your custom business logic to handle a message that was stored in the inbox. * @param message The inbox message with the payload to handle. * @param envOwnerClient The database client that is part of a DB env owner transaction to safely handle the inbox message. * @throws If something fails and the inbox message should NOT be acknowledged - throw an error. */ abstract handleMessage( message: TypedTransactionalMessage<TMessage>, envOwnerClient: DatabaseClient, context?: TContext, ): Promise<void>; /** * Optionally override this method with your custom business logic to handle * an error that was caused by the `handleMessage` method. The default * implementation logs the error if there are no further retries. * @param error The error that was thrown in the `handleMessage` method. * @param message The inbox message with the payload that was attempted to be handled. * @param envOwnerClient The database client that is part of a (new) transaction to safely handle the error. * @param retry True if the message will be retried again. * @param context An optional context that can be provided */ public async handleErrorMessage( error: Error, message: TypedTransactionalMessage<TMessage>, _envOwnerClient: DatabaseClient, retry: boolean, _context?: TContext, ): Promise<void> { if (retry) { return; } this.logger.error(error, { message: `The final message handling attempt failed: ${error.message}`, details: { ...message }, }); } /** * Is called from the transactional inbox and calls the abstract * `handleMessage` function with the correct message payload typing. Do not * override this message but implement the `handleMessage` message. * @param message The inbox message with the payload to handle. * @param envOwnerClient The database client that is part of a DB env owner transaction to safely handle the inbox message. */ public async handle( message: StoredTransactionalMessage, envOwnerClient: DatabaseClient, ): Promise<void> { const msg = <TypedTransactionalMessage<TMessage>>message; const context = await this.handlePreprocessor?.(msg, envOwnerClient); return this.handleMessage(msg, envOwnerClient, context); } /** * Is called from the transactional inbox and calls the `handleErrorMessage` * function with the correct message payload typing. Do not override this * message but override the `handleErrorMessage` message. * @param error The error that was thrown in the handle method. * @param message The inbox message with the payload that was attempted to be handled. * @param envOwnerClient The database client that is part of a (new) transaction to safely handle the error. * @param retry True if the message will be retried again. * @returns A flag that defines if the message should be retried ('transient_error') or not ('permanent_error') */ public async handleError( error: Error, message: StoredTransactionalMessage, envOwnerClient: DatabaseClient, retry: boolean, ): Promise<void> { const msg = <TypedTransactionalMessage<TMessage>>message; const e = this.mapError( error instanceof MessageError && !!error.innerError ? error.innerError : error, ); let context: TContext | undefined; try { context = await this.handlePreprocessor?.(msg, envOwnerClient, e); this.updateErrorDetails(e, message, context); } catch (err) { this.logger.warn(err instanceof Error ? err : new Error(String(err)), { message: 'The preprocessor in the inbox message error handler failed.', details: { ...msg }, }); } await this.handleErrorMessage( e, <TypedTransactionalMessage<TMessage>>message, envOwnerClient, retry, context, ); } /** * Function to map a thrown error of an unspecified type to a potentially more * human-readable error or remove unsafe properties. */ public mapError(error: Error): Error { return error; } /** * Modifies the original error by extending its details subobject, if exists. * Default behavior adds tenantId and environmentId to the details object by * parsing the routing key. If still not available - returns original error. * Some routing keys will not have tenantId and environmentId, e.g. when a * command is sent. A separate handling is needed to extact these values, e.g. * from the jwt. */ protected updateErrorDetails( error: Error & { details?: Record<string, unknown> }, message: StoredTransactionalMessage, _context?: TContext, ): void { const [_serviceId, tenantId, environmentId] = ( (message.metadata?.fields as Record<string, string>)?.routingKey ?? '' ).split('.'); if ( tenantId && tenantId !== '*' && environmentId && environmentId !== '*' ) { error.details = { tenantId, environmentId, ...(error.details ?? {}), }; } } }