mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
291 lines
8.7 KiB
TypeScript
291 lines
8.7 KiB
TypeScript
import fs from "node:fs";
|
|
|
|
import { DEFAULT_DATABASE_SETTINGS, type DatabaseSettings } from "@/types/databaseSettings";
|
|
|
|
import { backupDbFile } from "./backup";
|
|
import { DATA_DIR, SQLITE_FILE, getDbInstance } from "./core";
|
|
import { invalidateDbCache } from "./readCache";
|
|
import { getDatabaseStats } from "./stats";
|
|
|
|
const DATABASE_SETTINGS_NAMESPACE = "databaseSettings";
|
|
|
|
export type UserDatabaseSettings = Omit<DatabaseSettings, "location" | "stats">;
|
|
type DatabaseSettingsSection = keyof UserDatabaseSettings;
|
|
|
|
const DATABASE_SETTINGS_SECTIONS = Object.keys(
|
|
DEFAULT_DATABASE_SETTINGS
|
|
) as DatabaseSettingsSection[];
|
|
|
|
const LEGACY_FLAT_KEYS: {
|
|
[TSection in DatabaseSettingsSection]: Partial<
|
|
Record<keyof UserDatabaseSettings[TSection] & string, string[]>
|
|
>;
|
|
} = {
|
|
logs: {
|
|
detailedLogsEnabled: ["detailedLogsEnabled"],
|
|
callLogPipelineEnabled: ["callLogPipelineEnabled"],
|
|
maxDetailSizeKb: ["maxDetailSizeKb"],
|
|
ringBufferSize: ["ringBufferSize"],
|
|
},
|
|
backup: {
|
|
autoBackupEnabled: ["autoBackupEnabled"],
|
|
autoBackupFrequency: ["autoBackupFrequency"],
|
|
keepLastNBackups: ["keepLastNBackups"],
|
|
},
|
|
cache: {
|
|
semanticCacheEnabled: ["semanticCacheEnabled"],
|
|
semanticCacheMaxSize: ["semanticCacheMaxSize"],
|
|
semanticCacheTTL: ["semanticCacheTTL"],
|
|
promptCacheEnabled: ["promptCacheEnabled"],
|
|
promptCacheStrategy: ["promptCacheStrategy"],
|
|
alwaysPreserveClientCache: ["alwaysPreserveClientCache"],
|
|
},
|
|
retention: {
|
|
quotaSnapshots: ["quotaSnapshots"],
|
|
compressionAnalytics: ["compressionAnalytics"],
|
|
mcpAudit: ["mcpAudit"],
|
|
a2aEvents: ["a2aEvents"],
|
|
callLogs: ["callLogs"],
|
|
usageHistory: ["usageHistory"],
|
|
memoryEntries: ["memoryEntries"],
|
|
autoCleanupEnabled: ["autoCleanupEnabled"],
|
|
},
|
|
aggregation: {
|
|
enabled: ["aggregationEnabled", "enabled"],
|
|
rawDataRetentionDays: ["rawDataRetentionDays"],
|
|
granularity: ["granularity"],
|
|
},
|
|
optimization: {
|
|
autoVacuumMode: ["autoVacuumMode"],
|
|
scheduledVacuum: ["scheduledVacuum"],
|
|
vacuumHour: ["vacuumHour"],
|
|
pageSize: ["pageSize"],
|
|
cacheSize: ["cacheSize"],
|
|
optimizeOnStartup: ["optimizeOnStartup"],
|
|
},
|
|
};
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function cloneDefaultSettings(): UserDatabaseSettings {
|
|
return structuredClone(DEFAULT_DATABASE_SETTINGS) as UserDatabaseSettings;
|
|
}
|
|
|
|
function parseStoredValue(rawValue: unknown): unknown {
|
|
if (typeof rawValue !== "string") return rawValue;
|
|
|
|
try {
|
|
return JSON.parse(rawValue);
|
|
} catch {
|
|
return rawValue;
|
|
}
|
|
}
|
|
|
|
function toBooleanSetting(value: unknown): boolean | null {
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "number") return !Number.isNaN(value) && value !== 0;
|
|
if (typeof value !== "string") return null;
|
|
|
|
const normalized = value.trim().toLowerCase();
|
|
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
return null;
|
|
}
|
|
|
|
function readNamespace(namespace: string): Record<string, unknown> {
|
|
const db = getDbInstance();
|
|
const rows = db
|
|
.prepare("SELECT key, value FROM key_value WHERE namespace = ?")
|
|
.all(namespace) as Array<{ key: string; value: string }>;
|
|
|
|
const values: Record<string, unknown> = {};
|
|
for (const row of rows) {
|
|
values[row.key] = parseStoredValue(row.value);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function mergeSectionObject(
|
|
target: UserDatabaseSettings,
|
|
section: DatabaseSettingsSection,
|
|
value: unknown
|
|
) {
|
|
if (!isRecord(value)) return;
|
|
|
|
const sectionTarget = target[section] as Record<string, unknown>;
|
|
const defaultSection = DEFAULT_DATABASE_SETTINGS[section] as Record<string, unknown>;
|
|
|
|
for (const key of Object.keys(defaultSection)) {
|
|
if (value[key] !== undefined) {
|
|
sectionTarget[key] = value[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
function mergeTopLevelSections(target: UserDatabaseSettings, values: Record<string, unknown>) {
|
|
for (const section of DATABASE_SETTINGS_SECTIONS) {
|
|
mergeSectionObject(target, section, values[section]);
|
|
}
|
|
}
|
|
|
|
function mergeRuntimeLogSettings(target: UserDatabaseSettings, values: Record<string, unknown>) {
|
|
const pipelineEnabled = toBooleanSetting(values.call_log_pipeline_enabled);
|
|
if (pipelineEnabled !== null) {
|
|
target.logs.callLogPipelineEnabled = pipelineEnabled;
|
|
}
|
|
|
|
const legacyDetailedEnabled = toBooleanSetting(values.detailed_logs_enabled);
|
|
if (legacyDetailedEnabled !== null) {
|
|
target.logs.detailedLogsEnabled = legacyDetailedEnabled;
|
|
}
|
|
}
|
|
|
|
function mergeDatabaseSettingsNamespace(
|
|
target: UserDatabaseSettings,
|
|
values: Record<string, unknown>
|
|
) {
|
|
for (const section of DATABASE_SETTINGS_SECTIONS) {
|
|
const defaultSection = DEFAULT_DATABASE_SETTINGS[section] as Record<string, unknown>;
|
|
const sectionTarget = target[section] as Record<string, unknown>;
|
|
const flatAliases = LEGACY_FLAT_KEYS[section] as Partial<Record<string, string[]>>;
|
|
|
|
for (const key of Object.keys(defaultSection)) {
|
|
for (const alias of flatAliases[key] ?? []) {
|
|
if (values[alias] !== undefined) {
|
|
sectionTarget[key] = values[alias];
|
|
}
|
|
}
|
|
|
|
const nestedKey = `${section}.${key}`;
|
|
if (values[nestedKey] !== undefined) {
|
|
sectionTarget[key] = values[nestedKey];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getWalSizeBytes(): number {
|
|
if (!SQLITE_FILE) return 0;
|
|
|
|
try {
|
|
const walPath = `${SQLITE_FILE}-wal`;
|
|
return fs.existsSync(walPath) ? fs.statSync(walPath).size : 0;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function getSchemaVersion(): number {
|
|
const db = getDbInstance();
|
|
|
|
try {
|
|
const row = db
|
|
.prepare("SELECT MAX(CAST(version AS INTEGER)) AS version FROM _omniroute_migrations")
|
|
.get() as { version: number | null } | undefined;
|
|
return row?.version ?? 0;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function getFreelistCount(): number {
|
|
try {
|
|
return getDbInstance().pragma("freelist_count", { simple: true }) as number;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function getIntegrityCheck(): "ok" | "error" | null {
|
|
try {
|
|
const result = getDbInstance().pragma("quick_check", { simple: true }) as string;
|
|
return result === "ok" ? "ok" : "error";
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function getUserDatabaseSettings(): UserDatabaseSettings {
|
|
const settings = cloneDefaultSettings();
|
|
const mainSettings = readNamespace("settings");
|
|
const databaseSettingsValue = mainSettings[DATABASE_SETTINGS_NAMESPACE];
|
|
|
|
if (isRecord(databaseSettingsValue)) {
|
|
mergeTopLevelSections(settings, databaseSettingsValue);
|
|
}
|
|
|
|
mergeTopLevelSections(settings, mainSettings);
|
|
mergeDatabaseSettingsNamespace(settings, readNamespace(DATABASE_SETTINGS_NAMESPACE));
|
|
mergeRuntimeLogSettings(settings, mainSettings);
|
|
|
|
return settings;
|
|
}
|
|
|
|
export function getDatabaseSettings(): DatabaseSettings {
|
|
const dbStats = getDatabaseStats();
|
|
|
|
return {
|
|
...getUserDatabaseSettings(),
|
|
location: {
|
|
databasePath: SQLITE_FILE ?? ":memory:",
|
|
dataDir: DATA_DIR,
|
|
walSizeBytes: getWalSizeBytes(),
|
|
schemaVersion: getSchemaVersion(),
|
|
},
|
|
stats: {
|
|
databaseSizeBytes: dbStats.totalSize,
|
|
pageCount: dbStats.pageCount,
|
|
freelistCount: getFreelistCount(),
|
|
lastVacuumAt: null,
|
|
lastOptimizationAt: null,
|
|
integrityCheck: getIntegrityCheck(),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function updateDatabaseSettings(
|
|
updates: Partial<UserDatabaseSettings>
|
|
): UserDatabaseSettings {
|
|
const nextSettings = getUserDatabaseSettings();
|
|
|
|
for (const section of DATABASE_SETTINGS_SECTIONS) {
|
|
if (updates[section] !== undefined) {
|
|
mergeSectionObject(nextSettings, section, updates[section]);
|
|
}
|
|
}
|
|
|
|
const db = getDbInstance();
|
|
const insert = db.prepare(
|
|
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)"
|
|
);
|
|
const settingsInsert = db.prepare(
|
|
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', ?, ?)"
|
|
);
|
|
|
|
const requestedLogs = updates.logs as Partial<UserDatabaseSettings["logs"]> | undefined;
|
|
const pipelineEnabled = requestedLogs?.callLogPipelineEnabled;
|
|
const detailedEnabled = requestedLogs?.detailedLogsEnabled;
|
|
|
|
const tx = db.transaction(() => {
|
|
for (const section of DATABASE_SETTINGS_SECTIONS) {
|
|
const sectionValues = nextSettings[section] as Record<string, unknown>;
|
|
|
|
for (const [key, value] of Object.entries(sectionValues)) {
|
|
insert.run(DATABASE_SETTINGS_NAMESPACE, `${section}.${key}`, JSON.stringify(value));
|
|
}
|
|
}
|
|
|
|
if (pipelineEnabled !== undefined) {
|
|
settingsInsert.run("call_log_pipeline_enabled", JSON.stringify(Boolean(pipelineEnabled)));
|
|
}
|
|
});
|
|
tx();
|
|
|
|
backupDbFile("pre-write");
|
|
invalidateDbCache("settings");
|
|
|
|
return nextSettings;
|
|
}
|