UNPKG

pg-transactional-outbox

Version:

A PostgreSQL based transactional outbox and inbox pattern implementation to support exactly once message processing (with at least once message delivery).

115 lines (107 loc) 3.41 kB
import { DatabaseClient } from '../common/database'; import { MessageError } from '../common/error'; import { ListenerConfig, ListenerSettings } from '../common/listener-config'; import { TransactionalLogger } from '../common/logger'; import { TransactionalMessage } from './transactional-message'; export interface MessageStorage { (message: TransactionalMessage, client: DatabaseClient): Promise<void>; } /** * Initialize the message storage to store outbox or inbox messages in the corresponding table. * @param config The configuration object that defines the values on how to connect to the database and general settings. * @param logger A logger instance for logging trace up to error logs * @returns Initializes the function to store the outbox or inbox message data to the database and provides the shutdown action. */ export const initializeMessageStorage = ( { settings, outboxOrInbox, }: Pick<ListenerConfig, 'settings' | 'outboxOrInbox'>, logger: TransactionalLogger, ): MessageStorage => { /** * The function to store the message data to the database. * @param message The received message that should be stored as a outbox or inbox message * @param client A database client with an active transaction(!) can be provided. Otherwise * @throws Error if the message could not be stored */ return async ( message: TransactionalMessage, client: DatabaseClient, ): Promise<void> => { try { await insertMessage(message, client, settings, logger); } catch (err) { const messageError = new MessageError( `Could not store the ${outboxOrInbox} message with id ${message.id}`, 'MESSAGE_STORAGE_FAILED', message, err, ); logger.error( messageError, `Could not store the ${outboxOrInbox} message`, ); throw messageError; } }; }; const insertMessage = async ( message: TransactionalMessage, client: DatabaseClient, { dbSchema, dbTable }: ListenerSettings, logger: TransactionalLogger, ) => { const { id, aggregateType, aggregateId, messageType, segment, payload, metadata, concurrency, createdAt, lockedUntil, } = message; const insertValues = [ id, aggregateType, aggregateId, messageType, segment, payload, metadata, ]; let optionalPlaceholders = ''; const addPlaceholder = () => (optionalPlaceholders += `, $${insertValues.length}`); let optionalFields = ''; const addOptionalFields = (name: string) => (optionalFields += `, ${name}`); if (concurrency) { insertValues.push(concurrency); addOptionalFields('concurrency'); addPlaceholder(); } if (createdAt) { insertValues.push(createdAt); addOptionalFields('created_at'); addPlaceholder(); } if (lockedUntil) { insertValues.push(lockedUntil); addOptionalFields('locked_until'); addPlaceholder(); } const messageResult = await client.query( /* sql */ ` INSERT INTO ${dbSchema}.${dbTable} (id, aggregate_type, aggregate_id, message_type, segment, payload, metadata${optionalFields}) VALUES ($1, $2, $3, $4, $5, $6, $7${optionalPlaceholders}) ON CONFLICT (id) DO NOTHING`, insertValues, ); if (!messageResult.rowCount || messageResult.rowCount < 1) { logger.warn(message, `The message with id ${id} already existed`); } };