UNPKG

@aradox/multi-orm

Version:

Type-safe ORM with multi-datasource support, row-level security, and Prisma-like API for PostgreSQL, SQL Server, and HTTP APIs

327 lines 13.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MongoDBAdapter = void 0; const mongodb_1 = require("mongodb"); const logger_1 = require("../utils/logger"); class MongoDBAdapter { client; db; databaseName; modelMeta = new Map(); capabilities = { transactions: true, bulkByIds: true, maxIn: 100000, // MongoDB supports large arrays supportedOperators: ['eq', 'ne', 'in', 'notIn', 'gt', 'gte', 'lt', 'lte', 'contains', 'startsWith', 'endsWith'] }; /** * Set model metadata for collection name mapping */ setModelMetadata(models) { for (const [name, model] of Object.entries(models)) { this.modelMeta.set(name, model); } } constructor(connectionString) { if (typeof connectionString === 'string') { // Simple connection string (must include database name) const url = new URL(connectionString); const dbName = url.pathname.substring(1) || 'test'; this.databaseName = dbName; this.client = new mongodb_1.MongoClient(connectionString, { maxPoolSize: 10, minPoolSize: 0, maxIdleTimeMS: 30000, serverSelectionTimeoutMS: 30000 }); this.db = this.client.db(dbName); } else { // Full config with pool options this.databaseName = connectionString.database; this.client = new mongodb_1.MongoClient(connectionString.connectionString, { maxPoolSize: connectionString.maxPoolSize ?? 10, minPoolSize: connectionString.minPoolSize ?? 0, maxIdleTimeMS: connectionString.maxIdleTimeMS ?? 30000, serverSelectionTimeoutMS: connectionString.serverSelectionTimeoutMS ?? 30000 }); this.db = this.client.db(connectionString.database); logger_1.logger.info('mongodb', `Connection pool configured: max=${connectionString.maxPoolSize ?? 10}, min=${connectionString.minPoolSize ?? 0}`); } } getCollection(model) { // MongoDB typically uses lowercase collection names const collectionName = this.toCollectionName(model); return this.db.collection(collectionName); } toCollectionName(modelName) { // Convert PascalCase to lowercase (e.g., "User" -> "users", "OrderItem" -> "orderitems") return modelName.toLowerCase() + 's'; } async findMany(model, args) { const collection = this.getCollection(model); const filter = this.buildFilter(args.where); logger_1.logger.debug('mongodb', '📋 Find Many Filter:', JSON.stringify(filter)); let cursor = collection.find(filter); // Projection (select) if (args.select && Object.keys(args.select).length > 0) { const projection = {}; for (const [field, include] of Object.entries(args.select)) { if (include) { projection[field] = 1; } } cursor = cursor.project(projection); } // Sort (orderBy) if (args.orderBy) { const sort = {}; for (const [field, direction] of Object.entries(args.orderBy)) { sort[field] = direction === 'asc' ? 1 : -1; } cursor = cursor.sort(sort); } // Skip and Limit if (args.skip !== undefined) { cursor = cursor.skip(args.skip); } if (args.take !== undefined) { cursor = cursor.limit(args.take); } const results = await cursor.toArray(); return results.map((doc) => this.transformDocument(doc)); } async findUnique(model, args) { const collection = this.getCollection(model); const filter = this.buildFilter(args.where); logger_1.logger.debug('mongodb', '📋 Find Unique Filter:', JSON.stringify(filter)); const options = {}; if (args.select && Object.keys(args.select).length > 0) { const projection = {}; for (const [field, include] of Object.entries(args.select)) { if (include) { projection[field] = 1; } } options.projection = projection; } const doc = await collection.findOne(filter, options); return doc ? this.transformDocument(doc) : null; } async create(model, args) { const collection = this.getCollection(model); const doc = { ...args.data }; logger_1.logger.debug('mongodb', '📋 Insert Document:', JSON.stringify(doc)); const result = await collection.insertOne(doc); const inserted = await collection.findOne({ _id: result.insertedId }); return inserted ? this.transformDocument(inserted) : null; } async update(model, args) { const collection = this.getCollection(model); const filter = this.buildFilter(args.where); const update = { $set: args.data }; logger_1.logger.debug('mongodb', '📋 Update Filter:', JSON.stringify(filter)); logger_1.logger.debug('mongodb', '📋 Update Data:', JSON.stringify(update)); const result = await collection.findOneAndUpdate(filter, update, { returnDocument: 'after' }); return result ? this.transformDocument(result) : null; } async delete(model, args) { const collection = this.getCollection(model); const filter = this.buildFilter(args.where); logger_1.logger.debug('mongodb', '📋 Delete Filter:', JSON.stringify(filter)); const result = await collection.findOneAndDelete(filter); return result ? this.transformDocument(result) : null; } async count(model, args) { const collection = this.getCollection(model); const filter = this.buildFilter(args.where); logger_1.logger.debug('mongodb', '📋 Count Filter:', JSON.stringify(filter)); return await collection.countDocuments(filter); } buildFilter(where) { if (!where) return {}; const filter = {}; for (const [field, value] of Object.entries(where)) { // Handle _id field specially const mongoField = field === 'id' ? '_id' : field; if (value === null || value === undefined) { filter[mongoField] = value; } else if (typeof value === 'object' && !Array.isArray(value)) { // Operator object const conditions = {}; for (const [op, val] of Object.entries(value)) { const condition = this.buildOperatorCondition(op, val); if (condition !== null) { Object.assign(conditions, condition); } } if (Object.keys(conditions).length > 0) { filter[mongoField] = conditions; } } else { // Simple equality // Convert to ObjectId if field is _id and value is a string if (mongoField === '_id' && typeof value === 'string' && mongodb_1.ObjectId.isValid(value)) { filter[mongoField] = new mongodb_1.ObjectId(value); } else { filter[mongoField] = value; } } } return filter; } buildOperatorCondition(op, value) { switch (op) { case 'eq': return value; case 'ne': return { $ne: value }; case 'in': return { $in: value }; case 'notIn': return { $nin: value }; case 'gt': return { $gt: value }; case 'gte': return { $gte: value }; case 'lt': return { $lt: value }; case 'lte': return { $lte: value }; case 'contains': return { $regex: value, $options: 'i' }; case 'startsWith': return { $regex: `^${value}`, $options: 'i' }; case 'endsWith': return { $regex: `${value}$`, $options: 'i' }; default: return null; } } transformDocument(doc) { if (!doc) return doc; // Convert _id to id for consistency with SQL adapters const transformed = { ...doc }; if (transformed._id) { transformed.id = transformed._id.toString(); delete transformed._id; } return transformed; } async beginTransaction(options) { const session = this.client.startSession(); const txId = `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const self = this; logger_1.logger.debug('runtime', `Beginning MongoDB transaction ${txId}`); // Start transaction session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary' }); // Create transaction object const transaction = { id: txId, async commit() { logger_1.logger.debug('runtime', `Committing MongoDB transaction ${txId}`); await session.commitTransaction(); await session.endSession(); }, async rollback() { logger_1.logger.debug('runtime', `Rolling back MongoDB transaction ${txId}`); try { await session.abortTransaction(); } catch (error) { logger_1.logger.error('runtime', `Error rolling back MongoDB transaction ${txId}: ${error.message}`); } finally { await session.endSession(); } }, // CRUD operations within transaction use the session async findMany(model, args) { const collection = self.getCollection(model); const filter = self.buildFilter(args.where); let cursor = collection.find(filter, { session }); if (args.select && Object.keys(args.select).length > 0) { const projection = {}; for (const [field, include] of Object.entries(args.select)) { if (include) projection[field] = 1; } cursor = cursor.project(projection); } if (args.orderBy) { const sort = {}; for (const [field, direction] of Object.entries(args.orderBy)) { sort[field] = direction === 'asc' ? 1 : -1; } cursor = cursor.sort(sort); } if (args.skip !== undefined) cursor = cursor.skip(args.skip); if (args.take !== undefined) cursor = cursor.limit(args.take); const results = await cursor.toArray(); return results.map((doc) => self.transformDocument(doc)); }, async findUnique(model, args) { const collection = self.getCollection(model); const filter = self.buildFilter(args.where); const options = { session }; if (args.select && Object.keys(args.select).length > 0) { const projection = {}; for (const [field, include] of Object.entries(args.select)) { if (include) projection[field] = 1; } options.projection = projection; } const doc = await collection.findOne(filter, options); return doc ? self.transformDocument(doc) : null; }, async create(model, args) { const collection = self.getCollection(model); const doc = { ...args.data }; const result = await collection.insertOne(doc, { session }); const inserted = await collection.findOne({ _id: result.insertedId }, { session }); return inserted ? self.transformDocument(inserted) : null; }, async update(model, args) { const collection = self.getCollection(model); const filter = self.buildFilter(args.where); const update = { $set: args.data }; const result = await collection.findOneAndUpdate(filter, update, { returnDocument: 'after', session }); return result ? self.transformDocument(result) : null; }, async delete(model, args) { const collection = self.getCollection(model); const filter = self.buildFilter(args.where); const result = await collection.findOneAndDelete(filter, { session }); return result ? self.transformDocument(result) : null; }, async count(model, args) { const collection = self.getCollection(model); const filter = self.buildFilter(args.where); return await collection.countDocuments(filter, { session }); } }; return transaction; } async close() { await this.client.close(); } } exports.MongoDBAdapter = MongoDBAdapter; //# sourceMappingURL=mongodb.js.map