Merge pull request #3798 from bluewave-labs/chore/api-cleanup
chore: api cleanup
This commit is contained in:
56 files changed
+228
-548
No files matched your search
@@ -22,8 +22,6 @@ import {
|
||||
} from "@/api/validation/userValidation.js";
|
||||
import { IUserService } from "@/domain/users/user.service.js";
|
||||
|
||||
const SERVICE_NAME = "authController";
|
||||
|
||||
export interface IAuthController {
|
||||
registerUser: RequestHandler;
|
||||
createUser: RequestHandler;
|
||||
@@ -42,18 +40,12 @@ export interface IAuthController {
|
||||
}
|
||||
|
||||
class AuthController implements IAuthController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private userService: IUserService;
|
||||
|
||||
constructor(userService: IUserService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return AuthController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
registerUser = catchAsync(async (req: Request, res: Response) => {
|
||||
const newUser = req.body.user;
|
||||
const newUserToken = req.body.token;
|
||||
|
||||
@@ -11,10 +11,7 @@ import {
|
||||
import { ICheckService } from "@/domain/checks/check.service.js";
|
||||
import { requireTeamId } from "@/api/controllers/controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "checkController";
|
||||
|
||||
export interface ICheckController {
|
||||
serviceName: string;
|
||||
getChecksByMonitor: RequestHandler;
|
||||
getChecksByTeam: RequestHandler;
|
||||
getChecksSummaryByTeamId: RequestHandler;
|
||||
@@ -23,17 +20,11 @@ export interface ICheckController {
|
||||
}
|
||||
|
||||
class CheckController implements ICheckController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private checkService: ICheckService;
|
||||
constructor(checkService: ICheckService) {
|
||||
this.checkService = checkService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return CheckController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getChecksByMonitor = catchAsync(async (req: Request, res: Response) => {
|
||||
const validatedParams = getChecksParamValidation.parse(req.params);
|
||||
const validatedQuery = getChecksQueryValidation.parse(req.query);
|
||||
|
||||
@@ -54,24 +54,6 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde
|
||||
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
||||
};
|
||||
|
||||
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
||||
};
|
||||
|
||||
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
||||
const parseSingle = (input: unknown): MonitorType => {
|
||||
if (typeof input !== "string") {
|
||||
|
||||
@@ -2,25 +2,17 @@ import { IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js";
|
||||
import { Request, Response, RequestHandler } from "express";
|
||||
import { catchAsync } from "@/utils/catchAsync.js";
|
||||
|
||||
const SERVICE_NAME = "diagnosticController";
|
||||
|
||||
export interface IDiagnosticController {
|
||||
getSystemStats: RequestHandler;
|
||||
}
|
||||
|
||||
class DiagnosticController implements IDiagnosticController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private diagnosticService: IDiagnosticService;
|
||||
|
||||
constructor(diagnosticService: IDiagnosticService) {
|
||||
this.diagnosticService = diagnosticService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return DiagnosticController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getSystemStats = catchAsync(async (req: Request, res: Response) => {
|
||||
const diagnostics = await this.diagnosticService.getSystemStats();
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -4,23 +4,15 @@ import { getChecksParamValidation, getChecksQueryValidation } from "@/api/valida
|
||||
import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js";
|
||||
import { requireTeamId } from "./controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "geoCheckController";
|
||||
|
||||
export interface IGeoCheckController {
|
||||
getGeoChecksByMonitor: RequestHandler;
|
||||
}
|
||||
class GeoCheckController implements IGeoCheckController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private geoChecksService: IGeoChecksService;
|
||||
constructor(geoChecksService: IGeoChecksService) {
|
||||
this.geoChecksService = geoChecksService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return GeoCheckController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getGeoChecksByMonitor = catchAsync(async (req: Request, res: Response) => {
|
||||
const validatedParams = getChecksParamValidation.parse(req.params);
|
||||
const validatedQuery = getChecksQueryValidation.parse(req.query);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { catchAsync } from "@/utils/catchAsync.js";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/api/validation/authValidation.js";
|
||||
import { requireFirstName, requireTeamId, requireUserRoles } from "@/api/controllers/controllerUtils.js";
|
||||
import { IInviteService } from "@/domain/invites/invite.service.js";
|
||||
const SERVICE_NAME = "inviteController";
|
||||
|
||||
export interface IInviteController {
|
||||
getInviteToken: RequestHandler;
|
||||
@@ -12,16 +11,11 @@ export interface IInviteController {
|
||||
}
|
||||
|
||||
class InviteController implements IInviteController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private inviteService: IInviteService;
|
||||
constructor(inviteService: IInviteService) {
|
||||
this.inviteService = inviteService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return InviteController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getInviteToken = catchAsync(async (req: Request, res: Response) => {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userRoles = requireUserRoles(req.user?.role);
|
||||
|
||||
@@ -2,21 +2,15 @@ import { ILogger } from "@/utils/logger.js";
|
||||
import { Request, Response, RequestHandler } from "express";
|
||||
import { catchAsync } from "@/utils/catchAsync.js";
|
||||
|
||||
const SERVICE_NAME = "LogController";
|
||||
|
||||
export interface ILogController {
|
||||
getLogs: RequestHandler;
|
||||
}
|
||||
|
||||
class LogController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private logger: ILogger;
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
get serviceName() {
|
||||
return LogController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getLogs = catchAsync(async (req: Request, res: Response) => {
|
||||
const logs = this.logger.getLogs();
|
||||
|
||||
@@ -23,8 +23,6 @@ import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService } from "@/domain/monitors/monitor.service.js";
|
||||
import { INotificationsService } from "@/domain/notifications/notification.service.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
|
||||
export interface IMonitorController {
|
||||
getMonitorCertificate: RequestHandler;
|
||||
getUptimeDetailsById: RequestHandler;
|
||||
@@ -47,8 +45,6 @@ export interface IMonitorController {
|
||||
updateNotifications: RequestHandler;
|
||||
}
|
||||
class MonitorController implements IMonitorController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private monitorService: IMonitorService;
|
||||
private notificationsService: INotificationsService;
|
||||
|
||||
@@ -57,10 +53,6 @@ class MonitorController implements IMonitorController {
|
||||
this.notificationsService = notificationsService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MonitorController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getMonitorCertificate = catchAsync(async (req: Request, res: Response) => {
|
||||
const validatedParams = getCertificateParamValidation.parse(req.params);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
@@ -3,8 +3,6 @@ import { getQueueJobsQueryValidation } from "@/api/validation/queueValidation.js
|
||||
import { Request, Response, RequestHandler } from "express";
|
||||
import { catchAsync } from "@/utils/catchAsync.js";
|
||||
|
||||
const SERVICE_NAME = "JobQueueController";
|
||||
|
||||
export interface IJobQueueController {
|
||||
getMetrics: RequestHandler;
|
||||
getJobs: RequestHandler;
|
||||
@@ -13,13 +11,8 @@ export interface IJobQueueController {
|
||||
}
|
||||
|
||||
class JobQueueController implements IJobQueueController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor(private scheduler: IJobScheduler) {}
|
||||
|
||||
get serviceName() {
|
||||
return JobQueueController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getMetrics = catchAsync(async (req: Request, res: Response) => {
|
||||
const metrics = await this.scheduler.getMetrics();
|
||||
res.status(200).json({
|
||||
|
||||
@@ -7,17 +7,13 @@ import { ISettingsService } from "@/domain/app-settings/app-settings.service.js"
|
||||
import { IEmailService } from "@/service/emailService.js";
|
||||
import { Settings } from "@/domain/app-settings/app-settings.type.js";
|
||||
|
||||
const SERVICE_NAME = "SettingsController";
|
||||
|
||||
export interface ISettingsController {
|
||||
serviceName: string;
|
||||
getAppSettings: RequestHandler;
|
||||
updateAppSettings: RequestHandler;
|
||||
sendTestEmail: RequestHandler;
|
||||
}
|
||||
|
||||
class SettingsController implements ISettingsController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private settingsService: ISettingsService;
|
||||
private emailService: IEmailService;
|
||||
constructor(settingsService: ISettingsService, emailService: IEmailService) {
|
||||
@@ -25,10 +21,6 @@ class SettingsController implements ISettingsController {
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SettingsController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
buildAppSettings = (dbSettings: Settings) => {
|
||||
const sanitizedSettings: Record<string, unknown> = { ...dbSettings };
|
||||
delete sanitizedSettings.version;
|
||||
|
||||
@@ -13,10 +13,7 @@ import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils.
|
||||
import { IStatusPageService } from "@/domain/status-pages/status-page.service.js";
|
||||
import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageController";
|
||||
|
||||
export interface IStatusPageController {
|
||||
readonly serviceName: string;
|
||||
createStatusPage: RequestHandler;
|
||||
updateStatusPage: RequestHandler;
|
||||
getStatusPageByUrl: RequestHandler;
|
||||
@@ -26,16 +23,11 @@ export interface IStatusPageController {
|
||||
}
|
||||
|
||||
class StatusPageController implements IStatusPageController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private statusPageService: IStatusPageService;
|
||||
constructor(statusPageService: IStatusPageService) {
|
||||
this.statusPageService = statusPageService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return StatusPageController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createStatusPage = catchAsync(async (req: Request, res: Response) => {
|
||||
createStatusPageBodyValidation.parse(req.body);
|
||||
if (req.file) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { UserRoles } from "@/domain/users/user.type.js";
|
||||
import { nameValidation, lowercaseEmailValidation, passwordValidation } from "./shared.js";
|
||||
import { nameValidation, lowercaseEmailValidation, passwordValidation, booleanCoercion } from "@/api/validation/shared.js";
|
||||
|
||||
//****************************************
|
||||
// User Validations
|
||||
@@ -46,6 +46,7 @@ export const editUserBodyValidation = z
|
||||
profilePicture: z.string().optional(),
|
||||
password: passwordValidation.optional(),
|
||||
newPassword: passwordValidation.optional(),
|
||||
deleteProfileImage: booleanCoercion.optional(),
|
||||
})
|
||||
// Changing a password requires both the current password and the new one.
|
||||
// The service only runs the change-password flow when both are present, so
|
||||
|
||||
@@ -21,10 +21,6 @@ class MongoDB implements IDb<Mongoose> {
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MongoDB.SERVICE_NAME;
|
||||
}
|
||||
|
||||
connect = async () => {
|
||||
try {
|
||||
const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
|
||||
|
||||
@@ -20,8 +20,6 @@ export type EnvConfig = {
|
||||
};
|
||||
|
||||
export interface ISettingsService {
|
||||
readonly serviceName: string;
|
||||
loadSettings(): EnvConfig;
|
||||
getSettings(): EnvConfig;
|
||||
areStatusPageThemesEnabled(): boolean;
|
||||
getDBSettings(): Promise<Settings>;
|
||||
@@ -57,14 +55,6 @@ export class SettingsService implements ISettingsService {
|
||||
this.settingsRepository = settingsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SettingsService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
@@ -50,10 +50,6 @@ export class CheckService implements ICheckService {
|
||||
this.checksRepository = checksRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return CheckService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createChecks = async (checks: Check[]) => {
|
||||
return this.checksRepository.createChecks(checks);
|
||||
};
|
||||
|
||||
@@ -79,10 +79,6 @@ export class DiagnosticService implements IDiagnosticService {
|
||||
obs.observe({ entryTypes: ["measure"] });
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return DiagnosticService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private getCPUUsagePercentage = async (): Promise<number> => {
|
||||
const startUsage = process.cpuUsage();
|
||||
const timingPeriod = 1000; // measured in ms
|
||||
|
||||
@@ -11,7 +11,6 @@ import { AppError } from "@/utils/AppError.js";
|
||||
const SERVICE_NAME = "GeoChecksService";
|
||||
|
||||
export interface IGeoChecksService {
|
||||
readonly serviceName: string;
|
||||
buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null>;
|
||||
createGeoChecks(geoChecks: GeoCheck[]): Promise<GeoCheck[]>;
|
||||
getGeoChecksByMonitor(args: {
|
||||
@@ -50,10 +49,6 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return GeoChecksService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null> {
|
||||
try {
|
||||
if (!monitor.url) {
|
||||
|
||||
@@ -57,10 +57,6 @@ export class IncidentService implements IIncidentService {
|
||||
this.notificationMessageBuilder = notificationMessageBuilder;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return IncidentService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
handleIncident = async (
|
||||
monitor: Monitor,
|
||||
code: number,
|
||||
|
||||
@@ -35,10 +35,6 @@ export class InviteService implements IInviteService {
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return InviteService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getInviteToken = async ({ invite, teamId, userRoles }: { invite: Partial<Invite>; teamId: string; userRoles: UserRole[] }) => {
|
||||
invite.teamId = teamId;
|
||||
|
||||
|
||||
@@ -62,10 +62,6 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MaintenanceWindowService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private flipMonitorsLeavingMaintenance = async (monitorIds: string[], teamId: string, excludeWindowId: string, now: Date): Promise<void> => {
|
||||
if (monitorIds.length === 0) return;
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ const isUptimeChecksResult = (result: UptimeChecksResult | HardwareChecksResult
|
||||
supportsUptimeDetails(result.monitorType);
|
||||
|
||||
export interface IMonitorService {
|
||||
readonly serviceName: string;
|
||||
|
||||
// create
|
||||
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
|
||||
createMonitors(monitors: Array<Monitor>): Promise<Monitor[] | null>;
|
||||
@@ -135,10 +133,6 @@ export class MonitorService implements IMonitorService {
|
||||
this.incidentsRepository = incidentsRepository;
|
||||
}
|
||||
|
||||
get serviceName(): string {
|
||||
return MonitorService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private getDateRange = (dateRange: DateRangeKey) => {
|
||||
const startDates = {
|
||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||
|
||||
@@ -4,7 +4,8 @@ import { QueueWorkerDocument } from "@/domain/queue-workers/queue-worker.type.js
|
||||
|
||||
// Single source of truth for worker liveness: the TTL that GCs stale records and
|
||||
// the window readers use to count a worker as alive. Heartbeat cadence must stay well below it.
|
||||
export const WORKER_TTL_SECONDS = 180;
|
||||
const WORKER_TTL_SECONDS = 180;
|
||||
export const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000;
|
||||
|
||||
const QueueWorkerSchema = new Schema<QueueWorkerDocument>(
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ import { IUsersRepository } from "@/domain/users/user.repository.interface.js";
|
||||
import { UserModel, type UserDocument } from "@/domain/users/user.model.js";
|
||||
import type { User, UserProfileImage } from "@/domain/users/user.type.js";
|
||||
import { GenerateAvatarImage } from "@/utils/imageProcessing.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { toStringId, toDateString } from "@/utils/mongoMappers.js";
|
||||
const SERVICE_NAME = "MongoUsersRepository";
|
||||
@@ -87,9 +86,8 @@ class MongoUsersRepository implements IUsersRepository {
|
||||
const candidateUser = { ...patch };
|
||||
let unsetFields: Record<string, 1> | undefined;
|
||||
|
||||
if (ParseBoolean(candidateUser.deleteProfileImage) === true) {
|
||||
if (candidateUser.deleteProfileImage === true) {
|
||||
unsetFields = { profileImage: 1, avatarImage: 1 };
|
||||
delete candidateUser.deleteProfileImage;
|
||||
} else if (file) {
|
||||
// 1. Save the full size image
|
||||
candidateUser.profileImage = {
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface IUserService {
|
||||
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise<User>;
|
||||
loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
|
||||
editUser(
|
||||
updates: Partial<User & { newPassword?: string }>,
|
||||
updates: Partial<User & { newPassword?: string; deleteProfileImage?: boolean }>,
|
||||
file: Express.Multer.File | null,
|
||||
currentUserId: string,
|
||||
currentUserEmail: string
|
||||
@@ -103,10 +103,6 @@ export class UserService implements IUserService {
|
||||
this.teamsRepository = teamsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return UserService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
issueToken = (payload: Partial<User>, appSettings: EnvConfig) => {
|
||||
return this.jwt.sign(payload, appSettings.jwtSecret, { expiresIn: appSettings.jwtTTL });
|
||||
};
|
||||
@@ -242,7 +238,7 @@ export class UserService implements IUserService {
|
||||
};
|
||||
|
||||
editUser = async (
|
||||
updates: Partial<User & { newPassword?: string }>,
|
||||
updates: Partial<User & { newPassword?: string; deleteProfileImage?: boolean }>,
|
||||
file: Express.Multer.File | null,
|
||||
currentUserId: string,
|
||||
currentUserEmail: string
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ const startApp = async () => {
|
||||
const env = validateEnv(logger);
|
||||
logger.setLogLevel(env.LOG_LEVEL);
|
||||
const settingsService = new SettingsService(env);
|
||||
const envSettings = settingsService.loadSettings();
|
||||
const envSettings = settingsService.getSettings();
|
||||
const { queueMode, queuePrimaryProcesses } = envSettings;
|
||||
|
||||
logger.info({
|
||||
|
||||
@@ -51,10 +51,6 @@ export class BufferService implements IBufferService {
|
||||
});
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return BufferService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
addToBuffer(check: Check) {
|
||||
try {
|
||||
this.buffer.push(check);
|
||||
|
||||
@@ -56,10 +56,6 @@ export class EmailService implements IEmailService {
|
||||
this.init();
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return EmailService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
init = () => {
|
||||
const loadTemplate = (templateName: string) => {
|
||||
try {
|
||||
@@ -79,7 +75,6 @@ export class EmailService implements IEmailService {
|
||||
this.templateLookup = {
|
||||
welcomeEmailTemplate: loadTemplate("welcomeEmail"),
|
||||
employeeActivationTemplate: loadTemplate("employeeActivation"),
|
||||
noIncidentsThisWeekTemplate: loadTemplate("noIncidentsThisWeek"),
|
||||
passwordResetTemplate: loadTemplate("passwordReset"),
|
||||
testEmailTemplate: loadTemplate("testEmailTemplate"),
|
||||
unifiedNotificationTemplate: loadTemplate("unifiedNotification"),
|
||||
|
||||
@@ -61,7 +61,6 @@ interface GlobalPingProbeResult {
|
||||
}
|
||||
|
||||
export interface IGlobalPingService {
|
||||
readonly serviceName: string;
|
||||
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||
pollForResults(measurementId: string, timeoutMs?: number, customUpCodes?: HttpStatusCode[]): Promise<GeoCheckResult[]>;
|
||||
}
|
||||
@@ -75,10 +74,6 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return GlobalPingService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
|
||||
try {
|
||||
if (!supportsGeoCheck(monitorType)) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { IStatusProvider } from "./network/IStatusProvider.js";
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
|
||||
export interface INetworkService {
|
||||
readonly serviceName: string;
|
||||
requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
|
||||
requestWebhook(
|
||||
type: string,
|
||||
@@ -39,10 +38,6 @@ export class NetworkService implements INetworkService {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName(): string {
|
||||
return NetworkService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>> {
|
||||
const provider = this.providers.find((p) => p.supports(monitor.type));
|
||||
|
||||
@@ -57,10 +57,6 @@ export class StatusService implements IStatusService {
|
||||
this.monitorStatsRepository = monitorStatsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return StatusService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) {
|
||||
try {
|
||||
await this.monitorStatsRepository.updateByMonitorId(monitor.id, {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<!-- name -->
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font
|
||||
name="Roboto"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
|
||||
></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text
|
||||
font-weight="300"
|
||||
font-size="16px"
|
||||
color="#616161"
|
||||
line-height="24px"
|
||||
></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text
|
||||
align="left"
|
||||
font-size="10px"
|
||||
>
|
||||
Message from Checkmate Service
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column width="100%">
|
||||
<mj-text>
|
||||
<p>Hello {{ name }}!</p>
|
||||
<p>
|
||||
We hope you’re finding Checkmate helpful in monitoring your infrastructure. Your support means a lot to us, and we
|
||||
<b>truly appreciate</b> having you as part of our community.
|
||||
</p>
|
||||
<p>
|
||||
If you’re happy with Checkmate, we’d love to hear about your experience! Leaving a review on G2 helps others discover Checkmate and
|
||||
supports our ongoing improvements.
|
||||
</p>
|
||||
G2 Link: TBD
|
||||
<p>Thank you for taking the time to share your thoughts - we greatly appreciate it!</p>
|
||||
Checkmate Team
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="100%">
|
||||
<mj-divider
|
||||
border-width="1px"
|
||||
border-color="#E0E0E0"
|
||||
></mj-divider>
|
||||
<mj-text font-size="12px">
|
||||
<p>This email was sent by Checkmate.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -1,66 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font
|
||||
name="Roboto"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
|
||||
></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text
|
||||
font-weight="300"
|
||||
font-size="16px"
|
||||
color="#616161"
|
||||
line-height="24px"
|
||||
></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text
|
||||
align="left"
|
||||
font-size="10px"
|
||||
>Message from Checkmate Service</mj-text
|
||||
>
|
||||
</mj-column>
|
||||
<mj-column
|
||||
width="45%"
|
||||
padding-top="20px"
|
||||
>
|
||||
<mj-text
|
||||
align="center"
|
||||
font-weight="500"
|
||||
padding="0px"
|
||||
font-size="18px"
|
||||
color="green"
|
||||
>No incidents this week!</mj-text
|
||||
>
|
||||
<mj-divider
|
||||
border-width="2px"
|
||||
border-color="#616161"
|
||||
></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column width="100%">
|
||||
<mj-text>
|
||||
<p>Hello {{ name }}!</p>
|
||||
<p>There were no incidents this week. Good job!</p>
|
||||
<p><b>Current monitors:</b></p>
|
||||
<p><b>Google:</b> 100% availability</p>
|
||||
<p><b>Canada.ca:</b>100% availability</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="100%">
|
||||
<mj-divider
|
||||
border-width="1px"
|
||||
border-color="#E0E0E0"
|
||||
></mj-divider>
|
||||
<mj-text font-size="12px">
|
||||
<p>This email was sent by Checkmate.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -20,72 +20,45 @@ export const getDateForRange = (dateRange: string): Date | undefined => {
|
||||
}
|
||||
};
|
||||
|
||||
const calculatePercentile = <T extends HasResponseTime>(arr: T[], percentile: number): number => {
|
||||
const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime);
|
||||
const percentileBy = <T>(arr: T[], percentile: number, value: (item: T) => number): number => {
|
||||
const sorted = arr.slice().sort((a, b) => value(a) - value(b));
|
||||
const index = (percentile / 100) * (sorted.length - 1);
|
||||
const lower = Math.floor(index);
|
||||
const upper = lower + 1;
|
||||
const weight = index % 1;
|
||||
if (upper >= sorted.length) return sorted[lower]!.responseTime;
|
||||
return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight;
|
||||
if (upper >= sorted.length) return value(sorted[lower]!);
|
||||
return value(sorted[lower]!) * (1 - weight) + value(sorted[upper]!) * weight;
|
||||
};
|
||||
|
||||
const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => {
|
||||
const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime);
|
||||
const index = (percentile / 100) * (sorted.length - 1);
|
||||
const lower = Math.floor(index);
|
||||
const upper = lower + 1;
|
||||
const weight = index % 1;
|
||||
if (upper >= sorted.length) return sorted[lower]!.avgResponseTime;
|
||||
return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight;
|
||||
const rescale = (raw: number, min: number, max: number, rangeMin: number, rangeMax: number): number => {
|
||||
const scaled = rangeMin + ((raw - min) * (rangeMax - rangeMin)) / (max - min);
|
||||
return Math.max(rangeMin, Math.min(rangeMax, scaled));
|
||||
};
|
||||
|
||||
export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck<T>[] => {
|
||||
if (checks.length > 1) {
|
||||
const min = calculatePercentile(checks, 0);
|
||||
const max = calculatePercentile(checks, 95);
|
||||
const normalizedChecks = checks.map((check) => {
|
||||
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
|
||||
let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min);
|
||||
|
||||
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
|
||||
return {
|
||||
...check,
|
||||
responseTime: normalizedResponseTime,
|
||||
originalResponseTime: originalResponseTime,
|
||||
};
|
||||
});
|
||||
|
||||
return normalizedChecks;
|
||||
} else {
|
||||
return checks.map((check) => {
|
||||
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
|
||||
return { ...check, originalResponseTime: originalResponseTime };
|
||||
});
|
||||
const original = (check: T) => (typeof check.responseTime === "number" ? check.responseTime : 0);
|
||||
if (checks.length <= 1) {
|
||||
return checks.map((check) => ({ ...check, originalResponseTime: original(check) }));
|
||||
}
|
||||
const min = percentileBy(checks, 0, (c) => c.responseTime);
|
||||
const max = percentileBy(checks, 95, (c) => c.responseTime);
|
||||
return checks.map((check) => ({
|
||||
...check,
|
||||
responseTime: rescale(check.responseTime, min, max, rangeMin, rangeMax),
|
||||
originalResponseTime: original(check),
|
||||
}));
|
||||
};
|
||||
|
||||
export const NormalizeDataUptimeDetails = <T extends GroupedCheck>(checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck<T>[] => {
|
||||
if (checks.length > 1) {
|
||||
const min = calculatePercentileUptimeDetails(checks, 0);
|
||||
const max = calculatePercentileUptimeDetails(checks, 95);
|
||||
|
||||
const normalizedChecks = checks.map((check) => {
|
||||
const originalResponseTime = check.avgResponseTime;
|
||||
let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min);
|
||||
|
||||
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
|
||||
return {
|
||||
...check,
|
||||
avgResponseTime: normalizedResponseTime,
|
||||
originalAvgResponseTime: originalResponseTime,
|
||||
};
|
||||
});
|
||||
|
||||
return normalizedChecks;
|
||||
} else {
|
||||
return checks.map((check) => {
|
||||
return { ...check, originalAvgResponseTime: check.avgResponseTime };
|
||||
});
|
||||
const original = (check: T) => (typeof check.avgResponseTime === "number" ? check.avgResponseTime : 0);
|
||||
if (checks.length <= 1) {
|
||||
return checks.map((check) => ({ ...check, originalAvgResponseTime: original(check) }));
|
||||
}
|
||||
const min = percentileBy(checks, 0, (c) => c.avgResponseTime);
|
||||
const max = percentileBy(checks, 95, (c) => c.avgResponseTime);
|
||||
return checks.map((check) => ({
|
||||
...check,
|
||||
avgResponseTime: rescale(check.avgResponseTime, min, max, rangeMin, rangeMax),
|
||||
originalAvgResponseTime: original(check),
|
||||
}));
|
||||
};
|
||||
@@ -19,7 +19,6 @@ interface LogEntry extends LogConfig {
|
||||
}
|
||||
|
||||
export interface ILogger {
|
||||
readonly serviceName: string;
|
||||
info(config: LogConfig): void;
|
||||
warn(config: LogConfig): void;
|
||||
error(config: LogConfig): void;
|
||||
@@ -97,10 +96,6 @@ class Logger implements ILogger {
|
||||
});
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return Logger.SERVICE_NAME;
|
||||
}
|
||||
|
||||
info(config: LogConfig) {
|
||||
const logEntry = this.buildLogEntry("info", config);
|
||||
this.cacheLog(logEntry);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export const ParseBoolean = (value: boolean | string | null | undefined) => {
|
||||
if (value === true || value === "true") {
|
||||
return true;
|
||||
} else if (value === false || value === "false" || value === null || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getTokenFromHeaders = (headers: Record<string, string>) => {
|
||||
const authorizationHeader = headers.authorization;
|
||||
if (!authorizationHeader) throw new Error("No auth headers");
|
||||
|
||||
const parts = authorizationHeader.split(" ");
|
||||
if (parts.length !== 2 || parts[0] !== "Bearer") throw new Error("Invalid auth headers");
|
||||
|
||||
return parts[1];
|
||||
};
|
||||
@@ -11,14 +11,13 @@ import { ICheckPipeline } from "@/worker/worker.check-pipeline.js";
|
||||
import { IReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js";
|
||||
import { IWorkerHelper } from "@/worker/worker.helper.js";
|
||||
import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js";
|
||||
import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js";
|
||||
import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js";
|
||||
import { QueueMode } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { JobScheduler } from "@/worker/worker.job-scheduler.js";
|
||||
import { IBufferService } from "@/service/bufferService.js";
|
||||
import { POLL_MS } from "@/worker/worker.job-scheduler.js";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
const POLL_MS = 250; // base poll interval while a loop is actively claiming work
|
||||
const POLL_MAX_MS = 5000; // back off poll interval to 5s if no jobs
|
||||
const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window
|
||||
const LOCK_RENEW_MS = LOCK_MS / 3; // renew an in-flight job's lock at 1/3 the lease, so it survives two missed renewals
|
||||
const DRAIN_TIMEOUT_MS = 25_000;
|
||||
const DRAIN_POLL_MS = 200;
|
||||
@@ -95,10 +94,6 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
|
||||
this.isDbConnected = dependencies.isDbConnected;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return DBQueueWorker.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create(dependencies: DBQueueWorkerDependencies): Promise<DBQueueWorker> {
|
||||
const instance = new DBQueueWorker(dependencies);
|
||||
await instance.init();
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ITeamsRepository } from "@/domain/teams/team.repository.interface.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
|
||||
export interface IWorkerHelper {
|
||||
readonly serviceName: string;
|
||||
getCleanupOrphanedJob(): () => Promise<void>;
|
||||
getCleanupRetentionJob(): () => Promise<void>;
|
||||
}
|
||||
@@ -69,10 +68,6 @@ export class WorkerHelper implements IWorkerHelper {
|
||||
this.geoChecksRepository = geoChecksRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return WorkerHelper.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getCleanupOrphanedJob = () => {
|
||||
return async () => {
|
||||
try {
|
||||
|
||||
@@ -67,7 +67,6 @@ export type WorkerHealth = {
|
||||
inFlight: number; // total in-flight jobs across types
|
||||
};
|
||||
export interface IJobScheduler {
|
||||
readonly serviceName: string;
|
||||
addJob(monitorId: string, monitor: Monitor): Promise<void>;
|
||||
deleteJob(monitor: Monitor): Promise<void>;
|
||||
pauseJob(monitor: Monitor): Promise<void>;
|
||||
@@ -83,7 +82,6 @@ export interface IJobScheduler {
|
||||
}
|
||||
|
||||
export interface IQueueWorker extends IJobScheduler {
|
||||
readonly serviceName: string;
|
||||
getHealth(): WorkerHealth;
|
||||
countDueBacklog(): Promise<number>;
|
||||
countAliveWorkers(): Promise<number>;
|
||||
|
||||
@@ -5,13 +5,12 @@ import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.types.js";
|
||||
import { type Job, type JobSeed, type JobType, jobId } from "@/domain/jobs/job.type.js";
|
||||
import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js";
|
||||
import { WorkerJobsPagination, WorkerJobSummary, WorkerMetrics } from "@/worker/worker.interface.js";
|
||||
import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js";
|
||||
import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js";
|
||||
import { QueueMode } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
|
||||
const POLL_MS = 250; // base poll interval while a loop is actively claiming work
|
||||
export const POLL_MS = 250; // base poll interval while a loop is actively claiming work
|
||||
const SERVICE_NAME = "JobScheduler";
|
||||
const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window
|
||||
const HEARTBEAT_MS = WORKER_STALE_MS / 3; // Worker can miss two beats without being considered stale
|
||||
const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -26,9 +25,6 @@ export class JobScheduler implements IJobScheduler {
|
||||
protected readonly workerId: string // same id MongoJobsRepository stamps into lockedBy
|
||||
) {}
|
||||
|
||||
get serviceName() {
|
||||
return JobScheduler.SERVICE_NAME;
|
||||
}
|
||||
private heartbeatInFlight: Promise<void> = Promise.resolve();
|
||||
|
||||
protected processesJobs = false; // scheduler-only by default; DBQueueWorker flips this on when it runs job loops
|
||||
|
||||
@@ -71,17 +71,6 @@ describe("BufferService", () => {
|
||||
|
||||
// ── Static / instance properties ─────────────────────────────────────
|
||||
|
||||
describe("serviceName", () => {
|
||||
it("returns BufferService from static property", () => {
|
||||
expect(BufferService.SERVICE_NAME).toBe("BufferService");
|
||||
});
|
||||
|
||||
it("returns BufferService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("BufferService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── constructor ──────────────────────────────────────────────────────
|
||||
|
||||
describe("constructor", () => {
|
||||
|
||||
@@ -48,13 +48,6 @@ const makeStatusResponse = (overrides?: Partial<MonitorStatusResponse>): Monitor
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("CheckService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns checkService", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("checkService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── toCheck ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe("toCheck", () => {
|
||||
|
||||
@@ -56,17 +56,6 @@ describe("DiagnosticService", () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("serviceName", () => {
|
||||
it("returns diagnosticService from static property", () => {
|
||||
expect(DiagnosticService.SERVICE_NAME).toBe("diagnosticService");
|
||||
});
|
||||
|
||||
it("returns diagnosticService from instance getter", () => {
|
||||
const service = new DiagnosticService(makeDb());
|
||||
expect(service.serviceName).toBe("diagnosticService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── constructor ─────────────────────────────────────────────────────────
|
||||
|
||||
describe("constructor", () => {
|
||||
|
||||
@@ -82,11 +82,6 @@ describe("EmailService", () => {
|
||||
it("returns EmailService from static property", () => {
|
||||
expect(EmailService.SERVICE_NAME).toBe("EmailService");
|
||||
});
|
||||
|
||||
it("returns EmailService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("EmailService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── init / loadTemplate ──────────────────────────────────────────────
|
||||
@@ -95,14 +90,14 @@ describe("EmailService", () => {
|
||||
it("loads all expected templates on construction", () => {
|
||||
const { mockFs } = createService();
|
||||
|
||||
// 6 templates loaded
|
||||
expect(mockFs.readFileSync).toHaveBeenCalledTimes(6);
|
||||
// 5 templates loaded
|
||||
expect(mockFs.readFileSync).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("compiles each loaded template", () => {
|
||||
const { compile } = createService();
|
||||
|
||||
expect(compile).toHaveBeenCalledTimes(6);
|
||||
expect(compile).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("logs error when a template file is not found", () => {
|
||||
@@ -146,7 +141,7 @@ describe("EmailService", () => {
|
||||
|
||||
service.init();
|
||||
|
||||
expect(mockFs.readFileSync).toHaveBeenCalledTimes(initialCalls + 6);
|
||||
expect(mockFs.readFileSync).toHaveBeenCalledTimes(initialCalls + 5);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -48,17 +48,6 @@ const createService = (overrides?: Record<string, unknown>) => {
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("GeoChecksService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns GeoChecksService from static property", () => {
|
||||
expect(GeoChecksService.SERVICE_NAME).toBe("GeoChecksService");
|
||||
});
|
||||
|
||||
it("returns GeoChecksService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("GeoChecksService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildGeoCheck ───────────────────────────────────────────────────────
|
||||
|
||||
describe("buildGeoCheck", () => {
|
||||
|
||||
@@ -66,17 +66,6 @@ describe("GlobalPingService", () => {
|
||||
|
||||
// ── Static / instance properties ─────────────────────────────────────
|
||||
|
||||
describe("serviceName", () => {
|
||||
it("returns GlobalPingService from static property", () => {
|
||||
expect(GlobalPingService.SERVICE_NAME).toBe("GlobalPingService");
|
||||
});
|
||||
|
||||
it("returns GlobalPingService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("GlobalPingService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── createMeasurement ────────────────────────────────────────────────
|
||||
|
||||
describe("createMeasurement", () => {
|
||||
|
||||
@@ -91,13 +91,6 @@ const makeDecision = (overrides?: Partial<MonitorActionDecision>): MonitorAction
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("IncidentService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns incidentService", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("incidentService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── handleIncident ───────────────────────────────────────────────────────
|
||||
|
||||
describe("handleIncident", () => {
|
||||
|
||||
@@ -39,17 +39,6 @@ const createService = (overrides?: Record<string, unknown>) => {
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("InviteService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns inviteService from static property", () => {
|
||||
expect(InviteService.SERVICE_NAME).toBe("inviteService");
|
||||
});
|
||||
|
||||
it("returns inviteService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("inviteService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── getInviteToken ──────────────────────────────────────────────────────
|
||||
|
||||
describe("getInviteToken", () => {
|
||||
|
||||
@@ -94,17 +94,6 @@ const defaultCreateParams = {
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("MaintenanceWindowService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns maintenanceWindowService from static property", () => {
|
||||
expect(MaintenanceWindowService.SERVICE_NAME).toBe("maintenanceWindowService");
|
||||
});
|
||||
|
||||
it("returns maintenanceWindowService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("maintenanceWindowService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── createMaintenanceWindow ──────────────────────────────────────────────
|
||||
|
||||
describe("createMaintenanceWindow", () => {
|
||||
|
||||
@@ -111,13 +111,6 @@ const createService = (
|
||||
};
|
||||
|
||||
describe("MonitorService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns 'MonitorService'", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("MonitorService");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createMonitor", () => {
|
||||
it("creates a monitor and adds a job", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
|
||||
@@ -43,17 +43,6 @@ const createService = (providers: IStatusProvider<unknown>[] = []) => {
|
||||
describe("NetworkService", () => {
|
||||
// ── Static / instance properties ─────────────────────────────────────────
|
||||
|
||||
describe("serviceName", () => {
|
||||
it("returns NetworkService from static property", () => {
|
||||
expect(NetworkService.SERVICE_NAME).toBe("NetworkService");
|
||||
});
|
||||
|
||||
it("returns NetworkService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("NetworkService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── requestStatus ────────────────────────────────────────────────────────
|
||||
|
||||
describe("requestStatus", () => {
|
||||
|
||||
@@ -56,23 +56,12 @@ const createService = (envOverrides?: Partial<ValidatedEnv>) => {
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("SettingsService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns SettingsService from static property", () => {
|
||||
expect(SettingsService.SERVICE_NAME).toBe("SettingsService");
|
||||
});
|
||||
// ── getSettings ─────────────────────────────────────────────────────────
|
||||
|
||||
it("returns SettingsService from instance getter", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("SettingsService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── loadSettings ────────────────────────────────────────────────────────
|
||||
|
||||
describe("loadSettings", () => {
|
||||
describe("getSettings", () => {
|
||||
it("returns env config mapped from constructor", () => {
|
||||
const { service } = createService();
|
||||
const settings = service.loadSettings();
|
||||
const settings = service.getSettings();
|
||||
|
||||
expect(settings).toEqual({
|
||||
jwtSecret: "test-secret",
|
||||
@@ -93,7 +82,7 @@ describe("SettingsService", () => {
|
||||
CLIENT_CONFIG_LOG_LEVEL: "warn",
|
||||
});
|
||||
|
||||
expect(service.loadSettings().clientConfig).toEqual({
|
||||
expect(service.getSettings().clientConfig).toEqual({
|
||||
apiBaseUrl: "https://api.example.com/api/v1",
|
||||
logLevel: "warn",
|
||||
});
|
||||
@@ -101,23 +90,13 @@ describe("SettingsService", () => {
|
||||
|
||||
it("reflects custom env values", () => {
|
||||
const { service } = createService({ NODE_ENV: "production", LOG_LEVEL: "error" });
|
||||
const settings = service.loadSettings();
|
||||
const settings = service.getSettings();
|
||||
|
||||
expect(settings.nodeEnv).toBe("production");
|
||||
expect(settings.logLevel).toBe("error");
|
||||
});
|
||||
});
|
||||
|
||||
// ── getSettings ─────────────────────────────────────────────────────────
|
||||
|
||||
describe("getSettings", () => {
|
||||
it("returns the same config as loadSettings", () => {
|
||||
const { service } = createService();
|
||||
|
||||
expect(service.getSettings()).toEqual(service.loadSettings());
|
||||
});
|
||||
});
|
||||
|
||||
// ── areStatusPageThemesEnabled ──────────────────────────────────────────
|
||||
|
||||
describe("areStatusPageThemesEnabled", () => {
|
||||
|
||||
@@ -143,13 +143,6 @@ const makeExistingStats = (overrides?: Record<string, unknown>) => ({
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("StatusService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns StatusService", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("StatusService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateRunningStats ───────────────────────────────────────────────────
|
||||
|
||||
describe("updateRunningStats", () => {
|
||||
|
||||
@@ -98,13 +98,6 @@ const createService = (overrides?: Record<string, unknown>) => {
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("UserService", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns userService", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("userService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── issueToken ──────────────────────────────────────────────────────────
|
||||
|
||||
describe("issueToken", () => {
|
||||
|
||||
@@ -71,13 +71,6 @@ const createHelper = (overrides?: Record<string, unknown>) => {
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("WorkerHelper", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns JobQueueHelper", () => {
|
||||
const { helper } = createHelper();
|
||||
expect(helper.serviceName).toBe("JobQueueHelper");
|
||||
});
|
||||
});
|
||||
|
||||
// ── getCleanupOrphanedJob ────────────────────────────────────────────────
|
||||
|
||||
describe("getCleanupOrphanedJob", () => {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { getDateForRange, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts";
|
||||
import type { GroupedCheck } from "../../../src/domain/checks/check.type.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const makeCheck = (responseTime: number, overrides?: Record<string, unknown>) => ({
|
||||
id: `check-${responseTime}`,
|
||||
status: true,
|
||||
responseTime,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeGrouped = (avgResponseTime: number, overrides?: Partial<GroupedCheck>): GroupedCheck => ({
|
||||
bucketDate: `bucket-${avgResponseTime}`,
|
||||
avgResponseTime,
|
||||
totalChecks: 5,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("getDateForRange", () => {
|
||||
it.each([
|
||||
["recent", 2 * 60 * 60 * 1000],
|
||||
["hour", 60 * 60 * 1000],
|
||||
["day", 24 * 60 * 60 * 1000],
|
||||
["week", 7 * 24 * 60 * 60 * 1000],
|
||||
["month", 30 * 24 * 60 * 60 * 1000],
|
||||
])("returns now minus the %s window", (range, offsetMs) => {
|
||||
const before = Date.now();
|
||||
const result = getDateForRange(range as string);
|
||||
const after = Date.now();
|
||||
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result!.getTime()).toBeGreaterThanOrEqual(before - offsetMs);
|
||||
expect(result!.getTime()).toBeLessThanOrEqual(after - offsetMs);
|
||||
});
|
||||
|
||||
it("returns undefined for 'all'", () => {
|
||||
expect(getDateForRange("all")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for unknown ranges", () => {
|
||||
expect(getDateForRange("fortnight")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("NormalizeData", () => {
|
||||
// Fixture math: p0 of [100..500] is 100; p95 interpolates between the 4th and 5th
|
||||
// sorted values at weight 0.8 → 400*0.2 + 500*0.8 = 480. Values rescale into
|
||||
// [10, 100] via 10 + (rt - 100) * 90 / 380, clamped.
|
||||
it("rescales responseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => {
|
||||
const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt));
|
||||
|
||||
const result = NormalizeData(checks, 10, 100);
|
||||
|
||||
expect(result[0]!.responseTime).toBe(10);
|
||||
expect(result[1]!.responseTime).toBeCloseTo(33.6842, 4);
|
||||
expect(result[2]!.responseTime).toBeCloseTo(57.3684, 4);
|
||||
expect(result[3]!.responseTime).toBeCloseTo(81.0526, 4);
|
||||
});
|
||||
|
||||
it("clamps values above the p95 anchor to rangeMax", () => {
|
||||
const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt));
|
||||
|
||||
const result = NormalizeData(checks, 10, 100);
|
||||
|
||||
expect(result[4]!.responseTime).toBe(100);
|
||||
});
|
||||
|
||||
it("preserves the raw value in originalResponseTime and spreads other fields", () => {
|
||||
const checks = [makeCheck(100, { message: "OK" }), makeCheck(500)];
|
||||
|
||||
const result = NormalizeData(checks, 10, 100);
|
||||
|
||||
expect(result[0]!.originalResponseTime).toBe(100);
|
||||
expect(result[1]!.originalResponseTime).toBe(500);
|
||||
expect(result[0]).toMatchObject({ id: "check-100", status: true, message: "OK" });
|
||||
});
|
||||
|
||||
it("passes a single check through, only adding originalResponseTime", () => {
|
||||
const result = NormalizeData([makeCheck(250)], 10, 100);
|
||||
|
||||
expect(result).toEqual([{ id: "check-250", status: true, responseTime: 250, originalResponseTime: 250 }]);
|
||||
});
|
||||
|
||||
it("returns an empty array for empty input", () => {
|
||||
expect(NormalizeData([], 10, 100)).toEqual([]);
|
||||
});
|
||||
|
||||
it("records originalResponseTime as 0 for a non-numeric responseTime (failed checks)", () => {
|
||||
const checks = [makeCheck(undefined as unknown as number), makeCheck(100), makeCheck(200)];
|
||||
|
||||
const result = NormalizeData(checks, 10, 100);
|
||||
|
||||
expect(result[0]!.originalResponseTime).toBe(0);
|
||||
});
|
||||
|
||||
// Characterization, not endorsement: when every value is equal the p0–p95 window
|
||||
// has zero width and the rescale divides by zero. Fixing this is a planned
|
||||
// follow-up (docs/planning/dataUtils-generic-normalization-refactor.md); this
|
||||
// test pins the current behavior so the fix is a deliberate, visible change.
|
||||
it("produces NaN when all values are equal (division by zero — known edge case)", () => {
|
||||
const result = NormalizeData([makeCheck(50), makeCheck(50), makeCheck(50)], 10, 100);
|
||||
|
||||
result.forEach((check) => {
|
||||
expect(check.responseTime).toBeNaN();
|
||||
expect(check.originalResponseTime).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NormalizeDataUptimeDetails", () => {
|
||||
it("rescales avgResponseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => {
|
||||
const checks = [100, 200, 300, 400, 500].map((avg) => makeGrouped(avg));
|
||||
|
||||
const result = NormalizeDataUptimeDetails(checks, 10, 100);
|
||||
|
||||
expect(result[0]!.avgResponseTime).toBe(10);
|
||||
expect(result[1]!.avgResponseTime).toBeCloseTo(33.6842, 4);
|
||||
expect(result[4]!.avgResponseTime).toBe(100);
|
||||
});
|
||||
|
||||
it("preserves the raw value in originalAvgResponseTime and spreads other fields", () => {
|
||||
const checks = [makeGrouped(100), makeGrouped(500)];
|
||||
|
||||
const result = NormalizeDataUptimeDetails(checks, 10, 100);
|
||||
|
||||
expect(result[0]!.originalAvgResponseTime).toBe(100);
|
||||
expect(result[1]!.originalAvgResponseTime).toBe(500);
|
||||
expect(result[0]).toMatchObject({ bucketDate: "bucket-100", totalChecks: 5 });
|
||||
});
|
||||
|
||||
it("passes a single bucket through, only adding originalAvgResponseTime", () => {
|
||||
const result = NormalizeDataUptimeDetails([makeGrouped(250)], 10, 100);
|
||||
|
||||
expect(result).toEqual([{ bucketDate: "bucket-250", avgResponseTime: 250, totalChecks: 5, originalAvgResponseTime: 250 }]);
|
||||
});
|
||||
|
||||
it("returns an empty array for empty input", () => {
|
||||
expect(NormalizeDataUptimeDetails([], 10, 100)).toEqual([]);
|
||||
});
|
||||
|
||||
it("records originalAvgResponseTime as 0 for a null avgResponseTime ($avg over an empty bucket)", () => {
|
||||
const result = NormalizeDataUptimeDetails([makeGrouped(null as unknown as number)], 10, 100);
|
||||
|
||||
expect(result[0]!.originalAvgResponseTime).toBe(0);
|
||||
});
|
||||
|
||||
it("produces NaN when all values are equal (division by zero — known edge case)", () => {
|
||||
const result = NormalizeDataUptimeDetails([makeGrouped(50), makeGrouped(50)], 10, 100);
|
||||
|
||||
result.forEach((check) => {
|
||||
expect(check.avgResponseTime).toBeNaN();
|
||||
expect(check.originalAvgResponseTime).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,4 +43,25 @@ describe("editUserBodyValidation", () => {
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
// Regression: `deleteProfileImage` was missing from this schema, so z.object()
|
||||
// stripped it before it reached the repository and the remove-profile-image
|
||||
// flow was silently a no-op. The flag arrives as the multipart form-data
|
||||
// string "true", so it must both survive parsing and coerce to a boolean.
|
||||
it("retains deleteProfileImage and coerces the multipart string 'true' to boolean true", () => {
|
||||
const result = editUserBodyValidation.safeParse({ deleteProfileImage: "true" });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({ deleteProfileImage: true });
|
||||
});
|
||||
|
||||
it("coerces the multipart string 'false' to boolean false", () => {
|
||||
const result = editUserBodyValidation.safeParse({ deleteProfileImage: "false" });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({ deleteProfileImage: false });
|
||||
});
|
||||
|
||||
it("rejects a non-boolean deleteProfileImage value", () => {
|
||||
const result = editUserBodyValidation.safeParse({ deleteProfileImage: "banana" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user