flexi-db
Version:
A lightweight, flexible JSON-based database with caching, async operations, and customizable data storage.
230 lines (209 loc) • 10.2 kB
JavaScript
const fs = require('fs').promises; // Use Node.js file system with promises
const path = require('path'); // Helps with file paths
// Debounce function to delay saving data
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout); // Clear any existing timeout
timeout = setTimeout(() => func.apply(this, args), wait); // Call function after delay
};
}
// FlexiDB class for managing a JSON-based database
class FlexiDB {
// Constructor to set up the database
constructor(fileName = 'database.json', options = {}) {
this.dataDir = path.resolve(options.dataDir || 'FlexiDB'); // Custom or default folder for data
this.filePath = path.join(this.dataDir, fileName); // Path to the database file
this.cache = new Map(); // In-memory storage for fast access
this.isDirty = false; // Tracks if data has changed
this.autoBackupEnabled = false; // Option to enable/disable auto-backup
this.saveDataDebounced = debounce(this.saveData.bind(this), 500); // Delay saving data by 500ms
if (this.autoBackupEnabled) {
this.autoBackupInterval = setInterval(() => this.autoBackup(), 60000); // Auto-backup every minute
}
this.ready = this.init(); // Initialize the database automatically
}
// Initialize the database (must be called before using)
async init() {
await this.ensureDataDir(); // Create data folder if it doesn't exist
await this.loadData(); // Load data from file
}
// Create the data folder if it doesn't exist
async ensureDataDir() {
try {
await fs.mkdir(this.dataDir, { recursive: true }); // Create folder, including parent folders
} catch (error) {
throw new Error(`Failed to create data directory: ${error.message}`);
}
}
// Load data from the JSON file into memory
async loadData() {
try {
const exists = await fs.access(this.filePath).then(() => true).catch(() => false); // Check if file exists
if (!exists) {
await fs.writeFile(this.filePath, '{}', 'utf8'); // Create empty file if it doesn't exist
}
const data = await fs.readFile(this.filePath, 'utf8'); // Read file content
const json = JSON.parse(data); // Parse JSON
for (const [key, value] of Object.entries(json)) {
this.cache.set(key, value); // Load data into cache
}
} catch (error) {
throw new Error(`Failed to load database: ${error.message}`);
}
}
// Save data from memory to the JSON file
async saveData() {
if (!this.isDirty) return; // Skip if no changes
try {
const data = Object.fromEntries(this.cache); // Convert cache to object
await fs.writeFile(this.filePath, JSON.stringify(data, null, 2), 'utf8'); // Write to file with formatting
this.isDirty = false; // Mark as saved
} catch (error) {
throw new Error(`Failed to save database: ${error.message}`);
}
}
// Set a key-value pair in the database
async set(key, value) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
this.cache.set(key, value); // Store in cache
this.isDirty = true; // Mark as changed
await this.saveDataDebounced(); // Save data after delay
return value; // Return the value
}
// Get the value for a key
async get(key) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
return this.cache.get(key); // Return value from cache
}
// Check if a key exists
async has(key) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
return this.cache.has(key); // Return true if key exists
}
// Delete a key from the database
async delete(key) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
if (!this.cache.has(key)) throw new TypeError('Key does not exist!'); // Check if key exists
const result = this.cache.delete(key); // Delete from cache
this.isDirty = true; // Mark as changed
await this.saveDataDebounced(); // Save data after delay
return result; // Return true if deleted
}
// Get all key-value pairs (with optional limit)
async all(limit = 0) {
await this.ready; // Wait for initialization
const entries = Array.from(this.cache.entries()).map(([key, value]) => ({
data: key,
value
})); // Convert cache to array of objects
return limit > 0 ? entries.slice(0, limit) : entries; // Return limited or full list
}
// Add a number to a key's value
async add(key, value) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
if (isNaN(value)) throw new TypeError('Value must be a number!'); // Check for valid number
const current = Number(this.get(key) || 0); // Convert current value to number (default 0)
const newValue = current + value; // Add value
return this.set(key, newValue); // Save and return new value
}
// Subtract a number from a key's value
async subtract(key, value) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
if (isNaN(value)) throw new TypeError('Value must be a number!'); // Check for valid number
const current = Number(this.get(key) || 0); // Convert current value to number (default 0)
if (!this.has(key) && current === 0) throw new TypeError('Key does not exist!'); // Check if key exists
const newValue = current - value; // Subtract value
return this.set(key, newValue); // Save and return new value
}
// Perform a math operation on a key's value
async math(key, operator, value) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
if (!operator) throw new TypeError('Operator is not defined!'); // Check for valid operator
if (isNaN(value)) throw new TypeError('Value must be a number!'); // Check for valid number
const current = Number(this.get(key) || 0); // Convert current value to number (default 0)
if (!this.has(key) && current === 0) throw new TypeError('Key does not exist!'); // Check if key exists
let newValue;
switch (operator) {
case '+': newValue = current + value; break; // Addition
case '-': newValue = current - value; break; // Subtraction
case '*': newValue = current * value; break; // Multiplication
case '/':
if (value === 0) throw new TypeError('Cannot divide by zero!'); // Prevent division by zero
newValue = current / value; // Division
break;
case '%': newValue = current % value; break; // Modulus
default: throw new TypeError('Invalid operator!'); // Invalid operator
}
return this.set(key, newValue); // Save and return new value
}
// Add a value to an array at a key
async push(key, value) {
await this.ready; // Wait for initialization
if (!key) throw new TypeError('Key is not defined!'); // Check for valid key
const current = this.get(key) || []; // Get array or empty array
if (!Array.isArray(current)) throw new TypeError('Value at key is not an array!'); // Check if array
current.push(value); // Add value to array
return this.set(key, current); // Save and return array
}
// Create a backup of the database
async backup(fileName) {
await this.ready; // Wait for initialization
if (!fileName) throw new TypeError('Filename is not defined!'); // Check for valid filename
try {
const backupPath = path.join(this.dataDir, `${fileName}.json`); // Path for backup file
const data = Object.fromEntries(this.cache); // Convert cache to object
await fs.writeFile(backupPath, JSON.stringify(data, null, 2), 'utf8'); // Write backup file
return true; // Return success
} catch (error) {
throw new Error(`Failed to create backup: ${error.message}`);
}
}
// Create an automatic backup
async autoBackup() {
if (!this.autoBackupEnabled) return; // Skip if auto-backup is disabled
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // Create timestamp
await this.backup(`backup-${timestamp}`); // Save backup with timestamp
}
// Clear all data in the database
async reset() {
await this.ready; // Wait for initialization
this.cache.clear(); // Clear cache
this.isDirty = true; // Mark as changed
await this.saveDataDebounced(); // Save data after delay
}
// Close the database and save data
async destroy() {
await this.ready; // Wait for initialization
if (this.autoBackupEnabled) {
clearInterval(this.autoBackupInterval); // Stop auto-backup
}
await this.saveData(); // Save data immediately
}
// Run multiple operations as a single transaction
async transaction(operations) {
await this.ready; // Wait for initialization
const backup = new Map(this.cache); // Backup current cache
try {
for (const op of operations) {
if (op.type === 'set') await this.set(op.key, op.value); // Set operation
else if (op.type === 'delete') await this.delete(op.key); // Delete operation
else if (op.type === 'add') await this.add(op.key, op.value); // Add operation
else if (op.type === 'subtract') await this.subtract(op.key, op.value); // Subtract operation
else if (op.type === 'push') await this.push(op.key, op.value); // Push operation
else if (op.type === 'math') await this.math(op.key, op.operator, op.value); // Math operation
}
} catch (error) {
this.cache = backup; // Restore cache if transaction fails
throw new Error(`Transaction failed: ${error.message}`);
}
}
}
module.exports = FlexiDB; // Export the FlexiDB class