diff --git a/server/package.json b/server/package.json index ad266794d..e67bcdb78 100755 --- a/server/package.json +++ b/server/package.json @@ -15,6 +15,7 @@ "worker:dev": "QUEUE_MODE=worker nodemon --exec tsx src/index.js", "clean": "rm -rf dist", "build": "npm run clean && npm run build:openapi && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r src/templates dist/templates", + "typeCheck": "tsc --noEmit", "build:openapi": "tsc --noEmit && tsx scripts/buildOpenApi.ts", "lint": "eslint .", "lint-fix": "eslint --fix .", diff --git a/server/src/domain/checks/check.repository.interface.ts b/server/src/domain/checks/check.repository.interface.ts index 95c0ade80..8c921c843 100644 --- a/server/src/domain/checks/check.repository.interface.ts +++ b/server/src/domain/checks/check.repository.interface.ts @@ -8,6 +8,7 @@ import type { } from "@/domain/checks/check.type.js"; import type { MonitorType } from "@/domain/monitors/monitor.types.js"; import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js"; +import { DateRange } from "@/types/query.js"; export interface IChecksRepository { // create @@ -19,7 +20,7 @@ export interface IChecksRepository { findByMonitorId( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, filter: string | undefined, page: number, rowsPerPage: number, @@ -27,7 +28,7 @@ export interface IChecksRepository { ): Promise; findByTeamId( sortOrder: string, - dateRange: string, + dateRange: DateRange, filter: string | undefined, page: number, rowsPerPage: number, @@ -36,12 +37,10 @@ export interface IChecksRepository { findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findByDateRangeAndMonitorId( monitorId: string, - startDate: Date, - endDate: Date, - dateString: string, + dateRange: DateRange, options?: { type?: MonitorType } ): Promise; - findSummaryByTeamId(teamId: string, dateRange: string): Promise; + findSummaryByTeamId(teamId: string, dateRange: DateRange): Promise; findUnevaluatedByMonitorId(monitorId: string, since: number): Promise; // update //delete diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 1433c950a..4bfb65dd4 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -15,11 +15,12 @@ import type { import type { MonitorType } from "@/domain/monitors/monitor.types.js"; import { CheckModel, type CheckDocument } from "@/domain/checks/check.model.js"; import mongoose from "mongoose"; -import { getDateForRange } from "@/utils/dataUtils.js"; +import { getDateFormat, getDateForRange } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregations.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "StatusService"; @@ -207,7 +208,7 @@ class MongoChecksRepository implements IChecksRepository { findByMonitorId = async ( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, filter: string | undefined, page: number, rowsPerPage: number, @@ -217,11 +218,7 @@ class MongoChecksRepository implements IChecksRepository { const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; if (filter !== undefined) { @@ -265,14 +262,10 @@ class MongoChecksRepository implements IChecksRepository { return { checksCount, checks: this.mapDocuments(checks) }; }; - findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => { + findByTeamId = async (sortOrder: string, dateRange: DateRange, filter: string, page: number, rowsPerPage: number, teamId: string) => { const matchStage: Record = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; // Add filter to match stage if (filter !== undefined) { @@ -341,25 +334,25 @@ class MongoChecksRepository implements IChecksRepository { return mapped; }; - findByDateRangeAndMonitorId = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => { + findByDateRangeAndMonitorId = async (monitorId: string, dateRange: DateRange, options?: { type?: MonitorType }) => { const monitorObjectId = new mongoose.Types.ObjectId(monitorId); + const start = getDateForRange(dateRange); + const dateString = getDateFormat(dateRange); + + const end = new Date(); if (options?.type === "hardware") { - return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + return this.findHardwareDateRangeChecks(monitorObjectId, start, end, dateString); } if (options?.type === "pagespeed") { - return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + return this.findPageSpeedDateRangeChecks(monitorObjectId, start, end, dateString); } - return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString); + return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, start, end, dateString); }; - findSummaryByTeamId = async (teamId: string, dateRange: string) => { + findSummaryByTeamId = async (teamId: string, dateRange: DateRange) => { const baseMatch = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; const [totalResult, downResult] = await Promise.all([ diff --git a/server/src/domain/checks/check.service.ts b/server/src/domain/checks/check.service.ts index f3c18ec91..b577658bd 100644 --- a/server/src/domain/checks/check.service.ts +++ b/server/src/domain/checks/check.service.ts @@ -6,6 +6,7 @@ import type { MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.j import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import { ILogger } from "@/utils/logger.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "checkService"; @@ -18,7 +19,7 @@ export interface ICheckService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; filter?: string; @@ -27,12 +28,12 @@ export interface ICheckService { getChecksByTeam(params: { teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; filter?: string; }): Promise; - getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise; + getChecksSummaryByTeamId(params: { teamId: string; dateRange: DateRange }): Promise; deleteChecks(params: { monitorId: string; teamId: string }): Promise; deleteChecksByTeamId(params: { teamId: string }): Promise; deleteOlderThan(date: Date): Promise; @@ -164,7 +165,7 @@ export class CheckService implements ICheckService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; status?: boolean; @@ -198,7 +199,7 @@ export class CheckService implements ICheckService { }: { teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; filter?: string; @@ -210,7 +211,7 @@ export class CheckService implements ICheckService { return checkData; }; - getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => { + getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: DateRange }) => { const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange); return summary; }; diff --git a/server/src/domain/geo-checks/geo-check.repository.interface.ts b/server/src/domain/geo-checks/geo-check.repository.interface.ts index 2e6f33e33..a2cb7f84e 100644 --- a/server/src/domain/geo-checks/geo-check.repository.interface.ts +++ b/server/src/domain/geo-checks/geo-check.repository.interface.ts @@ -1,5 +1,6 @@ import type { GeoCheck, GroupedGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoContinent, FlatGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; +import { DateRange } from "@/types/query.js"; export interface GeoChecksQueryResult { geoChecksCount: number; @@ -16,19 +17,13 @@ export interface IGeoChecksRepository { findByMonitorId( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, continents?: GeoContinent[] ): Promise; findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise; - findGroupedByMonitorIdAndDateRange( - monitorId: string, - startDate: Date, - endDate: Date, - dateFormat: string, - continents?: GeoContinent[] - ): Promise; + findGroupedByMonitorIdAndDateRange(monitorId: string, DateRange: DateRange, continents?: GeoContinent[]): Promise; deleteByMonitorId(monitorId: string): Promise; deleteByTeamId(teamId: string): Promise; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise; diff --git a/server/src/domain/geo-checks/geo-check.repository.mongo.ts b/server/src/domain/geo-checks/geo-check.repository.mongo.ts index e73056e95..d1c56eb1c 100644 --- a/server/src/domain/geo-checks/geo-check.repository.mongo.ts +++ b/server/src/domain/geo-checks/geo-check.repository.mongo.ts @@ -3,9 +3,10 @@ import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoCo import type { FlatGeoChecksQueryResult } from "./geo-check.repository.interface.js"; import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/domain/geo-checks/geo-check.model.js"; import mongoose, { PipelineStage } from "mongoose"; -import { getDateForRange } from "@/utils/dataUtils.js"; +import { getDateForRange, getDateFormat } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "GeoChecksRepository"; @@ -90,7 +91,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { findByMonitorId = async ( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, continents?: GeoContinent[] @@ -98,11 +99,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { try { const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; const convertedSortOrder = sortOrder === "asc" ? 1 : -1; @@ -214,13 +211,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { } }; - findGroupedByMonitorIdAndDateRange = async ( - monitorId: string, - startDate: Date, - endDate: Date, - dateFormat: string, - continents?: GeoContinent[] - ): Promise => { + findGroupedByMonitorIdAndDateRange = async (monitorId: string, dateRange: DateRange, continents?: GeoContinent[]): Promise => { + const start = getDateForRange(dateRange); + const dateString = getDateFormat(dateRange); + + const end = new Date(); try { const pipeline: PipelineStage[] = [ // Match geo checks for this monitor in date range @@ -228,8 +223,8 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { $match: { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), createdAt: { - $gte: startDate, - $lte: endDate, + $gte: start, + $lte: end, }, }, }, @@ -253,7 +248,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { _id: { bucketDate: { $dateToString: { - format: dateFormat, + format: dateString, date: "$createdAt", }, }, diff --git a/server/src/domain/geo-checks/geo-check.service.ts b/server/src/domain/geo-checks/geo-check.service.ts index 040383811..26d347228 100644 --- a/server/src/domain/geo-checks/geo-check.service.ts +++ b/server/src/domain/geo-checks/geo-check.service.ts @@ -7,6 +7,7 @@ import type { IMonitorsRepository } from "@/domain/monitors/monitor.repository.i import type { IGlobalPingService } from "@/service/globalPingService.js"; import type { ILogger } from "@/utils/logger.js"; import { AppError } from "@/utils/AppError.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "GeoChecksService"; @@ -17,7 +18,7 @@ export interface IGeoChecksService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page?: number; rowsPerPage?: number; continent?: GeoContinent | GeoContinent[]; @@ -155,7 +156,7 @@ export class GeoChecksService implements IGeoChecksService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page?: number; rowsPerPage?: number; continent: GeoContinent | GeoContinent[]; diff --git a/server/src/domain/incidents/incident.service.ts b/server/src/domain/incidents/incident.service.ts index 98f092088..9b8fbf25e 100644 --- a/server/src/domain/incidents/incident.service.ts +++ b/server/src/domain/incidents/incident.service.ts @@ -11,6 +11,7 @@ import type { User } from "@/domain/users/user.type.js"; import type { MonitorActionDecision } from "@/worker/worker.helper.js"; import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js"; import type { ILogger } from "@/utils/logger.js"; +import { DateRange } from "@/types/query.js"; export interface IIncidentService { handleIncident( @@ -23,7 +24,7 @@ export interface IIncidentService { getIncidentsByTeam( teamId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, status: boolean | undefined, @@ -174,7 +175,7 @@ export class IncidentService implements IIncidentService { getIncidentsByTeam = async ( teamId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, status: boolean | undefined, diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index 96db34c15..d9e35855c 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -91,27 +91,26 @@ class MongoMonitorsRepository implements IMonitorsRepository { { $project: { stats: 0 } }, ]; - findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + private pageOptions = (config: TeamQueryConfig) => { const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {}; + return { sort: { [field]: order === "asc" ? 1 : -1 } as const, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage }; + }; + + findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { const query = this.queryBuilder(config, teamId); - const sort = { [field]: order === "asc" ? 1 : -1 } as const; - const skip = Math.max(page, 0) * rowsPerPage; - const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); + const { sort, skip, limit } = this.pageOptions(config); + const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limit); return this.mapDocuments(documents); }; findByTeamIdWithStats = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {}; - + const { sort, skip, limit } = this.pageOptions(config); const query = this.queryBuilder(config, teamId); - const sort = { [field]: order === "asc" ? 1 : -1 } as const; - const skip = Math.max(page, 0) * rowsPerPage; - const pipeline: PipelineStage[] = [ { $match: query }, { $sort: sort }, - ...(rowsPerPage ? [{ $skip: skip }, { $limit: rowsPerPage }] : []), + ...(limit ? [{ $skip: skip }, { $limit: limit }] : []), ...this.uptimeStatsLookupStages, ]; diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index e7a79a161..e6d339b80 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -23,9 +23,9 @@ import { AppError } from "@/utils/AppError.js"; import type { ImportedMonitor } from "@/api/validation/monitorValidation.js"; import { ILogger } from "@/utils/logger.js"; import { IJobScheduler } from "@/worker/worker.interface.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "MonitorService"; -type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; const isUptimeChecksResult = (result: UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult): result is UptimeChecksResult => supportsUptimeDetails(result.monitorType); @@ -37,13 +37,13 @@ export interface IMonitorService { addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read - getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise; + getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise; + getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise; getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; continents?: GeoContinent[]; }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; @@ -133,31 +133,6 @@ export class MonitorService implements IMonitorService { this.incidentsRepository = incidentsRepository; } - private getDateRange = (dateRange: DateRangeKey) => { - const startDates = { - recent: new Date(new Date().setHours(new Date().getHours() - 2)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: new Date(0), - }; - return { - start: startDates[dateRange], - end: new Date(), - }; - }; - - private getDateFormat = (dateRange: DateRangeKey): string => { - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - all: "%Y-%m-%dT00:00:00Z", - }; - return formatLookup[dateRange]; - }; - createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { const monitor = await this.monitorsRepository.create(body, teamId, userId); if (!monitor) { @@ -200,15 +175,13 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { type: monitor.type, }); const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); @@ -237,7 +210,7 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { @@ -247,9 +220,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 }); } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { type: monitor.type, }); @@ -279,7 +250,7 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { @@ -289,9 +260,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 }); } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { type: monitor.type, }); @@ -318,7 +287,7 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; continents?: GeoContinent[]; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); @@ -330,15 +299,7 @@ export class MonitorService implements IMonitorService { return { groupedGeoChecks: [] }; } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange( - monitor.id, - start, - end, - this.getDateFormat(rangeKey), - continents - ); + const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange(monitor.id, dateRange, continents); return { groupedGeoChecks }; }; diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index b264f85b1..0382abc57 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -1,6 +1,7 @@ import type { GroupedCheck, HasResponseTime, NormalizedCheck, NormalizedUptimeCheck } from "@/domain/checks/check.type.js"; +import { type DateRange } from "@/types/query.js"; -export const getDateForRange = (dateRange: string): Date | undefined => { +export const getDateForRange = (dateRange: DateRange): Date => { const now = Date.now(); switch (dateRange) { case "recent": @@ -14,12 +15,24 @@ export const getDateForRange = (dateRange: string): Date | undefined => { case "month": return new Date(now - 30 * 24 * 60 * 60 * 1000); // 30 days case "all": - return undefined; + return new Date(0); default: - return undefined; + return new Date(0); } }; +export const getDateFormat = (dateRange: DateRange): string => { + const formatLookup = { + hour: "%Y-%m-%dT%H:%M:00Z", + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT00:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + all: "%Y-%m-%dT00:00:00Z", + }; + return formatLookup[dateRange]; +}; + const percentileBy = (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); diff --git a/server/test/unit/utils/dataUtils.test.ts b/server/test/unit/utils/dataUtils.test.ts index f4893fe03..01f102cca 100644 --- a/server/test/unit/utils/dataUtils.test.ts +++ b/server/test/unit/utils/dataUtils.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "@jest/globals"; -import { getDateForRange, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts"; +import { getDateForRange, getDateFormat, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts"; import type { GroupedCheck } from "../../../src/domain/checks/check.type.ts"; +import { DateRanges, type DateRange } from "../../../src/types/query.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -29,20 +30,35 @@ describe("getDateForRange", () => { ["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 result = getDateForRange(range as DateRange); const after = Date.now(); expect(result).toBeInstanceOf(Date); - expect(result!.getTime()).toBeGreaterThanOrEqual(before - offsetMs); - expect(result!.getTime()).toBeLessThanOrEqual(after - offsetMs); + expect(result.getTime()).toBeGreaterThanOrEqual(before - offsetMs); + expect(result.getTime()).toBeLessThanOrEqual(after - offsetMs); }); - it("returns undefined for 'all'", () => { - expect(getDateForRange("all")).toBeUndefined(); + it("returns the epoch for 'all'", () => { + expect(getDateForRange("all")).toEqual(new Date(0)); + }); +}); + +describe("getDateFormat", () => { + it.each([ + ["recent", "%Y-%m-%dT%H:%M:00Z"], + ["hour", "%Y-%m-%dT%H:%M:00Z"], + ["day", "%Y-%m-%dT%H:00:00Z"], + ["week", "%Y-%m-%dT00:00:00Z"], + ["month", "%Y-%m-%dT00:00:00Z"], + ["all", "%Y-%m-%dT00:00:00Z"], + ])("returns the %s bucket format", (range, format) => { + expect(getDateFormat(range as DateRange)).toBe(format); }); - it("returns undefined for unknown ranges", () => { - expect(getDateForRange("fortnight")).toBeUndefined(); + it("has a format for every member of DateRanges", () => { + for (const range of DateRanges) { + expect(getDateFormat(range)).toMatch(/^%Y-%m-%d/); + } }); });