- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
122 lines
3.4 KiB
TypeScript
122 lines
3.4 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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|