milodb
Version:
A simple mini database with optional encryption to store key-value pairs
264 lines (219 loc) • 7.65 kB
JavaScript
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;