diff --git a/src/__tests__/analytics-helpers.test.ts b/src/__tests__/analytics-helpers.test.ts index c1a34a5..9295cb0 100644 --- a/src/__tests__/analytics-helpers.test.ts +++ b/src/__tests__/analytics-helpers.test.ts @@ -3,18 +3,36 @@ import { selectBucketTable, groupEndpoint } from "../lib/analytics-queries"; describe("selectBucketTable", () => { it("returns 10min table for <= 1 day", () => { - expect(selectBucketTable(1)).toEqual({ table: "traffic_10min", bucketSize: 600 }); - expect(selectBucketTable(0.5)).toEqual({ table: "traffic_10min", bucketSize: 600 }); + expect(selectBucketTable(1)).toEqual({ + table: "traffic_10min", + bucketSize: 600, + }); + expect(selectBucketTable(0.5)).toEqual({ + table: "traffic_10min", + bucketSize: 600, + }); }); it("returns hourly table for 2-30 days", () => { - expect(selectBucketTable(7)).toEqual({ table: "traffic_hourly", bucketSize: 3600 }); - expect(selectBucketTable(30)).toEqual({ table: "traffic_hourly", bucketSize: 3600 }); + expect(selectBucketTable(7)).toEqual({ + table: "traffic_hourly", + bucketSize: 3600, + }); + expect(selectBucketTable(30)).toEqual({ + table: "traffic_hourly", + bucketSize: 3600, + }); }); it("returns daily table for > 30 days", () => { - expect(selectBucketTable(31)).toEqual({ table: "traffic_daily", bucketSize: 86400 }); - expect(selectBucketTable(90)).toEqual({ table: "traffic_daily", bucketSize: 86400 }); + expect(selectBucketTable(31)).toEqual({ + table: "traffic_daily", + bucketSize: 86400, + }); + expect(selectBucketTable(90)).toEqual({ + table: "traffic_daily", + bucketSize: 86400, + }); }); }); diff --git a/src/__tests__/cache.integration.test.ts b/src/__tests__/cache.integration.test.ts index 1c34918..6ffcdbb 100644 --- a/src/__tests__/cache.integration.test.ts +++ b/src/__tests__/cache.integration.test.ts @@ -9,13 +9,17 @@ describe("SlackCache integration", () => { let cache: SlackCache; beforeAll(() => { - try { unlinkSync(TEST_DB_PATH); } catch {} + try { + unlinkSync(TEST_DB_PATH); + } catch {} cache = new SlackCache(TEST_DB_PATH, 24); }); afterAll(() => { cache.close(); - try { unlinkSync(TEST_DB_PATH); } catch {} + try { + unlinkSync(TEST_DB_PATH); + } catch {} }); describe("user CRUD", () => { @@ -38,7 +42,12 @@ describe("SlackCache integration", () => { }); it("normalizes userId to uppercase", async () => { - await cache.insertUser("u456", "Lower", "", "https://example.com/lower.png"); + await cache.insertUser( + "u456", + "Lower", + "", + "https://example.com/lower.png", + ); const user = await cache.getUser("u456"); expect(user).not.toBeNull(); expect(user!.userId).toBe("U456"); @@ -46,7 +55,12 @@ describe("SlackCache integration", () => { it("updates imageUrl on conflict (displayName/pronouns preserved from first insert)", async () => { await cache.insertUser("U789", "Old Name", "", "https://old.com/img.png"); - await cache.insertUser("U789", "New Name", "she/her", "https://new.com/img.png"); + await cache.insertUser( + "U789", + "New Name", + "she/her", + "https://new.com/img.png", + ); const user = await cache.getUser("U789"); // ON CONFLICT only updates imageUrl and expiration, not displayName/pronouns @@ -60,7 +74,12 @@ describe("SlackCache integration", () => { }); it("purges a specific user", async () => { - await cache.insertUser("UPURGE", "Purge Me", "", "https://example.com/p.png"); + await cache.insertUser( + "UPURGE", + "Purge Me", + "", + "https://example.com/p.png", + ); const purged = await cache.purgeUserCache("UPURGE"); expect(purged).toBe(true); @@ -71,7 +90,11 @@ describe("SlackCache integration", () => { describe("emoji CRUD", () => { it("inserts and retrieves an emoji", async () => { - const ok = await cache.insertEmoji("hackshark", null, "https://emoji.com/hackshark.png"); + const ok = await cache.insertEmoji( + "hackshark", + null, + "https://emoji.com/hackshark.png", + ); expect(ok).toBe(true); const emoji = await cache.getEmoji("hackshark"); @@ -89,7 +112,11 @@ describe("SlackCache integration", () => { }); it("handles emoji aliases", async () => { - await cache.insertEmoji("alias_emoji", "original", "https://emoji.com/alias.png"); + await cache.insertEmoji( + "alias_emoji", + "original", + "https://emoji.com/alias.png", + ); const emoji = await cache.getEmoji("alias_emoji"); expect(emoji!.alias).toBe("original"); }); @@ -104,7 +131,9 @@ describe("SlackCache integration", () => { expect(ok).toBe(true); const all = await cache.getAllEmojis(); - const batchNames = all.filter(e => e.name.startsWith("batch")).map(e => e.name); + const batchNames = all + .filter((e) => e.name.startsWith("batch")) + .map((e) => e.name); expect(batchNames).toContain("batch1"); expect(batchNames).toContain("batch2"); expect(batchNames).toContain("batch3"); @@ -158,7 +187,13 @@ describe("SlackCache integration", () => { describe("analytics recording", () => { it("records requests without throwing", () => { expect(() => { - cache.recordRequest("/test", 200, "TestAgent/1.0", 15.5, "https://example.com"); + cache.recordRequest( + "/test", + 200, + "TestAgent/1.0", + 15.5, + "https://example.com", + ); }).not.toThrow(); }); @@ -185,7 +220,13 @@ describe("SlackCache integration", () => { }); it("retrieves referers", async () => { - cache.recordRequest("/ref-test", 200, "Agent", 1.0, "https://referrer.example.com/page"); + cache.recordRequest( + "/ref-test", + 200, + "Agent", + 1.0, + "https://referrer.example.com/page", + ); const referers = await cache.getReferers(); expect(Array.isArray(referers)).toBe(true); }); diff --git a/src/__tests__/handlers.test.ts b/src/__tests__/handlers.test.ts index 5abe779..2279430 100644 --- a/src/__tests__/handlers.test.ts +++ b/src/__tests__/handlers.test.ts @@ -11,7 +11,11 @@ function createMockCache(overrides: Partial = {}): SlackCache { insertEmoji: mock(async () => true), batchInsertEmojis: mock(async () => true), purgeUserCache: mock(async () => true), - purgeAll: mock(async () => ({ message: "Cache purged", users: 0, emojis: 0 })), + purgeAll: mock(async () => ({ + message: "Cache purged", + users: 0, + emojis: 0, + })), queueUserUpdate: mock(() => {}), healthCheck: mock(async () => true), detailedHealthCheck: mock(async () => ({ @@ -24,8 +28,15 @@ function createMockCache(overrides: Partial = {}): SlackCache { }, uptime: 1234, })), - getEssentialStats: mock(async () => ({ totalRequests: 100, averageResponseTime: 25, uptime: 99.9 })), - getChartData: mock(async () => ({ requestsByDay: [], latencyOverTime: [] })), + getEssentialStats: mock(async () => ({ + totalRequests: 100, + averageResponseTime: 25, + uptime: 99.9, + })), + getChartData: mock(async () => ({ + requestsByDay: [], + latencyOverTime: [], + })), getUserAgents: mock(async () => []), getUserAgentCount: mock(async () => 0), getReferers: mock(async () => []), @@ -160,7 +171,9 @@ describe("handlers", () => { // Instead, test through the handler directly with a mock that has no token const cache = createMockCache(); const handlers = createHandlers(cache); - const request = new Request("http://localhost/users/U123/purge", { method: "POST" }); + const request = new Request("http://localhost/users/U123/purge", { + method: "POST", + }); const response = await handlers.handlePurgeUser(request, noopAnalytics); // This will depend on whether BEARER_TOKEN is set in the test env @@ -175,8 +188,13 @@ describe("handlers", () => { it("handleGetEssentialStats returns stats", async () => { const cache = createMockCache(); const handlers = createHandlers(cache); - const request = new Request("http://localhost/api/stats/essential?days=7"); - const response = await handlers.handleGetEssentialStats(request, noopAnalytics); + const request = new Request( + "http://localhost/api/stats/essential?days=7", + ); + const response = await handlers.handleGetEssentialStats( + request, + noopAnalytics, + ); const body = await response.json(); expect(response.status).toBe(200); @@ -187,7 +205,10 @@ describe("handlers", () => { const cache = createMockCache(); const handlers = createHandlers(cache); const request = new Request("http://localhost/api/stats/charts"); - const response = await handlers.handleGetChartData(request, noopAnalytics); + const response = await handlers.handleGetChartData( + request, + noopAnalytics, + ); expect(response.status).toBe(200); }); @@ -196,7 +217,10 @@ describe("handlers", () => { const cache = createMockCache(); const handlers = createHandlers(cache); const request = new Request("http://localhost/api/stats/useragents"); - const response = await handlers.handleGetUserAgents(request, noopAnalytics); + const response = await handlers.handleGetUserAgents( + request, + noopAnalytics, + ); const body = await response.json(); expect(response.status).toBe(200); diff --git a/src/__tests__/normalizeEndpoint.test.ts b/src/__tests__/normalizeEndpoint.test.ts index 73044ed..9909020 100644 --- a/src/__tests__/normalizeEndpoint.test.ts +++ b/src/__tests__/normalizeEndpoint.test.ts @@ -21,7 +21,9 @@ describe("normalizeEndpoint", () => { }); it("normalizes emoji redirect endpoints", () => { - expect(normalizeEndpoint("/emojis/hackshark/r")).toBe("/emojis/EMOJI_NAME/r"); + expect(normalizeEndpoint("/emojis/hackshark/r")).toBe( + "/emojis/EMOJI_NAME/r", + ); }); it("normalizes static routes", () => { @@ -42,14 +44,24 @@ describe("normalizeEndpoint", () => { }); it("extracts path from full URLs", () => { - expect(normalizeEndpoint("http://localhost:3000/users/U123")).toBe("/users/USER_ID"); - expect(normalizeEndpoint("https://example.com/emojis/test/r")).toBe("/emojis/EMOJI_NAME/r"); + expect(normalizeEndpoint("http://localhost:3000/users/U123")).toBe( + "/users/USER_ID", + ); + expect(normalizeEndpoint("https://example.com/emojis/test/r")).toBe( + "/emojis/EMOJI_NAME/r", + ); }); it("handles non-standard user/emoji formats via includes fallback", () => { - expect(normalizeEndpoint("/api/users/something/r")).toBe("/users/USER_ID/r"); + expect(normalizeEndpoint("/api/users/something/r")).toBe( + "/users/USER_ID/r", + ); expect(normalizeEndpoint("/api/users/something")).toBe("/users/USER_ID"); - expect(normalizeEndpoint("/api/emojis/something/r")).toBe("/emojis/EMOJI_NAME/r"); - expect(normalizeEndpoint("/api/emojis/something")).toBe("/emojis/EMOJI_NAME"); + expect(normalizeEndpoint("/api/emojis/something/r")).toBe( + "/emojis/EMOJI_NAME/r", + ); + expect(normalizeEndpoint("/api/emojis/something")).toBe( + "/emojis/EMOJI_NAME", + ); }); }); diff --git a/src/cache.ts b/src/cache.ts index f9483bf..68611f8 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -5,7 +5,11 @@ import { endpointGroupingMigration } from "./migrations/endpointGroupingMigratio import { logGroupingMigration } from "./migrations/logGroupingMigration"; import { MigrationManager } from "./migrations/migrationManager"; import type { SlackUserProvider, User, Emoji } from "./types/cache-entities"; -import type { FullAnalyticsData, EssentialStatsData, ChartData } from "./types/analytics"; +import type { + FullAnalyticsData, + EssentialStatsData, + ChartData, +} from "./types/analytics"; import { AnalyticsQueryService } from "./lib/analytics-queries"; import { HealthMonitor } from "./lib/health-monitor"; @@ -30,7 +34,6 @@ export type { TrafficOverview, } from "./types/analytics"; - const SECONDS_PER_10MIN = 600; const SECONDS_PER_DAY = 86400; const MS_PER_HOUR = 3600000; @@ -212,42 +215,59 @@ class Cache { this.onEmojiExpired(); } } - } private setupPurgeSchedule() { const cronOptions = { timezone: "Etc/UTC" }; - this.cronTasks.push(schedule("45 * * * *", async () => { - try { - await this.purgeExpiredItems(); - await this.lazyUserCleanup(); - } catch (error) { - console.error("Error during purge schedule:", error); - } - }, cronOptions)); + this.cronTasks.push( + schedule( + "45 * * * *", + async () => { + try { + await this.purgeExpiredItems(); + await this.lazyUserCleanup(); + } catch (error) { + console.error("Error during purge schedule:", error); + } + }, + cronOptions, + ), + ); - this.cronTasks.push(schedule("0 * * * *", async () => { - try { - console.log("Scheduled emoji update starting..."); - if (this.onEmojiExpired) { - await this.onEmojiExpired(); - console.log("Scheduled emoji update completed"); - } - } catch (error) { - console.error("Error during emoji update schedule:", error); - } - }, cronOptions)); + this.cronTasks.push( + schedule( + "0 * * * *", + async () => { + try { + console.log("Scheduled emoji update starting..."); + if (this.onEmojiExpired) { + await this.onEmojiExpired(); + console.log("Scheduled emoji update completed"); + } + } catch (error) { + console.error("Error during emoji update schedule:", error); + } + }, + cronOptions, + ), + ); - this.cronTasks.push(schedule("0 8 * * *", () => { - try { - console.log("Running scheduled VACUUM..."); - this.db.run("VACUUM"); - console.log("VACUUM completed"); - } catch (error) { - console.error("Error during VACUUM:", error); - } - }, cronOptions)); + this.cronTasks.push( + schedule( + "0 8 * * *", + () => { + try { + console.log("Running scheduled VACUUM..."); + this.db.run("VACUUM"); + console.log("VACUUM completed"); + } catch (error) { + console.error("Error during VACUUM:", error); + } + }, + cronOptions, + ), + ); } private async runMigrations() { @@ -394,7 +414,10 @@ class Cache { this.isProcessingQueue = true; try { - const usersToUpdate = Array.from(this.userUpdateQueue).slice(0, QUEUE_BATCH_SIZE); + const usersToUpdate = Array.from(this.userUpdateQueue).slice( + 0, + QUEUE_BATCH_SIZE, + ); for (const userId of usersToUpdate) { try { @@ -609,7 +632,13 @@ class Cache { responseTime?: number, referer?: string, ): void { - this.analytics.recordRequest(endpoint, statusCode, userAgent, responseTime, referer); + this.analytics.recordRequest( + endpoint, + statusCode, + userAgent, + responseTime, + referer, + ); } async getAnalytics(days: number = 7): Promise { diff --git a/src/config.ts b/src/config.ts index f76e735..f405e40 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,7 +17,10 @@ export interface AppConfig { }; } -export function parsePositiveInt(value: string | undefined, fallback: number): number { +export function parsePositiveInt( + value: string | undefined, + fallback: number, +): number { if (!value) return fallback; const n = Number.parseInt(value, 10); return Number.isFinite(n) && n > 0 ? n : fallback; @@ -41,14 +44,18 @@ function loadConfig(): AppConfig { if (portRaw) { port = Number.parseInt(portRaw, 10); if (!Number.isFinite(port) || port <= 0 || port > 65535) { - errors.push(`PORT must be a valid port number (1-65535), got "${portRaw}"`); + errors.push( + `PORT must be a valid port number (1-65535), got "${portRaw}"`, + ); port = 3000; } } const bearerToken = process.env.BEARER_TOKEN || null; if (!bearerToken) { - console.warn("BEARER_TOKEN is not set. Admin endpoints (/reset, /users/:id/purge) will return 500."); + console.warn( + "BEARER_TOKEN is not set. Admin endpoints (/reset, /users/:id/purge) will return 500.", + ); } if (errors.length > 0) { @@ -65,7 +72,10 @@ function loadConfig(): AppConfig { botToken, maxConcurrent: parsePositiveInt(process.env.SLACK_MAX_CONCURRENT, 3), minTimeMs: parsePositiveInt(process.env.SLACK_MIN_TIME_MS, 200), - requestTimeoutMs: parsePositiveInt(process.env.SLACK_REQUEST_TIMEOUT_MS, 5000), + requestTimeoutMs: parsePositiveInt( + process.env.SLACK_REQUEST_TIMEOUT_MS, + 5000, + ), }, }; diff --git a/src/handlers/index.ts b/src/handlers/index.ts index d5d8475..b48b087 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -9,7 +9,10 @@ import type { RouteHandlerWithAnalytics } from "../lib/analytics-wrapper"; /** * Parse a string to a positive integer, returning a fallback if invalid */ -export function parsePositiveInt(value: string | null, fallback: number): number { +export function parsePositiveInt( + value: string | null, + fallback: number, +): number { if (!value) return fallback; const n = Number.parseInt(value, 10); return Number.isFinite(n) && n > 0 ? n : fallback; @@ -20,7 +23,10 @@ export function parsePositiveInt(value: string | null, fallback: number): number * Eliminates global mutable state and injectDependencies pattern. */ export function createHandlers(cache: SlackCache) { - function requireAuth(request: Request, recordAnalytics: (code: number) => void): Response | null { + function requireAuth( + request: Request, + recordAnalytics: (code: number) => void, + ): Response | null { const token = config.bearerToken; if (!token) { console.error("BEARER_TOKEN is not configured"); diff --git a/src/index.ts b/src/index.ts index 88f19fc..0ecaa13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,47 +19,43 @@ const slackApp = new SlackWrapper({ minTimeMs: config.slack.minTimeMs, requestTimeoutMs: config.slack.requestTimeoutMs, }); -const cache = new SlackCache( - config.databasePath, - 25, - async () => { - console.log("Fetching emojis from Slack"); - const emojis = await slackApp.getEmojiList(); - const emojiEntries = Object.entries(emojis) - .map(([name, url]) => { - if (typeof url === "string" && url.startsWith("alias:")) { - const aliasName = url.substring(6); - const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName); - - if (!aliasUrl) { - console.warn(`Could not find alias for ${aliasName}`); - return null; - } - - return { - name, - imageUrl: aliasUrl, - alias: aliasName, - }; +const cache = new SlackCache(config.databasePath, 25, async () => { + console.log("Fetching emojis from Slack"); + const emojis = await slackApp.getEmojiList(); + const emojiEntries = Object.entries(emojis) + .map(([name, url]) => { + if (typeof url === "string" && url.startsWith("alias:")) { + const aliasName = url.substring(6); + const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName); + + if (!aliasUrl) { + console.warn(`Could not find alias for ${aliasName}`); + return null; } + return { name, - imageUrl: url, - alias: null, + imageUrl: aliasUrl, + alias: aliasName, }; - }) - .filter( - ( - entry, - ): entry is { name: string; imageUrl: string; alias: string | null } => - entry !== null, - ); - - console.log("Batch inserting emojis"); - await cache.batchInsertEmojis(emojiEntries); - console.log("Finished batch inserting emojis"); - }, -); + } + return { + name, + imageUrl: url, + alias: null, + }; + }) + .filter( + ( + entry, + ): entry is { name: string; imageUrl: string; alias: string | null } => + entry !== null, + ); + + console.log("Batch inserting emojis"); + await cache.batchInsertEmojis(emojiEntries); + console.log("Finished batch inserting emojis"); +}); // Inject SlackWrapper into cache for background user updates cache.setSlackWrapper(slackApp); @@ -71,9 +67,17 @@ const apiRoutes = createApiRoutes(cache, slackApp); swaggerGenerator.addRoutes(apiRoutes as Record); const generatedSwagger = swaggerGenerator.getSpec(); -const typedRoutes: Record Promise | Response>> = {}; -for (const [path, routeConfig] of Object.entries(apiRoutes as Record)) { - const bunRoute: Record Promise | Response> = {}; +const typedRoutes: Record< + string, + Record Promise | Response> +> = {}; +for (const [path, routeConfig] of Object.entries( + apiRoutes as Record, +)) { + const bunRoute: Record< + string, + (request: Request) => Promise | Response + > = {}; for (const [method, typedRoute] of Object.entries(routeConfig)) { if (typedRoute && "handler" in typedRoute) { bunRoute[method] = typedRoute.handler; @@ -82,8 +86,6 @@ for (const [path, routeConfig] of Object.entries(apiRoutes as Record number): Promise { + async getAnalytics( + days: number = 7, + getUptime: () => number, + ): Promise { const cacheKey = `analytics_${days}`; const cached = this.typedAnalyticsCache.getAnalyticsData(cacheKey); if (cached) { @@ -526,7 +528,10 @@ export class AnalyticsQueryService { /** * Gets essential stats only (fast loading) */ - async getEssentialStats(days: number = 7, getUptime: () => number): Promise { + async getEssentialStats( + days: number = 7, + getUptime: () => number, + ): Promise { const cacheKey = `essential_${days}`; const cached = this.typedAnalyticsCache.getEssentialStatsData(cacheKey); diff --git a/src/lib/analytics-wrapper.ts b/src/lib/analytics-wrapper.ts index b2e5623..0b407ff 100644 --- a/src/lib/analytics-wrapper.ts +++ b/src/lib/analytics-wrapper.ts @@ -13,8 +13,6 @@ export type RouteHandlerWithAnalytics = ( recordAnalytics: AnalyticsRecorder, ) => Promise | Response; - - /** * Creates analytics wrapper with injected cache */ @@ -62,4 +60,3 @@ export function createAnalyticsWrapper(cache: SlackCache) { }; }; } - diff --git a/src/lib/cors.ts b/src/lib/cors.ts index 9bbc20f..6bc2749 100644 --- a/src/lib/cors.ts +++ b/src/lib/cors.ts @@ -5,7 +5,8 @@ export const CORS_HEADERS: Record = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, X-Requested-With", "Access-Control-Max-Age": "86400", }; diff --git a/src/lib/health-monitor.ts b/src/lib/health-monitor.ts index 68cb8f4..234ff98 100644 --- a/src/lib/health-monitor.ts +++ b/src/lib/health-monitor.ts @@ -11,7 +11,11 @@ export class HealthMonitor { private userUpdateQueueSize: () => number; // Cached Slack API health check result (60 second TTL) - private slackHealthCache: { status: boolean; error?: string; timestamp: number } | null = null; + private slackHealthCache: { + status: boolean; + error?: string; + timestamp: number; + } | null = null; private slackHealthCacheTTL = 60000; // Cached detailed health check response (5 second TTL for high-load scenarios) @@ -21,10 +25,7 @@ export class HealthMonitor { } | null = null; private detailedHealthCacheTTL = 5000; - constructor( - db: Database, - userUpdateQueueSize: () => number, - ) { + constructor(db: Database, userUpdateQueueSize: () => number) { this.db = db; this.userUpdateQueueSize = userUpdateQueueSize; } @@ -147,7 +148,10 @@ export class HealthMonitor { */ async detailedHealthCheck(): Promise { const now = Date.now(); - if (this.detailedHealthCache && now - this.detailedHealthCache.timestamp < this.detailedHealthCacheTTL) { + if ( + this.detailedHealthCache && + now - this.detailedHealthCache.timestamp < this.detailedHealthCacheTTL + ) { return this.detailedHealthCache.response; } const checks: DetailedHealthResponse["checks"] = { @@ -171,16 +175,27 @@ export class HealthMonitor { if (this.slackWrapper) { const now = Date.now(); - if (this.slackHealthCache && now - this.slackHealthCache.timestamp < this.slackHealthCacheTTL) { - checks.slackApi = { status: this.slackHealthCache.status, error: this.slackHealthCache.error }; + if ( + this.slackHealthCache && + now - this.slackHealthCache.timestamp < this.slackHealthCacheTTL + ) { + checks.slackApi = { + status: this.slackHealthCache.status, + error: this.slackHealthCache.error, + }; } else { try { await this.slackWrapper.testAuth(); this.slackHealthCache = { status: true, timestamp: now }; checks.slackApi = { status: true }; } catch (error) { - const errorMsg = error instanceof Error ? error.message : "Unknown error"; - this.slackHealthCache = { status: false, error: errorMsg, timestamp: now }; + const errorMsg = + error instanceof Error ? error.message : "Unknown error"; + this.slackHealthCache = { + status: false, + error: errorMsg, + timestamp: now, + }; checks.slackApi = { status: false, error: errorMsg, diff --git a/src/migrations/bucketAnalyticsMigration.ts b/src/migrations/bucketAnalyticsMigration.ts index fd11c37..376316f 100644 --- a/src/migrations/bucketAnalyticsMigration.ts +++ b/src/migrations/bucketAnalyticsMigration.ts @@ -73,7 +73,9 @@ export const bucketAnalyticsMigration: Migration = { // Check if request_analytics table exists before attempting data migration const tableExists = db - .query("SELECT name FROM sqlite_master WHERE type='table' AND name='request_analytics'") + .query( + "SELECT name FROM sqlite_master WHERE type='table' AND name='request_analytics'", + ) .get() as { name: string } | null; if (!tableExists) { diff --git a/src/migrations/endpointGroupingMigration.ts b/src/migrations/endpointGroupingMigration.ts index e00e573..fb0ff90 100644 --- a/src/migrations/endpointGroupingMigration.ts +++ b/src/migrations/endpointGroupingMigration.ts @@ -15,11 +15,15 @@ export const endpointGroupingMigration: Migration = { // Check if request_analytics table exists (may have been dropped by later migration) const tableExists = db - .query("SELECT name FROM sqlite_master WHERE type='table' AND name='request_analytics'") + .query( + "SELECT name FROM sqlite_master WHERE type='table' AND name='request_analytics'", + ) .get() as { name: string } | null; if (!tableExists) { - console.log("request_analytics table not found, skipping endpoint grouping migration"); + console.log( + "request_analytics table not found, skipping endpoint grouping migration", + ); return; } @@ -42,7 +46,9 @@ export const endpointGroupingMigration: Migration = { } if (updates.length > 0) { - const stmt = db.prepare("UPDATE request_analytics SET endpoint = ? WHERE id = ?"); + const stmt = db.prepare( + "UPDATE request_analytics SET endpoint = ? WHERE id = ?", + ); db.transaction(() => { for (const update of updates) { stmt.run(update.newEndpoint, update.id); diff --git a/src/migrations/logGroupingMigration.ts b/src/migrations/logGroupingMigration.ts index 02f82e0..13fb639 100644 --- a/src/migrations/logGroupingMigration.ts +++ b/src/migrations/logGroupingMigration.ts @@ -15,11 +15,15 @@ export const logGroupingMigration: Migration = { // Check if request_analytics table exists (may have been dropped by later migration) const tableExists = db - .query("SELECT name FROM sqlite_master WHERE type='table' AND name='request_analytics'") + .query( + "SELECT name FROM sqlite_master WHERE type='table' AND name='request_analytics'", + ) .get() as { name: string } | null; if (!tableExists) { - console.log("request_analytics table not found, skipping log grouping migration"); + console.log( + "request_analytics table not found, skipping log grouping migration", + ); return; } @@ -52,7 +56,9 @@ export const logGroupingMigration: Migration = { } if (updates.length > 0) { - const stmt = db.prepare("UPDATE request_analytics SET endpoint = ? WHERE id = ?"); + const stmt = db.prepare( + "UPDATE request_analytics SET endpoint = ? WHERE id = ?", + ); db.transaction(() => { for (const update of updates) { stmt.run(update.newEndpoint, update.id); diff --git a/src/routes/api-routes.ts b/src/routes/api-routes.ts index e142149..17c6a8f 100644 --- a/src/routes/api-routes.ts +++ b/src/routes/api-routes.ts @@ -144,19 +144,23 @@ export function createApiRoutes(cache: SlackCache, _slackApp: SlackWrapper) { }, }, }), - apiResponse(202, "User not yet cached; background fetch queued. Returns placeholder image.", { - type: "object", - properties: { - id: { type: "string", example: "U062UG485EE" }, - userId: { type: "string", example: "U062UG485EE" }, - displayName: { type: "string", example: "Unknown" }, - pronouns: { type: "string", example: "" }, - imageUrl: { - type: "string", - example: "https://l4.dunkirk.sh/i/5DjfoBI58Pfw.webp", + apiResponse( + 202, + "User not yet cached; background fetch queued. Returns placeholder image.", + { + type: "object", + properties: { + id: { type: "string", example: "U062UG485EE" }, + userId: { type: "string", example: "U062UG485EE" }, + displayName: { type: "string", example: "Unknown" }, + pronouns: { type: "string", example: "" }, + imageUrl: { + type: "string", + example: "https://l4.dunkirk.sh/i/5DjfoBI58Pfw.webp", + }, }, }, - }), + ), ]), }, ), @@ -544,8 +548,6 @@ export function createApiRoutes(cache: SlackCache, _slackApp: SlackWrapper) { ), }, - - "/api/stats/referers": { GET: createRoute( withAnalytics("/api/stats/referers", "GET", handlers.handleGetReferers),