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.
- 🚀 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
npm install localgoose
const { localgoose } = require('localgoose');
// Connect to a local directory for storage
const db = await 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();
// Connect to database
const db = await localgoose.connect('./mydb');
// Create separate connection
const connection = await localgoose.createConnection('./mydb');
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' });
// Single document
const doc = await Model.create({
field: 'value'
});
// Multiple documents
const docs = await Model.create([
{ field: 'value1' },
{ field: 'value2' }
]);
// Find all
const docs = await Model.find();
// Find with conditions
const docs = await Model.find({
field: 'value'
});
// Find one
const doc = await Model.findOne({
field: 'value'
});
// Find by ID
const doc = await Model.findById(id);
// Find with population
const doc = await Model.findOne({ field: 'value' })
.populate('reference')
.exec();
// Update one
const result = await Model.updateOne(
{ field: 'value' },
{ $set: { newField: 'newValue' }}
);
// Update many
const result = await Model.updateMany(
{ field: 'value' },
{ $set: { newField: 'newValue' }}
);
// Save changes to document
doc.field = 'new value';
await doc.save();
// Delete one
const result = await Model.deleteOne({
field: 'value'
});
// Delete many
const result = await Model.deleteMany({
field: 'value'
});
// 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();
const results = await Model.aggregate()
.match({ field: 'value' })
.group({
_id: '$groupField',
total: { $sum: 1 },
avg: { $avg: '$numField' }
})
.sort({ total: -1 })
.limit(5)
.exec();
$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
$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
$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
$bit
: Performs bitwise AND, OR, and XOR updates of integer values
$currentDate
: Sets the value of a field to the current date
equals
: Exact matchgt
: Greater thangte
: Greater than or equallt
: Less thanlte
: Less than or equalin
: Match any value in arraynin
: Not match any value in arrayregex
: Regular expression matchexists
: Check for existence of a fieldsize
: Match the size of an arraymod
: Match documents where the value of a field modulo some divisor is equal to a specified remaindernear
: Find documents near a specified pointmaxDistance
: Limit the results to documents within a specified distance from the pointwithin
: Find documents within a specified shapebox
: Find documents within a rectangular boxcenter
: Find documents within a specified circlecenterSphere
: Find documents within a specified spherical circlepolygon
: Find documents within a specified polygongeoIntersects
: Find documents that intersect a specified geometrynearSphere
: Find documents near a specified point using spherical geometrytext
: Full-text searchor
: Logical ORnor
: Logical NORand
: Logical ANDelemMatch
: Match documents that contain an array field with at least one element that matches all the specified query criteria
$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
$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
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])/
}
});
// 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 } });
});
schema.virtual('posts', {
ref: 'Post',
localField: '_id',
foreignField: 'author',
justOne: false,
options: { sort: { createdAt: -1 } }
});
const baseSchema = new localgoose.Schema({
name: String,
createdAt: Date
});
const userSchema = new localgoose.Schema({
email: String,
password: String
});
userSchema.add(baseSchema);
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'
}
}
});
Each model's data is stored in a separate JSON file:
mydb/
├── User.json
├── Post.json
└── Comment.json
Localgoose provides detailed error messages for:
- Schema validation failures
- Required field violations
- Type casting errors
- Query execution errors
- Reference population errors
-
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
-
Querying
- Use proper query operators
- Limit result sets for better performance
- Use projection to select only needed fields
- Populate references only when needed
-
File Management
- Regularly backup your JSON files
- Monitor file sizes
- Implement proper error handling
- Use atomic operations when possible
-
Performance Optimization
- Use indexes for frequently queried fields
- Implement pagination for large datasets
- Cache frequently accessed data
- Use lean queries when possible
-
Data Integrity
- Implement proper validation
- Use transactions when needed
- Handle errors gracefully
- Keep backups up to date
- 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
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.
MIT
Inspired by Mongoose, the elegant MongoDB ODM for Node.js.