From ed8083781061773239d894686c2cb193b01d2efb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 22:33:25 +0000 Subject: [PATCH] related serverices to TS --- server/jest.config.ts | 16 +- server/src/service/business/checkService.ts | 3 + .../SuperSimpleQueueHelper.ts | 1 - .../{networkService.js => networkService.ts} | 141 +++++++++++++----- .../{statusService.js => statusService.ts} | 111 ++++++-------- server/test/superSimpleQueueHelper.test.ts | 4 +- 6 files changed, 162 insertions(+), 114 deletions(-) rename server/src/service/infrastructure/{networkService.js => networkService.ts} (84%) rename server/src/service/infrastructure/{statusService.js => statusService.ts} (78%) diff --git a/server/jest.config.ts b/server/jest.config.ts index 4fb2c9831..fd0c87ad7 100644 --- a/server/jest.config.ts +++ b/server/jest.config.ts @@ -1,14 +1,5 @@ -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import { pathsToModuleNameMapper } from "ts-jest"; import type { Config } from "jest"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const tsconfigPath = path.resolve(__dirname, "./tsconfig.json"); -const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")); - const config: Config = { rootDir: ".", testEnvironment: "node", @@ -17,7 +8,12 @@ const config: Config = { "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], }, moduleNameMapper: { - ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "/" }), + "^@/validation/(.*)\\.js$": "/src/validation/$1.js", + "^@/utils/(AppError)\\.js$": "/src/utils/$1.ts", + "^@/utils/(.*)\\.js$": "/src/utils/$1.js", + "^@/(.*)\\.ts$": "/src/$1.ts", + "^@/(.*)\\.js$": "/src/$1.ts", + "^@/(.*)$": "/src/$1", }, testMatch: ["/test/**/*.test.ts"], setupFilesAfterEnv: [], diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 61e2fb6ad..8aea221f6 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,4 +1,5 @@ import { IMonitorsRepository } from "@/repositories/index.js"; +import type { MonitorType } from "@/types/index.js"; const SERVICE_NAME = "checkService"; @@ -35,6 +36,8 @@ class CheckService { return CheckService.SERVICE_NAME; } + buildCheck = async (statusResponse: any, type: MonitorType) => {}; + getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 678986c62..b025a65c6 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,7 +1,6 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { error } from "winston"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.ts similarity index 84% rename from server/src/service/infrastructure/networkService.js rename to server/src/service/infrastructure/networkService.ts index 2375e7233..b9f02e7c9 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.ts @@ -1,10 +1,63 @@ +import { Got } from "got"; +import type { Monitor } from "@/types/index.js"; + import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; class NetworkService { static SERVICE_NAME = SERVICE_NAME; - constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) { + private TYPE_PING: string; + private TYPE_HTTP: string; + private TYPE_PAGESPEED: string; + private TYPE_HARDWARE: string; + private TYPE_DOCKER: string; + private TYPE_PORT: string; + private TYPE_GAME: string; + private SERVICE_NAME: string; + private NETWORK_ERROR: number; + private PING_ERROR: number; + + private axios: any; + private got: Got; + private https: any; + private jmespath: any; + private GameDig: any; + private ping: any; + private logger: any; + private http: any; + private Docker: any; + private net: any; + private stringService: any; + private settingsService: any; + + constructor({ + axios, + got, + https, + jmespath, + GameDig, + ping, + logger, + http, + Docker, + net, + stringService, + settingsService, + }: { + axios: any; + got: Got; + https: any; + jmespath: any; + GameDig: any; + ping: any; + logger: any; + http: any; + Docker: any; + net: any; + stringService: any; + settingsService: any; + }) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; @@ -17,11 +70,11 @@ class NetworkService { this.PING_ERROR = 5001; this.axios = axios; this.https = https; + this.http = http; this.jmespath = jmespath; this.GameDig = GameDig; this.ping = ping; this.logger = logger; - this.http = http; this.Docker = Docker; this.net = net; this.stringService = stringService; @@ -39,7 +92,7 @@ class NetworkService { } // Helper functions - async timeRequest(operation) { + async timeRequest(operation: any) { const start = process.hrtime.bigint(); try { const response = await operation(); @@ -52,7 +105,7 @@ class NetworkService { } // Main entry point - async requestStatus(monitor) { + async requestStatus(monitor: Monitor) { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: @@ -74,7 +127,7 @@ class NetworkService { } } - async requestPing(monitor) { + async requestPing(monitor: Monitor) { try { if (!monitor?.url) { throw new Error("Monitor URL is required"); @@ -112,16 +165,16 @@ class NetworkService { } return pingResponse; - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestPing"; throw err; } } - async requestHttp(monitor) { + async requestHttp(monitor: Monitor) { const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const httpResponse = { + const httpResponse: Record = { monitorId: id, teamId: teamId, type, @@ -131,7 +184,7 @@ class NetworkService { if (!url) { throw new Error("Monitor URL is required"); } - const config = { + const config: Record = { headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, }; @@ -145,7 +198,7 @@ class NetworkService { const response = await this.got(url, config); - let payload; + let payload: any; const contentType = response.headers["content-type"]; if (contentType && contentType.includes("application/json")) { @@ -231,7 +284,7 @@ class NetworkService { } } return httpResponse; - } catch (err) { + } catch (err: any) { if (err.name === "HTTPError" || err.name === "RequestError") { httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; httpResponse.status = false; @@ -247,7 +300,7 @@ class NetworkService { } } - async requestPageSpeed(monitor) { + async requestPageSpeed(monitor: Monitor) { try { const url = monitor.url; if (!url) { @@ -269,24 +322,24 @@ class NetworkService { ...monitor, url: pageSpeedUrl, }); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestPageSpeed"; throw err; } } - async requestHardware(monitor) { + async requestHardware(monitor: Monitor) { try { return await this.requestHttp(monitor); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestHardware"; throw err; } } - async requestDocker(monitor) { + async requestDocker(monitor: Monitor) { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -297,7 +350,7 @@ class NetworkService { handleError: true, // Enable error handling }); - const dockerResponse = { + const dockerResponse: Record = { monitorId: monitor.id, type: monitor.type, }; @@ -309,18 +362,18 @@ class NetworkService { // Priority-based matching to avoid ambiguity: // 1. Exact full ID match (64-char) - let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); + let exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput); // 2. Exact container name match (case-insensitive) - let exactNameMatch = containers.find((c) => - c.Names.some((name) => { + let exactNameMatch = containers.find((c: any) => + c.Names.some((name: string) => { const cleanName = name.replace(/^\/+/, "").toLowerCase(); return cleanName === normalizedInput; }) ); // 3. Partial ID match (fallback for backwards compatibility) - let partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); + let partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput)); // Select container based on priority let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; @@ -360,7 +413,9 @@ class NetworkService { } const container = docker.getContainer(targetContainer.Id); - const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); + const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() => + container.inspect() + ); dockerResponse.responseTime = responseTime; dockerResponse.status = response?.State?.Status === "running" ? true : false; @@ -375,14 +430,14 @@ class NetworkService { } return dockerResponse; - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestDocker"; throw err; } } - async requestPort(monitor) { + async requestPort(monitor: Monitor) { try { const { url, port } = monitor; const { response, responseTime, error } = await this.timeRequest(async () => { @@ -405,7 +460,7 @@ class NetworkService { reject(new Error("Connection timeout")); }); - socket.on("error", (err) => { + socket.on("error", (err: any) => { socket.destroy(); reject(err); }); @@ -429,18 +484,18 @@ class NetworkService { } return portResponse; - } catch (error) { + } catch (error: any) { error.service = this.SERVICE_NAME; error.method = "requestTCP"; throw error; } } - async requestGame(monitor) { + async requestGame(monitor: Monitor) { try { const { url, port, gameId } = monitor; - const gameResponse = { + const gameResponse: Record = { code: 200, status: true, message: "Success", @@ -452,7 +507,7 @@ class NetworkService { type: gameId, host: url, port: port, - }).catch((error) => { + }).catch((error: any) => { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -471,21 +526,21 @@ class NetworkService { gameResponse.responseTime = state.ping; gameResponse.payload = state; return gameResponse; - } catch (error) { + } catch (error: any) { error.service = this.SERVICE_NAME; error.method = "requestPing"; throw error; } } - async handleUnsupportedType(type) { - const err = new Error(`Unsupported type: ${type}`); + async handleUnsupportedType(type: string) { + const err: any = new Error(`Unsupported type: ${type}`); err.service = this.SERVICE_NAME; err.method = "getStatus"; throw err; } // Other network requests unrelated to monitoring: - async requestWebhook(type, url, body) { + async requestWebhook(type: string, url: string, body: any) { try { const response = await this.axios.post(url, body, { headers: { @@ -500,7 +555,7 @@ class NetworkService { message: `Successfully sent ${type} notification`, payload: response.data, }; - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -517,7 +572,7 @@ class NetworkService { } } - async requestPagerDuty({ message, routingKey, monitorUrl }) { + async requestPagerDuty({ message, routingKey, monitorUrl }: { message: string; routingKey: string; monitorUrl: string }) { try { const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, { routing_key: routingKey, @@ -532,7 +587,7 @@ class NetworkService { if (response?.data?.status !== "success") return false; return true; - } catch (error) { + } catch (error: any) { error.details = error.response?.data; error.service = this.SERVICE_NAME; error.method = "requestPagerDuty"; @@ -540,7 +595,17 @@ class NetworkService { } } - async requestMatrix({ homeserverUrl, accessToken, roomId, message }) { + async requestMatrix({ + homeserverUrl, + accessToken, + roomId, + message, + }: { + homeserverUrl: string; + accessToken: string; + roomId: string; + message: string; + }) { try { const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; const body = { @@ -560,7 +625,7 @@ class NetworkService { code: response.status, message: "Successfully sent Matrix notification", }; - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.ts similarity index 78% rename from server/src/service/infrastructure/statusService.js rename to server/src/service/infrastructure/statusService.ts index 763b6d1fa..d68ec345a 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.ts @@ -1,11 +1,30 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; +import type { Monitor } from "@/types/index.js"; const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; + private db: any; + private logger: any; + private buffer: any; + private incidentService: any; + private monitorsRepository: IMonitorsRepository; - constructor({ db, logger, buffer, incidentService, monitorsRepository }) { + constructor({ + db, + logger, + buffer, + incidentService, + monitorsRepository, + }: { + db: any; + logger: any; + buffer: any; + incidentService: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.logger = logger; this.buffer = buffer; @@ -17,7 +36,7 @@ class StatusService { return StatusService.SERVICE_NAME; } - async updateRunningStats({ monitor, networkResponse }) { + async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) { try { const monitorId = monitor.id; const { responseTime, status } = networkResponse; @@ -78,9 +97,9 @@ class StatusService { await stats.save(); return true; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, message: error.message, method: "updateRunningStats", stack: error.stack, @@ -89,23 +108,13 @@ class StatusService { } } - getStatusString = (status) => { + getStatusString = (status: boolean | undefined) => { if (status === true) return "up"; if (status === false) return "down"; return "unknown"; }; - /** - * Saves check if needed and adds to incident buffer - * Removes check from checks buffer if it was saved immediately - * - * @param {Object} check - The check object - * @param {Object} monitor - The monitor object - * @param {string} action - The incident action ("create" or "resolve") - * @param {string} errorContext - Context for error messages - * @returns {Promise} - */ - handleIncidentForCheck = async (check, monitor, action, errorContext = "incident handling") => { + handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => { try { let savedCheck = check; @@ -115,9 +124,9 @@ class StatusService { savedCheck = await checkModel.save(); this.buffer.removeCheckFromBuffer(check); - } catch (checkError) { + } catch (checkError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`, monitorId: monitor.id, @@ -130,9 +139,9 @@ class StatusService { if (savedCheck && savedCheck._id) { try { this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action }); - } catch (incidentError) { + } catch (incidentError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`, monitorId: monitor.id, @@ -141,9 +150,9 @@ class StatusService { }); } } - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Error in ${errorContext}: ${error.message}`, monitorId: monitor?.id, @@ -151,19 +160,8 @@ class StatusService { }); } }; - /** - * Updates the status of a monitor based on the network response. - * - * @param {Object} networkResponse - The network response containing monitorId and status. - * @param {string} networkResponse.monitorId - The ID of the monitor. - * @param {string} networkResponse.status - The new status of the monitor. - * @returns {Promise} - A promise that resolves to an object containinfg the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred. - * @returns {Promise} returnObject - The object returned by the function. - * @returns {Object} returnObject.monitor - The monitor object. - * @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed. - * @returns {boolean} returnObject.prevStatus - The previous status of the monitor - */ - updateStatus = async (networkResponse) => { + + updateStatus = async (networkResponse: any) => { const check = this.buildCheck(networkResponse); await this.insertCheck(check); try { @@ -221,7 +219,7 @@ class StatusService { if (statusChanged) { this.logger.info({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, prevStatus, newStatus, @@ -242,7 +240,7 @@ class StatusService { if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) { try { - const checksAfterResolution = await Check.find({ + const checksAfterResolution = await CheckModel.find({ monitorId: monitor.id, createdAt: { $gt: lastManuallyResolvedIncident.endTime }, }) @@ -258,9 +256,9 @@ class StatusService { } else { calculatedFailureRate = 0; } - } catch (checkQueryError) { + } catch (checkQueryError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "updateStatus", message: `Failed to query checks after manual resolution: ${checkQueryError.message}`, monitorId: monitor.id, @@ -272,9 +270,9 @@ class StatusService { if (calculatedFailureRate >= monitor.statusWindowThreshold) { await this.handleIncidentForCheck(check, monitor, "create", "threshold check without status change"); } - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "updateStatus", message: `Error handling threshold check without status change: ${error.message}`, monitorId: monitor.id, @@ -293,27 +291,14 @@ class StatusService { code, timestamp: new Date().getTime(), }; - } catch (error) { - error.service = this.SERVICE_NAME; + } catch (error: any) { + error.service = SERVICE_NAME; error.method = "updateStatus"; throw error; } }; - /** - * Builds a check object from the network response. - * - * @param {Object} networkResponse - The network response object. - * @param {string} networkResponse.monitorId - The monitor ID. - * @param {string} networkResponse.type - The type of the response. - * @param {string} networkResponse.status - The status of the response. - * @param {number} networkResponse.responseTime - The response time. - * @param {number} networkResponse.code - The status code. - * @param {string} networkResponse.message - The message. - * @param {Object} networkResponse.payload - The payload of the response. - * @returns {Object} The check object. - */ - buildCheck = (networkResponse) => { + buildCheck = (networkResponse: any) => { const { monitorId, teamId, @@ -355,7 +340,7 @@ class StatusService { if (typeof payload === "undefined") { this.logger.warn({ message: "Failed to build check", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "buildCheck", details: "empty payload", }); @@ -363,7 +348,7 @@ class StatusService { } const categories = payload?.lighthouseResult?.categories ?? {}; const audits = payload?.lighthouseResult?.audits ?? {}; - const mapAudit = (audit) => { + const mapAudit = (audit: any) => { if (!audit || typeof audit !== "object") { return undefined; } @@ -416,22 +401,22 @@ class StatusService { * @param {Object} networkResponse.payload - The payload of the response. * @returns {Promise} A promise that resolves when the check is inserted. */ - insertCheck = async (check) => { + insertCheck = async (check: any) => { try { if (typeof check === "undefined") { this.logger.warn({ message: "Failed to build check", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "insertCheck", }); return false; } this.buffer.addToBuffer({ check }); return true; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, - service: error.service || this.SERVICE_NAME, + service: error.service || SERVICE_NAME, method: error.method || "insertCheck", details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, stack: error.stack, diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 056173c82..997eb9e83 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; import type { Monitor } from "../src/types/monitor.ts"; -const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() }); +const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }); const createHelper = (overrides?: Partial[0]>) => { const maintenanceWindowModule = { @@ -27,7 +27,7 @@ describe("SuperSimpleQueueHelper", () => { const job = helper.getMonitorJob(); await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor); expect(helper["networkService"].requestStatus).not.toHaveBeenCalled(); - expect(helper["logger"].info).toHaveBeenCalledWith( + expect(helper["logger"].debug).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") }) ); spy.mockRestore();