supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
508 lines • 19.9 kB
JavaScript
;
/**
* Detection Result Caching System for Epic 2: Smart Platform Detection Engine
* Provides caching and reuse capabilities for detection results to improve performance
* Part of Task 2.3.4: Add detection result caching and reuse capabilities
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_CACHE_OPTIONS = exports.DetectionCacheUtils = exports.DetectionCacheManager = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const logger_1 = require("../../core/utils/logger");
/**
* Main Detection Cache Manager
*/
class DetectionCacheManager {
constructor(options = {}) {
this.statistics = { hits: 0, misses: 0, totalAccesses: 0 };
this.options = this.mergeWithDefaults(options);
this.cacheDir = this.options.cacheDir;
this.ensureCacheDirectory();
}
/**
* Generate cache key for detection request
*/
generateCacheKey(databaseUrl, schemaHash, options) {
const keyData = {
url: this.normalizeUrl(databaseUrl),
schema: schemaHash,
options: options || {}
};
return crypto
.createHash('sha256')
.update(JSON.stringify(keyData))
.digest('hex')
.substring(0, 16);
}
/**
* Store detection results in cache
*/
async store(cacheKey, databaseUrl, schemaHash, detectionResults, autoConfiguration, ttl) {
try {
// Check if results meet minimum confidence threshold
if (detectionResults.integration.overallConfidence < this.options.minConfidenceToCache) {
logger_1.Logger.debug(`Not caching detection results due to low confidence: ${detectionResults.integration.overallConfidence}`);
return;
}
const entry = {
key: cacheKey,
schemaHash,
databaseUrl: this.normalizeUrl(databaseUrl),
detectionResults,
autoConfiguration,
metadata: {
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
accessCount: 0,
engineVersion: '2.5.0',
ttl: ttl || this.options.defaultTTL,
confidenceLevel: detectionResults.integration.overallConfidence >= 0.8 ? 'high' : 'medium'
}
};
// Clean up old entries before storing new ones
await this.cleanupExpiredEntries();
await this.enforceMaxSize();
// Store the entry
const filePath = this.getCacheFilePath(cacheKey);
const serialized = JSON.stringify(entry, null, 2);
await fs.promises.writeFile(filePath, serialized, 'utf8');
logger_1.Logger.debug(`Cached detection results with key: ${cacheKey}`);
}
catch (error) {
logger_1.Logger.warn('Failed to store detection cache entry:', error.message);
}
}
/**
* Retrieve detection results from cache
*/
async retrieve(cacheKey, databaseUrl, currentSchemaHash) {
try {
this.statistics.totalAccesses++;
const filePath = this.getCacheFilePath(cacheKey);
if (!fs.existsSync(filePath)) {
this.statistics.misses++;
return null;
}
const fileContent = await fs.promises.readFile(filePath, 'utf8');
const entry = JSON.parse(fileContent);
// Validate cache entry
const validationResult = this.validateCacheEntry(entry, databaseUrl, currentSchemaHash);
if (!validationResult.valid) {
logger_1.Logger.debug(`Cache entry invalid: ${validationResult.reason}`);
this.statistics.misses++;
// Remove invalid entry
await this.remove(cacheKey);
return null;
}
// Update access metadata
entry.metadata.lastAccessedAt = new Date().toISOString();
entry.metadata.accessCount++;
// Update the file with new access data
await fs.promises.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf8');
this.statistics.hits++;
logger_1.Logger.debug(`Cache hit for key: ${cacheKey}`);
return entry;
}
catch (error) {
logger_1.Logger.debug('Failed to retrieve cache entry:', error.message);
this.statistics.misses++;
return null;
}
}
/**
* Check if a cache entry exists and is valid
*/
async has(cacheKey, databaseUrl, currentSchemaHash) {
const entry = await this.retrieve(cacheKey, databaseUrl, currentSchemaHash);
return entry !== null;
}
/**
* Remove a cache entry
*/
async remove(cacheKey) {
try {
const filePath = this.getCacheFilePath(cacheKey);
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
logger_1.Logger.debug(`Removed cache entry: ${cacheKey}`);
}
}
catch (error) {
logger_1.Logger.debug('Failed to remove cache entry:', error.message);
}
}
/**
* Clear all cache entries
*/
async clear() {
try {
const files = await fs.promises.readdir(this.cacheDir);
const cacheFiles = files.filter(file => file.endsWith('.cache.json'));
await Promise.all(cacheFiles.map(file => fs.promises.unlink(path.join(this.cacheDir, file))));
// Reset statistics
this.statistics = { hits: 0, misses: 0, totalAccesses: 0 };
logger_1.Logger.info(`Cleared ${cacheFiles.length} cache entries`);
}
catch (error) {
logger_1.Logger.error('Failed to clear cache:', error.message);
}
}
/**
* Get cache statistics
*/
async getStatistics() {
try {
const files = await fs.promises.readdir(this.cacheDir);
const cacheFiles = files.filter(file => file.endsWith('.cache.json'));
let totalSize = 0;
let totalAge = 0;
let oldestEntry = 0;
let mostFrequentKey = '';
let maxAccessCount = 0;
for (const file of cacheFiles) {
const filePath = path.join(this.cacheDir, file);
const stats = await fs.promises.stat(filePath);
totalSize += stats.size;
try {
const entry = JSON.parse(await fs.promises.readFile(filePath, 'utf8'));
const age = Date.now() - new Date(entry.metadata.createdAt).getTime();
totalAge += age;
oldestEntry = Math.max(oldestEntry, age);
if (entry.metadata.accessCount > maxAccessCount) {
maxAccessCount = entry.metadata.accessCount;
mostFrequentKey = entry.key;
}
}
catch {
// Skip corrupted entries
}
}
const hitRate = this.statistics.totalAccesses > 0
? this.statistics.hits / this.statistics.totalAccesses
: 0;
return {
totalEntries: cacheFiles.length,
cacheSize: totalSize,
hitRate,
totalHits: this.statistics.hits,
totalMisses: this.statistics.misses,
averageAge: cacheFiles.length > 0 ? totalAge / cacheFiles.length : 0,
oldestEntry,
mostFrequentKey
};
}
catch (error) {
logger_1.Logger.error('Failed to get cache statistics:', error.message);
throw error;
}
}
/**
* Cleanup expired cache entries
*/
async cleanupExpiredEntries() {
try {
const files = await fs.promises.readdir(this.cacheDir);
const cacheFiles = files.filter(file => file.endsWith('.cache.json'));
let removedCount = 0;
const now = Date.now();
for (const file of cacheFiles) {
try {
const filePath = path.join(this.cacheDir, file);
const entry = JSON.parse(await fs.promises.readFile(filePath, 'utf8'));
const createdAt = new Date(entry.metadata.createdAt).getTime();
const ttl = entry.metadata.ttl;
if (now - createdAt > ttl) {
await fs.promises.unlink(filePath);
removedCount++;
}
}
catch {
// Remove corrupted entries
await fs.promises.unlink(path.join(this.cacheDir, file));
removedCount++;
}
}
if (removedCount > 0) {
logger_1.Logger.debug(`Cleaned up ${removedCount} expired cache entries`);
}
return removedCount;
}
catch (error) {
logger_1.Logger.debug('Failed to cleanup expired entries:', error.message);
return 0;
}
}
/**
* Validate cache entry against current conditions
*/
validateCacheEntry(entry, databaseUrl, currentSchemaHash) {
// Check TTL
const now = Date.now();
const createdAt = new Date(entry.metadata.createdAt).getTime();
if (now - createdAt > entry.metadata.ttl) {
return { valid: false, reason: 'Entry expired' };
}
// Check database URL if provided
if (databaseUrl && this.normalizeUrl(databaseUrl) !== entry.databaseUrl) {
return { valid: false, reason: 'Database URL mismatch' };
}
// Check schema hash if provided (for schema change detection)
if (this.options.invalidationStrategy !== 'time' && currentSchemaHash) {
if (entry.schemaHash !== currentSchemaHash) {
return { valid: false, reason: 'Schema has changed' };
}
}
// Check engine version compatibility
if (entry.metadata.engineVersion !== '2.5.0') {
return { valid: false, reason: 'Engine version mismatch' };
}
return { valid: true };
}
/**
* Enforce maximum cache size
*/
async enforceMaxSize() {
try {
const stats = await this.getStatistics();
if (stats.cacheSize <= this.options.maxCacheSize && stats.totalEntries <= this.options.maxEntries) {
return;
}
// Get all cache files sorted by last access time (oldest first)
const files = await fs.promises.readdir(this.cacheDir);
const cacheFiles = files.filter(file => file.endsWith('.cache.json'));
const fileData = [];
for (const file of cacheFiles) {
try {
const filePath = path.join(this.cacheDir, file);
const entry = JSON.parse(await fs.promises.readFile(filePath, 'utf8'));
fileData.push({
file,
lastAccessed: new Date(entry.metadata.lastAccessedAt).getTime(),
accessCount: entry.metadata.accessCount
});
}
catch {
// Mark corrupted files for removal
fileData.push({
file,
lastAccessed: 0,
accessCount: 0
});
}
}
// Sort by least recently used and least frequently accessed
fileData.sort((a, b) => {
// First, prioritize by access count (lower = remove first)
if (a.accessCount !== b.accessCount) {
return a.accessCount - b.accessCount;
}
// Then by last access time (older = remove first)
return a.lastAccessed - b.lastAccessed;
});
// Remove oldest entries until we're under limits
let removedCount = 0;
while (fileData.length > removedCount &&
(stats.totalEntries - removedCount > this.options.maxEntries ||
stats.cacheSize > this.options.maxCacheSize)) {
const toRemove = fileData[removedCount];
await fs.promises.unlink(path.join(this.cacheDir, toRemove.file));
removedCount++;
}
if (removedCount > 0) {
logger_1.Logger.debug(`Removed ${removedCount} cache entries to enforce size limits`);
}
}
catch (error) {
logger_1.Logger.debug('Failed to enforce cache size limits:', error.message);
}
}
/**
* Get cache file path for a given key
*/
getCacheFilePath(cacheKey) {
return path.join(this.cacheDir, `${cacheKey}.cache.json`);
}
/**
* Normalize database URL for consistent caching
*/
normalizeUrl(url) {
try {
const urlObj = new URL(url);
// Remove credentials and normalize format
return `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`.toLowerCase();
}
catch {
return url.toLowerCase();
}
}
/**
* Ensure cache directory exists
*/
ensureCacheDirectory() {
try {
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true });
}
}
catch (error) {
logger_1.Logger.warn('Failed to create cache directory:', error.message);
// Fall back to temp directory
this.cacheDir = path.join(process.cwd(), '.supa-seed-cache');
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true });
}
}
}
/**
* Merge options with defaults
*/
mergeWithDefaults(options) {
const defaultCacheDir = path.join(process.cwd(), '.supa-seed-cache');
return {
cacheDir: options.cacheDir || defaultCacheDir,
defaultTTL: 24 * 60 * 60 * 1000, // 24 hours
maxCacheSize: 100 * 1024 * 1024, // 100MB
maxEntries: 100,
enableCompression: false,
invalidationStrategy: 'hybrid',
minConfidenceToCache: 0.6,
...options
};
}
}
exports.DetectionCacheManager = DetectionCacheManager;
/**
* Utility functions for cache management
*/
class DetectionCacheUtils {
/**
* Generate schema hash for cache invalidation
*/
static async generateSchemaHash(client) {
try {
// Get basic schema information for hashing
const { data: tables } = await client
.from('information_schema.tables')
.select('table_name, table_type')
.eq('table_schema', 'public')
.order('table_name');
const { data: columns } = await client
.from('information_schema.columns')
.select('table_name, column_name, data_type, is_nullable')
.eq('table_schema', 'public')
.order('table_name, ordinal_position');
// Create a stable hash from schema structure
const schemaData = {
tables: tables || [],
columns: columns || []
};
return crypto
.createHash('sha256')
.update(JSON.stringify(schemaData))
.digest('hex');
}
catch (error) {
logger_1.Logger.debug('Failed to generate schema hash:', error.message);
// Return timestamp-based hash as fallback
return crypto
.createHash('sha256')
.update(Date.now().toString())
.digest('hex');
}
}
/**
* Format cache statistics for display
*/
static formatStatistics(stats) {
const lines = [];
lines.push('📊 Detection Cache Statistics');
lines.push('─'.repeat(40));
lines.push(`📋 Total Entries: ${stats.totalEntries}`);
lines.push(`💾 Cache Size: ${this.formatBytes(stats.cacheSize)}`);
lines.push(`🎯 Hit Rate: ${(stats.hitRate * 100).toFixed(1)}%`);
lines.push(`✅ Cache Hits: ${stats.totalHits}`);
lines.push(`❌ Cache Misses: ${stats.totalMisses}`);
if (stats.totalEntries > 0) {
lines.push(`⏰ Average Age: ${this.formatDuration(stats.averageAge)}`);
lines.push(`⏳ Oldest Entry: ${this.formatDuration(stats.oldestEntry)}`);
if (stats.mostFrequentKey) {
lines.push(`🔥 Most Used: ${stats.mostFrequentKey.substring(0, 8)}...`);
}
}
return lines.join('\n');
}
/**
* Format bytes for human-readable display
*/
static formatBytes(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0)
return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
}
/**
* Format duration for human-readable display
*/
static formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0)
return `${days}d ${hours % 24}h`;
if (hours > 0)
return `${hours}h ${minutes % 60}m`;
if (minutes > 0)
return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
}
}
exports.DetectionCacheUtils = DetectionCacheUtils;
/**
* Default cache options
*/
exports.DEFAULT_CACHE_OPTIONS = {
cacheDir: path.join(process.cwd(), '.supa-seed-cache'),
defaultTTL: 24 * 60 * 60 * 1000, // 24 hours
maxCacheSize: 100 * 1024 * 1024, // 100MB
maxEntries: 100,
enableCompression: false,
invalidationStrategy: 'hybrid',
minConfidenceToCache: 0.6
};
//# sourceMappingURL=detection-cache.js.map