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.
102 lines
3.6 KiB
TypeScript
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 ?? '') };
|
|
}
|
|
}
|