UNPKG

outbox-service

Version:

MongoDB Outbox Pattern implementation with AWS SQS integration

139 lines (120 loc) 3.82 kB
const { MongoClient } = require('mongodb'); const AWS = require('aws-sdk'); class MongoDBOutboxSQS { constructor(config) { this.mongoUri = config.mongoUri; this.dbName = config.dbName; this.outboxCollection = config.outboxCollection || 'outbox'; this.sqsConfig = { region: config.awsRegion || 'ap-southeast-1', accessKeyId: config.awsAccessKeyId, secretAccessKey: config.awsSecretAccessKey, queueUrl: config.sqsQueueUrl, }; this.sqs = new AWS.SQS(this.sqsConfig); this.client = null; this.db = null; } async connect() { if (!this.client) { this.client = new MongoClient(this.mongoUri); await this.client.connect(); this.db = this.client.db(this.dbName); } return this.db; } async close() { if (this.client) { await this.client.close(); this.client = null; this.db = null; } } /** * Execute a transaction with the outbox pattern * @param {string} collectionName - The target collection name * @param {Function} operation - Function that takes a collection and performs the operation * @param {Object} eventPayload - The event payload to be sent to SQS * @param {string} eventType - The type of event * @returns {Object} - Result of the transaction */ async executeWithOutbox(collectionName, operation, eventPayload, eventType) { const db = await this.connect(); const session = this.client.startSession(); try { let result; await session.withTransaction(async () => { // 1. Perform the main operation on the target collection const collection = db.collection(collectionName); result = await operation(collection, session); // 2. Insert into outbox collection const outboxCollection = db.collection(this.outboxCollection); const outboxDocument = { eventType, payload: eventPayload, status: 'PENDING', createdAt: new Date(), }; await outboxCollection.insertOne(outboxDocument, { session }); }); // 3. After transaction is committed, process the outbox await this.processOutbox(); return result; } catch (error) { console.error('Transaction failed:', error); throw error; } finally { await session.endSession(); } } async processOutbox() { const db = await this.connect(); const outboxCollection = db.collection(this.outboxCollection); // Find all pending outbox messages const pendingEvents = await outboxCollection.find({ status: 'PENDING' }).toArray(); for (const event of pendingEvents) { try { // send to sqs await this.sendToSQS(event.payload); // udpate status to PROCESSED await outboxCollection.updateOne( { _id: event._id }, { $set: { status: 'PROCESSED', processedAt: new Date() } } ); } catch (error) { console.error(`Failed to process outbox event ${event._id}`, error); // update status to FAILED await outboxCollection.updateOne( { _id: event._id }, { $set: { status: 'FAILED', error: error.message, errorAt: new Date(), } } ); } } } /** * Send a message to SQS * @param {Object} message - The message to send */ async sendToSQS(message) { const params = { QueueUrl: this.sqsConfig.queueUrl, MessageBody: JSON.stringify(message), }; return new Promise((resolve, reject) => { this.sqs.sendMessage(params, (err, data) => { if (err) { reject(err); } else { resolve(data); } }) }) } } module.exports = { MongoDBOutboxSQS };