liekodb
v0.1.7
Published
Lightweight, MongoDB-like JSON database for Node.js
Maintainers
Readme
LiekoDB Documentation
Online documentation LiekoDB Documentation
Table of Contents
- Introduction
- Installation
- Quick Start
- Core Concepts
- Collection Methods
- Query Filters
- Examples
- Advanced Usage
- API Reference
- Best Practices
Introduction
LiekoDB is a lightweight, file-based database for Node.js with an optional HTTP adapter for browser and remote usage. It provides MongoDB-like query syntax with local file persistence and automatic caching.
Key Features
- 🚀 Zero-dependency for local usage
- 📁 File-based persistence with automatic caching
- 🔄 MongoDB-like query syntax
- 🌐 HTTP adapter for browser/remote usage
- ⚡ Auto-save with configurable intervals
- 🔒 Collection-level locking for concurrency
- 📊 Built-in logging and metrics
Installation
npm install liekodbQuick Start
Basic Setup
// database.js
const LiekoDB = require('liekodb');
// Create a database instance
const db = new LiekoDB({
debug: true, // Enable debug logging
autoSaveInterval: 10000 // Auto-save every 10 seconds
});
// Export for use in other files
module.exports = { db };Your First Collection
// userService.js
const { db } = require('./database');
class UserService {
constructor() {
this.collection = db.collection('users');
}
async createUser(userData) {
const { error, data } = await this.collection.insert({
name: userData.name,
email: userData.email,
age: userData.age,
createdAt: new Date().toISOString()
});
if (error) throw new Error(error.message);
return data;
}
async findUserByEmail(email) {
const { error, data } = await this.collection.findOne({ email });
if (error) throw new Error(error.message);
return data;
}
}
module.exports = new UserService();Core Concepts
1. Database Instance
Create a database instance with optional configuration:
const db = new LiekoDB({
debug: true, // Enable console logging
autoSaveInterval: 5000, // Auto-save interval in ms (0 to disable)
storagePath: './mydata', // Custom storage directory
token: 'your-token-here', // For HTTP adapter (browser/remote)
databaseUrl: 'http://api.example.com' // For remote database
});2. Collections
Collections are similar to tables in SQL or collections in MongoDB:
// Get or create a collection
const users = db.collection('users');
const products = db.collection('products');
const orders = db.collection('orders');3. Documents
Documents are JSON objects stored in collections:
const userDocument = {
id: 'abc123', // Unique identifier (auto-generated if not provided)
name: 'John Doe',
email: '[email protected]',
age: 30,
tags: ['admin', 'premium'],
settings: {
notifications: true,
theme: 'dark'
},
createdAt: '2024-01-15T10:30:00Z',
updatedAt: '2024-01-15T10:30:00Z'
};Collection Methods
CRUD Operations
1. Insert Documents
// Insert a single document
const { error, data } = await collection.insert({
name: 'Alice',
email: '[email protected]',
age: 25
});
// Insert multiple documents
const { error, data } = await collection.insert([
{ name: 'Bob', email: '[email protected]' },
{ name: 'Charlie', email: '[email protected]' }
]);
// Result: data = {
// insertedCount: 2,
// updatedCount: 0,
// totalDocuments: 5,
// insertedIds: ['abc123', 'def456']
// }2. Find Documents
// Find all documents
const { error, data } = await collection.find();
// Find with filters
const { error, data } = await collection.find({
age: { $gt: 18 }
});
// Find with pagination
const { error, data } = await collection.find({}, {
limit: 10,
skip: 20,
sort: { createdAt: -1 } // -1 = descending, 1 = ascending
});
// Find one document
const { error, data } = await collection.findOne({ email: '[email protected]' });
// Find by ID
const { error, data } = await collection.findById('document-id-here');3. Update Documents
// Update by ID
const { error, data } = await collection.updateById('doc-id', {
name: 'Updated Name',
age: 31
});
// Update multiple documents with filters
const { error, data } = await collection.update(
{ status: 'pending' }, // Filter
{ status: 'completed' }, // Update
{ returnType: 'count' } // Options
);
// Using update operators
const { error, data } = await collection.updateById('doc-id', {
$set: { status: 'active' },
$inc: { loginCount: 1 },
$push: { logs: 'User logged in' }
});4. Delete Documents
// Delete by ID
const { error, data } = await collection.deleteById('doc-id');
// Delete with filters
const { error, data } = await collection.delete({
status: 'inactive',
lastLogin: { $lt: '2023-01-01' }
});
// Delete entire collection
const { error, data } = await collection.drop();5. Count Documents
// Count all documents
const { error, data } = await collection.count();
// Count with filters
const { error, data } = await collection.count({
status: 'active',
age: { $gte: 18 }
});Query Options
const options = {
sort: { createdAt: -1, name: 1 }, // Sort by multiple fields
limit: 50, // Limit results
skip: 100, // Skip first 100 results
page: 3, // Page number (requires limit)
fields: { name: 1, email: 1 }, // Include only specific fields
returnType: 'documents', // 'count', 'ids', or 'documents'
maxReturn: 1000 // Maximum documents to return
};
const { error, data } = await collection.find({}, options);Query Filters
LiekoDB supports MongoDB-like query syntax:
Comparison Operators
// Equality
{ age: 25 }
{ name: 'John' }
{ status: { $eq: 'active' } }
// Inequality
{ age: { $ne: 25 } }
{ status: { $ne: 'inactive' } }
// Greater than / Less than
{ age: { $gt: 18 } } // Greater than
{ age: { $gte: 21 } } // Greater than or equal
{ age: { $lt: 65 } } // Less than
{ age: { $lte: 100 } } // Less than or equal
// In / Not in
{ role: { $in: ['admin', 'moderator'] } }
{ status: { $nin: ['banned', 'suspended'] } }
// Exists
{ email: { $exists: true } }
{ middleName: { $exists: false } }
// Regular expressions
{ email: { $regex: /@gmail\.com$/ } }
{ name: { $regex: '^J', $options: 'i' } } // Case insensitive
// Modulo
{ age: { $mod: [2, 0] } } // Even numbersLogical Operators
// AND (default)
{ age: { $gt: 18 }, status: 'active' }
// OR
{ $or: [{ status: 'active' }, { verified: true }] }
// AND with OR
{
$and: [
{ age: { $gte: 18 } },
{
$or: [
{ role: 'admin' },
{ premium: true }
]
}
]
}
// NOT
{ status: { $not: { $eq: 'banned' } } }
{ age: { $not: { $lt: 18 } } }Array Queries
// Match array element
{ tags: 'javascript' } // Array contains 'javascript'
{ tags: { $in: ['javascript', 'nodejs'] } } // Array contains any of these
// Array field queries
{ 'skills.level': { $gt: 3 } } // Nested array field
{ 'comments.0.author': 'admin' } // First element of arrayNested Field Queries
// Dot notation for nested objects
{ 'address.city': 'New York' }
{ 'settings.theme': 'dark' }
{ 'metrics.visits.count': { $gt: 100 } }Examples
Example 1: Todo List Application
// todoService.js
const { db } = require('./database');
class TodoService {
constructor() {
this.collection = db.collection('todos');
}
async createTodo(userId, text) {
const { error, data } = await this.collection.insert({
userId,
text,
completed: false,
createdAt: new Date().toISOString(),
priority: 'medium'
});
if (error) throw new Error(error.message);
return data;
}
async getUserTodos(userId, options = {}) {
const { error, data } = await this.collection.find(
{ userId },
{
sort: { createdAt: -1 },
...options
}
);
if (error) throw new Error(error.message);
return data.foundDocuments;
}
async completeTodo(todoId) {
const { error, data } = await this.collection.updateById(todoId, {
completed: true,
completedAt: new Date().toISOString()
});
if (error) throw new Error(error.message);
return data;
}
async deleteCompletedTodos(userId) {
const { error, data } = await this.collection.delete({
userId,
completed: true
});
if (error) throw new Error(error.message);
return data.deletedCount;
}
async getTodoStats(userId) {
const total = await this.collection.count({ userId });
const completed = await this.collection.count({
userId,
completed: true
});
const pending = await this.collection.count({
userId,
completed: false
});
return { total, completed, pending };
}
}
module.exports = new TodoService();Example 2: Blog System
// blogService.js
const { db } = require('./database');
class BlogService {
constructor() {
this.posts = db.collection('posts');
this.comments = db.collection('comments');
}
async createPost(authorId, title, content, tags = []) {
const { error, data } = await this.posts.insert({
authorId,
title,
content,
tags,
status: 'published',
views: 0,
likes: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
if (error) throw new Error(error.message);
return data;
}
async getPosts(options = {}) {
const { error, data } = await this.posts.find(
{ status: 'published' },
{
sort: { createdAt: -1 },
limit: options.limit || 20,
skip: options.skip || 0,
fields: {
title: 1,
excerpt: 1,
authorId: 1,
tags: 1,
createdAt: 1,
views: 1,
likes: 1
}
}
);
if (error) throw new Error(error.message);
return data;
}
async incrementViews(postId) {
const { error, data } = await this.posts.updateById(postId, {
$inc: { views: 1 }
});
if (error) throw new Error(error.message);
return data;
}
async addComment(postId, userId, text) {
const { error, data } = await this.comments.insert({
postId,
userId,
text,
createdAt: new Date().toISOString()
});
if (error) throw new Error(error.message);
return data;
}
async getPostComments(postId) {
const { error, data } = await this.comments.find(
{ postId },
{ sort: { createdAt: -1 } }
);
if (error) throw new Error(error.message);
return data.foundDocuments;
}
async searchPosts(query) {
const { error, data } = await this.posts.find({
$or: [
{ title: { $regex: query, $options: 'i' } },
{ content: { $regex: query, $options: 'i' } },
{ tags: query }
]
});
if (error) throw new Error(error.message);
return data.foundDocuments;
}
}
module.exports = new BlogService();Example 3: E-commerce Product Catalog
// productService.js
const { db } = require('./database');
class ProductService {
constructor() {
this.products = db.collection('products');
this.categories = db.collection('categories');
}
async addProduct(productData) {
const { error, data } = await this.products.insert({
...productData,
sku: this.generateSKU(),
stock: productData.stock || 0,
price: parseFloat(productData.price),
rating: 0,
reviewCount: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
if (error) throw new Error(error.message);
return data;
}
async getProducts(filters = {}, options = {}) {
const query = { ...filters, active: true };
const { error, data } = await this.products.find(query, {
sort: options.sort || { createdAt: -1 },
limit: options.limit || 50,
skip: options.skip || 0,
...options
});
if (error) throw new Error(error.message);
return data;
}
async updateStock(productId, quantity) {
const { error, data } = await this.products.updateById(productId, {
$inc: { stock: quantity }
});
if (error) throw new Error(error.message);
return data;
}
async addReview(productId, rating, review) {
// Update product rating (average calculation)
const product = await this.products.findById(productId);
const newRating = (
(product.rating * product.reviewCount + rating) /
(product.reviewCount + 1)
).toFixed(1);
const { error, data } = await this.products.updateById(productId, {
$inc: { reviewCount: 1 },
$set: { rating: parseFloat(newRating) }
});
if (error) throw new Error(error.message);
// Store individual review
await db.collection('reviews').insert({
productId,
rating,
review,
createdAt: new Date().toISOString()
});
return data;
}
async getProductsByCategory(categoryId, options = {}) {
const { error, data } = await this.products.find(
{ categoryId, active: true },
{
sort: { price: options.sortPrice ? 1 : -1 },
limit: options.limit || 20,
...options
}
);
if (error) throw new Error(error.message);
return data.foundDocuments;
}
async searchProducts(searchTerm, filters = {}) {
const query = {
$and: [
{
$or: [
{ name: { $regex: searchTerm, $options: 'i' } },
{ description: { $regex: searchTerm, $options: 'i' } },
{ tags: searchTerm }
]
},
{ ...filters, active: true }
]
};
const { error, data } = await this.products.find(query);
if (error) throw new Error(error.message);
return data.foundDocuments;
}
generateSKU() {
return 'SKU-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5).toUpperCase();
}
}
module.exports = new ProductService();Example 4: Real-time Chat Application
// chatService.js
const { db } = require('./database');
class ChatService {
constructor() {
this.messages = db.collection('messages');
this.rooms = db.collection('chat_rooms');
this.users = db.collection('chat_users');
}
async createRoom(name, creatorId, isPrivate = false) {
const { error, data } = await this.rooms.insert({
name,
creatorId,
isPrivate,
members: [creatorId],
createdAt: new Date().toISOString()
});
if (error) throw new Error(error.message);
return data;
}
async sendMessage(roomId, userId, text) {
const { error, data } = await this.messages.insert({
roomId,
userId,
text,
timestamp: new Date().toISOString(),
readBy: [userId]
});
if (error) throw new Error(error.message);
return data;
}
async getRoomMessages(roomId, options = {}) {
const { error, data } = await this.messages.find(
{ roomId },
{
sort: { timestamp: -1 },
limit: options.limit || 50,
...options
}
);
if (error) throw new Error(error.message);
return data.foundDocuments.reverse(); // Return oldest first
}
async markAsRead(messageId, userId) {
const { error, data } = await this.messages.updateById(messageId, {
$addToSet: { readBy: userId }
});
if (error) throw new Error(error.message);
return data;
}
async getUnreadCount(roomId, userId) {
const { error, data } = await this.messages.count({
roomId,
readBy: { $nin: [userId] }
});
if (error) throw new Error(error.message);
return data;
}
async getUserRooms(userId) {
const { error, data } = await this.rooms.find({
members: userId
});
if (error) throw new Error(error.message);
return data.foundDocuments;
}
async addMemberToRoom(roomId, userId) {
const { error, data } = await this.rooms.updateById(roomId, {
$addToSet: { members: userId }
});
if (error) throw new Error(error.message);
return data;
}
async searchMessages(roomId, searchTerm) {
const { error, data } = await this.messages.find({
roomId,
text: { $regex: searchTerm, $options: 'i' }
});
if (error) throw new Error(error.message);
return data.foundDocuments;
}
}
module.exports = new ChatService();Example 5: Analytics Tracking System
// analyticsService.js
const { db } = require('./database');
class AnalyticsService {
constructor() {
this.events = db.collection('analytics_events');
this.sessions = db.collection('user_sessions');
}
async trackEvent(userId, eventType, properties = {}) {
const { error, data } = await this.events.insert({
userId: userId || 'anonymous',
eventType,
properties,
timestamp: new Date().toISOString(),
userAgent: properties.userAgent,
ip: properties.ip,
path: properties.path
});
if (error) throw new Error(error.message);
return data;
}
async startSession(userId, userAgent, ip) {
const sessionId = require('crypto').randomBytes(16).toString('hex');
const { error, data } = await this.sessions.insert({
sessionId,
userId,
userAgent,
ip,
startTime: new Date().toISOString(),
lastActivity: new Date().toISOString(),
events: []
});
if (error) throw new Error(error.message);
return sessionId;
}
async updateSessionActivity(sessionId) {
const { error, data } = await this.sessions.update(
{ sessionId },
{ lastActivity: new Date().toISOString() }
);
if (error) throw new Error(error.message);
return data;
}
async getEventStats(eventType, startDate, endDate) {
const { error, data } = await this.events.count({
eventType,
timestamp: {
$gte: startDate,
$lte: endDate
}
});
if (error) throw new Error(error.message);
return data;
}
async getUserActivity(userId, days = 7) {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const { error, data } = await this.events.find({
userId,
timestamp: { $gte: startDate.toISOString() }
}, {
sort: { timestamp: -1 },
fields: {
eventType: 1,
timestamp: 1,
'properties.path': 1
}
});
if (error) throw new Error(error.message);
return data.foundDocuments;
}
async getPopularPages(limit = 10) {
// Group by path using multiple queries (simplified)
const allEvents = await this.events.find({}, {
fields: { 'properties.path': 1 },
limit: 10000
});
const pathCounts = {};
allEvents.foundDocuments.forEach(event => {
const path = event.properties?.path;
if (path) {
pathCounts[path] = (pathCounts[path] || 0) + 1;
}
});
return Object.entries(pathCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, limit)
.map(([path, count]) => ({ path, count }));
}
async cleanupOldData(daysToKeep = 90) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
// Delete old events
const eventsResult = await this.events.delete({
timestamp: { $lt: cutoffDate.toISOString() }
});
// Delete old sessions
const sessionsResult = await this.sessions.delete({
lastActivity: { $lt: cutoffDate.toISOString() }
});
return {
deletedEvents: eventsResult.deletedCount || 0,
deletedSessions: sessionsResult.deletedCount || 0
};
}
}
module.exports = new AnalyticsService();Advanced Usage
Database Management
// Get database status
const status = await db.status();
console.log(status);
// {
// storagePath: './storage',
// collections: [...],
// totalCollections: 5,
// totalDocuments: 1234,
// totalCollectionsSize: 1024576,
// totalCollectionsSizeFormatted: '1.02 MB'
// }
// List all collections
const collections = await db.listCollections();
collections.forEach(col => {
console.log(`${col.name}: ${col.totalDocuments} docs (${col.sizeFormatted})`);
});
// Drop a collection
await db.dropCollection('old_data');
// Close database (flushes all pending writes)
await db.close();HTTP Adapter (Browser/Remote)
// Browser usage
const db = new LiekoDB({
token: 'your-api-token',
databaseUrl: 'https://api.yourservice.com'
});
// The API is the same as local usage
const users = db.collection('users');
const { data } = await users.find({ active: true });Custom Storage Path
const db = new LiekoDB({
storagePath: './myapp/data',
autoSaveInterval: 30000 // Save every 30 seconds
});
// Or set via environment variable
const db = new LiekoDB({
storagePath: process.env.DB_PATH || './storage'
});Error Handling
async function safeDatabaseOperation() {
try {
const { error, data } = await collection.insert(document);
if (error) {
// Handle specific error types
if (error.code === 404) {
console.error('Collection not found');
} else if (error.message.includes('validation')) {
console.error('Validation error:', error.message);
} else {
console.error('Database error:', error);
}
return null;
}
return data;
} catch (err) {
console.error('Unexpected error:', err);
throw err;
}
}Performance Optimization
const db = new LiekoDB({
autoSaveInterval: 30000, // Longer interval for write-heavy apps
debug: false // Disable debug in production
});
// Use projection to reduce data transfer
const { data } = await collection.find({}, {
fields: { name: 1, email: 1 }, // Only get these fields
limit: 100
});
// Use count instead of find when only need count
const { data: count } = await collection.count(filters);
// Use pagination for large datasets
async function getAllPaginated(collection, batchSize = 1000) {
let allDocuments = [];
let skip = 0;
let hasMore = true;
while (hasMore) {
const { data } = await collection.find({}, {
limit: batchSize,
skip: skip
});
if (data.foundDocuments.length === 0) {
hasMore = false;
} else {
allDocuments = allDocuments.concat(data.foundDocuments);
skip += batchSize;
}
}
return allDocuments;
}API Reference
LiekoDB Constructor
new LiekoDB(options)Options:
debug(boolean): Enable debug logging (default: false)autoSaveInterval(number): Auto-save interval in ms (default: 5000)storagePath(string): Path for file storage (default: './storage')token(string): Authentication token for HTTP adapterdatabaseUrl(string): URL for remote databasepoolSize(number): HTTP connection pool size (default: 10)maxRetries(number): HTTP retry attempts (default: 3)timeout(number): HTTP timeout in ms (default: 15000)
Collection Methods
All methods return a Promise resolving to { error?, data? }
insert(documents)
Insert one or multiple documents.
find(filters?, options?)
Find documents matching filters.
findOne(filters?, options?)
Find a single document.
findById(id, options?)
Find document by ID.
update(filters, update, options?)
Update multiple documents.
updateById(id, update, options?)
Update document by ID.
delete(filters)
Delete documents matching filters.
deleteById(id)
Delete document by ID.
count(filters?)
Count documents matching filters.
drop()
Delete entire collection.
Query Operators
Comparison
$eq- Equal$ne- Not equal$gt- Greater than$gte- Greater than or equal$lt- Less than$lte- Less than or equal$in- In array$nin- Not in array$exists- Field exists$regex- Regular expression$mod- Modulo operation
Logical
$and- Logical AND$or- Logical OR$not- Logical NOT$nor- Logical NOR
Update Operators
$set- Set field value$unset- Remove field$inc- Increment field$push- Append to array$pull- Remove from array$addToSet- Add to array if not exists
Best Practices
1. Always Handle Errors
async function safeOperation() {
const { error, data } = await collection.operation();
if (error) {
// Handle appropriately
throw new Error(`Database error: ${error.message}`);
}
return data;
}2. Use Indexes Wisely
While LiekoDB doesn't have traditional indexes, you can:
- Keep collections small and focused
- Use meaningful IDs for quick lookups
- Consider splitting large collections
3. Implement Data Validation
class UserService {
async createUser(userData) {
// Validate before inserting
if (!userData.email || !userData.email.includes('@')) {
throw new Error('Invalid email');
}
const { error, data } = await this.collection.insert({
...userData,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
return data;
}
}4. Optimize for Your Use Case
// For read-heavy: Larger auto-save interval
const readHeavyDB = new LiekoDB({
autoSaveInterval: 60000 // 1 minute
});
// For write-heavy: Smaller auto-save interval
const writeHeavyDB = new LiekoDB({
autoSaveInterval: 1000 // 1 second
});5. Backup Strategy
// Simple backup function
async function backupDatabase(sourcePath, backupPath) {
const fs = require('fs').promises;
try {
const files = await fs.readdir(sourcePath);
for (const file of files) {
if (file.endsWith('.json')) {
const source = `${sourcePath}/${file}`;
const backup = `${backupPath}/${file}.${Date.now()}.bak`;
await fs.copyFile(source, backup);
}
}
console.log('Backup completed successfully');
} catch (error) {
console.error('Backup failed:', error);
}
}6. Monitor Performance
// Add performance logging
const db = new LiekoDB({
debug: process.env.NODE_ENV === 'development'
});
// Log slow queries
const start = Date.now();
const { data } = await collection.find(complexQuery);
const duration = Date.now() - start;
if (duration > 1000) { // 1 second
console.warn(`Slow query: ${duration}ms`);
}Troubleshooting
Common Issues
"Collection name contains invalid characters"
- Use only alphanumeric characters, underscores, and hyphens
- Cannot start with numbers
Data not persisting
- Check
autoSaveIntervalsetting - Call
db.close()before exiting - Check file permissions
- Check
Slow queries
- Use projection to limit returned fields
- Implement pagination
- Consider splitting large collections
Memory usage
- Monitor cache size for large collections
- Use
limitin queries - Consider disabling auto-save for read-only operations
Debug Mode
Enable debug mode to see detailed logs:
const db = new LiekoDB({ debug: true });This will log:
- All database operations
- Execution times
- Response sizes
- Errors and warnings
Conclusion
LiekoDB provides a simple yet powerful solution for local data persistence in Node.js applications. With its MongoDB-like API, automatic persistence, and flexible adapters, it's suitable for a wide range of applications from small projects to production systems.
Remember to:
- Always handle errors properly
- Implement appropriate data validation
- Choose the right adapter for your environment
- Monitor performance and implement backups
For more examples and advanced usage, check the source code and experiment with different configurations to find what works best for your specific use case.
