Openstatus www.openstatus.dev

๐Ÿš€ API lgtm (#1289)

* ๐Ÿš€ lgtm

* ci: apply automated fixes

* fix test

* ci: apply automated fixes

* ๐Ÿš€

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Thibault Le Ouay
autofix-ci[bot]
and committed by
GitHub
d077cdb3 86a17b39

+485 -90
+4
apps/server/src/routes/v1/monitors/index.ts
··· 9 9 import { registerPostMonitorHTTP } from "./post_http"; 10 10 import { registerPostMonitorTCP } from "./post_tcp"; 11 11 import { registerPutMonitor } from "./put"; 12 + import { registerPutHTTPMonitor } from "./put_http"; 13 + import { registerPutTCPMonitor } from "./put_tcp"; 12 14 import { registerGetMonitorResult } from "./results/get"; 13 15 import { registerRunMonitor } from "./run/post"; 14 16 import { registerGetMonitorSummary } from "./summary/get"; ··· 25 27 registerPostMonitor(monitorsApi); 26 28 registerPostMonitorHTTP(monitorsApi); 27 29 registerPostMonitorTCP(monitorsApi); 30 + registerPutHTTPMonitor(monitorsApi); 31 + registerPutTCPMonitor(monitorsApi); 28 32 // 29 33 registerGetMonitorSummary(monitorsApi); 30 34 registerTriggerMonitor(monitorsApi);
-8
apps/server/src/routes/v1/monitors/post_tcp.test.ts
··· 21 21 }, 22 22 active: true, 23 23 public: true, 24 - assertions: [ 25 - { 26 - type: "status", 27 - compare: "eq", 28 - target: 200, 29 - }, 30 - { type: "header", compare: "not_eq", key: "key", target: "value" }, 31 - ], 32 24 }), 33 25 }); 34 26
+8 -5
apps/server/src/routes/v1/monitors/put.test.ts
··· 1 1 import { expect, test } from "bun:test"; 2 2 3 3 import { app } from "@/index"; 4 + import { MonitorSchema } from "./schema"; 4 5 5 - test("Partial update the monitor", async () => { 6 + test("update the monitor", async () => { 6 7 const res = await app.request("/v1/monitor/1", { 7 8 method: "PUT", 8 9 headers: { ··· 13 14 name: "New Name", 14 15 }), 15 16 }); 16 - 17 - expect(res.status).toBe(400); 17 + const data = await res.json(); 18 + const monitor = MonitorSchema.parse(data); 19 + expect(res.status).toBe(200); 20 + expect(monitor.name).toBe("New Name"); 18 21 }); 19 22 20 23 test("invalid monitor id should return 404", async () => { 21 - const res = await app.request("/v1/page/404", { 24 + const res = await app.request("/v1/monitor/404", { 22 25 method: "PUT", 23 26 headers: { 24 27 "x-openstatus-key": "1", ··· 33 36 }); 34 37 35 38 test("no auth key should return 401", async () => { 36 - const res = await app.request("/v1/page/2", { 39 + const res = await app.request("/v1/monitor/2", { 37 40 method: "PUT", 38 41 headers: { 39 42 "content-type": "application/json",
+28 -77
apps/server/src/routes/v1/monitors/put.ts
··· 8 8 import { Events } from "@openstatus/analytics"; 9 9 import { serialize } from "@openstatus/assertions"; 10 10 import type { monitorsApi } from "./index"; 11 - import { 12 - HTTPMonitorSchema, 13 - MonitorSchema, 14 - ParamsSchema, 15 - TCPMonitorSchema, 16 - } from "./schema"; 17 - import { getAssertionNew } from "./utils"; 11 + import { MonitorSchema, ParamsSchema } from "./schema"; 12 + import { getAssertions } from "./utils"; 18 13 19 14 const putRoute = createRoute({ 20 15 method: "put", ··· 28 23 description: "The monitor to update", 29 24 content: { 30 25 "application/json": { 31 - schema: HTTPMonitorSchema.or(TCPMonitorSchema), 26 + schema: MonitorSchema.omit({ id: true }).partial(), 32 27 }, 33 28 }, 34 29 }, ··· 53 48 const { id } = c.req.valid("param"); 54 49 const input = c.req.valid("json"); 55 50 56 - if (input.frequency && !limits.periodicity.includes(input.frequency)) { 51 + if (input.periodicity && !limits.periodicity.includes(input.periodicity)) { 57 52 throw new OpenStatusApiError({ 58 53 code: "PAYMENT_REQUIRED", 59 54 message: "Upgrade for more periodicity", ··· 90 85 }); 91 86 } 92 87 93 - if (_monitor.jobType === "http") { 94 - const data = HTTPMonitorSchema.parse(input); 95 - const { request, regions, assertions, otelHeaders, ...rest } = data; 88 + if (input.jobType && input.jobType !== _monitor.jobType) { 89 + throw new OpenStatusApiError({ 90 + code: "BAD_REQUEST", 91 + message: 92 + "Cannot change jobType. Please delete and create a new monitor instead.", 93 + }); 94 + } 96 95 97 - const headers = data?.request?.headers 98 - ? Object.entries(data?.request.headers) 99 - : undefined; 96 + const { headers, regions, assertions, ...rest } = input; 100 97 101 - const otelHeadersEntries = otelHeaders 102 - ? Object.entries(otelHeaders).map(([key, value]) => ({ 103 - key: key, 104 - value: value, 105 - })) 106 - : undefined; 107 - const headersEntries = headers 108 - ? headers.map(([key, value]) => ({ key: key, value: value })) 109 - : undefined; 98 + const assert = assertions ? getAssertions(assertions) : []; 110 99 111 - const assert = assertions ? getAssertionNew(assertions) : []; 100 + const _newMonitor = await db 101 + .update(monitor) 102 + .set({ 103 + ...rest, 104 + regions: regions ? regions.join(",") : undefined, 105 + headers: input.headers ? JSON.stringify(input.headers) : undefined, 106 + assertions: assert.length > 0 ? serialize(assert) : undefined, 107 + timeout: input.timeout || 45000, 108 + updatedAt: new Date(), 109 + }) 110 + .where(eq(monitor.id, Number(_monitor.id))) 111 + .returning() 112 + .get(); 112 113 113 - const _newMonitor = await db 114 - .update(monitor) 115 - .set({ 116 - ...rest, 117 - regions: regions.join(","), 118 - headers: headersEntries ? JSON.stringify(headersEntries) : undefined, 119 - otelHeaders: otelHeadersEntries 120 - ? JSON.stringify(otelHeadersEntries) 121 - : undefined, 122 - assertions: assert.length > 0 ? serialize(assert) : undefined, 123 - timeout: input.timeout || 45000, 124 - updatedAt: new Date(), 125 - }) 126 - .where(eq(monitor.id, Number(_monitor.id))) 127 - .returning() 128 - .get(); 129 - const r = MonitorSchema.parse(_newMonitor); 130 - return c.json(r, 200); 131 - } 132 - if (_monitor.jobType === "tcp") { 133 - const data = TCPMonitorSchema.parse(input); 134 - const { request, regions, otelHeaders, ...rest } = data; 135 - 136 - const otelHeadersEntries = otelHeaders 137 - ? Object.entries(otelHeaders).map(([key, value]) => ({ 138 - key: key, 139 - value: value, 140 - })) 141 - : undefined; 142 - 143 - const _newMonitor = await db 144 - .update(monitor) 145 - .set({ 146 - ...rest, 147 - regions: regions.join(","), 148 - otelHeaders: otelHeadersEntries 149 - ? JSON.stringify(otelHeadersEntries) 150 - : undefined, 151 - timeout: input.timeout || 45000, 152 - updatedAt: new Date(), 153 - }) 154 - .where(eq(monitor.id, Number(_monitor.id))) 155 - .returning() 156 - .get(); 157 - const r = MonitorSchema.parse(_newMonitor); 158 - return c.json(r, 200); 159 - } 160 - 161 - throw new OpenStatusApiError({ 162 - code: "NOT_FOUND", 163 - message: "Something went wrong", 164 - }); 114 + const data = MonitorSchema.parse(_newMonitor); 115 + return c.json(data, 200); 165 116 }); 166 117 }
+131
apps/server/src/routes/v1/monitors/put_http.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { app } from "@/index"; 4 + import { MonitorSchema } from "./schema"; 5 + 6 + test("update the monitor", async () => { 7 + const res = await app.request("/v1/monitor/http/1", { 8 + method: "PUT", 9 + headers: { 10 + "x-openstatus-key": "1", 11 + "Content-Type": "application/json", 12 + }, 13 + body: JSON.stringify({ 14 + name: "New Name", 15 + }), 16 + }); 17 + 18 + expect(res.status).toBe(400); 19 + }); 20 + 21 + test("invalid monitor id should return 404", async () => { 22 + const res = await app.request("/v1/monitor/http/404", { 23 + method: "PUT", 24 + headers: { 25 + "x-openstatus-key": "1", 26 + "Content-Type": "application/json", 27 + }, 28 + body: JSON.stringify({ 29 + frequency: "10m", 30 + name: "OpenStatus", 31 + description: "OpenStatus website", 32 + regions: ["ams", "gru"], 33 + request: { 34 + url: "https://www.openstatus.dev", 35 + method: "POST", 36 + body: '{"hello":"world"}', 37 + headers: { "content-type": "application/json" }, 38 + }, 39 + active: true, 40 + public: true, 41 + assertions: [ 42 + { 43 + kind: "statusCode", 44 + compare: "eq", 45 + target: 200, 46 + }, 47 + { kind: "header", compare: "not_eq", key: "key", target: "value" }, 48 + ], 49 + }), 50 + }); 51 + 52 + expect(res.status).toBe(404); 53 + }); 54 + 55 + test("Update a valid monitor", async () => { 56 + const res = await app.request("/v1/monitor/http", { 57 + method: "POST", 58 + headers: { 59 + "x-openstatus-key": "1", 60 + "content-type": "application/json", 61 + }, 62 + body: JSON.stringify({ 63 + frequency: "10m", 64 + name: "OpenStatus", 65 + description: "OpenStatus website", 66 + regions: ["ams", "gru"], 67 + request: { 68 + url: "https://www.openstatus.dev", 69 + method: "POST", 70 + body: '{"hello":"world"}', 71 + headers: { "content-type": "application/json" }, 72 + }, 73 + active: true, 74 + public: true, 75 + assertions: [ 76 + { 77 + kind: "statusCode", 78 + compare: "eq", 79 + target: 200, 80 + }, 81 + { kind: "header", compare: "not_eq", key: "key", target: "value" }, 82 + ], 83 + }), 84 + }); 85 + 86 + const result = MonitorSchema.parse(await res.json()); 87 + 88 + expect(res.status).toBe(200); 89 + 90 + const updated = await app.request(`/v1/monitor/http/${result.id}`, { 91 + method: "PUT", 92 + headers: { 93 + "x-openstatus-key": "1", 94 + "content-type": "application/json", 95 + }, 96 + body: JSON.stringify({ 97 + frequency: "30m", 98 + name: "newName", 99 + description: "OpenStatus website", 100 + regions: ["ams", "gru"], 101 + request: { 102 + url: "https://www.openstatus.dev", 103 + method: "POST", 104 + body: '{"hello":"world"}', 105 + headers: { "content-type": "application/json" }, 106 + }, 107 + active: true, 108 + public: true, 109 + }), 110 + 111 + // expect(r.success).toBe(true); 112 + }); 113 + const r = MonitorSchema.parse(await updated.json()); 114 + expect(r.assertions?.length).toBe(0); 115 + expect(r.periodicity).toBe("30m"); 116 + expect(r.name).toBe("newName"); 117 + }); 118 + 119 + test("no auth key should return 401", async () => { 120 + const res = await app.request("/v1/monitor/http/2", { 121 + method: "PUT", 122 + headers: { 123 + "content-type": "application/json", 124 + }, 125 + body: JSON.stringify({ 126 + /* */ 127 + }), 128 + }); 129 + 130 + expect(res.status).toBe(401); 131 + });
+136
apps/server/src/routes/v1/monitors/put_http.ts
··· 1 + import { createRoute } from "@hono/zod-openapi"; 2 + 3 + import { and, db, eq, isNull } from "@openstatus/db"; 4 + import { monitor } from "@openstatus/db/src/schema"; 5 + 6 + import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors"; 7 + import { trackMiddleware } from "@/libs/middlewares"; 8 + import { Events } from "@openstatus/analytics"; 9 + import { serialize } from "@openstatus/assertions"; 10 + import type { monitorsApi } from "./index"; 11 + import { HTTPMonitorSchema, MonitorSchema, ParamsSchema } from "./schema"; 12 + import { getAssertionNew } from "./utils"; 13 + 14 + const putRoute = createRoute({ 15 + method: "put", 16 + tags: ["monitor"], 17 + summary: "Update an HTTP monitor", 18 + path: "/http/{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: HTTPMonitorSchema, 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 + 44 + export function registerPutHTTPMonitor(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.frequency && !limits.periodicity.includes(input.frequency)) { 52 + throw new OpenStatusApiError({ 53 + code: "PAYMENT_REQUIRED", 54 + message: "Upgrade for more periodicity", 55 + }); 56 + } 57 + 58 + if (input.regions) { 59 + for (const region of input.regions) { 60 + if (!limits.regions.includes(region)) { 61 + throw new OpenStatusApiError({ 62 + code: "PAYMENT_REQUIRED", 63 + message: "Upgrade for more regions", 64 + }); 65 + } 66 + } 67 + } 68 + 69 + const _monitor = await db 70 + .select() 71 + .from(monitor) 72 + .where( 73 + and( 74 + eq(monitor.id, Number(id)), 75 + isNull(monitor.deletedAt), 76 + eq(monitor.workspaceId, workspaceId), 77 + ), 78 + ) 79 + .get(); 80 + 81 + if (!_monitor) { 82 + throw new OpenStatusApiError({ 83 + code: "NOT_FOUND", 84 + message: `Monitor ${id} not found`, 85 + }); 86 + } 87 + 88 + if (_monitor.jobType !== "http") { 89 + throw new OpenStatusApiError({ 90 + code: "NOT_FOUND", 91 + message: `Monitor ${id} not found`, 92 + }); 93 + } 94 + 95 + const { request, regions, assertions, otelHeaders, ...rest } = input; 96 + 97 + const headers = input.request.headers 98 + ? Object.entries(input.request.headers) 99 + : undefined; 100 + 101 + const otelHeadersEntries = otelHeaders 102 + ? Object.entries(otelHeaders).map(([key, value]) => ({ 103 + key: key, 104 + value: value, 105 + })) 106 + : undefined; 107 + const headersEntries = headers 108 + ? headers.map(([key, value]) => ({ key: key, value: value })) 109 + : undefined; 110 + const assert = assertions ? getAssertionNew(assertions) : []; 111 + 112 + const _newMonitor = await db 113 + .update(monitor) 114 + .set({ 115 + ...rest, 116 + periodicity: input.frequency, 117 + url: input.request.url, 118 + method: input.request.method, 119 + body: input.request.body, 120 + regions: regions ? regions.join(",") : undefined, 121 + headers: headersEntries ? JSON.stringify(headersEntries) : undefined, 122 + otelHeaders: otelHeadersEntries 123 + ? JSON.stringify(otelHeadersEntries) 124 + : undefined, 125 + assertions: assert ? serialize(assert) : "", 126 + timeout: input.timeout || 45000, 127 + updatedAt: new Date(), 128 + }) 129 + .where(eq(monitor.id, Number(_monitor.id))) 130 + .returning() 131 + .get(); 132 + 133 + const data = MonitorSchema.parse(_newMonitor); 134 + return c.json(data, 200); 135 + }); 136 + }
+56
apps/server/src/routes/v1/monitors/put_tcp.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { app } from "@/index"; 4 + 5 + test("update the monitor", async () => { 6 + const res = await app.request("/v1/monitor/tcp/1", { 7 + method: "PUT", 8 + headers: { 9 + "x-openstatus-key": "1", 10 + "Content-Type": "application/json", 11 + }, 12 + body: JSON.stringify({ 13 + name: "New Name", 14 + }), 15 + }); 16 + 17 + expect(res.status).toBe(400); 18 + }); 19 + 20 + test("invalid monitor id should return 404", async () => { 21 + const res = await app.request("/v1/monitor/tcp/404", { 22 + method: "PUT", 23 + headers: { 24 + "x-openstatus-key": "1", 25 + "Content-Type": "application/json", 26 + }, 27 + body: JSON.stringify({ 28 + frequency: "10m", 29 + name: "OpenStatus", 30 + description: "OpenStatus website", 31 + regions: ["ams", "gru"], 32 + request: { 33 + host: "openstatus.dev", 34 + port: 443, 35 + }, 36 + active: true, 37 + public: true, 38 + }), 39 + }); 40 + 41 + expect(res.status).toBe(404); 42 + }); 43 + 44 + test("no auth key should return 401", async () => { 45 + const res = await app.request("/v1/monitor/tcp/2", { 46 + method: "PUT", 47 + headers: { 48 + "content-type": "application/json", 49 + }, 50 + body: JSON.stringify({ 51 + /* */ 52 + }), 53 + }); 54 + 55 + expect(res.status).toBe(401); 56 + });
+122
apps/server/src/routes/v1/monitors/put_tcp.ts
··· 1 + import { createRoute } from "@hono/zod-openapi"; 2 + 3 + import { and, db, eq, isNull } from "@openstatus/db"; 4 + import { monitor } from "@openstatus/db/src/schema"; 5 + 6 + import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors"; 7 + import { trackMiddleware } from "@/libs/middlewares"; 8 + import { Events } from "@openstatus/analytics"; 9 + import type { monitorsApi } from "./index"; 10 + import { MonitorSchema, ParamsSchema, TCPMonitorSchema } from "./schema"; 11 + 12 + const putRoute = createRoute({ 13 + method: "put", 14 + tags: ["monitor"], 15 + summary: "Update an TCP monitor", 16 + path: "/tcp/{id}", 17 + middleware: [trackMiddleware(Events.UpdateMonitor)], 18 + request: { 19 + params: ParamsSchema, 20 + body: { 21 + description: "The monitor to update", 22 + content: { 23 + "application/json": { 24 + schema: TCPMonitorSchema, 25 + }, 26 + }, 27 + }, 28 + }, 29 + responses: { 30 + 200: { 31 + content: { 32 + "application/json": { 33 + schema: MonitorSchema, 34 + }, 35 + }, 36 + description: "Update a monitor", 37 + }, 38 + ...openApiErrorResponses, 39 + }, 40 + }); 41 + 42 + export function registerPutTCPMonitor(api: typeof monitorsApi) { 43 + return api.openapi(putRoute, async (c) => { 44 + const workspaceId = c.get("workspace").id; 45 + const limits = c.get("workspace").limits; 46 + const { id } = c.req.valid("param"); 47 + const input = c.req.valid("json"); 48 + 49 + if (input.frequency && !limits.periodicity.includes(input.frequency)) { 50 + throw new OpenStatusApiError({ 51 + code: "PAYMENT_REQUIRED", 52 + message: "Upgrade for more periodicity", 53 + }); 54 + } 55 + 56 + if (input.regions) { 57 + for (const region of input.regions) { 58 + if (!limits.regions.includes(region)) { 59 + throw new OpenStatusApiError({ 60 + code: "PAYMENT_REQUIRED", 61 + message: "Upgrade for more regions", 62 + }); 63 + } 64 + } 65 + } 66 + 67 + const _monitor = await db 68 + .select() 69 + .from(monitor) 70 + .where( 71 + and( 72 + eq(monitor.id, Number(id)), 73 + isNull(monitor.deletedAt), 74 + eq(monitor.workspaceId, workspaceId), 75 + ), 76 + ) 77 + .get(); 78 + 79 + if (!_monitor) { 80 + throw new OpenStatusApiError({ 81 + code: "NOT_FOUND", 82 + message: `Monitor ${id} not found`, 83 + }); 84 + } 85 + 86 + if (_monitor.jobType !== "tcp") { 87 + throw new OpenStatusApiError({ 88 + code: "NOT_FOUND", 89 + message: `Monitor ${id} not found`, 90 + }); 91 + } 92 + 93 + const { request, regions, otelHeaders, ...rest } = input; 94 + 95 + const otelHeadersEntries = otelHeaders 96 + ? Object.entries(otelHeaders).map(([key, value]) => ({ 97 + key: key, 98 + value: value, 99 + })) 100 + : undefined; 101 + 102 + const _newMonitor = await db 103 + .update(monitor) 104 + .set({ 105 + ...rest, 106 + periodicity: input.frequency, 107 + url: `${request.host}:${request.port}`, 108 + regions: regions ? regions.join(",") : undefined, 109 + otelHeaders: otelHeadersEntries 110 + ? JSON.stringify(otelHeadersEntries) 111 + : undefined, 112 + timeout: input.timeout || 45000, 113 + updatedAt: new Date(), 114 + }) 115 + .where(eq(monitor.id, Number(_monitor.id))) 116 + .returning() 117 + .get(); 118 + 119 + const data = MonitorSchema.parse(_newMonitor); 120 + return c.json(data, 200); 121 + }); 122 + }