UNPKG

adonis-odm

Version:

A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support

229 lines (228 loc) 8.44 kB
import { MongoClient, } from 'mongodb'; import { ConnectionException, ConfigurationException, TransactionException, } from './exceptions/index.js'; import { ConcreteMongoTransactionClient } from './transaction_client.js'; /** * MongoDB Database Manager */ export class MongoDatabaseManager { clients = new Map(); databases = new Map(); config; constructor(config) { this.config = config; } /** * Get a MongoDB client connection */ connection(name) { const connectionName = name || this.config.connection; if (!this.clients.has(connectionName)) { throw new ConnectionException(connectionName, new Error('Connection not established')); } return this.clients.get(connectionName); } /** * Get a database instance */ db(name) { const connectionName = name || this.config.connection; if (!this.databases.has(connectionName)) { throw new ConnectionException(connectionName, new Error('Database not available')); } return this.databases.get(connectionName); } /** * Get a collection instance */ collection(name, connectionName) { const database = this.db(connectionName); return database.collection(name); } /** * Connect to all configured MongoDB instances */ async connect() { const connectionPromises = Object.entries(this.config.connections).map(async ([name, config]) => { try { const client = await this.createConnection(config); this.clients.set(name, client); // Get database name from URL or config const dbName = this.extractDatabaseName(config); const database = client.db(dbName); this.databases.set(name, database); } catch (error) { throw new ConnectionException(name, error); } }); await Promise.all(connectionPromises); } /** * Close all connections */ async close() { const closePromises = Array.from(this.clients.values()).map((client) => client.close()); await Promise.all(closePromises); this.clients.clear(); this.databases.clear(); } /** * Create a MongoDB client connection */ async createConnection(config) { let uri; if (config.connection.url) { uri = config.connection.url; } else { // Build URI from individual components const host = config.connection.host || 'localhost'; const port = config.connection.port || 27017; const database = config.connection.database || 'test'; const username = config.connection.username; const password = config.connection.password; // Build authentication part of URI let authString = ''; if (username && password) { // Encode username and password to handle special characters const encodedUsername = encodeURIComponent(username); const encodedPassword = encodeURIComponent(password); authString = `${encodedUsername}:${encodedPassword}@`; } else if (username) { // Username without password const encodedUsername = encodeURIComponent(username); authString = `${encodedUsername}@`; } uri = `mongodb://${authString}${host}:${port}/${database}`; } const options = { ...config.connection.options, }; // Apply driver-specific options if (config.useNewUrlParser !== undefined) { // Note: useNewUrlParser is deprecated in newer MongoDB drivers // but we'll include it for compatibility } if (config.useUnifiedTopology !== undefined) { // Note: useUnifiedTopology is deprecated in newer MongoDB drivers // but we'll include it for compatibility } const client = new MongoClient(uri, options); await client.connect(); // Test the connection await client.db().admin().ping(); return client; } /** * Extract database name from connection configuration */ extractDatabaseName(config) { if (config.connection.database) { return config.connection.database; } if (config.connection.url) { // Extract database name from URL const url = new URL(config.connection.url.replace('mongodb://', 'http://').replace('mongodb+srv://', 'https://')); const pathname = url.pathname.replace('/', ''); if (pathname) { return pathname; } } // Default database name return 'test'; } /** * Get connection configuration */ getConnectionConfig(name) { const connectionName = name || this.config.connection; const config = this.config.connections[connectionName]; if (!config) { throw new ConfigurationException(`Connection "${connectionName}" not found in configuration`); } return config; } /** * Check if a connection exists */ hasConnection(name) { return this.clients.has(name); } /** * Get all connection names */ getConnectionNames() { return Object.keys(this.config.connections); } /** * Start a transaction - Implementation */ async transaction(callbackOrOptions, options) { try { const actualOptions = typeof callbackOrOptions === 'function' ? options : callbackOrOptions; const connectionName = this.config.connection; // Get default connection let mongoClient; try { mongoClient = this.connection(connectionName); } catch (error) { throw new TransactionException('get connection', error); } let session; try { session = mongoClient.startSession(); } catch (error) { throw new TransactionException('start session', error); } const trxClient = new ConcreteMongoTransactionClient(session, this, connectionName); if (typeof callbackOrOptions === 'function') { const callback = callbackOrOptions; try { // Use MongoDB driver's withTransaction which handles retries for transient errors return await session.withTransaction(async () => { return callback(trxClient); }, actualOptions); } catch (error) { // Rollback is handled by withTransaction on error throw new TransactionException('execute transaction callback', error); } finally { try { session.endSession(); } catch (error) { // Log but don't throw session cleanup errors console.warn('Failed to end transaction session:', error); } } } else { // Manual transaction: Start it, but user must commit/rollback try { session.startTransaction(actualOptions); return trxClient; } catch (error) { try { session.endSession(); } catch (cleanupError) { // Log but don't throw session cleanup errors console.warn('Failed to end transaction session after start error:', cleanupError); } throw new TransactionException('start manual transaction', error); } } } catch (error) { // Re-throw TransactionException as-is, wrap others if (error instanceof TransactionException) { throw error; } throw new TransactionException('transaction operation', error); } } }