Files
iistwin/server/finance-di2/db-core.ts
Ильяс Султанов 66f4e5ef53 DI2: удаление старого модуля финансов — пул БД и аудит перенесены в finance-di2
- server/finance/ (19 файлов) удалён: пул финансовой БД перенесён в server/finance-di2/db-core.ts, db-client.ts теперь реэкспортирует из ./db-core
- Шедулер аудита перенесён в server/finance-di2/audit-scheduler.ts (03:00 МСК, setInterval, без node-cron), запуск из registerDi2Routes с защитой от повторного старта
- История аудита DI2 теперь персистентна: загрузка/запись data/finance-audit-history.json в audit-agent.ts (порт из удалённого server/finance/audit.ts)
- registerFinanceRoutes убран из server/routes/index.ts, registerDi2Routes остался на его месте
- Клиент: удалены pages/finance/, components/finance/, hooks/useFinance.ts, hooks/useColumnLabels.ts, lib/format.ts, pages/salary.tsx; роуты /finance*, /income*, /expenses*, /salary и lazy-импорты убраны из App.tsx
- Навигация: «Финансы» в Sidebar и MobileBottomNav ведёт на субапп /di2 (внешняя ссылка), гейт moduleAccess.finance + finance.view сохранён
- index.css: удалён блок .hover-elevate/.hover-elevate-2/.active-elevate (использовался только старым модульным DataTable)
- FinanceBottomNav.tsx сохранён как референс для этапа 3 (.superpowers/sdd/di2-restyle/FinanceBottomNav.reference.tsx)
- Не тронуто: client-di2/, сиды прав finance.view/manage, finance_access (module-access)
2026-09-06 21:53:47 +03:00

140 lines
4.3 KiB
TypeScript

import { Pool, type PoolClient, type QueryResult, type QueryResultRow } from "pg";
import { readFileSync, existsSync, writeFileSync } from "fs";
import { resolve } from "path";
interface DbConfig {
host: string;
port: number;
database: string;
user: string;
password: string;
ssl?: boolean;
}
const CONFIG_PATH = resolve(process.cwd(), "data", "db-config.json");
let pool: Pool | null = null;
let currentConfig: DbConfig | null = null;
function loadConfigFromEnv(): DbConfig {
return {
host: process.env.FINANCE_DB_HOST || process.env.DB_HOST || "localhost",
port: parseInt(process.env.FINANCE_DB_PORT || process.env.DB_PORT || "5432", 10),
database: process.env.FINANCE_DB_NAME || process.env.DB_NAME || "postgres",
user: process.env.FINANCE_DB_USER || process.env.DB_USER || "postgres",
password: process.env.FINANCE_DB_PASSWORD || process.env.DB_PASSWORD || "",
ssl: process.env.FINANCE_DB_SSL === "true" || process.env.DB_SSL === "true",
};
}
function loadConfigFromFile(): DbConfig | null {
if (!existsSync(CONFIG_PATH)) return null;
try {
const raw = readFileSync(CONFIG_PATH, "utf-8");
const parsed = JSON.parse(raw) as DbConfig;
if (parsed.host && parsed.database && parsed.user) {
return parsed;
}
return null;
} catch {
return null;
}
}
function createPool(config: DbConfig): Pool {
return new Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
ssl: config.ssl ? { rejectUnauthorized: false } : false,
max: 10,
connectionTimeoutMillis: 10000,
});
}
export function getPool(): Pool {
if (!pool) {
const fileConfig = loadConfigFromFile();
const envConfig = loadConfigFromEnv();
currentConfig = fileConfig || envConfig;
if (!currentConfig) {
throw new Error("Database configuration not found");
}
pool = createPool(currentConfig);
pool.on("error", (err) => {
console.error("Unexpected finance DB pool error:", err);
});
}
return pool;
}
export async function query<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
const p = getPool();
return p.query<T>(sql, params);
}
// Клиент для ручных транзакций (BEGIN/COMMIT/ROLLBACK) — см. google-auth.ts (синхронизация зарплат)
export async function getPoolClient(): Promise<PoolClient> {
const p = getPool();
return p.connect();
}
export async function testConnection(config: DbConfig): Promise<{ ok: boolean; message: string }> {
const testPool = createPool(config);
try {
const client = await testPool.connect();
try {
await client.query("SELECT 1");
return { ok: true, message: "Подключение успешно" };
} finally {
client.release();
}
} catch (err: any) {
return { ok: false, message: err?.message || "Ошибка подключения" };
} finally {
await testPool.end();
}
}
export function setConfig(config: DbConfig): void {
if (pool) {
pool.end().catch(() => {});
}
pool = null;
currentConfig = config;
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
pool = createPool(config);
pool.on("error", (err) => {
console.error("Unexpected finance DB pool error:", err);
});
}
export function getConfig(): Omit<DbConfig, "password"> | null {
if (!currentConfig) return null;
const { password: _password, ...rest } = currentConfig;
return rest;
}
export async function isConnected(): Promise<boolean> {
try {
await query("SELECT 1");
return true;
} catch {
return false;
}
}
const CONNECTION_ERROR_CODES = new Set(["57P01", "57P02", "57P03", "08000", "08001", "08003", "08006"]);
export function isConnectionError(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const code = (err as any).code;
if (code && CONNECTION_ERROR_CODES.has(code)) return true;
const msg = err.message.toLowerCase();
return msg.includes("econnrefused") || msg.includes("etimedout") || msg.includes("enotfound") ||
msg.includes("connection terminated") || msg.includes("connection timeout") ||
msg.includes("закрытие подключения") || msg.includes("the database system is shutting down");
}