forked from boos/BoosAPI
Add tests for settings database runtime configuration API
- Implement tests for retrieving and saving runtime database configuration. - Validate handling of invalid configuration payloads. - Utilize Fastify for API testing with Vitest. - Ensure proper cleanup of database state between tests.
This commit is contained in:
+32
-3
@@ -9,7 +9,7 @@ import { config } from '../config.js';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
|
||||
type RuntimeDbDialect = 'sqlite' | 'mysql' | 'postgres';
|
||||
export type RuntimeDbDialect = 'sqlite' | 'mysql' | 'postgres';
|
||||
type SqlMethod = 'all' | 'get' | 'run' | 'values' | 'execute';
|
||||
|
||||
const TABLES_WITH_NUMERIC_ID = new Set([
|
||||
@@ -26,7 +26,7 @@ const TABLES_WITH_NUMERIC_ID = new Set([
|
||||
'events',
|
||||
]);
|
||||
|
||||
export const runtimeDbDialect: RuntimeDbDialect = config.dbType;
|
||||
export let runtimeDbDialect: RuntimeDbDialect = config.dbType;
|
||||
|
||||
let sqliteConnection: Database.Database | null = null;
|
||||
let mysqlPool: mysql.Pool | null = null;
|
||||
@@ -590,7 +590,13 @@ function initDb(): AppDb {
|
||||
return initSqliteDb();
|
||||
}
|
||||
|
||||
export const db: any = initDb();
|
||||
let activeDb: AppDb = initDb();
|
||||
|
||||
export const db: any = new Proxy({}, {
|
||||
get(_target, prop) {
|
||||
return (activeDb as any)?.[prop as keyof typeof activeDb];
|
||||
},
|
||||
});
|
||||
export { schema };
|
||||
|
||||
export async function closeDbConnections(): Promise<void> {
|
||||
@@ -607,3 +613,26 @@ export async function closeDbConnections(): Promise<void> {
|
||||
sqliteConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function switchRuntimeDatabase(nextDialect: RuntimeDbDialect, nextDbUrl: string): Promise<void> {
|
||||
const previousDialect = runtimeDbDialect;
|
||||
const previousDbUrl = config.dbUrl;
|
||||
const previousConfigDialect = config.dbType;
|
||||
|
||||
await closeDbConnections();
|
||||
|
||||
runtimeDbDialect = nextDialect;
|
||||
config.dbType = nextDialect;
|
||||
config.dbUrl = nextDbUrl;
|
||||
|
||||
try {
|
||||
activeDb = initDb();
|
||||
} catch (error) {
|
||||
await closeDbConnections();
|
||||
runtimeDbDialect = previousDialect;
|
||||
config.dbType = previousConfigDialect;
|
||||
config.dbUrl = previousDbUrl;
|
||||
activeDb = initDb();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
+79
-31
@@ -23,14 +23,13 @@ import { startProxyLogRetentionService, stopProxyLogRetentionService } from './s
|
||||
import { buildStartupSummaryLines } from './services/startupInfo.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { normalize, resolve, sep } from 'path';
|
||||
import { db, schema } from './db/index.js';
|
||||
import { db, runtimeDbDialect, schema, switchRuntimeDatabase, type RuntimeDbDialect } from './db/index.js';
|
||||
|
||||
// Load runtime config overrides from settings
|
||||
try {
|
||||
const rows = await db.select().from(schema.settings).all();
|
||||
const settingsMap = new Map(rows.map((row) => [row.key, row.value]));
|
||||
function toSettingsMap(rows: Array<{ key: string; value: string }>) {
|
||||
return new Map(rows.map((row) => [row.key, row.value]));
|
||||
}
|
||||
|
||||
const parseSetting = <T>(key: string): T | undefined => {
|
||||
function parseSettingFromMap<T>(settingsMap: Map<string, string>, key: string): T | undefined {
|
||||
const raw = settingsMap.get(key);
|
||||
if (typeof raw !== 'string' || !raw) return undefined;
|
||||
try {
|
||||
@@ -38,9 +37,9 @@ try {
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const toStringList = (value: unknown): string[] => {
|
||||
function toStringList(value: unknown): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
||||
@@ -53,21 +52,51 @@ try {
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
}
|
||||
|
||||
const authToken = parseSetting<string>('auth_token');
|
||||
function normalizeSavedDbType(value: unknown): RuntimeDbDialect | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === 'sqlite') return 'sqlite';
|
||||
if (normalized === 'mysql') return 'mysql';
|
||||
if (normalized === 'postgres' || normalized === 'postgresql') return 'postgres';
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateSavedDbUrl(dialect: RuntimeDbDialect, value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return null;
|
||||
if (dialect === 'sqlite') return normalized;
|
||||
if (dialect === 'mysql' && normalized.startsWith('mysql://')) return normalized;
|
||||
if (dialect === 'postgres' && (normalized.startsWith('postgres://') || normalized.startsWith('postgresql://'))) return normalized;
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractSavedRuntimeDatabaseConfig(settingsMap: Map<string, string>): { dialect: RuntimeDbDialect; dbUrl: string } | null {
|
||||
const rawType = parseSettingFromMap<unknown>(settingsMap, 'db_type');
|
||||
const rawUrl = parseSettingFromMap<unknown>(settingsMap, 'db_url');
|
||||
const dialect = normalizeSavedDbType(rawType);
|
||||
if (!dialect) return null;
|
||||
const dbUrl = validateSavedDbUrl(dialect, rawUrl);
|
||||
if (!dbUrl) return null;
|
||||
return { dialect, dbUrl };
|
||||
}
|
||||
|
||||
function applyRuntimeSettings(settingsMap: Map<string, string>) {
|
||||
const authToken = parseSettingFromMap<string>(settingsMap, 'auth_token');
|
||||
if (typeof authToken === 'string' && authToken) config.authToken = authToken;
|
||||
|
||||
const proxyToken = parseSetting<string>('proxy_token');
|
||||
const proxyToken = parseSettingFromMap<string>(settingsMap, 'proxy_token');
|
||||
if (typeof proxyToken === 'string' && proxyToken) config.proxyToken = proxyToken;
|
||||
|
||||
const checkinCron = parseSetting<string>('checkin_cron');
|
||||
const checkinCron = parseSettingFromMap<string>(settingsMap, 'checkin_cron');
|
||||
if (typeof checkinCron === 'string' && checkinCron) config.checkinCron = checkinCron;
|
||||
|
||||
const balanceRefreshCron = parseSetting<string>('balance_refresh_cron');
|
||||
const balanceRefreshCron = parseSettingFromMap<string>(settingsMap, 'balance_refresh_cron');
|
||||
if (typeof balanceRefreshCron === 'string' && balanceRefreshCron) config.balanceRefreshCron = balanceRefreshCron;
|
||||
|
||||
const routingWeights = parseSetting<Partial<typeof config.routingWeights>>('routing_weights');
|
||||
const routingWeights = parseSettingFromMap<Partial<typeof config.routingWeights>>(settingsMap, 'routing_weights');
|
||||
if (routingWeights && typeof routingWeights === 'object') {
|
||||
config.routingWeights = {
|
||||
...config.routingWeights,
|
||||
@@ -75,65 +104,84 @@ try {
|
||||
};
|
||||
}
|
||||
|
||||
const routingFallbackUnitCost = parseSetting<number>('routing_fallback_unit_cost');
|
||||
const routingFallbackUnitCost = parseSettingFromMap<number>(settingsMap, 'routing_fallback_unit_cost');
|
||||
if (typeof routingFallbackUnitCost === 'number' && Number.isFinite(routingFallbackUnitCost) && routingFallbackUnitCost > 0) {
|
||||
config.routingFallbackUnitCost = Math.max(1e-6, routingFallbackUnitCost);
|
||||
}
|
||||
|
||||
const webhookUrl = parseSetting<string>('webhook_url');
|
||||
const webhookUrl = parseSettingFromMap<string>(settingsMap, 'webhook_url');
|
||||
if (typeof webhookUrl === 'string') config.webhookUrl = webhookUrl;
|
||||
|
||||
const barkUrl = parseSetting<string>('bark_url');
|
||||
const barkUrl = parseSettingFromMap<string>(settingsMap, 'bark_url');
|
||||
if (typeof barkUrl === 'string') config.barkUrl = barkUrl;
|
||||
|
||||
const serverChanKey = parseSetting<string>('serverchan_key');
|
||||
const serverChanKey = parseSettingFromMap<string>(settingsMap, 'serverchan_key');
|
||||
if (typeof serverChanKey === 'string') config.serverChanKey = serverChanKey;
|
||||
|
||||
const telegramEnabled = parseSetting<boolean>('telegram_enabled');
|
||||
const telegramEnabled = parseSettingFromMap<boolean>(settingsMap, 'telegram_enabled');
|
||||
if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled;
|
||||
|
||||
const telegramBotToken = parseSetting<string>('telegram_bot_token');
|
||||
const telegramBotToken = parseSettingFromMap<string>(settingsMap, 'telegram_bot_token');
|
||||
if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken;
|
||||
|
||||
const telegramChatId = parseSetting<string>('telegram_chat_id');
|
||||
const telegramChatId = parseSettingFromMap<string>(settingsMap, 'telegram_chat_id');
|
||||
if (typeof telegramChatId === 'string') config.telegramChatId = telegramChatId;
|
||||
|
||||
const smtpEnabled = parseSetting<boolean>('smtp_enabled');
|
||||
const smtpEnabled = parseSettingFromMap<boolean>(settingsMap, 'smtp_enabled');
|
||||
if (typeof smtpEnabled === 'boolean') config.smtpEnabled = smtpEnabled;
|
||||
|
||||
const smtpHost = parseSetting<string>('smtp_host');
|
||||
const smtpHost = parseSettingFromMap<string>(settingsMap, 'smtp_host');
|
||||
if (typeof smtpHost === 'string') config.smtpHost = smtpHost;
|
||||
|
||||
const smtpPort = parseSetting<number>('smtp_port');
|
||||
const smtpPort = parseSettingFromMap<number>(settingsMap, 'smtp_port');
|
||||
if (typeof smtpPort === 'number' && Number.isFinite(smtpPort) && smtpPort > 0) {
|
||||
config.smtpPort = smtpPort;
|
||||
}
|
||||
|
||||
const smtpSecure = parseSetting<boolean>('smtp_secure');
|
||||
const smtpSecure = parseSettingFromMap<boolean>(settingsMap, 'smtp_secure');
|
||||
if (typeof smtpSecure === 'boolean') config.smtpSecure = smtpSecure;
|
||||
|
||||
const smtpUser = parseSetting<string>('smtp_user');
|
||||
const smtpUser = parseSettingFromMap<string>(settingsMap, 'smtp_user');
|
||||
if (typeof smtpUser === 'string') config.smtpUser = smtpUser;
|
||||
|
||||
const smtpPass = parseSetting<string>('smtp_pass');
|
||||
const smtpPass = parseSettingFromMap<string>(settingsMap, 'smtp_pass');
|
||||
if (typeof smtpPass === 'string') config.smtpPass = smtpPass;
|
||||
|
||||
const smtpFrom = parseSetting<string>('smtp_from');
|
||||
const smtpFrom = parseSettingFromMap<string>(settingsMap, 'smtp_from');
|
||||
if (typeof smtpFrom === 'string') config.smtpFrom = smtpFrom;
|
||||
|
||||
const smtpTo = parseSetting<string>('smtp_to');
|
||||
const smtpTo = parseSettingFromMap<string>(settingsMap, 'smtp_to');
|
||||
if (typeof smtpTo === 'string') config.smtpTo = smtpTo;
|
||||
|
||||
const notifyCooldownSec = parseSetting<number>('notify_cooldown_sec');
|
||||
const notifyCooldownSec = parseSettingFromMap<number>(settingsMap, 'notify_cooldown_sec');
|
||||
if (typeof notifyCooldownSec === 'number' && Number.isFinite(notifyCooldownSec) && notifyCooldownSec >= 0) {
|
||||
config.notifyCooldownSec = Math.trunc(notifyCooldownSec);
|
||||
}
|
||||
|
||||
const adminIpAllowlist = parseSetting<string[] | string>('admin_ip_allowlist');
|
||||
const adminIpAllowlist = parseSettingFromMap<string[] | string>(settingsMap, 'admin_ip_allowlist');
|
||||
if (adminIpAllowlist !== undefined) {
|
||||
config.adminIpAllowlist = toStringList(adminIpAllowlist);
|
||||
}
|
||||
}
|
||||
|
||||
// Load runtime config overrides from settings
|
||||
try {
|
||||
const initialRows = await db.select().from(schema.settings).all();
|
||||
const initialMap = toSettingsMap(initialRows);
|
||||
const savedDbConfig = extractSavedRuntimeDatabaseConfig(initialMap);
|
||||
const activeDbUrl = (config.dbUrl || '').trim();
|
||||
if (savedDbConfig && (savedDbConfig.dialect !== runtimeDbDialect || savedDbConfig.dbUrl !== activeDbUrl)) {
|
||||
try {
|
||||
await switchRuntimeDatabase(savedDbConfig.dialect, savedDbConfig.dbUrl);
|
||||
console.log(`Loaded runtime DB config from settings: ${savedDbConfig.dialect}`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to switch runtime DB from settings: ${(error as Error)?.message || 'unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
const finalRows = await db.select().from(schema.settings).all();
|
||||
const finalMap = toSettingsMap(finalRows);
|
||||
applyRuntimeSettings(finalMap);
|
||||
console.log('Loaded runtime settings overrides');
|
||||
} catch { /* first run, table may not exist */ }
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
type DbModule = typeof import('../../db/index.js');
|
||||
type ConfigModule = typeof import('../../config.js');
|
||||
|
||||
describe('settings database runtime config api', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: DbModule['db'];
|
||||
let schema: DbModule['schema'];
|
||||
let config: ConfigModule['config'];
|
||||
let dataDir = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
dataDir = mkdtempSync(join(tmpdir(), 'metapi-settings-db-runtime-'));
|
||||
process.env.DATA_DIR = dataDir;
|
||||
|
||||
await import('../../db/migrate.js');
|
||||
const dbModule = await import('../../db/index.js');
|
||||
const configModule = await import('../../config.js');
|
||||
const settingsRoutesModule = await import('./settings.js');
|
||||
|
||||
db = dbModule.db;
|
||||
schema = dbModule.schema;
|
||||
config = configModule.config;
|
||||
|
||||
app = Fastify();
|
||||
await app.register(settingsRoutesModule.settingsRoutes);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.delete(schema.settings).run();
|
||||
await db.delete(schema.events).run();
|
||||
|
||||
config.dbType = 'sqlite';
|
||||
config.dbUrl = '';
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
delete process.env.DATA_DIR;
|
||||
});
|
||||
|
||||
it('gets runtime database config state', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/database/runtime',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as {
|
||||
success?: boolean;
|
||||
active?: { dialect?: string };
|
||||
saved?: unknown;
|
||||
restartRequired?: boolean;
|
||||
};
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.active?.dialect).toBe('sqlite');
|
||||
expect(body.saved).toBeNull();
|
||||
expect(body.restartRequired).toBe(false);
|
||||
});
|
||||
|
||||
it('saves runtime database config and marks restart required', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/database/runtime',
|
||||
payload: {
|
||||
dialect: 'postgres',
|
||||
connectionString: 'postgres://metapi:secret@127.0.0.1:5432/postgres',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as {
|
||||
success?: boolean;
|
||||
saved?: { dialect?: string; connection?: string };
|
||||
restartRequired?: boolean;
|
||||
};
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.saved?.dialect).toBe('postgres');
|
||||
expect(body.saved?.connection).toContain('***');
|
||||
expect(body.restartRequired).toBe(true);
|
||||
|
||||
const dbType = await db.select().from(schema.settings).where(eq(schema.settings.key, 'db_type')).get();
|
||||
const dbUrl = await db.select().from(schema.settings).where(eq(schema.settings.key, 'db_url')).get();
|
||||
expect(dbType?.value).toBe(JSON.stringify('postgres'));
|
||||
expect(dbUrl?.value).toBe(JSON.stringify('postgres://metapi:secret@127.0.0.1:5432/postgres'));
|
||||
});
|
||||
|
||||
it('rejects invalid runtime database config payload', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/database/runtime',
|
||||
payload: {
|
||||
dialect: 'postgres',
|
||||
connectionString: 'mysql://root:pass@127.0.0.1:3306/mysql',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = response.json() as { success?: boolean; message?: string };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.message || '').toContain('postgres');
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,19 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import cron from 'node-cron';
|
||||
import { config } from '../../config.js';
|
||||
import { db, schema } from '../../db/index.js';
|
||||
import { db, runtimeDbDialect, schema } from '../../db/index.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import { updateBalanceRefreshCron, updateCheckinCron } from '../../services/checkinScheduler.js';
|
||||
import { sendNotification } from '../../services/notifyService.js';
|
||||
import { exportBackup, importBackup, type BackupExportType } from '../../services/backupService.js';
|
||||
import { startBackgroundTask } from '../../services/backgroundTaskService.js';
|
||||
import { migrateCurrentDatabase, testDatabaseConnection } from '../../services/databaseMigrationService.js';
|
||||
import {
|
||||
maskConnectionString,
|
||||
migrateCurrentDatabase,
|
||||
normalizeMigrationInput,
|
||||
testDatabaseConnection,
|
||||
type MigrationDialect,
|
||||
} from '../../services/databaseMigrationService.js';
|
||||
import { extractClientIp, isIpAllowed } from '../../middleware/auth.js';
|
||||
|
||||
type RoutingWeights = typeof config.routingWeights;
|
||||
@@ -45,7 +51,14 @@ interface DatabaseMigrationBody {
|
||||
overwrite?: unknown;
|
||||
}
|
||||
|
||||
type RuntimeDatabaseConfig = {
|
||||
dialect: MigrationDialect;
|
||||
connectionString: string;
|
||||
};
|
||||
|
||||
const PROXY_TOKEN_PREFIX = 'sk-';
|
||||
const DB_TYPE_SETTING_KEY = 'db_type';
|
||||
const DB_URL_SETTING_KEY = 'db_url';
|
||||
|
||||
function isValidProxyToken(value: string): boolean {
|
||||
return value.startsWith(PROXY_TOKEN_PREFIX) && value.length >= 6;
|
||||
@@ -281,6 +294,66 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
|
||||
};
|
||||
}
|
||||
|
||||
function parseJsonValue(raw: unknown): unknown {
|
||||
if (typeof raw !== 'string' || !raw) return undefined;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function maskRuntimeConnection(dialect: MigrationDialect, connectionString: string): string {
|
||||
const normalized = connectionString.trim();
|
||||
if (dialect === 'sqlite' && !normalized) return '(default sqlite path)';
|
||||
return maskConnectionString(normalized);
|
||||
}
|
||||
|
||||
async function loadSavedRuntimeDatabaseConfig(): Promise<RuntimeDatabaseConfig | null> {
|
||||
const settingsRows = await db.select().from(schema.settings).all();
|
||||
const map = new Map(settingsRows.map((row) => [row.key, row.value]));
|
||||
const rawDialect = parseJsonValue(map.get(DB_TYPE_SETTING_KEY));
|
||||
const rawConnection = parseJsonValue(map.get(DB_URL_SETTING_KEY));
|
||||
if (typeof rawDialect !== 'string' || typeof rawConnection !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const normalized = normalizeMigrationInput({
|
||||
dialect: rawDialect,
|
||||
connectionString: rawConnection,
|
||||
});
|
||||
return {
|
||||
dialect: normalized.dialect,
|
||||
connectionString: normalized.connectionString,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRuntimeDatabaseState(saved: RuntimeDatabaseConfig | null) {
|
||||
const activeDialect = runtimeDbDialect;
|
||||
const activeConnection = (config.dbUrl || '').trim();
|
||||
const restartRequired = !!saved && (
|
||||
saved.dialect !== activeDialect || saved.connectionString.trim() !== activeConnection
|
||||
);
|
||||
|
||||
return {
|
||||
active: {
|
||||
dialect: activeDialect,
|
||||
connection: maskRuntimeConnection(activeDialect, activeConnection),
|
||||
},
|
||||
saved: saved
|
||||
? {
|
||||
dialect: saved.dialect,
|
||||
connection: maskRuntimeConnection(saved.dialect, saved.connectionString),
|
||||
}
|
||||
: null,
|
||||
restartRequired,
|
||||
};
|
||||
}
|
||||
|
||||
export async function settingsRoutes(app: FastifyInstance) {
|
||||
await app.get('/api/settings/runtime', async (request) => {
|
||||
const currentAdminIp = extractClientIp(request.ip, request.headers['x-forwarded-for']);
|
||||
@@ -602,6 +675,44 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/settings/database/runtime', async () => {
|
||||
const saved = await loadSavedRuntimeDatabaseConfig();
|
||||
return {
|
||||
success: true,
|
||||
...buildRuntimeDatabaseState(saved),
|
||||
};
|
||||
});
|
||||
|
||||
app.put<{ Body: DatabaseMigrationBody }>('/api/settings/database/runtime', async (request, reply) => {
|
||||
try {
|
||||
const normalized = normalizeMigrationInput(request.body || {});
|
||||
await upsertSetting(DB_TYPE_SETTING_KEY, normalized.dialect);
|
||||
await upsertSetting(DB_URL_SETTING_KEY, normalized.connectionString);
|
||||
|
||||
await appendSettingsEvent({
|
||||
type: 'status',
|
||||
title: '数据库运行配置已更新',
|
||||
message: `已保存运行数据库配置:${normalized.dialect}(重启后生效)`,
|
||||
});
|
||||
|
||||
const saved: RuntimeDatabaseConfig = {
|
||||
dialect: normalized.dialect,
|
||||
connectionString: normalized.connectionString,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '数据库运行配置已保存,重启容器后生效',
|
||||
...buildRuntimeDatabaseState(saved),
|
||||
};
|
||||
} catch (err: any) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: err?.message || '数据库运行配置保存失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post<{ Body: DatabaseMigrationBody }>('/api/settings/database/test-connection', async (request, reply) => {
|
||||
try {
|
||||
const result = await testDatabaseConnection(request.body || {});
|
||||
|
||||
@@ -202,6 +202,12 @@ export const api = {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
getRuntimeDatabaseConfig: () => request('/api/settings/database/runtime'),
|
||||
updateRuntimeDatabaseConfig: (data: { dialect: 'sqlite' | 'mysql' | 'postgres'; connectionString: string }) =>
|
||||
request('/api/settings/database/runtime', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
testExternalDatabaseConnection: (data: { dialect: 'sqlite' | 'mysql' | 'postgres'; connectionString: string; overwrite?: boolean }) =>
|
||||
request('/api/settings/database/test-connection', {
|
||||
method: 'POST',
|
||||
|
||||
+355
-123
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user