From 9cc8482adc19a66f35ff32f313e4ee25eabea507 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Wed, 14 Jan 2026 14:55:08 +0100 Subject: [PATCH] Clean up the clickhouse client --- src/lib/clickhouse-client.test.ts | 135 +++++++++++++++++++++++++ src/lib/clickhouse-client.ts | 159 +++++++++++++----------------- src/lib/clickhouse-model.ts | 35 +++---- src/lib/typesense-client.test.ts | 2 +- 4 files changed, 223 insertions(+), 108 deletions(-) create mode 100644 src/lib/clickhouse-client.test.ts diff --git a/src/lib/clickhouse-client.test.ts b/src/lib/clickhouse-client.test.ts new file mode 100644 index 0000000..53aee41 --- /dev/null +++ b/src/lib/clickhouse-client.test.ts @@ -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; + 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; + 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; + 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; + 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'); + } + }); +}); diff --git a/src/lib/clickhouse-client.ts b/src/lib/clickhouse-client.ts index e900ec2..f6ea677 100644 --- a/src/lib/clickhouse-client.ts +++ b/src/lib/clickhouse-client.ts @@ -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(query: string): Promise> return await response.json() as ClickHouseResponse; } -export async function FetchManufacturingJobs(): Promise { +export async function FetchActiveJobs(activeCharacters: string[]): Promise { + 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(query); + const result = await queryClickHouse(query); return result.data; } -export async function FetchHistoricJobs(characterNames: string[], limit: number): Promise { - const characterFilter = characterNames.map(n => `'${n.replace(/'/g, "''")}'`).join(', '); +export async function FetchHistoricJobs(activeCharacters: string[]): Promise { + 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(query); + const result = await queryClickHouse(query); return result.data; } -export async function FetchBlueprintQueue(): Promise { +export async function FetchBlueprintQueue(): Promise { const query = ` SELECT * FROM default.blueprint_queue ORDER BY added_at DESC FORMAT JSON `; - const result = await queryClickHouse(query); - return result.data; -} - -export async function FetchCompletedJobs(characterNames: string[], limit: number): Promise { - 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(query); + const result = await queryClickHouse(query); return result.data; } diff --git a/src/lib/clickhouse-model.ts b/src/lib/clickhouse-model.ts index 29cfa24..4eadcd9 100644 --- a/src/lib/clickhouse-model.ts +++ b/src/lib/clickhouse-model.ts @@ -5,41 +5,38 @@ export interface ClickHouseResponse { 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; } diff --git a/src/lib/typesense-client.test.ts b/src/lib/typesense-client.test.ts index bbf55ff..c61c1bc 100644 --- a/src/lib/typesense-client.test.ts +++ b/src/lib/typesense-client.test.ts @@ -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(); }); -}); \ No newline at end of file +});