Clean up the clickhouse client

This commit is contained in:
2026-01-14 14:55:08 +01:00
parent f5c917f6da
commit 9cc8482adc
4 changed files with 223 additions and 108 deletions

View File

@@ -0,0 +1,135 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { FetchActiveJobs, FetchBlueprintQueue, FetchHistoricJobs } from './clickhouse-client';
describe('ClickHouse Client FetchManufacturingJobs', () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); });
afterEach(() => { fetchSpy.mockRestore(); });
it('fetches manufacturing jobs from latest snapshot', async () => {
const result = await FetchActiveJobs(['John D Pipe']);
expect(Array.isArray(result)).toBe(true);
expect(fetchSpy).toHaveBeenCalledOnce();
const url = new URL(fetchSpy.mock.calls[0][0] as string);
expect(url.hostname).toBe('mclickhouse.site.quack-lab.dev');
});
it('returns array of ManufacturingJobRow with correct shape', async () => {
const result = await FetchActiveJobs(['John D Pipe']);
if (result.length > 0) {
const job = result[0];
expect(job).toHaveProperty('activity_id');
expect(job).toHaveProperty('blueprint_id');
expect(job).toHaveProperty('job_id');
expect(job).toHaveProperty('name');
expect(job).toHaveProperty('product_type_id');
expect(job).toHaveProperty('start_date');
expect(job).toHaveProperty('end_date');
expect(job).toHaveProperty('status');
}
});
});
describe('ClickHouse Client FetchHistoricJobs', () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); });
afterEach(() => { fetchSpy.mockRestore(); });
it('fetches historic jobs with character filter', async () => {
const characterNames = ['John D Pipe'];
const result = await FetchHistoricJobs(characterNames);
expect(Array.isArray(result)).toBe(true);
expect(fetchSpy).toHaveBeenCalledOnce();
});
it('fetches historic jobs for multiple characters', async () => {
const characterNames = ['John D Pipe', 'BastardSlavDave', 'Primuskov'];
const result = await FetchHistoricJobs(characterNames);
expect(Array.isArray(result)).toBe(true);
expect(fetchSpy).toHaveBeenCalledOnce();
});
it('returns array of ManufacturingJobRow with correct shape', async () => {
const characterNames = ['John D Pipe'];
const result = await FetchHistoricJobs(characterNames);
if (result.length > 0) {
const job = result[0];
expect(job).toHaveProperty('activity_id');
expect(job).toHaveProperty('blueprint_id');
expect(job).toHaveProperty('job_id');
expect(job).toHaveProperty('name');
expect(job).toHaveProperty('product_type_id');
expect(job).toHaveProperty('start_date');
expect(job).toHaveProperty('end_date');
expect(job).toHaveProperty('status');
}
});
});
describe('ClickHouse Client FetchBlueprintQueue', () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); });
afterEach(() => { fetchSpy.mockRestore(); });
it('fetches blueprint queue items', async () => {
const result = await FetchBlueprintQueue();
expect(Array.isArray(result)).toBe(true);
expect(fetchSpy).toHaveBeenCalledOnce();
});
it('returns array of BlueprintQueueRow with correct shape', async () => {
const result = await FetchBlueprintQueue();
if (result.length > 0) {
const item = result[0];
expect(item).toHaveProperty('id');
expect(item).toHaveProperty('type_id');
expect(item).toHaveProperty('added_at');
}
});
});
describe('ClickHouse Client FetchHistoricJobs', () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); });
afterEach(() => { fetchSpy.mockRestore(); });
it('fetches completed jobs with character filter', async () => {
const characterNames = ['John D Pipe'];
const result = await FetchHistoricJobs(characterNames);
expect(Array.isArray(result)).toBe(true);
expect(fetchSpy).toHaveBeenCalledOnce();
});
it('fetches completed jobs for multiple characters', async () => {
const characterNames = ['John D Pipe', 'BastardSlavDave'];
const result = await FetchHistoricJobs(characterNames);
expect(Array.isArray(result)).toBe(true);
expect(fetchSpy).toHaveBeenCalledOnce();
});
it('returns array of ManufacturingJobRow with correct shape', async () => {
const characterNames = ['John D Pipe'];
const result = await FetchHistoricJobs(characterNames);
if (result.length > 0) {
const job = result[0];
expect(job).toHaveProperty('activity_id');
expect(job).toHaveProperty('blueprint_id');
expect(job).toHaveProperty('job_id');
expect(job).toHaveProperty('name');
expect(job).toHaveProperty('product_type_id');
expect(job).toHaveProperty('start_date');
expect(job).toHaveProperty('end_date');
expect(job).toHaveProperty('status');
}
});
});

View File

@@ -1,4 +1,4 @@
import { ClickHouseResponse, ManufacturingJobRow, BlueprintQueueRow } from "./clickhouse-model";
import { ClickHouseResponse, ManufacturingJob, BlueprintQueueItem } from "./clickhouse-model";
const CLICKHOUSE_URL = "https://mclickhouse.site.quack-lab.dev";
const CLICKHOUSE_USER = "indy_jobs_ro_user";
@@ -20,121 +20,104 @@ async function queryClickHouse<T>(query: string): Promise<ClickHouseResponse<T>>
return await response.json() as ClickHouseResponse<T>;
}
export async function FetchManufacturingJobs(): Promise<ManufacturingJobRow[]> {
export async function FetchActiveJobs(activeCharacters: string[]): Promise<ManufacturingJob[]> {
const characterFilter = activeCharacters.join(', ');
const query = `
WITH latest_snapshot AS (
SELECT MAX(created_at) AS max_created_at
FROM default.manufacturing_jobs
SELECT MAX(created_at) AS max_created_at
FROM default.manufacturing_jobs
)
SELECT * FROM default.manufacturing_jobs
SELECT
job_id,
activity_id,
blueprint_id,
blueprint_location_id,
blueprint_type_id,
cost,
duration,
end_date,
facility_id,
installer_id,
licensed_runs,
location_id,
output_location_id,
probability,
product_type_id,
runs,
start_date,
status,
birthday,
bloodline_id,
corporation_id,
description,
gender,
name,
race_id,
security_status,
max(created_at) as created_at
FROM default.manufacturing_jobs
WHERE created_at = (SELECT max_created_at FROM latest_snapshot)
AND name IN (${characterFilter})
GROUP BY job_id
ORDER BY end_date DESC
FORMAT JSON
`;
const result = await queryClickHouse<ManufacturingJobRow>(query);
const result = await queryClickHouse<ManufacturingJob>(query);
return result.data;
}
export async function FetchHistoricJobs(characterNames: string[], limit: number): Promise<ManufacturingJobRow[]> {
const characterFilter = characterNames.map(n => `'${n.replace(/'/g, "''")}'`).join(', ');
export async function FetchHistoricJobs(activeCharacters: string[]): Promise<ManufacturingJob[]> {
const characterFilter = activeCharacters.join(', ');
const query = `
SELECT
job_id,
argMax(activity_id, created_at) as activity_id,
argMax(blueprint_id, created_at) as blueprint_id,
argMax(blueprint_location_id, created_at) as blueprint_location_id,
argMax(blueprint_type_id, created_at) as blueprint_type_id,
argMax(cost, created_at) as cost,
argMax(duration, created_at) as duration,
argMax(end_date, created_at) as end_date,
argMax(facility_id, created_at) as facility_id,
argMax(installer_id, created_at) as installer_id,
argMax(licensed_runs, created_at) as licensed_runs,
argMax(location_id, created_at) as location_id,
argMax(output_location_id, created_at) as output_location_id,
argMax(probability, created_at) as probability,
argMax(product_type_id, created_at) as product_type_id,
argMax(runs, created_at) as runs,
argMax(start_date, created_at) as start_date,
argMax(status, created_at) as status,
argMax(birthday, created_at) as birthday,
argMax(bloodline_id, created_at) as bloodline_id,
argMax(corporation_id, created_at) as corporation_id,
argMax(description, created_at) as description,
argMax(gender, created_at) as gender,
argMax(name, created_at) as name,
argMax(race_id, created_at) as race_id,
argMax(security_status, created_at) as security_status,
max(created_at) as created_at
job_id,
argMax(activity_id, job_id) as activity_id,
argMax(blueprint_id, job_id) as blueprint_id,
argMax(blueprint_location_id, job_id) as blueprint_location_id,
argMax(blueprint_type_id, job_id) as blueprint_type_id,
argMax(cost, job_id) as cost,
argMax(duration, job_id) as duration,
argMax(end_date, job_id) as end_date,
argMax(facility_id, job_id) as facility_id,
argMax(installer_id, job_id) as installer_id,
argMax(licensed_runs, job_id) as licensed_runs,
argMax(location_id, job_id) as location_id,
argMax(output_location_id, job_id) as output_location_id,
argMax(probability, job_id) as probability,
argMax(product_type_id, job_id) as product_type_id,
argMax(runs, job_id) as runs,
argMax(start_date, job_id) as start_date,
argMax(status, job_id) as status,
argMax(birthday, job_id) as birthday,
argMax(bloodline_id, job_id) as bloodline_id,
argMax(corporation_id, job_id) as corporation_id,
argMax(description, job_id) as description,
argMax(gender, job_id) as gender,
argMax(name, job_id) as name,
argMax(race_id, job_id) as race_id,
argMax(security_status, job_id) as security_status,
max(created_at) as created_at
FROM default.manufacturing_jobs
WHERE name IN (${characterFilter})
GROUP BY job_id
ORDER BY end_date DESC
LIMIT ${limit}
FORMAT JSON
`;
const result = await queryClickHouse<ManufacturingJobRow>(query);
const result = await queryClickHouse<ManufacturingJob>(query);
return result.data;
}
export async function FetchBlueprintQueue(): Promise<BlueprintQueueRow[]> {
export async function FetchBlueprintQueue(): Promise<BlueprintQueueItem[]> {
const query = `
SELECT * FROM default.blueprint_queue
ORDER BY added_at DESC
FORMAT JSON
`;
const result = await queryClickHouse<BlueprintQueueRow>(query);
return result.data;
}
export async function FetchCompletedJobs(characterNames: string[], limit: number): Promise<ManufacturingJobRow[]> {
const characterFilter = characterNames.map(n => `'${n.replace(/'/g, "''")}'`).join(', ');
const query = `
WITH latest_snapshot AS (
SELECT MAX(created_at) AS latest_created_at
FROM default.manufacturing_jobs
)
SELECT
job_id,
argMax(activity_id, created_at) as activity_id,
argMax(blueprint_id, created_at) as blueprint_id,
argMax(blueprint_location_id, created_at) as blueprint_location_id,
argMax(blueprint_type_id, created_at) as blueprint_type_id,
argMax(cost, created_at) as cost,
argMax(duration, created_at) as duration,
argMax(end_date, created_at) as end_date,
argMax(facility_id, created_at) as facility_id,
argMax(installer_id, created_at) as installer_id,
argMax(licensed_runs, created_at) as licensed_runs,
argMax(location_id, created_at) as location_id,
argMax(output_location_id, created_at) as output_location_id,
argMax(probability, created_at) as probability,
argMax(product_type_id, created_at) as product_type_id,
argMax(runs, created_at) as runs,
argMax(start_date, created_at) as start_date,
argMax(status, created_at) as status,
argMax(birthday, created_at) as birthday,
argMax(bloodline_id, created_at) as bloodline_id,
argMax(corporation_id, created_at) as corporation_id,
argMax(description, created_at) as description,
argMax(gender, created_at) as gender,
argMax(name, created_at) as name,
argMax(race_id, created_at) as race_id,
argMax(security_status, created_at) as security_status,
max(created_at) as created_at
FROM default.manufacturing_jobs
WHERE name IN (${characterFilter})
GROUP BY job_id
HAVING created_at < (SELECT latest_created_at FROM latest_snapshot)
ORDER BY end_date DESC
LIMIT ${limit}
FORMAT JSON
`;
const result = await queryClickHouse<ManufacturingJobRow>(query);
const result = await queryClickHouse<BlueprintQueueItem>(query);
return result.data;
}

View File

@@ -5,41 +5,38 @@ export interface ClickHouseResponse<T> {
statistics: any;
}
export interface ManufacturingJobRow {
export interface ManufacturingJob {
activity_id: number;
blueprint_id: string;
blueprint_location_id: string;
blueprint_id: number;
blueprint_location_id: number;
blueprint_type_id: number;
cost: number;
duration: number;
end_date: string;
facility_id: string;
installer_id: string;
job_id: string;
end_date: Date;
facility_id: number;
installer_id: number;
job_id: number;
licensed_runs: number;
location_id: string;
output_location_id: string;
location_id: number;
output_location_id: number;
probability: number;
product_type_id: number;
runs: number;
start_date: string;
start_date: Date;
status: string;
birthday: string;
birthday: Date;
bloodline_id: number;
corporation_id: string;
corporation_id: number;
description: string;
gender: string;
name: string;
race_id: number;
security_status: number;
created_at: string;
created_at: Date;
}
export interface BlueprintQueueRow {
id: string;
export interface BlueprintQueueItem {
id: number;
type_id: number;
quantity: number | null;
added_by: string;
added_at: string;
completed: number;
added_at: Date;
}

View File

@@ -278,4 +278,4 @@ describe('Typesense Client GetTypeIconURL', () => {
expect(results).toBe(`https://proxy.site.quack-lab.dev/?url=https://images.evetech.net/types/11135/bpo?size=64`);
expect(fetchSpy).toHaveBeenCalled();
});
});
});