@spfn/core
Version:
SPFN Framework Core - File-based routing, transactions, repository pattern
1,659 lines (1,638 loc) • 112 kB
JavaScript
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);