UNPKG

milodb

Version:

A simple mini database with optional encryption to store key-value pairs

264 lines (219 loc) 7.65 kB
const fs = require('fs').promises; const crypto = require('crypto'); const path = require('path'); const zlib = require('zlib'); const { promisify } = require('util'); const gzip = promisify(zlib.gzip); const gunzip = promisify(zlib.gunzip); // Helper function for encryption with salt function encrypt(text, password) { if (!password) throw new Error('Password is required for encryption'); const salt = crypto.randomBytes(16); const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256'); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { encrypted, salt: salt.toString('hex'), iv: iv.toString('hex'), authTag: authTag.toString('hex') }; } // Helper function for decryption with salt function decrypt(encryptedData, password) { if (!password) throw new Error('Password is required for decryption'); const { encrypted, salt, iv, authTag } = encryptedData; const key = crypto.pbkdf2Sync(password, Buffer.from(salt, 'hex'), 100000, 32, 'sha256'); const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex')); decipher.setAuthTag(Buffer.from(authTag, 'hex')); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // Helper function to compress data async function compress(data) { return await gzip(Buffer.from(data)); } // Helper function to decompress data async function decompress(data) { return (await gunzip(data)).toString(); } // Basic mini database class class MinoDB { constructor(password = null, encryptData = true) { this.dbPath = path.join(__dirname, '..', 'db', 'database.json'); this.password = password; this.encryptData = encryptData; this.db = {}; this.initialized = false; } // Initialize the database async init() { if (this.initialized) return; await this.loadDatabase(); this.initialized = true; } // Load the database from the file async loadDatabase() { try { const rawData = await fs.readFile(this.dbPath, 'utf8'); const data = this.encryptData ? decrypt(JSON.parse(rawData), this.password) : rawData; this.db = JSON.parse(data); // Clean expired entries await this.cleanExpired(); } catch (err) { if (err.code !== 'ENOENT') throw err; this.db = {}; } } // Save the database to the file async saveDatabase() { const data = JSON.stringify(this.db, null, 2); let dataToSave; if (this.encryptData) { dataToSave = JSON.stringify(encrypt(data, this.password)); } else { dataToSave = data; } await fs.writeFile(this.dbPath, dataToSave, 'utf8'); } // Validate key and value validateKeyValue(key, value) { if (typeof key !== 'string') throw new Error('Key must be a string'); if (key.length === 0) throw new Error('Key cannot be empty'); if (value === undefined) throw new Error('Value cannot be undefined'); } // Add or update an entry (asynchronous) async set(key, value, ttl = null) { await this.init(); this.validateKeyValue(key, value); const entry = { value, createdAt: Date.now(), expiresAt: ttl ? Date.now() + ttl : null }; this.db[key] = entry; await this.saveDatabase(); } // Get an entry by key (asynchronous) async get(key) { await this.init(); const entry = this.db[key]; if (!entry) return null; if (entry.expiresAt && entry.expiresAt < Date.now()) { delete this.db[key]; await this.saveDatabase(); return null; } return entry.value; } // Delete an entry by key (asynchronous) async delete(key) { await this.init(); if (this.db[key]) { delete this.db[key]; await this.saveDatabase(); return true; } return false; } // List all entries (asynchronous) async list() { await this.init(); await this.cleanExpired(); const result = {}; for (const [key, entry] of Object.entries(this.db)) { result[key] = entry.value; } return result; } // Clear the database (asynchronous) async clear() { await this.init(); this.db = {}; await this.saveDatabase(); return 'Database cleared.'; } // Search for values (asynchronous) async search(query, options = {}) { await this.init(); await this.cleanExpired(); const { regex = false, caseSensitive = false, limit = 0 } = options; const results = {}; let count = 0; for (const [key, entry] of Object.entries(this.db)) { if (limit > 0 && count >= limit) break; const value = entry.value; const searchStr = JSON.stringify(value); let matches = false; if (regex) { const flags = caseSensitive ? '' : 'i'; matches = new RegExp(query, flags).test(searchStr); } else { const searchValue = caseSensitive ? query : query.toLowerCase(); const compareValue = caseSensitive ? searchStr : searchStr.toLowerCase(); matches = compareValue.includes(searchValue); } if (matches) { results[key] = value; count++; } } return results; } // Clean expired entries async cleanExpired() { const now = Date.now(); let cleaned = false; for (const [key, entry] of Object.entries(this.db)) { if (entry.expiresAt && entry.expiresAt < now) { delete this.db[key]; cleaned = true; } } if (cleaned) { await this.saveDatabase(); } } // Batch operations async batch(operations) { await this.init(); const results = []; for (const op of operations) { switch (op.type) { case 'set': await this.set(op.key, op.value, op.ttl); results.push({ success: true }); break; case 'delete': results.push({ success: await this.delete(op.key) }); break; default: results.push({ success: false, error: 'Invalid operation type' }); } } return results; } // Get database statistics async stats() { await this.init(); await this.cleanExpired(); const total = Object.keys(this.db).length; const expired = Object.values(this.db).filter(entry => entry.expiresAt && entry.expiresAt < Date.now() ).length; return { total, expired, active: total - expired }; } } module.exports = MinoDB;