UNPKG

@spfn/core

Version:

SPFN Framework Core - File-based routing, transactions, repository pattern

1,659 lines (1,638 loc) 112 kB
import pino from 'pino'; import { readFileSync, existsSync, readdirSync, statSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, createWriteStream, renameSync } from 'fs'; import { join, dirname, relative, basename } from 'path'; import { config } from 'dotenv'; import postgres from 'postgres'; import { drizzle } from 'drizzle-orm/postgres-js'; import { timestamp, bigserial, pgSchema } from 'drizzle-orm/pg-core'; import { AsyncLocalStorage } from 'async_hooks'; import { randomUUID, randomBytes } from 'crypto'; import { createMiddleware } from 'hono/factory'; import { eq, and } from 'drizzle-orm'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { readdir, stat } from 'fs/promises'; import { serve } from '@hono/node-server'; import { networkInterfaces } from 'os'; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var PinoAdapter; var init_pino = __esm({ "src/logger/adapters/pino.ts"() { PinoAdapter = class _PinoAdapter { logger; constructor(config) { this.logger = pino({ level: config.level, // 기본 필드 base: config.module ? { module: config.module } : void 0 }); } child(module) { const childLogger = new _PinoAdapter({ level: this.logger.level, module }); childLogger.logger = this.logger.child({ module }); return childLogger; } debug(message, context) { this.logger.debug(context || {}, message); } info(message, context) { this.logger.info(context || {}, message); } warn(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.warn({ err: errorOrContext, ...context }, message); } else { this.logger.warn(errorOrContext || {}, message); } } error(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.error({ err: errorOrContext, ...context }, message); } else { this.logger.error(errorOrContext || {}, message); } } fatal(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.fatal({ err: errorOrContext, ...context }, message); } else { this.logger.fatal(errorOrContext || {}, message); } } async close() { } }; } }); // src/logger/types.ts var LOG_LEVEL_PRIORITY; var init_types = __esm({ "src/logger/types.ts"() { LOG_LEVEL_PRIORITY = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }; } }); // src/logger/formatters.ts function isSensitiveKey(key) { const lowerKey = key.toLowerCase(); return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive)); } function maskSensitiveData(data) { if (data === null || data === void 0) { return data; } if (Array.isArray(data)) { return data.map((item) => maskSensitiveData(item)); } if (typeof data === "object") { const masked = {}; for (const [key, value] of Object.entries(data)) { if (isSensitiveKey(key)) { masked[key] = MASKED_VALUE; } else if (typeof value === "object" && value !== null) { masked[key] = maskSensitiveData(value); } else { masked[key] = value; } } return masked; } return data; } function formatTimestamp(date) { return date.toISOString(); } function formatTimestampHuman(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); const ms = String(date.getMilliseconds()).padStart(3, "0"); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`; } function formatError(error) { const lines = []; lines.push(`${error.name}: ${error.message}`); if (error.stack) { const stackLines = error.stack.split("\n").slice(1); lines.push(...stackLines); } return lines.join("\n"); } function formatConsole(metadata, colorize = true) { const parts = []; const timestamp2 = formatTimestampHuman(metadata.timestamp); if (colorize) { parts.push(`${COLORS.gray}[${timestamp2}]${COLORS.reset}`); } else { parts.push(`[${timestamp2}]`); } if (metadata.module) { if (colorize) { parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`); } else { parts.push(`[module=${metadata.module}]`); } } if (metadata.context && Object.keys(metadata.context).length > 0) { Object.entries(metadata.context).forEach(([key, value]) => { const valueStr = typeof value === "string" ? value : String(value); if (colorize) { parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`); } else { parts.push(`[${key}=${valueStr}]`); } }); } const levelStr = metadata.level.toUpperCase(); if (colorize) { const color = COLORS[metadata.level]; parts.push(`${color}(${levelStr})${COLORS.reset}:`); } else { parts.push(`(${levelStr}):`); } if (colorize) { parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`); } else { parts.push(metadata.message); } let output = parts.join(" "); if (metadata.error) { output += "\n" + formatError(metadata.error); } return output; } function formatJSON(metadata) { const obj = { timestamp: formatTimestamp(metadata.timestamp), level: metadata.level, message: metadata.message }; if (metadata.module) { obj.module = metadata.module; } if (metadata.context) { obj.context = metadata.context; } if (metadata.error) { obj.error = { name: metadata.error.name, message: metadata.error.message, stack: metadata.error.stack }; } return JSON.stringify(obj); } var SENSITIVE_KEYS, MASKED_VALUE, COLORS; var init_formatters = __esm({ "src/logger/formatters.ts"() { SENSITIVE_KEYS = [ "password", "passwd", "pwd", "secret", "token", "apikey", "api_key", "accesstoken", "access_token", "refreshtoken", "refresh_token", "authorization", "auth", "cookie", "session", "sessionid", "session_id", "privatekey", "private_key", "creditcard", "credit_card", "cardnumber", "card_number", "cvv", "ssn", "pin" ]; MASKED_VALUE = "***MASKED***"; COLORS = { reset: "\x1B[0m", bright: "\x1B[1m", dim: "\x1B[2m", // 로그 레벨 컬러 debug: "\x1B[36m", // cyan info: "\x1B[32m", // green warn: "\x1B[33m", // yellow error: "\x1B[31m", // red fatal: "\x1B[35m", // magenta // 추가 컬러 gray: "\x1B[90m" }; } }); // src/logger/logger.ts var Logger; var init_logger = __esm({ "src/logger/logger.ts"() { init_types(); init_formatters(); Logger = class _Logger { config; module; constructor(config) { this.config = config; this.module = config.module; } /** * Get current log level */ get level() { return this.config.level; } /** * Create child logger (per module) */ child(module) { return new _Logger({ ...this.config, module }); } /** * Debug log */ debug(message, context) { this.log("debug", message, void 0, context); } /** * Info log */ info(message, context) { this.log("info", message, void 0, context); } warn(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.log("warn", message, errorOrContext, context); } else { this.log("warn", message, void 0, errorOrContext); } } error(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.log("error", message, errorOrContext, context); } else { this.log("error", message, void 0, errorOrContext); } } fatal(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.log("fatal", message, errorOrContext, context); } else { this.log("fatal", message, void 0, errorOrContext); } } /** * Log processing (internal) */ log(level, message, error, context) { if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) { return; } const metadata = { timestamp: /* @__PURE__ */ new Date(), level, message, module: this.module, error, // Mask sensitive information in context to prevent credential leaks context: context ? maskSensitiveData(context) : void 0 }; this.processTransports(metadata); } /** * Process Transports */ processTransports(metadata) { const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata)); Promise.all(promises).catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[Logger] Transport error: ${errorMessage} `); }); } /** * Transport log (error-safe) */ async safeTransportLog(transport, metadata) { try { await transport.log(metadata); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage} `); } } /** * Close all Transports */ async close() { const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close()); await Promise.all(closePromises); } }; } }); // src/logger/transports/console.ts var ConsoleTransport; var init_console = __esm({ "src/logger/transports/console.ts"() { init_types(); init_formatters(); ConsoleTransport = class { name = "console"; level; enabled; colorize; constructor(config) { this.level = config.level; this.enabled = config.enabled; this.colorize = config.colorize ?? true; } async log(metadata) { if (!this.enabled) { return; } if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) { return; } const message = formatConsole(metadata, this.colorize); if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") { console.error(message); } else { console.log(message); } } }; } }); var FileTransport; var init_file = __esm({ "src/logger/transports/file.ts"() { init_types(); init_formatters(); FileTransport = class { name = "file"; level; enabled; logDir; maxFileSize; maxFiles; currentStream = null; currentFilename = null; constructor(config) { this.level = config.level; this.enabled = config.enabled; this.logDir = config.logDir; this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024; this.maxFiles = config.maxFiles ?? 10; if (!existsSync(this.logDir)) { mkdirSync(this.logDir, { recursive: true }); } } async log(metadata) { if (!this.enabled) { return; } if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) { return; } const message = formatJSON(metadata); const filename = this.getLogFilename(metadata.timestamp); if (this.currentFilename !== filename) { await this.rotateStream(filename); await this.cleanOldFiles(); } else if (this.currentFilename) { await this.checkAndRotateBySize(); } if (this.currentStream) { return new Promise((resolve, reject) => { this.currentStream.write(message + "\n", "utf-8", (error) => { if (error) { process.stderr.write(`[FileTransport] Failed to write log: ${error.message} `); reject(error); } else { resolve(); } }); }); } } /** * 스트림 교체 (날짜 변경 시) */ async rotateStream(filename) { if (this.currentStream) { await this.closeStream(); } const filepath = join(this.logDir, filename); this.currentStream = createWriteStream(filepath, { flags: "a", // append mode encoding: "utf-8" }); this.currentFilename = filename; this.currentStream.on("error", (error) => { process.stderr.write(`[FileTransport] Stream error: ${error.message} `); this.currentStream = null; this.currentFilename = null; }); } /** * 현재 스트림 닫기 */ async closeStream() { if (!this.currentStream) { return; } return new Promise((resolve, reject) => { this.currentStream.end((error) => { if (error) { reject(error); } else { this.currentStream = null; this.currentFilename = null; resolve(); } }); }); } /** * 파일 크기 체크 및 크기 기반 로테이션 */ async checkAndRotateBySize() { if (!this.currentFilename) { return; } const filepath = join(this.logDir, this.currentFilename); if (!existsSync(filepath)) { return; } try { const stats = statSync(filepath); if (stats.size >= this.maxFileSize) { await this.rotateBySize(); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage} `); } } /** * 크기 기반 로테이션 수행 * 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log */ async rotateBySize() { if (!this.currentFilename) { return; } await this.closeStream(); const baseName = this.currentFilename.replace(/\.log$/, ""); const files = readdirSync(this.logDir); const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse(); for (const file of relatedFiles) { const match = file.match(/\.(\d+)\.log$/); if (match) { const oldNum = parseInt(match[1], 10); const newNum = oldNum + 1; const oldPath = join(this.logDir, file); const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`); try { renameSync(oldPath, newPath2); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage} `); } } } const currentPath = join(this.logDir, this.currentFilename); const newPath = join(this.logDir, `${baseName}.1.log`); try { if (existsSync(currentPath)) { renameSync(currentPath, newPath); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage} `); } await this.rotateStream(this.currentFilename); } /** * 오래된 로그 파일 정리 * maxFiles 개수를 초과하는 로그 파일 삭제 */ async cleanOldFiles() { try { if (!existsSync(this.logDir)) { return; } const files = readdirSync(this.logDir); const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => { const filepath = join(this.logDir, file); const stats = statSync(filepath); return { file, mtime: stats.mtime }; }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); if (logFiles.length > this.maxFiles) { const filesToDelete = logFiles.slice(this.maxFiles); for (const { file } of filesToDelete) { const filepath = join(this.logDir, file); try { unlinkSync(filepath); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage} `); } } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage} `); } } /** * 날짜별 로그 파일명 생성 */ getLogFilename(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}.log`; } async close() { await this.closeStream(); } }; } }); function isFileLoggingEnabled() { return process.env.LOGGER_FILE_ENABLED === "true"; } function getDefaultLogLevel() { const isProduction = process.env.NODE_ENV === "production"; const isDevelopment = process.env.NODE_ENV === "development"; if (isDevelopment) { return "debug"; } if (isProduction) { return "info"; } return "warn"; } function getConsoleConfig() { const isProduction = process.env.NODE_ENV === "production"; return { level: "debug", enabled: true, colorize: !isProduction // Dev: colored output, Production: plain text }; } function getFileConfig() { const isProduction = process.env.NODE_ENV === "production"; return { level: "info", enabled: isProduction, // File logging in production only logDir: process.env.LOG_DIR || "./logs", maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 10 }; } function validateDirectoryWritable(dirPath) { if (!existsSync(dirPath)) { try { mkdirSync(dirPath, { recursive: true }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`); } } try { accessSync(dirPath, constants.W_OK); } catch { throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`); } const testFile = join(dirPath, ".logger-write-test"); try { writeFileSync(testFile, "test", "utf-8"); unlinkSync(testFile); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`); } } function validateFileConfig() { if (!isFileLoggingEnabled()) { return; } const logDir = process.env.LOG_DIR; if (!logDir) { throw new Error( "LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp" ); } validateDirectoryWritable(logDir); } function validateSlackConfig() { const webhookUrl = process.env.SLACK_WEBHOOK_URL; if (!webhookUrl) { return; } if (!webhookUrl.startsWith("https://hooks.slack.com/")) { throw new Error( `Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"` ); } } function validateEmailConfig() { const smtpHost = process.env.SMTP_HOST; const smtpPort = process.env.SMTP_PORT; const emailFrom = process.env.EMAIL_FROM; const emailTo = process.env.EMAIL_TO; const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo; if (!hasAnyEmailConfig) { return; } const missingFields = []; if (!smtpHost) missingFields.push("SMTP_HOST"); if (!smtpPort) missingFields.push("SMTP_PORT"); if (!emailFrom) missingFields.push("EMAIL_FROM"); if (!emailTo) missingFields.push("EMAIL_TO"); if (missingFields.length > 0) { throw new Error( `Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.` ); } const port = parseInt(smtpPort, 10); if (isNaN(port) || port < 1 || port > 65535) { throw new Error( `Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.` ); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(emailFrom)) { throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`); } const recipients = emailTo.split(",").map((e) => e.trim()); for (const email of recipients) { if (!emailRegex.test(email)) { throw new Error(`Invalid email address in EMAIL_TO: "${email}"`); } } } function validateEnvironment() { const nodeEnv = process.env.NODE_ENV; if (!nodeEnv) { process.stderr.write( "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n" ); } } function validateConfig() { try { validateEnvironment(); validateFileConfig(); validateSlackConfig(); validateEmailConfig(); } catch (error) { if (error instanceof Error) { throw new Error(`[Logger] Configuration validation failed: ${error.message}`); } throw error; } } var init_config = __esm({ "src/logger/config.ts"() { } }); // src/logger/adapters/custom.ts function initializeTransports() { const transports = []; const consoleConfig = getConsoleConfig(); transports.push(new ConsoleTransport(consoleConfig)); const fileConfig = getFileConfig(); if (fileConfig.enabled) { transports.push(new FileTransport(fileConfig)); } return transports; } var CustomAdapter; var init_custom = __esm({ "src/logger/adapters/custom.ts"() { init_logger(); init_console(); init_file(); init_config(); CustomAdapter = class _CustomAdapter { logger; constructor(config) { this.logger = new Logger({ level: config.level, module: config.module, transports: initializeTransports() }); } child(module) { const adapter = new _CustomAdapter({ level: this.logger.level, module }); adapter.logger = this.logger.child(module); return adapter; } debug(message, context) { this.logger.debug(message, context); } info(message, context) { this.logger.info(message, context); } warn(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.warn(message, errorOrContext, context); } else { this.logger.warn(message, errorOrContext); } } error(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.error(message, errorOrContext, context); } else { this.logger.error(message, errorOrContext); } } fatal(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.fatal(message, errorOrContext, context); } else { this.logger.fatal(message, errorOrContext); } } async close() { await this.logger.close(); } }; } }); // src/logger/adapter-factory.ts function createAdapter(type) { const level = getDefaultLogLevel(); switch (type) { case "pino": return new PinoAdapter({ level }); case "custom": return new CustomAdapter({ level }); default: return new PinoAdapter({ level }); } } function getAdapterType() { const adapterEnv = process.env.LOGGER_ADAPTER; if (adapterEnv === "custom" || adapterEnv === "pino") { return adapterEnv; } return "pino"; } function initializeLogger() { validateConfig(); return createAdapter(getAdapterType()); } var logger; var init_adapter_factory = __esm({ "src/logger/adapter-factory.ts"() { init_pino(); init_custom(); init_config(); logger = initializeLogger(); } }); // src/logger/index.ts var init_logger2 = __esm({ "src/logger/index.ts"() { init_adapter_factory(); } }); // src/route/function-routes.ts var function_routes_exports = {}; __export(function_routes_exports, { discoverFunctionRoutes: () => discoverFunctionRoutes }); function discoverFunctionRoutes(cwd = process.cwd()) { const functions = []; const nodeModulesPath = join(cwd, "node_modules"); try { const projectPkgPath = join(cwd, "package.json"); const projectPkg = JSON.parse(readFileSync(projectPkgPath, "utf-8")); const dependencies = { ...projectPkg.dependencies, ...projectPkg.devDependencies }; for (const [packageName] of Object.entries(dependencies)) { if (!packageName.startsWith("@spfn/") && !packageName.startsWith("spfn-")) { continue; } try { const pkgPath = join(nodeModulesPath, ...packageName.split("/"), "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); if (pkg.spfn?.routes?.dir) { const { dir } = pkg.spfn.routes; const prefix = pkg.spfn.prefix; const packagePath = dirname(pkgPath); const routesDir = join(packagePath, dir); functions.push({ packageName, routesDir, packagePath, prefix // Include prefix in function info }); routeLogger.debug("Discovered function routes", { package: packageName, dir, prefix: prefix || "(none)" }); } } catch (error) { } } } catch (error) { routeLogger.warn("Failed to discover function routes", { error: error instanceof Error ? error.message : "Unknown error" }); } return functions; } var routeLogger; var init_function_routes = __esm({ "src/route/function-routes.ts"() { init_logger2(); routeLogger = logger.child("function-routes"); } }); // src/errors/database-errors.ts var DatabaseError, ConnectionError, QueryError, ConstraintViolationError, TransactionError, DeadlockError, DuplicateEntryError; var init_database_errors = __esm({ "src/errors/database-errors.ts"() { DatabaseError = class extends Error { statusCode; details; timestamp; constructor(message, statusCode = 500, details) { super(message); this.name = "DatabaseError"; this.statusCode = statusCode; this.details = details; this.timestamp = /* @__PURE__ */ new Date(); Error.captureStackTrace(this, this.constructor); } /** * Serialize error for API response */ toJSON() { return { name: this.name, message: this.message, statusCode: this.statusCode, details: this.details, timestamp: this.timestamp.toISOString() }; } }; ConnectionError = class extends DatabaseError { constructor(message, details) { super(message, 503, details); this.name = "ConnectionError"; } }; QueryError = class extends DatabaseError { constructor(message, statusCode = 500, details) { super(message, statusCode, details); this.name = "QueryError"; } }; ConstraintViolationError = class extends QueryError { constructor(message, details) { super(message, 400, details); this.name = "ConstraintViolationError"; } }; TransactionError = class extends DatabaseError { constructor(message, statusCode = 500, details) { super(message, statusCode, details); this.name = "TransactionError"; } }; DeadlockError = class extends TransactionError { constructor(message, details) { super(message, 409, details); this.name = "DeadlockError"; } }; DuplicateEntryError = class extends QueryError { constructor(field, value) { super(`${field} '${value}' already exists`, 409, { field, value }); this.name = "DuplicateEntryError"; } }; } }); // src/errors/http-errors.ts var init_http_errors = __esm({ "src/errors/http-errors.ts"() { } }); // src/errors/error-utils.ts var init_error_utils = __esm({ "src/errors/error-utils.ts"() { init_database_errors(); init_http_errors(); } }); // src/errors/index.ts var init_errors = __esm({ "src/errors/index.ts"() { init_database_errors(); init_http_errors(); init_error_utils(); } }); // src/env/config.ts var ENV_FILE_PRIORITY, TEST_ONLY_FILES; var init_config2 = __esm({ "src/env/config.ts"() { ENV_FILE_PRIORITY = [ ".env", // Base configuration (lowest priority) ".env.{NODE_ENV}", // Environment-specific ".env.local", // Local overrides (excluded in test) ".env.{NODE_ENV}.local" // Local environment-specific (highest priority) ]; TEST_ONLY_FILES = [ ".env.test", ".env.test.local" ]; } }); function buildFileList(basePath, nodeEnv) { const files = []; if (!nodeEnv) { files.push(join(basePath, ".env")); files.push(join(basePath, ".env.local")); return files; } for (const pattern of ENV_FILE_PRIORITY) { const fileName = pattern.replace("{NODE_ENV}", nodeEnv); if (nodeEnv === "test" && fileName === ".env.local") { continue; } if (nodeEnv === "local" && pattern === ".env.local") { continue; } if (nodeEnv !== "test" && TEST_ONLY_FILES.includes(fileName)) { continue; } files.push(join(basePath, fileName)); } return files; } function loadSingleFile(filePath, debug) { if (!existsSync(filePath)) { if (debug) { envLogger.debug("Environment file not found (optional)", { path: filePath }); } return { success: false, parsed: {}, error: "File not found" }; } try { const result = config({ path: filePath }); if (result.error) { envLogger.warn("Failed to parse environment file", { path: filePath, error: result.error.message }); return { success: false, parsed: {}, error: result.error.message }; } const parsed = result.parsed || {}; if (debug) { envLogger.debug("Environment file loaded successfully", { path: filePath, variables: Object.keys(parsed), count: Object.keys(parsed).length }); } return { success: true, parsed }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; envLogger.error("Error loading environment file", { path: filePath, error: message }); return { success: false, parsed: {}, error: message }; } } function validateRequiredVars(required, debug) { const missing = []; for (const varName of required) { if (!process.env[varName]) { missing.push(varName); } } if (missing.length > 0) { const error = `Required environment variables missing: ${missing.join(", ")}`; envLogger.error("Environment validation failed", { missing, required }); throw new Error(error); } if (debug) { envLogger.debug("Required environment variables validated", { required, allPresent: true }); } } function loadEnvironment(options = {}) { const { basePath = process.cwd(), customPaths = [], debug = false, nodeEnv = process.env.NODE_ENV || "", required = [], useCache = true } = options; if (useCache && environmentLoaded && cachedLoadResult) { if (debug) { envLogger.debug("Returning cached environment", { loaded: cachedLoadResult.loaded.length, variables: Object.keys(cachedLoadResult.parsed).length }); } return cachedLoadResult; } if (debug) { envLogger.debug("Loading environment variables", { basePath, nodeEnv, customPaths, required }); } const result = { success: true, loaded: [], failed: [], parsed: {}, warnings: [] }; const standardFiles = buildFileList(basePath, nodeEnv); const allFiles = [...standardFiles, ...customPaths]; if (debug) { envLogger.debug("Environment files to load", { standardFiles, customPaths, total: allFiles.length }); } const reversedFiles = [...allFiles].reverse(); for (const filePath of reversedFiles) { const fileResult = loadSingleFile(filePath, debug); if (fileResult.success) { result.loaded.push(filePath); Object.assign(result.parsed, fileResult.parsed); if (fileResult.parsed["NODE_ENV"]) { const fileName = filePath.split("/").pop() || filePath; result.warnings.push( `NODE_ENV found in ${fileName}. It's recommended to set NODE_ENV via CLI (e.g., 'spfn dev', 'spfn build') instead of .env files for consistent environment behavior.` ); } } else if (fileResult.error) { result.failed.push({ path: filePath, reason: fileResult.error }); } } if (debug || result.loaded.length > 0) { envLogger.info("Environment loading complete", { loaded: result.loaded.length, failed: result.failed.length, variables: Object.keys(result.parsed).length, files: result.loaded }); } if (required.length > 0) { try { validateRequiredVars(required, debug); } catch (error) { result.success = false; result.errors = [ error instanceof Error ? error.message : "Validation failed" ]; throw error; } } if (result.warnings.length > 0) { for (const warning of result.warnings) { envLogger.warn(warning); } } environmentLoaded = true; cachedLoadResult = result; return result; } var envLogger, environmentLoaded, cachedLoadResult; var init_loader = __esm({ "src/env/loader.ts"() { init_logger2(); init_config2(); envLogger = logger.child("environment"); environmentLoaded = false; } }); // src/env/validator.ts var init_validator = __esm({ "src/env/validator.ts"() { } }); // src/env/index.ts var init_env = __esm({ "src/env/index.ts"() { init_loader(); init_config2(); init_validator(); } }); // src/db/postgres-errors.ts function parseUniqueViolation(message) { const patterns = [ // Standard format: Key (field)=(value) /Key \(([^)]+)\)=\(([^)]+)\)/i, // With quotes: Key ("field")=('value') /Key \(["']?([^)"']+)["']?\)=\(["']?([^)"']+)["']?\)/i, // Alternative format /Key `([^`]+)`=`([^`]+)`/i ]; for (const pattern of patterns) { const match = message.match(pattern); if (match) { const field = match[1].trim().replace(/["'`]/g, ""); const value = match[2].trim().replace(/["'`]/g, ""); return { field, value }; } } return null; } function fromPostgresError(error) { const code = error?.code; const message = error?.message || "Database error occurred"; switch (code) { // Class 08 — Connection Exception case "08000": // connection_exception case "08001": // sqlclient_unable_to_establish_sqlconnection case "08003": // connection_does_not_exist case "08004": // sqlserver_rejected_establishment_of_sqlconnection case "08006": // connection_failure case "08007": // transaction_resolution_unknown case "08P01": return new ConnectionError(message, { code }); // Class 23 — Integrity Constraint Violation case "23000": // integrity_constraint_violation case "23001": return new ConstraintViolationError(message, { code, constraint: "integrity" }); case "23502": return new ConstraintViolationError(message, { code, constraint: "not_null" }); case "23503": return new ConstraintViolationError(message, { code, constraint: "foreign_key" }); case "23505": const parsed = parseUniqueViolation(message); if (parsed) { return new DuplicateEntryError(parsed.field, parsed.value); } return new DuplicateEntryError("field", "value"); case "23514": return new ConstraintViolationError(message, { code, constraint: "check" }); // Class 40 — Transaction Rollback case "40000": // transaction_rollback case "40001": // serialization_failure case "40002": // transaction_integrity_constraint_violation case "40003": return new TransactionError(message, 500, { code }); case "40P01": return new DeadlockError(message, { code }); // Class 42 — Syntax Error or Access Rule Violation case "42000": // syntax_error_or_access_rule_violation case "42601": // syntax_error case "42501": // insufficient_privilege case "42602": // invalid_name case "42622": // name_too_long case "42701": // duplicate_column case "42702": // ambiguous_column case "42703": // undefined_column case "42704": // undefined_object case "42P01": // undefined_table case "42P02": return new QueryError(message, 400, { code }); // Class 53 — Insufficient Resources case "53000": // insufficient_resources case "53100": // disk_full case "53200": // out_of_memory case "53300": return new ConnectionError(message, { code }); // Class 57 — Operator Intervention case "57000": // operator_intervention case "57014": // query_canceled case "57P01": // admin_shutdown case "57P02": // crash_shutdown case "57P03": return new ConnectionError(message, { code }); // Default: Unknown error default: return new QueryError(message, 500, { code }); } } var init_postgres_errors = __esm({ "src/db/postgres-errors.ts"() { init_errors(); } }); function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function createDatabaseConnection(connectionString, poolConfig, retryConfig) { let lastError; for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) { try { const client = postgres(connectionString, { max: poolConfig.max, idle_timeout: poolConfig.idleTimeout }); await client`SELECT 1 as test`; if (attempt > 0) { dbLogger.info(`Database connected successfully after ${attempt} retries`); } else { dbLogger.info("Database connected successfully"); } return client; } catch (error) { lastError = fromPostgresError(error); if (attempt < retryConfig.maxRetries) { const delayMs = Math.min( retryConfig.initialDelay * Math.pow(retryConfig.factor, attempt), retryConfig.maxDelay ); dbLogger.warn( `Connection failed (attempt ${attempt + 1}/${retryConfig.maxRetries + 1}), retrying in ${delayMs}ms...`, lastError, { attempt: attempt + 1, maxRetries: retryConfig.maxRetries + 1, delayMs } ); await delay(delayMs); } } } const errorMessage = `Failed to connect to database after ${retryConfig.maxRetries + 1} attempts: ${lastError?.message || "Unknown error"}`; throw new ConnectionError(errorMessage); } async function checkConnection(client) { try { await client`SELECT 1 as health_check`; return true; } catch (error) { dbLogger.error("Database health check failed", error); return false; } } var dbLogger; var init_connection = __esm({ "src/db/manager/connection.ts"() { init_logger2(); init_errors(); init_postgres_errors(); dbLogger = logger.child("database"); } }); // src/db/manager/config.ts function parseEnvNumber(key, prodDefault, devDefault) { const isProduction = process.env.NODE_ENV === "production"; const envValue = parseInt(process.env[key] || "", 10); return isNaN(envValue) ? isProduction ? prodDefault : devDefault : envValue; } function parseEnvBoolean(key, defaultValue) { const value = process.env[key]; if (value === void 0) return defaultValue; return value.toLowerCase() === "true"; } function getPoolConfig(options) { return { max: options?.max ?? parseEnvNumber("DB_POOL_MAX", 20, 10), idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20) }; } function getRetryConfig() { return { maxRetries: parseEnvNumber("DB_RETRY_MAX", 5, 3), initialDelay: parseEnvNumber("DB_RETRY_INITIAL_DELAY", 100, 50), maxDelay: parseEnvNumber("DB_RETRY_MAX_DELAY", 1e4, 5e3), factor: parseEnvNumber("DB_RETRY_FACTOR", 2, 2) }; } function buildHealthCheckConfig(options) { return { enabled: options?.enabled ?? parseEnvBoolean("DB_HEALTH_CHECK_ENABLED", true), interval: options?.interval ?? parseEnvNumber("DB_HEALTH_CHECK_INTERVAL", 6e4, 6e4), reconnect: options?.reconnect ?? parseEnvBoolean("DB_HEALTH_CHECK_RECONNECT", true), maxRetries: options?.maxRetries ?? parseEnvNumber("DB_HEALTH_CHECK_MAX_RETRIES", 3, 3), retryInterval: options?.retryInterval ?? parseEnvNumber("DB_HEALTH_CHECK_RETRY_INTERVAL", 5e3, 5e3) }; } function buildMonitoringConfig(options) { const isDevelopment = process.env.NODE_ENV !== "production"; return { enabled: options?.enabled ?? parseEnvBoolean("DB_MONITORING_ENABLED", isDevelopment), slowThreshold: options?.slowThreshold ?? parseEnvNumber("DB_MONITORING_SLOW_THRESHOLD", 1e3, 1e3), logQueries: options?.logQueries ?? parseEnvBoolean("DB_MONITORING_LOG_QUERIES", false) }; } var init_config3 = __esm({ "src/db/manager/config.ts"() { } }); function hasDatabaseConfig() { return !!(process.env.DATABASE_URL || process.env.DATABASE_WRITE_URL || process.env.DATABASE_READ_URL); } function detectDatabasePattern() { if (process.env.DATABASE_WRITE_URL && process.env.DATABASE_READ_URL) { return { type: "write-read", write: process.env.DATABASE_WRITE_URL, read: process.env.DATABASE_READ_URL }; } if (process.env.DATABASE_URL && process.env.DATABASE_REPLICA_URL) { return { type: "legacy", primary: process.env.DATABASE_URL, replica: process.env.DATABASE_REPLICA_URL }; } if (process.env.DATABASE_URL) { return { type: "single", url: process.env.DATABASE_URL }; } if (process.env.DATABASE_WRITE_URL) { return { type: "single", url: process.env.DATABASE_WRITE_URL }; } return { type: "none" }; } async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig) { const writeClient = await createDatabaseConnection(writeUrl, poolConfig, retryConfig); const readClient = await createDatabaseConnection(readUrl, poolConfig, retryConfig); return { write: drizzle(writeClient), read: drizzle(readClient), writeClient, readClient }; } async function createSingleClient(url, poolConfig, retryConfig) { const client = await createDatabaseConnection(url, poolConfig, retryConfig); const db = drizzle(client); return { write: db, read: db, writeClient: client, readClient: client }; } async function createDatabaseFromEnv(options) { if (!hasDatabaseConfig()) { dbLogger2.debug("No DATABASE_URL found, loading environment variables"); const result = loadEnvironment({ debug: true }); dbLogger2.debug("Environment variables loaded", { success: result.success, loaded: result.loaded.length, hasDatabaseUrl: !!process.env.DATABASE_URL, hasWriteUrl: !!process.env.DATABASE_WRITE_URL, hasReadUrl: !!process.env.DATABASE_READ_URL }); } if (!hasDatabaseConfig()) { dbLogger2.warn("No database configuration found", { cwd: process.cwd(), nodeEnv: process.env.NODE_ENV, checkedVars: ["DATABASE_URL", "DATABASE_WRITE_URL", "DATABASE_READ_URL"] }); return { write: void 0, read: void 0 }; } try { const poolConfig = getPoolConfig(options?.pool); const retryConfig = getRetryConfig(); const pattern = detectDatabasePattern(); switch (pattern.type) { case "write-read": dbLogger2.debug("Using write-read pattern", { write: pattern.write.replace(/:[^:@]+@/, ":***@"), read: pattern.read.replace(/:[^:@]+@/, ":***@") }); return await createWriteReadClients( pattern.write, pattern.read, poolConfig, retryConfig ); case "legacy": dbLogger2.debug("Using legacy replica pattern", { primary: pattern.primary.replace(/:[^:@]+@/, ":***@"), replica: pattern.replica.replace(/:[^:@]+@/, ":***@") }); return await createWriteReadClients( pattern.primary, pattern.replica, poolConfig, retryConfig ); case "single": dbLogger2.debug("Using single database pattern", { url: pattern.url.replace(/:[^:@]+@/, ":***@") }); return await createSingleClient(pattern.url, poolConfig, retryConfig); case "none": dbLogger2.warn("No database pattern detected"); return { write: void 0, read: void 0 }; } } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; dbLogger2.error("Failed to create database connection", { error: message, stage: "initialization", hasWriteUrl: !!process.env.DATABASE_WRITE_URL, hasReadUrl: !!process.env.DATABASE_READ_URL, hasUrl: !!process.env.DATABASE_URL, hasReplicaUrl: !!process.env.DATABASE_REPLICA_URL }); throw new Error(`Database connection failed: ${message}`, { cause: error }); } } var dbLogger2; var init_factory = __esm({ "src/db/manager/factory.ts"() { init_logger2(); init_env(); init_connection(); init_config3(); dbLogger2 = logger.child("database"); } }); // src/db/manager/global-state.ts var getWriteInstance, setWriteInstance, getReadInstance, setReadInstance, getWriteClient, setWriteClient, getReadClient, setReadClient, getHealthCheckInterval, setHealthCheckInterval, setMonitoringConfig; var init_global_state = __esm({ "src/db/manager/global-state.ts"() { getWriteInstance = () => globalThis.__SPFN_DB_WRITE__; setWriteInstance = (instance) => { globalThis.__SPFN_DB_WRITE__ = instance; }; getReadInstance = () => globalThis.__SPFN_DB_READ__; setReadInstance = (instance) => { globalThis.__SPFN_DB_READ__ = instance; }; getWriteClient = () => globalThis.__SPFN_DB_WRITE_CLIENT__; setWriteClient = (client) => { globalThis.__SPFN_DB_WRITE_CLIENT__ = client; }; getReadClient = () => globalThis.__SPFN_DB_READ_CLIENT__; setReadClient = (client) => { globalThis.__SPFN_DB_READ_CLIENT__ = client; }; getHealthCheckInterval = () => globalThis.__SPFN_DB_HEALTH_CHECK__; setHealthCheckInterval = (interval) => { globalThis.__SPFN_DB_HEALTH_CHECK__ = interval; }; setMonitoringConfig = (config) => { globalThis.__SPFN_DB_MONITORING__ = config; }; } }); // src/db/manager/health-check.ts function startHealthCheck(config, options, getDatabase2, closeDatabase2) { const healthCheck = getHealthCheckInterval(); if (healthCheck) { dbLogger3.debug("Health check already running"); return; } dbLogger3.info("Starting database health check", { interval: `${config.interval}ms`, reconnect: config.reconnect }); const interval = setInterval(async () => { try { const write = getDatabase2("write"); const read = getDatabase2("read"); if (write) { await write.execute("SELECT 1"); } if (read && read !== write) { await read.execute("SELECT 1"); } } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; dbLogger3.error("Database health check failed", { error: message }); if (config.reconnect) { await attemptReconnection(config, options, closeDatabase2); } } }, config.interval); setHealthCheckInterval(interval); } async function attemptReconnection(config, options, closeDatabase2) { dbLogger3.warn("Attempting database reconnection", { maxRetries: config.maxRetries, retryInterval: `${config.retryInterval}ms` }); for (let attempt = 1; attempt <= config.maxRetries; attempt++) { try { dbLogger3.debug(`Reconnection attempt ${attempt}/${config.maxRetries}`); await closeDatabase2(); await new Promise((resolve) => setTimeout(resolve, config.retryInterval)); const result = await createDatabaseFromEnv(options); if (result.write) { await result.write.execute("SELECT 1"); setWriteInstance(result.write);