Initial commit: Inventory Barcode System

This commit is contained in:
2025-07-22 20:24:51 -04:00
commit 511b01748d
63 changed files with 26932 additions and 0 deletions

275
utils/backup.js Normal file
View File

@ -0,0 +1,275 @@
/**
* Database Backup and Recovery Utilities
* Provides automated backup and recovery functionality for SQLite database
*/
const fs = require('fs').promises;
const path = require('path');
const { createReadStream, createWriteStream } = require('fs');
const { pipeline } = require('stream/promises');
const logger = require('./logger');
const config = require('../config/production');
class BackupManager {
constructor() {
this.dbPath = config.database.path;
this.backupDir = config.database.backupPath;
this.retentionDays = config.backup.retentionDays;
}
/**
* Initialize backup directory
*/
async initialize() {
try {
await fs.mkdir(this.backupDir, { recursive: true });
logger.info('Backup directory initialized', { path: this.backupDir });
} catch (error) {
logger.error('Failed to initialize backup directory', {
error: error.message,
path: this.backupDir
});
throw error;
}
}
/**
* Create a backup of the database
*/
async createBackup() {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFileName = `inventory-backup-${timestamp}.db`;
const backupPath = path.join(this.backupDir, backupFileName);
// Check if source database exists
try {
await fs.access(this.dbPath);
} catch (error) {
throw new Error(`Source database not found: ${this.dbPath}`);
}
// Create backup using file copy
await pipeline(
createReadStream(this.dbPath),
createWriteStream(backupPath)
);
// Verify backup file
const stats = await fs.stat(backupPath);
if (stats.size === 0) {
throw new Error('Backup file is empty');
}
logger.info('Database backup created successfully', {
backupPath,
size: stats.size,
timestamp
});
return {
success: true,
backupPath,
size: stats.size,
timestamp
};
} catch (error) {
logger.error('Database backup failed', { error: error.message });
throw error;
}
}
/**
* Restore database from backup
*/
async restoreBackup(backupPath) {
try {
// Verify backup file exists
try {
await fs.access(backupPath);
} catch (error) {
throw new Error(`Backup file not found: ${backupPath}`);
}
// Create backup of current database before restore
const currentBackupPath = `${this.dbPath}.pre-restore-${Date.now()}.bak`;
try {
await pipeline(
createReadStream(this.dbPath),
createWriteStream(currentBackupPath)
);
logger.info('Current database backed up before restore', {
path: currentBackupPath
});
} catch (error) {
logger.warn('Could not backup current database before restore', {
error: error.message
});
}
// Restore from backup
await pipeline(
createReadStream(backupPath),
createWriteStream(this.dbPath)
);
// Verify restored database
const stats = await fs.stat(this.dbPath);
if (stats.size === 0) {
throw new Error('Restored database is empty');
}
logger.info('Database restored successfully', {
backupPath,
restoredSize: stats.size
});
return {
success: true,
restoredFrom: backupPath,
size: stats.size
};
} catch (error) {
logger.error('Database restore failed', {
error: error.message,
backupPath
});
throw error;
}
}
/**
* List available backups
*/
async listBackups() {
try {
const files = await fs.readdir(this.backupDir);
const backups = [];
for (const file of files) {
if (file.endsWith('.db')) {
const filePath = path.join(this.backupDir, file);
const stats = await fs.stat(filePath);
backups.push({
filename: file,
path: filePath,
size: stats.size,
created: stats.birthtime,
modified: stats.mtime
});
}
}
// Sort by creation date (newest first)
backups.sort((a, b) => b.created - a.created);
return backups;
} catch (error) {
logger.error('Failed to list backups', { error: error.message });
throw error;
}
}
/**
* Clean up old backups based on retention policy
*/
async cleanupOldBackups() {
try {
const backups = await this.listBackups();
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.retentionDays);
let deletedCount = 0;
let deletedSize = 0;
for (const backup of backups) {
if (backup.created < cutoffDate) {
try {
await fs.unlink(backup.path);
deletedCount++;
deletedSize += backup.size;
logger.info('Old backup deleted', {
filename: backup.filename,
age: Math.floor((Date.now() - backup.created) / (1000 * 60 * 60 * 24))
});
} catch (error) {
logger.warn('Failed to delete old backup', {
filename: backup.filename,
error: error.message
});
}
}
}
logger.info('Backup cleanup completed', {
deletedCount,
deletedSize,
retentionDays: this.retentionDays
});
return { deletedCount, deletedSize };
} catch (error) {
logger.error('Backup cleanup failed', { error: error.message });
throw error;
}
}
/**
* Schedule automatic backups
*/
scheduleBackups() {
const interval = config.database.backupInterval;
if (!config.backup.enabled) {
logger.info('Automatic backups disabled');
return;
}
logger.info('Scheduling automatic backups', {
intervalMs: interval,
intervalHours: interval / (1000 * 60 * 60)
});
setInterval(async () => {
try {
await this.createBackup();
await this.cleanupOldBackups();
} catch (error) {
logger.error('Scheduled backup failed', { error: error.message });
}
}, interval);
}
/**
* Get backup statistics
*/
async getBackupStats() {
try {
const backups = await this.listBackups();
const totalSize = backups.reduce((sum, backup) => sum + backup.size, 0);
const oldestBackup = backups.length > 0 ? backups[backups.length - 1] : null;
const newestBackup = backups.length > 0 ? backups[0] : null;
return {
count: backups.length,
totalSize,
oldestBackup: oldestBackup ? {
filename: oldestBackup.filename,
created: oldestBackup.created,
size: oldestBackup.size
} : null,
newestBackup: newestBackup ? {
filename: newestBackup.filename,
created: newestBackup.created,
size: newestBackup.size
} : null
};
} catch (error) {
logger.error('Failed to get backup statistics', { error: error.message });
throw error;
}
}
}
module.exports = BackupManager;

166
utils/logger.js Normal file
View File

@ -0,0 +1,166 @@
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const path = require('path');
// Create logs directory if it doesn't exist
const logsDir = path.join(__dirname, '..', 'logs');
// Define log levels and colors
const logLevels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4
};
const logColors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'blue'
};
winston.addColors(logColors);
// Custom format for structured logging
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
let logMessage = `${timestamp} [${level.toUpperCase()}]: ${message}`;
// Add stack trace for errors
if (stack) {
logMessage += `\nStack: ${stack}`;
}
// Add metadata if present
if (Object.keys(meta).length > 0) {
logMessage += `\nMeta: ${JSON.stringify(meta, null, 2)}`;
}
return logMessage;
})
);
// Console format for development
const consoleFormat = winston.format.combine(
winston.format.colorize({ all: true }),
winston.format.timestamp({ format: 'HH:mm:ss' }),
winston.format.printf(({ timestamp, level, message, stack }) => {
let logMessage = `${timestamp} ${level}: ${message}`;
if (stack) {
logMessage += `\n${stack}`;
}
return logMessage;
})
);
// Create transports
const transports = [
// Console transport for development
new winston.transports.Console({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: consoleFormat,
handleExceptions: true,
handleRejections: true
}),
// File transport for all logs
new DailyRotateFile({
filename: path.join(logsDir, 'application-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
level: 'debug',
format: logFormat,
handleExceptions: true,
handleRejections: true
}),
// Separate file for errors
new DailyRotateFile({
filename: path.join(logsDir, 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '30d',
level: 'error',
format: logFormat,
handleExceptions: true,
handleRejections: true
}),
// HTTP requests log
new DailyRotateFile({
filename: path.join(logsDir, 'http-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '7d',
level: 'http',
format: logFormat
})
];
// Create logger instance
const logger = winston.createLogger({
levels: logLevels,
transports,
exitOnError: false
});
// Add request logging helper
logger.logRequest = (req, res, responseTime) => {
const logData = {
method: req.method,
url: req.originalUrl,
ip: req.ip || req.connection.remoteAddress,
userAgent: req.get('User-Agent'),
statusCode: res.statusCode,
responseTime: `${responseTime}ms`,
contentLength: res.get('Content-Length') || 0
};
// Log based on status code
if (res.statusCode >= 500) {
logger.error('HTTP Request Error', logData);
} else if (res.statusCode >= 400) {
logger.warn('HTTP Request Warning', logData);
} else {
logger.http('HTTP Request', logData);
}
};
// Add database operation logging helper
logger.logDbOperation = (operation, table, data = {}, duration = null) => {
const logData = {
operation,
table,
duration: duration ? `${duration}ms` : null,
...data
};
logger.debug('Database Operation', logData);
};
// Add error context helper
logger.logError = (error, context = {}) => {
const errorData = {
message: error.message,
stack: error.stack,
name: error.name,
code: error.code,
...context
};
logger.error('Application Error', errorData);
};
// Add business logic logging helper
logger.logBusinessEvent = (event, data = {}) => {
logger.info(`Business Event: ${event}`, data);
};
module.exports = logger;

262
utils/retry.js Normal file
View File

@ -0,0 +1,262 @@
const logger = require('./logger');
/**
* Retry configuration options
*/
const DEFAULT_RETRY_OPTIONS = {
maxAttempts: 3,
baseDelay: 1000, // 1 second
maxDelay: 10000, // 10 seconds
backoffFactor: 2,
jitter: true,
retryCondition: (error) => {
// Default retry condition - retry on network errors and 5xx responses
const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'];
const retryableStatusCodes = [500, 502, 503, 504];
return retryableCodes.includes(error.code) ||
retryableStatusCodes.includes(error.statusCode) ||
(error.status && retryableStatusCodes.includes(error.status));
}
};
/**
* Calculate delay with exponential backoff and optional jitter
*/
const calculateDelay = (attempt, options) => {
const { baseDelay, maxDelay, backoffFactor, jitter } = options;
let delay = baseDelay * Math.pow(backoffFactor, attempt - 1);
// Apply maximum delay limit
delay = Math.min(delay, maxDelay);
// Add jitter to prevent thundering herd
if (jitter) {
delay = delay * (0.5 + Math.random() * 0.5);
}
return Math.floor(delay);
};
/**
* Sleep utility function
*/
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Retry wrapper for async operations
*/
const withRetry = async (operation, options = {}) => {
const config = { ...DEFAULT_RETRY_OPTIONS, ...options };
const { maxAttempts, retryCondition } = config;
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
logger.debug('Retry Attempt', {
operation: operation.name || 'anonymous',
attempt,
maxAttempts
});
const result = await operation();
if (attempt > 1) {
logger.info('Retry Successful', {
operation: operation.name || 'anonymous',
attempt,
totalAttempts: attempt
});
}
return result;
} catch (error) {
lastError = error;
logger.warn('Retry Attempt Failed', {
operation: operation.name || 'anonymous',
attempt,
maxAttempts,
error: error.message,
errorCode: error.code,
statusCode: error.statusCode
});
// Check if we should retry
if (attempt === maxAttempts || !retryCondition(error)) {
break;
}
// Calculate and apply delay
const delay = calculateDelay(attempt, config);
logger.debug('Retry Delay', {
operation: operation.name || 'anonymous',
attempt,
delay: `${delay}ms`
});
await sleep(delay);
}
}
// All attempts failed
logger.error('Retry Exhausted', {
operation: operation.name || 'anonymous',
totalAttempts: maxAttempts,
finalError: lastError.message
});
throw lastError;
};
/**
* Retry wrapper for database operations
*/
const withDatabaseRetry = async (operation, options = {}) => {
const dbRetryOptions = {
maxAttempts: 3,
baseDelay: 500,
maxDelay: 5000,
retryCondition: (error) => {
// Retry on database connection errors and lock timeouts
const retryableCodes = [
'SQLITE_BUSY',
'SQLITE_LOCKED',
'SQLITE_PROTOCOL',
'ECONNRESET',
'ETIMEDOUT'
];
return retryableCodes.includes(error.code) ||
error.message.includes('database is locked') ||
error.message.includes('connection') ||
error.message.includes('timeout');
},
...options
};
return withRetry(operation, dbRetryOptions);
};
/**
* Retry wrapper for file operations
*/
const withFileRetry = async (operation, options = {}) => {
const fileRetryOptions = {
maxAttempts: 2,
baseDelay: 100,
maxDelay: 1000,
retryCondition: (error) => {
// Retry on temporary file system errors
const retryableCodes = [
'EBUSY',
'EMFILE',
'ENFILE',
'ENOENT'
];
return retryableCodes.includes(error.code);
},
...options
};
return withRetry(operation, fileRetryOptions);
};
/**
* Circuit breaker pattern implementation
*/
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000; // 1 minute
this.monitoringPeriod = options.monitoringPeriod || 10000; // 10 seconds
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.lastFailureTime = null;
this.successCount = 0;
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
this.state = 'HALF_OPEN';
this.successCount = 0;
logger.info('Circuit Breaker Half-Open', {
operation: operation.name || 'anonymous'
});
} else {
const error = new Error('Circuit breaker is OPEN');
error.code = 'CIRCUIT_BREAKER_OPEN';
throw error;
}
}
try {
const result = await operation();
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= 3) {
this.reset();
logger.info('Circuit Breaker Closed', {
operation: operation.name || 'anonymous'
});
}
} else {
this.reset();
}
return result;
} catch (error) {
this.recordFailure();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.lastFailureTime = Date.now();
logger.error('Circuit Breaker Opened', {
operation: operation.name || 'anonymous',
failureCount: this.failureCount,
threshold: this.failureThreshold
});
}
throw error;
}
}
reset() {
this.state = 'CLOSED';
this.failureCount = 0;
this.lastFailureTime = null;
this.successCount = 0;
}
recordFailure() {
this.failureCount++;
}
getState() {
return {
state: this.state,
failureCount: this.failureCount,
lastFailureTime: this.lastFailureTime,
successCount: this.successCount
};
}
}
module.exports = {
withRetry,
withDatabaseRetry,
withFileRetry,
CircuitBreaker,
calculateDelay,
sleep,
DEFAULT_RETRY_OPTIONS
};