From 34feb6d3fa2425e73dbcdd42c82cdac73db7dad4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 1 May 2026 11:24:19 -0700 Subject: [PATCH] replace inline objects with defined constants --- server/openapi.json | 158 ++++++++++++--------- server/openapi/routes/auth.ts | 36 ++--- server/openapi/routes/monitor.ts | 71 ++++----- server/src/validation/monitorValidation.ts | 42 +++++- server/src/validation/userValidation.ts | 16 +++ 5 files changed, 195 insertions(+), 128 deletions(-) diff --git a/server/openapi.json b/server/openapi.json index ec3a7f925..f62062332 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -81,58 +81,72 @@ "$ref": "#/components/schemas/User" }, "token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + "type": "string" } }, - "required": ["user"] + "required": ["user"], + "example": { + "user": { + "_id": "65f1c2a4d8b9e0123456789a", + "firstName": "Ada", + "lastName": "Lovelace", + "email": "ada@example.com", + "role": ["admin"], + "teamId": "65f1c2a4d8b9e01234567890", + "createdAt": "2026-04-01T10:00:00.000Z", + "updatedAt": "2026-04-15T14:30:00.000Z" + }, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } }, "User": { "type": "object", "properties": { "_id": { - "type": "string", - "example": "65f1c2a4d8b9e0123456789a" + "type": "string" }, "firstName": { - "type": "string", - "example": "Ada" + "type": "string" }, "lastName": { - "type": "string", - "example": "Lovelace" + "type": "string" }, "email": { - "type": "string", - "example": "ada@example.com" + "type": "string" }, "role": { "type": "array", "items": { "type": "string" - }, - "example": ["admin"] + } }, "teamId": { - "type": "string", - "example": "65f1c2a4d8b9e01234567890" + "type": "string" }, "profileImage": { "type": "string", "nullable": true }, "createdAt": { - "type": "string", - "example": "2026-04-01T10:00:00.000Z" + "type": "string" }, "updatedAt": { - "type": "string", - "example": "2026-04-15T14:30:00.000Z" + "type": "string" } }, "required": ["_id", "firstName", "lastName", "email", "role", "createdAt", "updatedAt"], "additionalProperties": { "nullable": true + }, + "example": { + "_id": "65f1c2a4d8b9e0123456789a", + "firstName": "Ada", + "lastName": "Lovelace", + "email": "ada@example.com", + "role": ["admin"], + "teamId": "65f1c2a4d8b9e01234567890", + "createdAt": "2026-04-01T10:00:00.000Z", + "updatedAt": "2026-04-15T14:30:00.000Z" } }, "Incident": { @@ -322,58 +336,45 @@ "type": "object", "properties": { "_id": { - "type": "string", - "example": "65f1c2a4d8b9e0123456789a" + "type": "string" }, "name": { - "type": "string", - "example": "Marketing site" + "type": "string" }, "description": { - "type": "string", - "example": "Production marketing site monitored from the EU region" + "type": "string" }, "type": { "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"], - "example": "http" + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] }, "url": { - "type": "string", - "example": "https://www.example.com" + "type": "string" }, "port": { - "type": "number", - "example": 443 + "type": "number" }, "isActive": { - "type": "boolean", - "example": true + "type": "boolean" }, "interval": { - "type": "number", - "example": 60000 + "type": "number" }, "status": { "type": "string", - "enum": ["up", "down", "paused", "initializing", "maintenance", "breached"], - "example": "up" + "enum": ["up", "down", "paused", "initializing", "maintenance", "breached"] }, "statusWindowSize": { - "type": "number", - "example": 5 + "type": "number" }, "statusWindowThreshold": { - "type": "number", - "example": 3 + "type": "number" }, "ignoreTlsErrors": { - "type": "boolean", - "example": false + "type": "boolean" }, "useAdvancedMatching": { - "type": "boolean", - "example": false + "type": "boolean" }, "jsonPath": { "type": "string" @@ -389,34 +390,28 @@ "type": "array", "items": { "type": "string" - }, - "example": ["65f1c2a4d8b9e0123456789b"] + } }, "secret": { "type": "string" }, "cpuAlertThreshold": { - "type": "number", - "example": 90 + "type": "number" }, "memoryAlertThreshold": { - "type": "number", - "example": 90 + "type": "number" }, "diskAlertThreshold": { - "type": "number", - "example": 90 + "type": "number" }, "tempAlertThreshold": { - "type": "number", - "example": 80 + "type": "number" }, "selectedDisks": { "type": "array", "items": { "type": "string" - }, - "example": [] + } }, "gameId": { "type": "string" @@ -429,36 +424,29 @@ "nullable": true }, "geoCheckEnabled": { - "type": "boolean", - "example": false + "type": "boolean" }, "geoCheckLocations": { "type": "array", "items": { "type": "string", "enum": ["EU", "NA", "AS", "SA", "AF", "OC"] - }, - "example": [] + } }, "geoCheckInterval": { - "type": "number", - "example": 300000 + "type": "number" }, "teamId": { - "type": "string", - "example": "65f1c2a4d8b9e01234567890" + "type": "string" }, "userId": { - "type": "string", - "example": "65f1c2a4d8b9e01234567891" + "type": "string" }, "createdAt": { - "type": "string", - "example": "2026-04-01T10:00:00.000Z" + "type": "string" }, "updatedAt": { - "type": "string", - "example": "2026-04-15T14:30:00.000Z" + "type": "string" } }, "required": [ @@ -489,6 +477,34 @@ ], "additionalProperties": { "nullable": true + }, + "example": { + "_id": "65f1c2a4d8b9e0123456789a", + "name": "Marketing site", + "description": "Production marketing site monitored from the EU region", + "type": "http", + "url": "https://www.example.com", + "port": 443, + "isActive": true, + "interval": 60000, + "status": "up", + "statusWindowSize": 5, + "statusWindowThreshold": 3, + "ignoreTlsErrors": false, + "useAdvancedMatching": false, + "notifications": ["65f1c2a4d8b9e0123456789b"], + "cpuAlertThreshold": 90, + "memoryAlertThreshold": 90, + "diskAlertThreshold": 90, + "tempAlertThreshold": 80, + "selectedDisks": [], + "geoCheckEnabled": false, + "geoCheckLocations": [], + "geoCheckInterval": 300000, + "teamId": "65f1c2a4d8b9e01234567890", + "userId": "65f1c2a4d8b9e01234567891", + "createdAt": "2026-04-01T10:00:00.000Z", + "updatedAt": "2026-04-15T14:30:00.000Z" } }, "NotificationChannelBody": { diff --git a/server/openapi/routes/auth.ts b/server/openapi/routes/auth.ts index 91254edf3..66b74382b 100644 --- a/server/openapi/routes/auth.ts +++ b/server/openapi/routes/auth.ts @@ -15,31 +15,35 @@ import { editUserPasswordByIdBodyValidation, createUserBodyValidation, editUserBodyValidation, + userResponseSchema, } from "@/validation/userValidation.js"; const tags = ["auth"]; -const userObject = z - .object({ - _id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }), - firstName: z.string().openapi({ example: "Ada" }), - lastName: z.string().openapi({ example: "Lovelace" }), - email: z.string().openapi({ example: "ada@example.com" }), - role: z.array(z.string()).openapi({ example: ["admin"] }), - teamId: z.string().optional().openapi({ example: "65f1c2a4d8b9e01234567890" }), - profileImage: z.string().nullable().optional(), - createdAt: z.string().openapi({ example: "2026-04-01T10:00:00.000Z" }), - updatedAt: z.string().openapi({ example: "2026-04-15T14:30:00.000Z" }), - }) - .passthrough() - .openapi("User"); +const userExample = { + _id: "65f1c2a4d8b9e0123456789a", + firstName: "Ada", + lastName: "Lovelace", + email: "ada@example.com", + role: ["admin"], + teamId: "65f1c2a4d8b9e01234567890", + createdAt: "2026-04-01T10:00:00.000Z", + updatedAt: "2026-04-15T14:30:00.000Z", +}; + +const userObject = userResponseSchema.openapi("User", { example: userExample }); const authPayload = z .object({ user: userObject, - token: z.string().optional().openapi({ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }), + token: z.string().optional(), }) - .openapi("AuthPayload"); + .openapi("AuthPayload", { + example: { + user: userExample, + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + }, + }); registry.registerPath({ method: "post", diff --git a/server/openapi/routes/monitor.ts b/server/openapi/routes/monitor.ts index 0e6187318..10f988fa8 100644 --- a/server/openapi/routes/monitor.ts +++ b/server/openapi/routes/monitor.ts @@ -1,8 +1,6 @@ import { z } from "zod"; import { registry } from "../registry.js"; import { bearer, json, okJson, okJsonNoData, okUnknown, standardErrors } from "../helpers.js"; -import { GeoContinents } from "@/types/geoCheck.js"; -import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js"; import { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, @@ -17,49 +15,42 @@ import { importMonitorsBodyValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, + monitorResponseSchema, } from "@/validation/monitorValidation.js"; import { updateNotificationsValidation } from "@/validation/notificationValidation.js"; const tags = ["monitors"]; -const monitorObject = z - .object({ - _id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }), - name: z.string().openapi({ example: "Marketing site" }), - description: z.string().optional().openapi({ example: "Production marketing site monitored from the EU region" }), - type: z.enum(MonitorTypes).openapi({ example: "http" }), - url: z.string().openapi({ example: "https://www.example.com" }), - port: z.number().optional().openapi({ example: 443 }), - isActive: z.boolean().openapi({ example: true }), - interval: z.number().openapi({ example: 60000 }), - status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).openapi({ example: "up" }), - statusWindowSize: z.number().openapi({ example: 5 }), - statusWindowThreshold: z.number().openapi({ example: 3 }), - ignoreTlsErrors: z.boolean().openapi({ example: false }), - useAdvancedMatching: z.boolean().openapi({ example: false }), - jsonPath: z.string().optional(), - expectedValue: z.string().optional(), - matchMethod: z.enum(MonitorMatchMethods).optional(), - notifications: z.array(z.string()).openapi({ example: ["65f1c2a4d8b9e0123456789b"] }), - secret: z.string().optional(), - cpuAlertThreshold: z.number().openapi({ example: 90 }), - memoryAlertThreshold: z.number().openapi({ example: 90 }), - diskAlertThreshold: z.number().openapi({ example: 90 }), - tempAlertThreshold: z.number().openapi({ example: 80 }), - selectedDisks: z.array(z.string()).openapi({ example: [] }), - gameId: z.string().optional(), - grpcServiceName: z.string().optional(), - group: z.string().nullable().optional(), - geoCheckEnabled: z.boolean().openapi({ example: false }), - geoCheckLocations: z.array(z.enum(GeoContinents)).openapi({ example: [] }), - geoCheckInterval: z.number().openapi({ example: 300000 }), - teamId: z.string().openapi({ example: "65f1c2a4d8b9e01234567890" }), - userId: z.string().openapi({ example: "65f1c2a4d8b9e01234567891" }), - createdAt: z.string().openapi({ example: "2026-04-01T10:00:00.000Z" }), - updatedAt: z.string().openapi({ example: "2026-04-15T14:30:00.000Z" }), - }) - .passthrough() - .openapi("Monitor"); +const monitorObject = monitorResponseSchema.openapi("Monitor", { + example: { + _id: "65f1c2a4d8b9e0123456789a", + name: "Marketing site", + description: "Production marketing site monitored from the EU region", + type: "http", + url: "https://www.example.com", + port: 443, + isActive: true, + interval: 60000, + status: "up", + statusWindowSize: 5, + statusWindowThreshold: 3, + ignoreTlsErrors: false, + useAdvancedMatching: false, + notifications: ["65f1c2a4d8b9e0123456789b"], + cpuAlertThreshold: 90, + memoryAlertThreshold: 90, + diskAlertThreshold: 90, + tempAlertThreshold: 80, + selectedDisks: [], + geoCheckEnabled: false, + geoCheckLocations: [], + geoCheckInterval: 300000, + teamId: "65f1c2a4d8b9e01234567890", + userId: "65f1c2a4d8b9e01234567891", + createdAt: "2026-04-01T10:00:00.000Z", + updatedAt: "2026-04-15T14:30:00.000Z", + }, +}); registry.registerPath({ method: "get", diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index df000ecef..57099e7ff 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; import { GeoContinents } from "@/types/geoCheck.js"; -import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js"; +import { MonitorMatchMethods, MonitorStatuses, MonitorTypes } from "@/types/monitor.js"; export const getMonitorByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -177,3 +177,43 @@ export const getHardwareDetailsByIdParamValidation = z.object({ export const getHardwareDetailsByIdQueryValidation = z.object({ dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), }); + +// Canonical monitor shape returned by /monitors endpoints. Keep aligned with +// what the controllers actually serialize. +export const monitorResponseSchema = z + .object({ + _id: z.string(), + name: z.string(), + description: z.string().optional(), + type: z.enum(MonitorTypes), + url: z.string(), + port: z.number().optional(), + isActive: z.boolean(), + interval: z.number(), + status: z.enum(MonitorStatuses), + statusWindowSize: z.number(), + statusWindowThreshold: z.number(), + ignoreTlsErrors: z.boolean(), + useAdvancedMatching: z.boolean(), + jsonPath: z.string().optional(), + expectedValue: z.string().optional(), + matchMethod: z.enum(MonitorMatchMethods).optional(), + notifications: z.array(z.string()), + secret: z.string().optional(), + cpuAlertThreshold: z.number(), + memoryAlertThreshold: z.number(), + diskAlertThreshold: z.number(), + tempAlertThreshold: z.number(), + selectedDisks: z.array(z.string()), + gameId: z.string().optional(), + grpcServiceName: z.string().optional(), + group: z.string().nullable().optional(), + geoCheckEnabled: z.boolean(), + geoCheckLocations: z.array(z.enum(GeoContinents)), + geoCheckInterval: z.number(), + teamId: z.string(), + userId: z.string(), + createdAt: z.string(), + updatedAt: z.string(), + }) + .passthrough(); diff --git a/server/src/validation/userValidation.ts b/server/src/validation/userValidation.ts index 9e11c8c57..e613b86e6 100644 --- a/server/src/validation/userValidation.ts +++ b/server/src/validation/userValidation.ts @@ -50,3 +50,19 @@ export const editUserBodyValidation = z.object({ email: lowercaseEmailValidation.optional(), profilePicture: z.string().optional(), }); + +// Canonical user shape returned by auth/user endpoints. Keep aligned with what +// the controllers actually serialize (password is intentionally omitted). +export const userResponseSchema = z + .object({ + _id: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string(), + role: z.array(z.string()), + teamId: z.string().optional(), + profileImage: z.string().nullable().optional(), + createdAt: z.string(), + updatedAt: z.string(), + }) + .passthrough();