mongo-bolt
Version:
A lightweight, beginner-friendly MongoDB wrapper with inbuilt caching and simplified joins/indexing.
364 lines (363 loc) • 12.8 kB
JavaScript
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;