UNPKG

@voilajsx/appkit

Version:

Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development

242 lines 8.58 kB
/** * Ultra-simple file storage that just works with automatic Local/S3/R2 strategy * @module @voilajsx/appkit/storage * @file src/storage/index.ts * * @llm-rule WHEN: Building apps that need file storage with zero configuration * @llm-rule AVOID: Complex storage setups - this auto-detects Local/S3/R2 from environment * @llm-rule NOTE: Uses storageClass.get() pattern like auth - get() → storage.put() → distributed * @llm-rule NOTE: Common pattern - storageClass.get() → storage.put() → storage.url() → served */ import { StorageClass } from './storage.js'; import { getSmartDefaults } from './defaults.js'; // Global storage instance for performance (like auth module) let globalStorage = null; /** * Get storage instance - the only function you need to learn * Strategy auto-detected from environment (S3/R2 env vars → Cloud, nothing → Local) * @llm-rule WHEN: Need file storage in any part of your app - this is your main entry point * @llm-rule AVOID: Creating StorageClass directly - always use this function * @llm-rule NOTE: Typical flow - get() → storage.put() → storage.url() → file served */ function get(overrides = {}) { // Lazy initialization - parse environment once (like auth) if (!globalStorage) { const defaults = getSmartDefaults(); const config = { ...defaults, ...overrides }; globalStorage = new StorageClass(config); } return globalStorage; } /** * Clear storage instance and disconnect - essential for testing * @llm-rule WHEN: Testing storage logic with different configurations or app shutdown * @llm-rule AVOID: Using in production except for graceful shutdown */ async function clear() { if (globalStorage) { await globalStorage.disconnect(); globalStorage = null; } } /** * Reset storage configuration (useful for testing) * @llm-rule WHEN: Testing storage logic with different environment configurations * @llm-rule AVOID: Using in production - only for tests and development */ function reset(newConfig = {}) { // Clear existing instance if (globalStorage) { globalStorage.disconnect().catch(console.error); globalStorage = null; } // Create new instance with config const defaults = getSmartDefaults(); const config = { ...defaults, ...newConfig }; globalStorage = new StorageClass(config); return globalStorage; } /** * Get active storage strategy for debugging * @llm-rule WHEN: Debugging or health checks to see which strategy is active (Local vs S3 vs R2) * @llm-rule AVOID: Using for application logic - storage should be transparent */ function getStrategy() { const storage = get(); return storage.getStrategy(); } /** * Get storage configuration summary for debugging * @llm-rule WHEN: Health checks or debugging storage configuration * @llm-rule AVOID: Exposing sensitive connection details - this only shows safe info */ function getConfig() { const storage = get(); return storage.getConfig(); } /** * Check if cloud storage is available and configured * @llm-rule WHEN: Conditional logic based on storage capabilities * @llm-rule AVOID: Complex storage detection - just use storage normally, strategy handles it */ function hasCloudStorage() { const strategy = getStrategy(); return strategy === 's3' || strategy === 'r2'; } /** * Check if local storage is being used * @llm-rule WHEN: Development vs production feature detection * @llm-rule AVOID: Using for business logic - storage should be transparent */ function isLocal() { return getStrategy() === 'local'; } /** * Validate storage configuration at startup * @llm-rule WHEN: App startup to ensure storage is properly configured * @llm-rule AVOID: Skipping validation - missing storage config causes runtime failures */ function validateConfig() { try { const strategy = getStrategy(); if (strategy === 'local' && process.env.NODE_ENV === 'production') { console.warn('[VoilaJSX AppKit] Using local storage in production. ' + 'Files will only exist on single server instance. ' + 'Set AWS_S3_BUCKET or CLOUDFLARE_R2_BUCKET for distributed storage.'); } if (process.env.NODE_ENV === 'production' && !hasCloudStorage()) { console.warn('[VoilaJSX AppKit] No cloud storage configured in production. ' + 'Set AWS_S3_BUCKET or CLOUDFLARE_R2_BUCKET for scalable file storage.'); } } catch (error) { console.error('[VoilaJSX AppKit] Storage configuration validation failed:', error.message); } } /** * Get storage statistics for monitoring * @llm-rule WHEN: Monitoring storage system health and usage * @llm-rule AVOID: Using for business logic - this is for monitoring only */ function getStats() { const config = getConfig(); return { strategy: config.strategy, connected: config.connected, maxFileSize: `${Math.round(config.maxFileSize / 1048576)}MB`, environment: process.env.NODE_ENV || 'development', }; } /** * Graceful shutdown for storage system * @llm-rule WHEN: App shutdown or process termination * @llm-rule AVOID: Abrupt process exit - graceful shutdown prevents data corruption */ async function shutdown() { console.log('🔄 [AppKit] Storage graceful shutdown...'); try { await clear(); console.log('✅ [AppKit] Storage shutdown complete'); } catch (error) { console.error('❌ [AppKit] Storage shutdown error:', error.message); } } /** * Upload helper with common patterns * @llm-rule WHEN: Quick file uploads with automatic naming and validation * @llm-rule AVOID: Manual key generation - this handles common upload patterns */ async function upload(file, options) { const storage = get(); // Generate key with folder structure const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); const folder = options?.folder ? `${options.folder}/` : ''; const filename = options?.filename || `file-${timestamp}-${random}`; const key = `${folder}${filename}`; // Upload file const resultKey = await storage.put(key, file, { contentType: options?.contentType, }); // Get public URL const url = storage.url(resultKey); return { key: resultKey, url }; } /** * Download helper with error handling * @llm-rule WHEN: Quick file downloads with automatic error handling * @llm-rule AVOID: Manual error handling - this provides consistent download experience */ async function download(key) { const storage = get(); try { const data = await storage.get(key); // Try to determine content type from extension const ext = key.split('.').pop()?.toLowerCase(); const contentTypes = { 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'pdf': 'application/pdf', 'txt': 'text/plain', 'json': 'application/json', }; return { data, contentType: ext ? contentTypes[ext] : undefined, }; } catch (error) { throw new Error(`Failed to download file: ${key}`); } } /** * Single storage export with minimal API (like auth module) */ export const storageClass = { // Core method (like auth.get()) get, // Utility methods clear, reset, getStrategy, getConfig, hasCloudStorage, isLocal, getStats, validateConfig, shutdown, // Helper methods upload, download, }; export { StorageClass } from './storage.js'; // Default export export default StorageClass; // Auto-setup graceful shutdown handlers if (typeof process !== 'undefined') { // Handle graceful shutdown const shutdownHandler = () => { shutdown().finally(() => { process.exit(0); }); }; process.on('SIGTERM', shutdownHandler); process.on('SIGINT', shutdownHandler); // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('[AppKit] Uncaught exception during storage operation:', error); shutdown().finally(() => { process.exit(1); }); }); process.on('unhandledRejection', (reason) => { console.error('[AppKit] Unhandled rejection during storage operation:', reason); shutdown().finally(() => { process.exit(1); }); }); } //# sourceMappingURL=index.js.map