Merge pull request #3161 from bluewave-labs/fix/cleanup

Fix/cleanup
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-21 10:18:44 -08:00
commit 760abe69ed
15 files changed
+195 -1449

No files matched your search

-7
View File
@@ -36,7 +36,6 @@
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"papaparse": "^5.5.2",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
@@ -11567,12 +11566,6 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/papaparse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
"license": "MIT"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-1
View File
@@ -48,7 +48,6 @@
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"papaparse": "^5.5.2",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
-2
View File
@@ -24,7 +24,6 @@ import InviteService from "../service/business/inviteService.js";
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import { MonitorService } from "@/service/index.js";
import IncidentService from "../service/business/incidentService.js";
import papaparse from "papaparse";
import axios from "axios";
import got from "got";
import ping from "ping";
@@ -276,7 +275,6 @@ export const initializeServices = async ({
const monitorService = new MonitorService({
jobQueue: superSimpleQueue,
emailService,
papaparse,
logger,
errorService,
games,
+14 -4
View File
@@ -1,5 +1,6 @@
import { AppError } from "@/utils/AppError.js";
import { Request, Response, NextFunction } from "express";
import { requireTeamId } from "./controllerUtils.js";
const SERVICE_NAME = "incidentController";
@@ -17,10 +18,19 @@ class IncidentController {
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.incidentService.getIncidentsByTeam({
teamId: req?.user?.teamId,
query: req?.query,
});
const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
const teamId = requireTeamId(req.user?.teamId);
const result = await this.incidentService.getIncidentsByTeam(
teamId,
sortOrder,
dateRange,
page,
rowsPerPage,
status,
monitorId,
resolutionType
);
return res.status(200).json({
success: true,
+5 -17
View File
@@ -174,27 +174,15 @@ class MonitorController {
createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.file) {
throw new AppError({ message: "No file uploaded", status: 400 });
const userId = req.user?.id;
if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 });
}
if (!req.file.mimetype.includes("csv")) {
throw new AppError({ message: "File is not a CSV", status: 400 });
}
if (req.file.size === 0) {
throw new AppError({ message: "File is empty", status: 400 });
}
const userId = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const fileData = req?.file?.buffer?.toString("utf-8");
if (!fileData) {
throw new AppError({ message: "Cannot get file from buffer", status: 400 });
}
const monitors = await this.monitorService.createBulkMonitors(fileData, userId, teamId);
const data = req.body;
const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId);
return res.status(200).json({
success: true,
@@ -1,4 +1,4 @@
import { type MonitorType, type Monitor } from "@/types/index.js";
import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js";
export interface TeamQueryConfig {
limit?: number;
@@ -37,6 +37,6 @@ export interface IMonitorsRepository {
findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise<number>;
// other
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<any>;
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
findGroupsByTeamId(teamId: string): Promise<string[]>;
}
@@ -1,6 +1,6 @@
import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument } from "@/db/models/index.js";
import type { Monitor } from "@/types/index.js";
import type { Monitor, MonitorsSummary } from "@/types/index.js";
import mongoose, { type FilterQuery } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { AppError } from "@/utils/AppError.js";
@@ -146,10 +146,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return { monitors: this.mapDocuments(monitors), deletedCount };
};
findMonitorsSummaryByTeamId = async (
teamId: string,
config?: SummaryConfig
): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => {
findMonitorsSummaryByTeamId = async (teamId: string, config?: SummaryConfig): Promise<MonitorsSummary> => {
const match: FilterQuery<MonitorDocument> = { teamId: new mongoose.Types.ObjectId(teamId) };
if (config?.type !== undefined) {
match.type = Array.isArray(config.type) ? { $in: config.type } : config.type;
+10 -2
View File
@@ -124,13 +124,21 @@ class IncidentService {
}
};
getIncidentsByTeam = async ({ teamId, query }: { teamId: string; query?: any }) => {
getIncidentsByTeam = async (
teamId: string,
sortOrder: string,
dateRange: string,
page: string,
rowsPerPage: string,
status: string,
monitorId: string,
resolutionType: string
) => {
try {
if (!teamId) {
throw this.errorService.createBadRequestError("No team ID in request");
}
const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {};
const startDate = dateRangeLookup[dateRange];
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
+55 -77
View File
@@ -1,13 +1,20 @@
import { createMonitorsBodyValidation } from "@/validation/joi.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
import { type Monitor } from "@/types/index.js";
import type { MonitorType } from "@/types/monitor.js";
import type {
MonitorType,
MonitorsWithChecksByTeamIdResult,
UptimeDetailsResult,
HardwareDetailsResult,
PageSpeedDetailsResult,
} from "@/types/monitor.js";
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js";
import fs from "fs";
import { fileURLToPath } from "url";
import path from "path";
import { AppError } from "../infrastructure/errorService.js";
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
const SERVICE_NAME = "MonitorService";
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
@@ -17,13 +24,13 @@ export interface IMonitorService {
// create
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
createBulkMonitors(fileData: string, userId: string, teamId: string): Promise<any>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<any[]>;
createBulkMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
// read
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<UptimeDetailsResult>;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
getMonitorsByTeamId(args: {
teamId: string;
@@ -34,7 +41,7 @@ export interface IMonitorService {
filter?: string;
field?: string;
order?: "asc" | "desc";
}): Promise<any>;
}): Promise<Monitor[] | null>;
getMonitorsWithChecksByTeamId(args: {
teamId: string;
limit?: number;
@@ -45,7 +52,7 @@ export interface IMonitorService {
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<{ summary: any; count: number; monitors: any[] }>;
}): Promise<MonitorsWithChecksByTeamIdResult>;
getAllGames(): any;
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
@@ -59,15 +66,14 @@ export interface IMonitorService {
// other
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<any[]>;
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
}
export class MonitorService implements IMonitorService {
static SERVICE_NAME = SERVICE_NAME;
private jobQueue: any;
private jobQueue: ISuperSimpleQueue;
private emailService: any;
private papaparse: any;
private logger: any;
private errorService: any;
private games: any;
@@ -79,7 +85,6 @@ export class MonitorService implements IMonitorService {
constructor({
jobQueue,
emailService,
papaparse,
logger,
errorService,
games,
@@ -88,9 +93,8 @@ export class MonitorService implements IMonitorService {
monitorStatsRepository,
statusPagesRepository,
}: {
jobQueue: any;
jobQueue: ISuperSimpleQueue;
emailService: any;
papaparse: any;
logger: any;
errorService: any;
games: any;
@@ -101,7 +105,6 @@ export class MonitorService implements IMonitorService {
}) {
this.jobQueue = jobQueue;
this.emailService = emailService;
this.papaparse = papaparse;
this.logger = logger;
this.errorService = errorService;
this.games = games;
@@ -149,63 +152,14 @@ export class MonitorService implements IMonitorService {
this.jobQueue.addJob(monitor.id, monitor);
};
createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise<any> => {
const { parse } = this.papaparse;
createBulkMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
if (!monitors || monitors.length === 0) {
throw new AppError("Failed to create monitors", 500);
}
return new Promise<any>((resolve, reject) => {
parse(fileData, {
header: true,
skipEmptyLines: true,
transform: (value: string, header: string): string | number | undefined => {
if (value === "") return undefined; // Empty fields become undefined
// Handle 'port' and 'interval' fields, check if they're valid numbers
if (["port", "interval"].includes(header)) {
const num = parseInt(value, 10);
if (isNaN(num)) {
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
}
return num;
}
return value;
},
complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise<void> => {
try {
if (errors.length > 0) {
throw this.errorService.createServerError("Error parsing CSV");
}
if (!data || data.length === 0) {
throw this.errorService.createServerError("CSV file contains no data rows");
}
const enrichedData = data.map((monitor: Monitor) => ({
...monitor,
userId,
teamId,
description: monitor.description || monitor.name || monitor.url,
name: monitor.name || monitor.url,
type: monitor.type || "http",
}));
await createMonitorsBodyValidation.validateAsync(enrichedData);
const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData);
await Promise.all(
monitors.map(async (monitor: Monitor) => {
this.jobQueue.addJob(monitor.id, monitor);
})
);
resolve(monitors);
} catch (error) {
reject(error);
}
},
});
});
await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
return createdMonitors;
};
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
@@ -241,7 +195,7 @@ export class MonitorService implements IMonitorService {
monitorId: string;
dateRange: string;
normalize?: boolean;
}): Promise<any> => {
}): Promise<UptimeDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -276,7 +230,15 @@ export class MonitorService implements IMonitorService {
};
};
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
getHardwareDetailsById = async ({
teamId,
monitorId,
dateRange,
}: {
teamId: string;
monitorId: string;
dateRange: string;
}): Promise<HardwareDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -307,7 +269,15 @@ export class MonitorService implements IMonitorService {
};
};
getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
getPageSpeedDetailsById = async ({
teamId,
monitorId,
dateRange,
}: {
teamId: string;
monitorId: string;
dateRange: string;
}): Promise<PageSpeedDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -336,12 +306,20 @@ export class MonitorService implements IMonitorService {
monitorStats,
};
};
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
return monitor;
};
getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise<any> => {
getMonitorsByTeamId = async ({
teamId,
type,
filter,
}: {
teamId: string;
type?: MonitorType | MonitorType[];
filter?: string;
}): Promise<Monitor[] | null> => {
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
type,
filter,
@@ -369,7 +347,7 @@ export class MonitorService implements IMonitorService {
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<{ summary: any; count: number; monitors: any[] }> => {
}): Promise<MonitorsWithChecksByTeamIdResult> => {
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId);
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
+17 -3
View File
@@ -156,11 +156,17 @@ export interface HardwareChecksResult {
}>;
}
export interface GroupedCheck {
_id: string;
avgResponseTime: number;
totalChecks: number;
}
export interface UptimeChecksResult {
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedChecks: GroupedCheck[];
groupedUpChecks: GroupedCheck[];
groupedDownChecks: GroupedCheck[];
uptimePercentage: number;
avgResponseTime: number;
}
@@ -171,3 +177,11 @@ export interface ChecksSummary {
downChecks: number;
cannotResolveChecks: number;
}
export type NormalizedCheck<T extends Check = Check> = T & {
originalResponseTime: number;
};
export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
originalAvgResponseTime: number;
};
+75
View File
@@ -44,3 +44,78 @@ export interface Monitor {
createdAt: string;
updatedAt: string;
}
export interface MonitorsSummary {
totalMonitors: number;
upMonitors: number;
downMonitors: number;
pausedMonitors: number;
}
export interface MonitorWithChecks extends Monitor {
checks: import("./check.js").Check[];
}
export interface MonitorsWithChecksByTeamIdResult {
summary: MonitorsSummary | null;
count: number;
monitors: MonitorWithChecks[];
}
export interface UptimeDetailsResult {
monitorData: {
monitor: Monitor;
groupedChecks: import("./check.js").GroupedCheck[];
groupedUpChecks: import("./check.js").GroupedCheck[];
groupedDownChecks: import("./check.js").GroupedCheck[];
groupedAvgResponseTime: number;
groupedUptimePercentage: number;
};
monitorStats: import("./monitorStats.js").MonitorStats | null;
}
export interface HardwareDetailsResult extends Monitor {
stats: {
aggregateData: {
latestCheck: import("./check.js").Check | null;
totalChecks: number;
};
upChecks: {
totalChecks: number;
};
checks: Array<{
_id: string;
avgCpuUsage: number;
avgMemoryUsage: number;
avgTemperature: number[];
disks: Array<{
name: string;
readSpeed: number;
writeSpeed: number;
totalBytes: number;
freeBytes: number;
usagePercent: number;
}>;
net: Array<{
name: string;
bytesSentPerSecond: number;
deltaBytesRecv: number;
deltaPacketsSent: number;
deltaPacketsRecv: number;
deltaErrIn: number;
deltaErrOut: number;
deltaDropIn: number;
deltaDropOut: number;
deltaFifoIn: number;
deltaFifoOut: number;
}>;
}>;
};
}
export interface PageSpeedDetailsResult {
monitor: Monitor & {
checks: import("./check.js").Check[];
};
monitorStats: import("./monitorStats.js").MonitorStats | null;
}
@@ -1,35 +1,33 @@
const calculatePercentile = (arr, percentile) => {
import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js";
const calculatePercentile = (arr: Check[], percentile: number): number => {
const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime);
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 sorted[lower]!.responseTime;
return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight;
};
const calculatePercentileUptimeDetails = (arr, percentile) => {
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;
if (upper >= sorted.length) return sorted[lower]!.avgResponseTime;
return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight;
};
const NormalizeData = (checks, rangeMin, rangeMax) => {
export const NormalizeData = <T extends Check>(checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck<T>[] => {
if (checks.length > 1) {
// Get the 5th and 95th percentile
const min = calculatePercentile(checks, 0);
const max = calculatePercentile(checks, 95);
const normalizedChecks = checks.map((check) => {
const originalResponseTime = check.responseTime;
// Normalize the response time between 1 and 100
let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min);
// Put a floor on the response times so we don't have extreme outliers
// Better visuals
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
return {
...check,
@@ -45,19 +43,16 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
});
}
};
const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => {
export const NormalizeDataUptimeDetails = <T extends GroupedCheck>(checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck<T>[] => {
if (checks.length > 1) {
// Get the 5th and 95th percentile
const min = calculatePercentileUptimeDetails(checks, 0);
const max = calculatePercentileUptimeDetails(checks, 95);
const normalizedChecks = checks.map((check) => {
const originalResponseTime = check.avgResponseTime;
// Normalize the response time between 1 and 100
let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min);
// Put a floor on the response times so we don't have extreme outliers
// Better visuals
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
return {
...check,
@@ -69,32 +64,7 @@ const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => {
return normalizedChecks;
} else {
return checks.map((check) => {
return { ...check, originalResponseTime: check.responseTime };
return { ...check, originalAvgResponseTime: check.avgResponseTime };
});
}
};
const safelyParseFloat = (value, defaultValue = 0) => {
if (value === null || typeof value === "undefined") {
return defaultValue;
}
const stringValue = String(value).trim();
if (typeof value === "number" && !isNaN(value)) {
return value;
}
if (stringValue === "") {
return defaultValue;
}
const parsedValue = parseFloat(stringValue);
if (isNaN(parsedValue) || !isFinite(parsedValue)) {
return defaultValue;
}
return parsedValue;
};
export { safelyParseFloat, calculatePercentile, NormalizeData, calculatePercentileUptimeDetails, NormalizeDataUptimeDetails };
File diff suppressed because it is too large. Load diff
-13
View File
@@ -1,13 +0,0 @@
export const ROLES = {
SUPERADMIN: "superadmin",
ADMIN: "admin",
USER: "user",
DEMO: "demo",
};
export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO];
export const EDITABLE_ROLES = [
{ role: ROLES.ADMIN, _id: ROLES.ADMIN },
{ role: ROLES.USER, _id: ROLES.USER },
];
+3 -3
View File
@@ -1,5 +1,5 @@
import joi from "joi";
import { ROLES, VALID_ROLES } from "../utils/roleUtils.js";
import { UserRoles } from "@/types/user.js";
//****************************************
// Custom Validators
@@ -689,7 +689,7 @@ const editUserByIdBodyValidation = joi.object({
email: joi.string().email().required(),
role: joi
.array()
.items(joi.string().valid(...VALID_ROLES))
.items(joi.string().valid(...UserRoles))
.min(1)
.required(),
});
@@ -700,7 +700,7 @@ const editSuperadminUserByIdBodyValidation = joi.object({
email: joi.string().email().required(),
role: joi
.array()
.items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN))
.items(joi.string().valid(...UserRoles))
.min(1)
.required(),
});