ceo-api/src/modules/intelligence/intelligence.service.ts
CEO-OS Deploy 1ac0dfc4ab fix(build): remove escaped backtick in intelligence.service.ts money_raise_all query
Escaped backtick (`) on line 13 caused TypeScript parser to fail on all
subsequent template literals in the file. Fixes CI build failure introduced
in the TradeModule commit.
2026-08-27 22:19:25 +02:00

102 lines
3.6 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
export interface ChQueryResult {
data: Record<string, unknown>[];
rows: number;
elapsed: number;
error?: string;
}
const SAFE_QUERIES: Record<string, (params: Record<string, string>) => string> = {
money_raise_all: () =>
`SELECT category, subcategory, region, stage, title, content, format, tags, source, language
FROM money_raise.knowledge_base
ORDER BY category, subcategory LIMIT 500`,
tables: () =>
`SELECT database, name, engine, total_rows, formatReadableSize(total_bytes) AS size
FROM system.tables
WHERE database NOT IN ('system','information_schema','INFORMATION_SCHEMA')
ORDER BY database, name LIMIT 100`,
legislation_search: ({ q }) =>
`SELECT * FROM default.legislation
WHERE lower(content) LIKE lower('%${q.replace(/'/g, "\\'")}%')
LIMIT 20`,
count_rows: ({ table }) =>
`SELECT count() AS rows FROM ${table.replace(/[^a-zA-Z0-9_.]/g, '')}`,
recent_rows: ({ table, limit = '10' }) =>
`SELECT * FROM ${table.replace(/[^a-zA-Z0-9_.]/g, '')} LIMIT ${Math.min(parseInt(limit) || 10, 100)}`,
databases: () =>
`SELECT name, engine FROM system.databases ORDER BY name`,
};
@Injectable()
export class IntelligenceService {
private readonly logger = new Logger(IntelligenceService.name);
private readonly baseUrl: string;
private readonly user: string;
private readonly password: string;
private readonly database: string;
constructor(private readonly config: ConfigService) {
this.baseUrl = config.get('CLICKHOUSE_URL', 'http://152.53.112.35:8123');
this.user = config.get('CLICKHOUSE_USER', 'default');
this.password = config.get('CLICKHOUSE_PASSWORD', '');
this.database = config.get('CLICKHOUSE_DATABASE', 'default');
}
private authHeader(): string {
return 'Basic ' + Buffer.from(`${this.user}:${this.password}`).toString('base64');
}
async query(sql: string): Promise<ChQueryResult> {
const url = new URL(this.baseUrl);
url.searchParams.set('database', this.database);
url.searchParams.set('default_format', 'JSON');
const t0 = Date.now();
let res: Response;
try {
res = await fetch(url.toString(), {
method: 'POST',
headers: {
'Authorization': this.authHeader(),
'X-ClickHouse-Format': 'JSON',
'Content-Type': 'text/plain',
},
body: sql,
signal: AbortSignal.timeout(15_000),
});
} catch (err: any) {
this.logger.error(`ClickHouse unreachable: ${err.message}`);
return { data: [], rows: 0, elapsed: Date.now() - t0, error: 'ClickHouse unreachable. Check CLICKHOUSE_URL.' };
}
const elapsed = Date.now() - t0;
if (!res.ok) {
const body = await res.text();
return { data: [], rows: 0, elapsed, error: `ClickHouse ${res.status}: ${body.slice(0, 200)}` };
}
const json = await res.json() as { data: Record<string, unknown>[]; rows: number };
return { data: json.data ?? [], rows: json.rows ?? 0, elapsed };
}
async safeQuery(queryKey: string, params: Record<string, string> = {}): Promise<ChQueryResult> {
const builder = SAFE_QUERIES[queryKey];
if (!builder) return { data: [], rows: 0, elapsed: 0, error: `Unknown query: ${queryKey}` };
return this.query(builder(params));
}
async status(): Promise<{ ok: boolean; version?: string; error?: string }> {
const r = await this.query('SELECT version() AS version');
if (r.error) return { ok: false, error: r.error };
return { ok: true, version: String(r.data[0]?.version ?? '') };
}
}