@cpbs-cp/js-mongoose
v1.0.7
Published
A Mongoose-like ODM for MongoDB — built from scratch. Supports JS and TypeScript.
Maintainers
Readme
@cpbs-cp/js-mongoose
A Mongoose-compatible ODM for MongoDB — built from scratch. Works as a drop-in replacement for Mongoose in JavaScript and TypeScript projects. No Mongoose dependency — only the official mongodb driver.
Installation
npm install @cpbs-cp/js-mongooseQuick Start
const mongoose = require('@cpbs-cp/js-mongoose');
const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: Number,
}, { timestamps: true });
const User = mongoose.model('User', UserSchema);
async function main() {
await mongoose.connect('mongodb://localhost:27017/mydb');
const user = await User.create({ name: 'Alice', email: '[email protected]', age: 30 });
console.log(user._id, user.name);
const found = await User.findOne({ email: '[email protected]' });
console.log(found.toObject());
await mongoose.disconnect();
}
main();TypeScript Usage
import mongoose from '@cpbs-cp/js-mongoose/ts';
// or
import mongoose = require('@cpbs-cp/js-mongoose/ts');
const schema = new mongoose.Schema({
name: String,
createdAt: Date,
});
const MyModel = mongoose.model('MyModel', schema);API Reference
Connection
// Connect
await mongoose.connect('mongodb://localhost:27017/dbname', options);
// Disconnect
await mongoose.disconnect();
// Create a separate connection
const conn = mongoose.createConnection('mongodb://localhost:27017/dbname');
// Access default connection
mongoose.connection.readyState; // 0=disconnected, 1=connected, 2=connecting
// Switch database on the same client
mongoose.connection.useDb('otherDb');
// List all collections
const cols = await mongoose.connection.listCollections();
// Events
mongoose.on('connected', () => console.log('DB connected'));
mongoose.on('error', (err) => console.error(err));Schema
const { Schema } = require('@cpbs-cp/js-mongoose');
const schema = new Schema(
{
name: { type: String, required: true, trim: true },
email: { type: String, lowercase: true, unique: true },
age: { type: Number, min: 0, max: 120 },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
tags: [String],
meta: Schema.Types.Mixed,
ref: { type: Schema.Types.ObjectId, ref: 'OtherModel' },
},
{
timestamps: true, // adds createdAt, updatedAt
versionKey: '__v', // default version key
collection: 'myitems', // override collection name
}
);
// Hooks
schema.pre('save', function (next) {
this.name = this.name.toUpperCase();
next();
});
schema.post('save', function (doc) {
console.log('saved:', doc._id);
});
// Regex hooks (matches any event)
schema.pre(/.*/, function (next) { next(); });
// Virtuals
schema.virtual('fullName').get(function () {
return `${this.firstName} ${this.lastName}`;
});
// Virtual populate
schema.virtual('orders', {
ref: 'Order',
localField: '_id',
foreignField: 'userId',
justOne: false,
});
// Instance methods
schema.methods.greet = function () {
return `Hello, ${this.name}`;
};
// Static methods
schema.statics.findByEmail = function (email) {
return this.findOne({ email });
};
// Indexes
schema.index({ email: 1 }, { unique: true });
schema.index({ createdAt: -1 });
// Plugins
schema.plugin(require('some-plugin'), { option: true });Schema Types
| Type | Shorthand | Notes |
|---|---|---|
| String | String | trim, lowercase, uppercase, minlength, maxlength, match |
| Number | Number | min, max |
| Boolean | Boolean | |
| Date | Date | min, max |
| Schema.Types.ObjectId | ObjectId | auto-cast from 24-char hex strings |
| Array | [String] | array of any type |
| Schema.Types.Mixed | Object | any value |
| Buffer | Buffer | |
| Map | Map | |
Model
const MyModel = mongoose.model('MyModel', schema);
// ── Create ────────────────────────────────────────────────────────────────
await MyModel.create({ name: 'Alice' });
await MyModel.create([{ name: 'A' }, { name: 'B' }]);
await MyModel.create({ name: 'Alice' }, { session }); // transaction
await MyModel.insertMany([{ name: 'A' }, { name: 'B' }]);
await MyModel.insertMany(docs, { session }); // transaction
// ── Read ──────────────────────────────────────────────────────────────────
await MyModel.find({ age: { $gte: 18 } });
await MyModel.find({ role: 'admin' }).select('name email').sort('-createdAt').limit(10).lean();
await MyModel.findOne({ email: '[email protected]' });
await MyModel.findById('64abc123...');
await MyModel.exists({ email: '[email protected]' }); // returns { _id } or null
await MyModel.countDocuments({ role: 'user' });
await MyModel.estimatedDocumentCount();
await MyModel.distinct('role');
// ── Update ────────────────────────────────────────────────────────────────
await MyModel.updateOne({ _id: id }, { $set: { name: 'Bob' } });
await MyModel.updateMany({ role: 'user' }, { $set: { active: true } });
await MyModel.findOneAndUpdate(
{ _id: id },
{ $set: { name: 'Bob' } },
{ new: true, upsert: false }
);
await MyModel.findOneAndUpdate(filter, update, { session }); // transaction
await MyModel.findOneAndUpdate(filter, update).lean();
await MyModel.findByIdAndUpdate(id, update, { new: true });
// ── Delete ────────────────────────────────────────────────────────────────
await MyModel.deleteOne({ _id: id });
await MyModel.deleteMany({ active: false });
await MyModel.findOneAndDelete({ _id: id });
await MyModel.findByIdAndDelete(id);
// ── Aggregate ─────────────────────────────────────────────────────────────
const results = await MyModel.aggregate([
{ $match: { role: 'admin' } },
{ $group: { _id: '$role', count: { $sum: 1 } } },
]);
// Chainable aggregate
const agg = MyModel.aggregate()
.match({ active: true })
.group({ _id: '$role', total: { $sum: 1 } })
.sort({ total: -1 });
const data = await agg;
// ── Pagination ────────────────────────────────────────────────────────────
const page = await MyModel.paginate(
{ role: 'user' },
{ page: 1, limit: 10, sort: '-createdAt', lean: true }
);
// page.docs, page.totalDocs, page.totalPages, page.hasNextPage ...
const aggPage = await MyModel.aggregatePaginate(
MyModel.aggregate([{ $match: { active: true } }]),
{ page: 1, limit: 10 }
);
// ── Populate ──────────────────────────────────────────────────────────────
await MyModel.find().populate('userId');
await MyModel.find().populate({ path: 'userId', select: 'name email' });
// Static populate
await MyModel.populate(docs, { path: 'userId', model: 'User' });
// ── Other ─────────────────────────────────────────────────────────────────
await MyModel.bulkWrite([
{ insertOne: { document: { name: 'X' } } },
{ updateOne: { filter: { name: 'Y' }, update: { $set: { active: false } } } },
]);
await MyModel.watch([{ $match: { operationType: 'insert' } }]);
await MyModel.init(); // wait for connection + ensure indexes
await MyModel.syncIndexes(); // create all schema indexes
await MyModel.validate(obj); // validate a plain object against schema
MyModel.castObject(obj); // cast a plain object to schema typesDocument (Model Instance)
const doc = new MyModel({ name: 'Alice', email: '[email protected]' });
// Save
await doc.save();
await doc.save({ session }); // inside a transaction
// Field access
doc.get('address.city');
doc.set('address.city', 'Chennai');
doc.$get('name');
doc.$set('name', 'Bob');
// Dirty tracking
doc.isModified(); // true if any field changed
doc.isModified('name'); // true if 'name' changed
doc.markModified('meta'); // force mark as modified
doc.unmarkModified('meta');
doc.modifiedPaths(); // ['name', 'email']
// Validation
await doc.validate();
doc.invalidate('email', 'invalid format');
doc.$isDefault('role'); // true if value equals schema default
// Serialisation
doc.toObject();
doc.toObject({ virtuals: true });
doc.toJSON();
// Population
await doc.populate('userId');
doc.populated('userId'); // returns ObjectId if populated
doc.depopulate('userId'); // resets back to ObjectId
// Delete
await doc.remove();
await doc.deleteOne();Query Chaining
MyModel
.find({ active: true })
.select('name email role')
.sort({ createdAt: -1 })
.skip(0)
.limit(20)
.lean()
.populate('userId')
.session(session) // attach ClientSession (transaction)
.allowDiskUse(true)
.hint({ email: 1 })
.maxTimeMS(5000)
.collation({ locale: 'en' })
.comment('my query')
.exec();
// Clone a query
const base = MyModel.find({ active: true });
const q1 = base.clone().limit(5);
const q2 = base.clone().sort('-createdAt');
// Options helpers
query.getOptions(); // { sort, limit, skip, ... }
query.setOptions({ limit: 10, sort: { name: 1 } });Transactions
// Using withTransaction (auto session management)
await mongoose.connection.withTransaction(async (session) => {
await OrderModel.create({ items: [...] }, { session });
await InventoryModel.updateOne({ _id: itemId }, { $inc: { stock: -1 } }, { session });
});
// Using transaction() shorthand (Mongoose 6 compat)
await mongoose.connection.transaction(async (session) => {
const txn = await TransactionModel.create({ rrn: '123456' }, { session });
await LedgerModel.create({ ref: txn._id }, { session });
});
// Manual session
const session = await mongoose.connection.startSession();
session.startTransaction();
try {
await MyModel.create({ name: 'X' }, { session });
await MyModel.updateOne({ _id: id }, { $set: { done: true } }, { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}
// Query-level session
await MyModel.find({ active: true }).session(session);
await MyModel.updateOne(filter, update).session(session);
await MyModel.deleteOne(filter).session(session);
// Document-level session
const doc = await MyModel.findById(id).session(session);
await doc.save({ session });ObjectId Utilities
const { ObjectId } = require('@cpbs-cp/js-mongoose');
mongoose.isValidObjectId('64abc1234567890123456789'); // true
mongoose.isObjectIdOrHexString(id); // true
mongoose.Types.ObjectId; // MongoDB ObjectId class
// Auto-cast: any 24-char hex string in a filter is cast to ObjectId
await MyModel.findOne({ _id: '64abc1234567890123456789' }); // works
await MyModel.find({ userId: '64abc1234567890123456789' }); // works if userId is ObjectId type
// $or / $and filters also auto-cast
await MyModel.find({
$or: [
{ _id: '64abc1234567890123456789' },
{ userId: '64abc1234567890123456789' }
]
});Global Settings
mongoose.set('debug', true); // log all queries
mongoose.set('debug', (col, method, q) => console.log(col, method));
mongoose.set('bufferCommands', true); // buffer ops until connected
mongoose.set('bufferTimeoutMS', 10000); // buffer timeout
mongoose.get('debug'); // read a setting
mongoose.pluralize(null); // disable pluralization
mongoose.now(); // current Date
mongoose.Decimal128; // BSON Decimal128 type
mongoose.trusted(val); // Mongoose 6 compat
mongoose.sanitizeFilter(filter); // Mongoose 6 compat
mongoose.overwriteMiddlewareResult(val); // Mongoose 6 compatBSON Cross-Version Compatibility
This package handles the BSON version mismatch between bson 4.x (used by some microservices) and mongodb driver 6.x (which bundles bson 6.x) automatically.
- All filters, documents, and arguments passed to the MongoDB driver are sanitized via
bson-sanitize.js - Foreign ObjectIds (from any bson version) are converted to the driver's own ObjectId using the hex string as a bridge
- No "Unsupported BSON version" errors
Differences from Mongoose
| Feature | Notes |
|---|---|
| Model.discriminator() | Not implemented |
| mongoose.mquery | Internal — not exposed |
| Global mongoose.plugin() | Stored but not auto-applied to future schemas |
| Strict mode | Defaults to true; src/ strips unknown fields, ts-src/ preserves them |
Import Paths
// JavaScript (default) — with BSON sanitize proxy
const mongoose = require('@cpbs-cp/js-mongoose');
// TypeScript / NAC microservice path — without BSON proxy layer
const mongoose = require('@cpbs-cp/js-mongoose/ts');
import mongoose from '@cpbs-cp/js-mongoose/ts';License
MIT © varadharaj
