Openstatus www.openstatus.dev
at main 146 lines 4.1 kB view raw
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}