UNPKG

localgoose

Version:

A lightweight, file-based ODM Database for Node.js, inspired by Mongoose

655 lines (541 loc) 15.8 kB
# Localgoose A lightweight, file-based ODM (Object-Document Mapper) for Node.js, inspired by Mongoose but designed for local JSON storage. Perfect for prototypes, small applications, and scenarios where a full MongoDB setup isn't needed. ## Features - 🚀 Mongoose-like API for familiar development experience - 📁 JSON file-based storage - 🔄 Schema validation and type casting - 🎯 Rich query API with chainable methods - 📊 Aggregation pipeline support - 🔌 Virtual properties and middleware hooks - 🏃‍♂️ Zero external dependencies (except BSON for ObjectIds) - 🔗 Support for related models and references - 📝 Comprehensive CRUD operations - 🔍 Advanced querying and filtering - 🔎 Full-text search capabilities - 📑 Compound indexing support - 🔄 Schema inheritance and discrimination - 🎨 Custom type casting and validation - 🗄️ Backup and restore functionality - 🧩 Custom types and schema inheritance - 🛠️ Middleware hooks for documents, queries, and aggregations - 🌐 Geospatial queries and indexing - 📅 Date operators and bitwise operators ## Installation ```bash npm install localgoose ``` ## Quick Start ```javascript const { localgoose } = require('localgoose'); // Connect to a local directory for storage const db = localgoose.connect('./mydb'); // Define schemas for related models const userSchema = new localgoose.Schema({ username: { type: String, required: true }, email: { type: String, required: true }, age: { type: Number, required: true }, tags: { type: Array, default: [] } }); const postSchema = new localgoose.Schema({ title: { type: String, required: true }, content: { type: String, required: true }, author: { type: localgoose.Schema.Types.ObjectId, ref: 'User' }, likes: { type: Number, default: 0 } }); // Create models const User = db.model('User', userSchema); const Post = db.model('Post', postSchema); // Create a user const user = await User.create({ username: 'john', email: 'john@example.com', age: 25, tags: ['developer'] }); // Create a post with reference to user const post = await Post.create({ title: 'Getting Started', content: 'Hello World!', author: user._id }); // Query with population const posts = await Post.find() .populate('author') .sort('-likes') .exec(); // Use aggregation pipeline const stats = await Post.aggregate() .match({ author: user._id }) .group({ _id: null, totalPosts: { $sum: 1 }, avgLikes: { $avg: '$likes' } }) .exec(); ``` ## API Reference ### Connection ```javascript // Connect to database const db = await localgoose.connect('./mydb'); // Create separate connection const connection = await localgoose.createConnection('./mydb'); ``` ### Schema Definition ```javascript const schema = new localgoose.Schema({ // Basic types string: { type: String, required: true }, number: { type: Number, default: 0 }, boolean: { type: Boolean }, date: { type: Date, default: Date.now }, objectId: { type: localgoose.Schema.Types.ObjectId, ref: 'OtherModel' }, buffer: localgoose.Schema.Types.Buffer, uuid: localgoose.Schema.Types.UUID, bigInt: localgoose.Schema.Types.BigInt, mixed: localgoose.Schema.Types.Mixed, map: localgoose.Schema.Types.Map, // Arrays and Objects array: { type: Array, default: [] }, object: { type: Object, default: { key: 'value' } } }); // Virtual properties schema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`; }); // Instance methods schema.method('getInfo', function() { return `${this.username} (${this.age})`; }); // Static methods schema.static('findByEmail', function(email) { return this.findOne({ email }); }); // Middleware schema.pre('save', function() { this.updatedAt = new Date(); }); schema.post('save', function() { console.log('Document saved:', this._id); }); // Indexes schema.index({ email: 1 }, { unique: true }); schema.index({ title: 'text', content: 'text' }); ``` ### Model Operations #### Create ```javascript // Create a single document const doc = await Model.create({ field: 'value' }); // Create multiple documents const docs = await Model.create([ { field: 'value1' }, { field: 'value2' } ]); // Insert many documents const docs = await Model.insertMany([ { field: 'value1' }, { field: 'value2' } ], { ordered: true, // Optional: documents are inserted in order lean: true // Optional: returns plain objects instead of documents }); ``` #### Read ```javascript // Find all documents const docs = await Model.find(); // Find with specific conditions const docs = await Model.find({ field: 'value', number: { $gt: 10 } }); // Find one document const doc = await Model.findOne({ field: 'value' }); // Find by ID const doc = await Model.findById(id); // Count documents const count = await Model.countDocuments({ field: 'value' }); // Estimated count (faster but not exact) const count = await Model.estimatedDocumentCount(); // Check if document exists const exists = await Model.exists({ field: 'value' }); // Get distinct values const values = await Model.distinct('field', { type: 'specific' }); // Find with population const doc = await Model.findOne({ field: 'value' }) .populate('reference') .exec(); ``` #### Update ```javascript // Update one document const result = await Model.updateOne( { field: 'value' }, // filter { $set: { newField: 'new' }}, // update { upsert: true } // options ); // Update many documents const result = await Model.updateMany( { field: 'value' }, { $set: { newField: 'new' }} ); // Find one and update const doc = await Model.findOneAndUpdate( { field: 'value' }, { $set: { newField: 'new' }}, { new: true, // return updated document upsert: true // create if not exists } ); // Find by ID and update const doc = await Model.findByIdAndUpdate( id, { $set: { field: 'new' }}, { new: true } ); // Replace one document const result = await Model.replaceOne( { field: 'value' }, { newDocument: true } ); // Increment a field const result = await Model.increment( { field: 'value' }, // filter 'counter', // field to increment 5 // increment amount (default: 1) ); ``` #### Delete ```javascript // Delete one document const result = await Model.deleteOne({ field: 'value' }); // Delete many documents const result = await Model.deleteMany({ field: 'value' }); // Find one and delete const doc = await Model.findOneAndDelete({ field: 'value' }); // Find by ID and delete const doc = await Model.findByIdAndDelete(id); // Find by ID and remove (alias for findByIdAndDelete) const doc = await Model.findByIdAndRemove(id); ``` #### Bulk Operations ```javascript // Bulk write operations const result = await Model.bulkWrite([ { insertOne: { document: { field: 'value' } } }, { updateOne: { filter: { field: 'value' }, update: { $set: { field: 'new' }} } }, { deleteOne: { filter: { field: 'value' } } } ], { ordered: true // Optional: operations are executed in order }); // Bulk save documents const result = await Model.bulkSave([ new Model({ field: 'value1' }), new Model({ field: 'value2' }) ], { ordered: true }); ``` ### Query API ```javascript // Chainable query methods const docs = await Model.find() .where('field').equals('value') .where('number').gt(10).lt(20) .where('tags').in(['tag1', 'tag2']) .select('field1 field2') .sort('-field') .skip(10) .limit(5) .populate('reference') .exec(); // Advanced queries with geospatial support const docs = await Model.find() .where('location') .near({ center: [longitude, latitude], maxDistance: 5000 }) .exec(); // Text search const docs = await Model.find() .where('$text') .equals({ $search: 'keyword' }) .exec(); ``` ### Aggregation Pipeline ```javascript const results = await Model.aggregate() .match({ field: 'value' }) .group({ _id: '$groupField', total: { $sum: 1 }, avg: { $avg: '$numField' } }) .sort({ total: -1 }) .limit(5) .exec(); ``` ## Backup and Restore ```javascript // Create backup const backupPath = await Model.backup(); // Restore from backup await Model.restore(backupPath); // List backups const backups = await Model.listBackups(); // Clean up old backups await Model.cleanupBackups(); ``` ### Supported Update Operators #### Field Update Operators - `$set`: Sets the value of a field - `$unset`: Removes the specified field from a document - `$rename`: Renames a field - `$setOnInsert`: Sets the value of a field if an update results in an insert #### Increment/Decrement Operators - `$inc`: Increments the value of a field by the specified amount - `$mul`: Multiplies the value of a field by the specified amount - `$min`: Updates the field only if the specified value is less than the existing value - `$max`: Updates the field only if the specified value is greater than the existing value #### Array Update Operators - `$push`: Adds an item to an array - `$pull`: Removes all array elements that match a specified query - `$addToSet`: Adds elements to an array only if they do not already exist - `$pop`: Removes the first or last item from an array - `$pullAll`: Removes all matching values from an array #### Bitwise Operators - `$bit`: Performs bitwise AND, OR, and XOR updates of integer values #### Date Operators - `$currentDate`: Sets the value of a field to the current date ### Supported Query Operators - `equals`: Exact match - `gt`: Greater than - `gte`: Greater than or equal - `lt`: Less than - `lte`: Less than or equal - `in`: Match any value in array - `nin`: Not match any value in array - `regex`: Regular expression match - `exists`: Check for existence of a field - `size`: Match the size of an array - `mod`: Match documents where the value of a field modulo some divisor is equal to a specified remainder - `near`: Find documents near a specified point - `maxDistance`: Limit the results to documents within a specified distance from the point - `within`: Find documents within a specified shape - `box`: Find documents within a rectangular box - `center`: Find documents within a specified circle - `centerSphere`: Find documents within a specified spherical circle - `polygon`: Find documents within a specified polygon - `geoIntersects`: Find documents that intersect a specified geometry - `nearSphere`: Find documents near a specified point using spherical geometry - `text`: Full-text search - `or`: Logical OR - `nor`: Logical NOR - `and`: Logical AND - `elemMatch`: Match documents that contain an array field with at least one element that matches all the specified query criteria ### Supported Aggregation Operators - `$match`: Filter documents - `$group`: Group documents by expression - `$sort`: Sort documents - `$limit`: Limit number of documents - `$skip`: Skip number of documents - `$unwind`: Deconstruct array field - `$lookup`: Perform left outer join - `$project`: Reshape documents - `$addFields`: Add new fields - `$facet`: Process multiple aggregation pipelines - `$bucket`: Categorize documents into buckets - `$sortByCount`: Group and count documents - `$densify`: Fill gaps in time-series data - `$graphLookup`: Perform recursive search on a collection - `$unionWith`: Combine documents from another collection - `$count`: Count the number of documents - `$out`: Write the result to a collection - `$merge`: Merge the result with a collection - `$replaceRoot`: Replace the input document with the specified document - `$set`: Add new fields or update existing fields in documents - `$unset`: Remove specified fields from documents ### Supported Group Accumulators - `$sum`: Calculate sum - `$avg`: Calculate average - `$min`: Get minimum value - `$max`: Get maximum value - `$push`: Accumulate values into array - `$first`: Get first value - `$last`: Get last value - `$addToSet`: Add unique values to array - `$stdDevPop`: Calculate population standard deviation - `$stdDevSamp`: Calculate sample standard deviation - `$mergeObjects`: Merge objects into a single object ## Advanced Features ### Schema Validation ```javascript const schema = new localgoose.Schema({ email: { type: String, required: true, validate: { validator: function(v) { return /\S+@\S+\.\S+/.test(v); }, message: props => `${props.value} is not a valid email!` } }, age: { type: Number, min: [18, 'Must be at least 18'], max: [120, 'Must be no more than 120'] }, password: { type: String, minlength: 8, match: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/ } }); ``` ### Middleware Hooks ```javascript // Document middleware schema.pre('save', async function() { if (this.isModified('password')) { this.password = await hash(this.password); } }); // Query middleware schema.pre('find', function() { this.where({ isActive: true }); }); // Aggregation middleware schema.pre('aggregate', function() { this.pipeline().unshift({ $match: { isDeleted: false } }); }); ``` ### Virtual Population ```javascript schema.virtual('posts', { ref: 'Post', localField: '_id', foreignField: 'author', justOne: false, options: { sort: { createdAt: -1 } } }); ``` ### Schema Inheritance ```javascript const baseSchema = new localgoose.Schema({ name: String, createdAt: Date }); const userSchema = new localgoose.Schema({ email: String, password: String }); userSchema.add(baseSchema); ``` ### Custom Types ```javascript class Point { constructor(x, y) { this.x = x; this.y = y; } } const pointSchema = new localgoose.Schema({ location: { type: Point, validate: { validator: v => v instanceof Point, message: 'Invalid point' } } }); ``` ## File Structure Each model's data is stored in a separate JSON file: ``` mydb/ ├── User.json ├── Post.json └── Comment.json ``` ## Error Handling Localgoose provides detailed error messages for: - Schema validation failures - Required field violations - Type casting errors - Query execution errors - Reference population errors ## Best Practices 1. **Schema Design** - Define schemas with proper types and validation - Use references for related data - Implement virtual properties for computed fields - Add middleware for common operations 2. **Querying** - Use proper query operators - Limit result sets for better performance - Use projection to select only needed fields - Populate references only when needed 3. **File Management** - Regularly backup your JSON files - Monitor file sizes - Implement proper error handling - Use atomic operations when possible 4. **Performance Optimization** - Use indexes for frequently queried fields - Implement pagination for large datasets - Cache frequently accessed data - Use lean queries when possible 5. **Data Integrity** - Implement proper validation - Use transactions when needed - Handle errors gracefully - Keep backups up to date ## Limitations - Not suitable for large datasets (>10MB per collection) - No support for transactions - Limited query performance compared to real databases - Basic relationship support through references - No real-time updates or change streams - No distributed operations ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. ## License MIT ## Author [Anas Qiblawi](https://github.com/AnasQiblawi) ## Acknowledgments Inspired by Mongoose, the elegant MongoDB ODM for Node.js.