Openstatus
www.openstatus.dev
1import { createRoute, z } from "@hono/zod-openapi";
2
3import { and, db, eq, isNull } from "@openstatus/db";
4import { monitor } from "@openstatus/db/src/schema";
5
6import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors";
7import { trackMiddleware } from "@/libs/middlewares";
8import { Events } from "@openstatus/analytics";
9import { serialize } from "@openstatus/assertions";
10import type { monitorsApi } from "./index";
11import { MonitorSchema, ParamsSchema } from "./schema";
12import { getAssertions } from "./utils";
13
14const putRoute = createRoute({
15 method: "put",
16 tags: ["monitor"],
17 summary: "Update a monitor",
18 path: "/{id}",
19 middleware: [trackMiddleware(Events.UpdateMonitor)],
20 request: {
21 params: ParamsSchema,
22 body: {
23 description: "The monitor to update",
24 content: {
25 "application/json": {
26 schema: MonitorSchema.omit({ id: true }).partial(),
27 },
28 },
29 },
30 },
31 responses: {
32 200: {
33 content: {
34 "application/json": {
35 schema: MonitorSchema,
36 },
37 },
38 description: "Update a monitor",
39 },
40 ...openApiErrorResponses,
41 },
42});
43
44export function registerPutMonitor(api: typeof monitorsApi) {
45 return api.openapi(putRoute, async (c) => {
46 const workspaceId = c.get("workspace").id;
47 const limits = c.get("workspace").limits;
48 const { id } = c.req.valid("param");
49 const input = c.req.valid("json");
50
51 if (input.periodicity && !limits.periodicity.includes(input.periodicity)) {
52 throw new OpenStatusApiError({
53 code: "PAYMENT_REQUIRED",
54 message: "Upgrade for more periodicity",
55 });
56 }
57
58 if (input.regions) {
59 if (limits["max-regions"] < input.regions.length) {
60 throw new OpenStatusApiError({
61 code: "PAYMENT_REQUIRED",
62 message: "Upgrade for more regions",
63 });
64 }
65
66 for (const region of input.regions) {
67 if (!limits.regions.includes(region)) {
68 throw new OpenStatusApiError({
69 code: "PAYMENT_REQUIRED",
70 message: "Upgrade for more regions",
71 });
72 }
73 }
74 }
75
76 const _monitor = await db
77 .select()
78 .from(monitor)
79 .where(
80 and(
81 eq(monitor.id, Number(id)),
82 isNull(monitor.deletedAt),
83 eq(monitor.workspaceId, workspaceId),
84 ),
85 )
86 .get();
87
88 if (!_monitor) {
89 throw new OpenStatusApiError({
90 code: "NOT_FOUND",
91 message: `Monitor ${id} not found`,
92 });
93 }
94
95 if (input.jobType && input.jobType !== _monitor.jobType) {
96 throw new OpenStatusApiError({
97 code: "BAD_REQUEST",
98 message:
99 "Cannot change jobType. Please delete and create a new monitor instead.",
100 });
101 }
102
103 const { headers, regions, assertions, ...rest } = input;
104
105 const assert = assertions ? getAssertions(assertions) : [];
106
107 const _newMonitor = await db
108 .update(monitor)
109 .set({
110 ...rest,
111 regions: regions ? regions.join(",") : undefined,
112 description: input.description ?? undefined,
113 headers: input.headers ? JSON.stringify(input.headers) : undefined,
114 assertions: assert.length > 0 ? serialize(assert) : undefined,
115 timeout: input.timeout || 45000,
116 updatedAt: new Date(),
117 })
118 .where(eq(monitor.id, Number(_monitor.id)))
119 .returning()
120 .get();
121
122 const otelHeader = _newMonitor.otelHeaders
123 ? z
124 .array(
125 z.object({
126 key: z.string(),
127 value: z.string(),
128 }),
129 )
130 .parse(JSON.parse(_newMonitor.otelHeaders))
131 // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
132 .reduce((a, v) => ({ ...a, [v.key]: v.value }), {})
133 : undefined;
134
135 const data = MonitorSchema.parse({
136 ..._newMonitor,
137 openTelemetry: _newMonitor.otelEndpoint
138 ? {
139 headers: otelHeader,
140 endpoint: _newMonitor.otelEndpoint ?? undefined,
141 }
142 : undefined,
143 });
144 return c.json(data, 200);
145 });
146}