UNPKG

mongo-bolt

Version:

A lightweight, beginner-friendly MongoDB wrapper with inbuilt caching and simplified joins/indexing.

364 lines (363 loc) 12.8 kB
import mongoose from 'mongoose'; import CacheManager from '../cache_manager/CacheManager.js'; class MongoDBConnector { constructor(config, maxSize = 100) { this.cache = null; this.aggregateCache = null; this.session = null; this.config = config; this.db = null; this.cache = new CacheManager(maxSize); this.aggregateCache = new CacheManager(maxSize); } async connect() { try { console.log('Connecting to MongoDB...'); try { await mongoose.connect(this.config.uri, this.config.options); this.db = mongoose.connection; } catch (err) { console.log('Error connecting to MongoDB:', err); } } catch (err) { if (err instanceof Error) { if (err.name === 'MongoServerError') { console.log(`Error connecting to MongoDB: - Check credentials (username, password, typos, special characters) - Verify database name - Ensure user has necessary permissions - Review connection string (hostname, port, username, password, URL format)`); } console.error(`Error: ${err.message}`); } else { console.error('An unknown error occurred'); } throw err; } } async getMongoose() { if (!this.db) { throw new Error('Database not connected'); } return mongoose; } async createStore(storeName, schema) { if (!this.db) { throw new Error('Database not connected'); } try { const collectionSchema = new mongoose.Schema(schema); mongoose.model(storeName, collectionSchema); } catch (err) { console.log(`There is an issue in creating the collection ${storeName}`); throw err; } } async insertItem(storeName, documentData) { if (!this.db) { throw new Error('Database not connected'); } try { const collection = this.db.collection(storeName); await collection.insertOne(documentData, this.session ? { session: this.session } : undefined); } catch (err) { console.log(`Error inserting item into ${storeName}`); throw err; } } async insertItems(storeName, documentsData) { if (!this.db) { throw new Error('Database not connected'); } try { console.log(documentsData, typeof documentsData); const collection = this.db.collection(storeName); await collection.insertMany(documentsData, this.session ? { session: this.session } : undefined); } catch (err) { console.log(`Error inserting item into ${storeName}`); throw err; } } async findItem(storeName, query = {}) { if (!this.db) { throw new Error('Database not connected'); } try { if (this.cache?.has(JSON.stringify(query))) { return this.cache?.get(JSON.stringify(query)); } const Collection = this.db.collection(storeName); const data = await Collection.find(query).toArray(); this.cache?.set(JSON.stringify(query), data); return data; } catch (err) { console.log(`Error reading from store ${storeName}`); throw err; } } async updateItems(storeName, query, update) { if (!this.db) { throw new Error('Database not connected'); } try { const Collection = this.db.collection(storeName); const result = await Collection.updateMany(query, update, this.session ? { session: this.session } : undefined); return result.modifiedCount; } catch (err) { console.log(`Error updating items in ${storeName}`); throw err; } } async deleteStore(storeName) { if (!this.db) { throw new Error('Database not connected'); } try { const Collection = this.db.collection(storeName); await Collection.drop(this.session ? { session: this.session } : undefined); } catch (err) { console.log(`Error deleting store ${storeName}`); throw err; } } async alterStore(storeName, newSchema, changeValues) { if (!this.db) { throw new Error('Database not connected'); } let originalSchema = null; let docs = []; try { const collection = mongoose.model(storeName); originalSchema = collection.schema; docs = await collection.find({}); await collection.deleteMany({}); delete mongoose.models[storeName]; const newCollection = mongoose.model(storeName, new mongoose.Schema(newSchema, { changeStreamPreAndPostImages: { enabled: true }, _id: true, id: true, typeKey: '_type', timestamps: true, versionKey: '__v' })); const promises = docs.map(async (doc) => { const newDoc = { ...doc, ...changeValues }; await newCollection.create(newDoc); }); await Promise.all(promises); } catch (err) { console.log('Datatypes are not compatible' + err); if (originalSchema) { const originalCollection = mongoose.model(storeName, originalSchema); await originalCollection.insertMany(docs); } throw new Error('Datatypes are not compatible' + err); } } async dropItem(storeName, itemInfo) { if (!this.db) { throw new Error('Database not connected'); } try { const Collection = this.db.collection(storeName); await Collection.deleteOne(itemInfo, this.session ? { session: this.session } : undefined); } catch (err) { console.log(`Failed to drop the item from ${storeName}`); throw new Error('Failed to drop the item'); } } async dropItems(storeName, itemInfo) { if (!this.db) { throw new Error('Database not connected'); } try { const Collection = this.db.collection(storeName); await Collection.deleteMany(itemInfo, this.session ? { session: this.session } : undefined); } catch (err) { console.log(`Failed to drop all the matching items from ${storeName}`); throw new Error('Failed to drop all the matching items'); } } async truncateStore(storeName) { if (!this.db) { throw new Error('Database not connected'); } try { const Collection = this.db.collection(storeName); await Collection.deleteMany({}, this.session ? { session: this.session } : undefined); } catch (err) { console.log('Truncate Store Error'); throw new Error(err); } } async findAndPopulateById(storeName, thingsToBePopulated, query) { if (!this.db) { throw new Error('Database not connected'); } try { const model = mongoose.model(storeName); return await model.find(query).populate(thingsToBePopulated).exec(); } catch (err) { console.log(`Error finding and populating item in ${storeName}`); throw err; } } async aggregate(storeName, pipeline) { if (!this.db) { throw new Error('Database not connected'); } try { if (this.aggregateCache?.has(JSON.stringify(pipeline))) { return this.aggregateCache?.get(JSON.stringify(pipeline)); } const collection = this.db.collection(storeName); const data = await collection.aggregate(pipeline).toArray(); this.aggregateCache?.set(JSON.stringify(pipeline), data); return data; } catch (err) { console.log(`Error aggregating data from ${storeName}`); throw err; } } async createIndex(storeName, column, order, options) { if (!this.db) { throw new Error('Database not connected'); } try { const collection = this.db.collection(storeName); const indexes = await this.viewIndexes(storeName); var found = false; indexes.forEach((index) => { if (index.key[column]) { console.log(`Index ${index.name} already exists in ${storeName}`); found = true; return; } }); if (found) { return; } await collection.createIndex({ [column]: order === 'asc' ? 1 : -1 }, options); } catch (err) { console.log(`Error creating index in ${storeName}`); throw err; } } async viewIndexes(storeName) { if (!this.db) { throw new Error('Database not connected'); } try { const collection = this.db.collection(storeName); return await collection.indexes(); } catch (err) { console.log(`Error viewing indexes in ${storeName}`); throw err; } } async dropIndex(storeName, indexName) { if (!this.db) { throw new Error('Database not connected'); } try { const collection = this.db.collection(storeName); await collection.dropIndex(indexName); } catch (err) { console.log(`Error dropping index ${indexName} in ${storeName}`); throw err; } } async findAndPopulateByKey(fromstoreName, query, fromKey, findinstoreName, inKey) { if (!this.db) { throw new Error('Database not connected'); } try { const collection = this.db.collection(fromstoreName); try { this.createIndex(findinstoreName, inKey, 'asc', true); } catch (err) { console.log('Error creating index for lookup collection'); } const pipeline = [ { $match: query }, { $lookup: { from: findinstoreName, let: { keys: `$${fromKey}` }, pipeline: [ { $match: { $expr: { $in: [`$${inKey}`, `$$keys`] } } } ], as: "populatedDocs" } } ]; if (this.aggregateCache?.has(JSON.stringify(pipeline))) { console.log('Cache hit for pipeline:', pipeline); return this.aggregateCache?.get(JSON.stringify(pipeline)); } const data = await collection.aggregate(pipeline).toArray(); this.aggregateCache?.set(JSON.stringify(pipeline), data); return data; } catch (err) { console.log(`Error finding and populating by key`); throw err; } } async startTransaction() { if (this.session) throw new Error('Transaction already in progress'); this.session = await mongoose.startSession(); this.session.startTransaction(); } async commitTransaction() { if (!this.session) throw new Error('No active transaction'); await this.session.commitTransaction(); this.session.endSession(); this.session = null; } async abortTransaction() { if (!this.session) throw new Error('No active transaction'); await this.session.abortTransaction(); this.session.endSession(); this.session = null; } async getSession() { if (!this.session) throw new Error('No active transaction'); return this.session; } async disconnect() { if (this.db) { try { await mongoose.disconnect(); } catch (err) { console.log('Error disconnecting from the database'); throw err; } } } } export default MongoDBConnector;