@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
JavaScript
;
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