The hand-maintained openapi.json has drifted significantly from the
actual API surface (PUT vs PATCH mismatches, missing query params,
stale endpoints, missing routes). This is a spike of the long-term
fix: derive the spec from the Zod validation schemas we already have.
Scope: monitors resource only. Other resources still come from the
static openapi.json — the new merger splices generated paths in and
drops the static ones the registry now owns. Migration can proceed
one resource per PR.
What this PR does:
- Adds @asteasolutions/zod-to-openapi (peerDep zod 4 satisfied).
- New src/openapi/ module:
- registry.ts — shared OpenAPIRegistry, bearerAuth scheme.
- schemas/common.ts — success/error envelope helpers.
- schemas/monitor.ts — Monitor / MonitorWithChecksList / etc.
- routes/monitor.ts — registers all 19 monitor routes against
the existing Zod validation schemas (no validation files modified).
- index.ts — getOpenApiSpec() merges generated monitor paths into
the static openapi.json and drops 2 stale entries.
- src/index.ts now calls getOpenApiSpec() instead of reading
openapi.json directly. The /api-docs Swagger UI is unchanged.
Drift fixes that fall out automatically once monitors are derived:
- PUT /monitors/{monitorId} -> PATCH (matches actual route)
- POST /monitors response 201 -> 200 (matches actual handler)
- GET /monitors/team/with-checks: 1 documented param -> 8 actual
(this is what #3354 fixed manually; now structural)
- /monitors/bulk and /monitors/test-email removed (no such routes)
- PATCH /monitors/notifications, POST /monitors/import/json, and
GET /monitors/{id}/geo-checks are now documented (were missing)
Verified by booting the merged spec in isolation: 16 monitor paths
emitted, all stale entries dropped, Monitor schema and bearerAuth
scheme registered.
Follow-ups (separate PRs):
- Migrate auth, statusPage, maintenanceWindow, notification, etc.
- Once all resources are migrated, delete the hand-edited
openapi.json and ship the generator output as the canonical doc.
- Optionally promote response shapes from passthrough() Monitor
to fully typed response Zod schemas.
302 lines
7.7 KiB
TypeScript
302 lines
7.7 KiB
TypeScript
import "../registry.js";
|
|
import { z } from "zod";
|
|
import { registry } from "../registry.js";
|
|
import { successEnvelope, successEnvelopeNoData, errorEnvelope } from "../schemas/common.js";
|
|
import {
|
|
MonitorSchema,
|
|
MonitorWithChecksListSchema,
|
|
CertificateInfoSchema,
|
|
NotificationsUpdateResultSchema,
|
|
ImportResultSchema,
|
|
} from "../schemas/monitor.js";
|
|
import {
|
|
getMonitorByIdParamValidation,
|
|
getMonitorByIdQueryValidation,
|
|
getMonitorsByTeamIdQueryValidation,
|
|
getMonitorsWithChecksQueryValidation,
|
|
getCertificateParamValidation,
|
|
createMonitorBodyValidation,
|
|
editMonitorBodyValidation,
|
|
pauseMonitorParamValidation,
|
|
getUptimeDetailsByIdParamValidation,
|
|
getUptimeDetailsByIdQueryValidation,
|
|
importMonitorsBodyValidation,
|
|
getHardwareDetailsByIdParamValidation,
|
|
getHardwareDetailsByIdQueryValidation,
|
|
} from "@/validation/monitorValidation.js";
|
|
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
|
|
|
|
const bearer = [{ bearerAuth: [] }];
|
|
const tags = ["monitors"];
|
|
|
|
const json = <T extends z.ZodTypeAny>(schema: T) => ({
|
|
"application/json": { schema },
|
|
});
|
|
|
|
const standardErrors = {
|
|
"401": { description: "Unauthorized", content: json(errorEnvelope) },
|
|
"403": { description: "Forbidden", content: json(errorEnvelope) },
|
|
"500": { description: "Internal server error", content: json(errorEnvelope) },
|
|
};
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/team",
|
|
tags,
|
|
summary: "List monitors for the caller's team",
|
|
security: bearer,
|
|
request: { query: getMonitorsByTeamIdQueryValidation },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(z.array(MonitorSchema))) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/team/with-checks",
|
|
tags,
|
|
summary: "List team monitors with their most recent checks (paginated)",
|
|
security: bearer,
|
|
request: { query: getMonitorsWithChecksQueryValidation },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(MonitorWithChecksListSchema)) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/team/groups",
|
|
tags,
|
|
summary: "List monitor groups for the caller's team",
|
|
security: bearer,
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(z.array(z.string()))) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/uptime/details/{monitorId}",
|
|
tags,
|
|
summary: "Get uptime details for a monitor",
|
|
security: bearer,
|
|
request: {
|
|
params: getUptimeDetailsByIdParamValidation,
|
|
query: getUptimeDetailsByIdQueryValidation,
|
|
},
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(z.unknown())) },
|
|
"400": { description: "Validation error", content: json(errorEnvelope) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/hardware/details/{monitorId}",
|
|
tags,
|
|
summary: "Get hardware metrics detail for a monitor",
|
|
security: bearer,
|
|
request: {
|
|
params: getHardwareDetailsByIdParamValidation,
|
|
query: getHardwareDetailsByIdQueryValidation,
|
|
},
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(z.unknown())) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/pagespeed/details/{monitorId}",
|
|
tags,
|
|
summary: "Get PageSpeed detail for a monitor",
|
|
security: bearer,
|
|
request: {
|
|
params: getHardwareDetailsByIdParamValidation,
|
|
query: getHardwareDetailsByIdQueryValidation,
|
|
},
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelopeNoData) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/{monitorId}/geo-checks",
|
|
tags,
|
|
summary: "Get geo check results for a monitor",
|
|
security: bearer,
|
|
request: {
|
|
params: getMonitorByIdParamValidation,
|
|
query: getMonitorByIdQueryValidation,
|
|
},
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelopeNoData) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/monitors/pause/{monitorId}",
|
|
tags,
|
|
summary: "Toggle pause state for a monitor",
|
|
security: bearer,
|
|
request: { params: pauseMonitorParamValidation },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/certificate/{monitorId}",
|
|
tags,
|
|
summary: "Get SSL certificate info for a monitor",
|
|
security: bearer,
|
|
request: { params: getCertificateParamValidation },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(CertificateInfoSchema)) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/monitors/notifications",
|
|
tags,
|
|
summary: "Bulk update notifications across monitors",
|
|
security: bearer,
|
|
request: { body: { content: json(updateNotificationsValidation) } },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(NotificationsUpdateResultSchema)) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/monitors",
|
|
tags,
|
|
summary: "Create a new monitor",
|
|
security: bearer,
|
|
request: { body: { content: json(createMonitorBodyValidation) } },
|
|
responses: {
|
|
"200": { description: "Monitor created", content: json(successEnvelope(MonitorSchema)) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/monitors",
|
|
tags,
|
|
summary: "Delete every monitor (superadmin only)",
|
|
security: bearer,
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelopeNoData) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/monitors/demo",
|
|
tags,
|
|
summary: "Insert preconfigured demo monitors",
|
|
security: bearer,
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(z.number())) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/export/json",
|
|
tags,
|
|
summary: "Export all team monitors as JSON",
|
|
security: bearer,
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(z.unknown())) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/monitors/import/json",
|
|
tags,
|
|
summary: "Import monitors from JSON",
|
|
security: bearer,
|
|
request: { body: { content: json(importMonitorsBodyValidation) } },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(ImportResultSchema)) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/games",
|
|
tags,
|
|
summary: "List supported game-server types",
|
|
security: bearer,
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(z.array(z.string()))) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/monitors/{monitorId}",
|
|
tags,
|
|
summary: "Get a monitor by id",
|
|
security: bearer,
|
|
request: { params: getMonitorByIdParamValidation },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
|
|
"404": { description: "Monitor not found", content: json(errorEnvelope) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/monitors/{monitorId}",
|
|
tags,
|
|
summary: "Edit a monitor",
|
|
security: bearer,
|
|
request: {
|
|
params: getMonitorByIdParamValidation,
|
|
body: { content: json(editMonitorBodyValidation) },
|
|
},
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
|
|
"404": { description: "Monitor not found", content: json(errorEnvelope) },
|
|
...standardErrors,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/monitors/{monitorId}",
|
|
tags,
|
|
summary: "Delete a monitor",
|
|
security: bearer,
|
|
request: { params: getMonitorByIdParamValidation },
|
|
responses: {
|
|
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
|
|
"404": { description: "Monitor not found", content: json(errorEnvelope) },
|
|
...standardErrors,
|
|
},
|
|
});
|