Openstatus www.openstatus.dev

Improve api (#427)

* ๐Ÿ‘ทโ€โ™€๏ธ

* ๐Ÿ‘ทโ€โ™€๏ธ

* ๐Ÿ‘ทโ€โ™€๏ธ

* ๐Ÿ‘ทโ€โ™€๏ธ

* ๐Ÿš€

* ๐Ÿš€

authored by

Thibault Le Ouay and committed by
GitHub
cf439ed9 7b35222d

+279 -49
+10 -8
apps/server/package.json
··· 7 7 "scripts": { 8 8 "dev": "bun run --hot src/index.ts", 9 9 "start": "NODE_ENV=production bun run src/index.ts", 10 - "test": "vitest run" 10 + "test": "vitest run", 11 + "lint": "eslint \"**/*.ts*\"" 11 12 }, 12 13 "dependencies": { 13 - "@hono/sentry": "^1.0.0", 14 - "@hono/zod-openapi": "0.7.1", 14 + "@hono/sentry": "1.0.0", 15 + "@hono/zod-openapi": "0.8.3", 15 16 "@openstatus/db": "workspace:*", 16 17 "@openstatus/notification-discord": "workspace:*", 17 18 "@openstatus/notification-emails": "workspace:*", ··· 19 20 "@openstatus/plans": "workspace:*", 20 21 "@openstatus/tinybird": "workspace:*", 21 22 "@openstatus/upstash": "workspace:*", 22 - "@t3-oss/env-core": "0.7.0", 23 - "@unkey/api": "0.10.0", 24 - "hono": "3.7.6", 25 - "nanoid": "5.0.1", 23 + "@t3-oss/env-core": "0.7.1", 24 + "@unkey/api": "0.11.0", 25 + "hono": "3.9.1", 26 + "nanoid": "5.0.2", 26 27 "zod": "3.22.2" 27 28 }, 28 29 "devDependencies": { 29 30 "@openstatus/tsconfig": "workspace:*", 30 31 "dotenv": "16.3.1", 31 - "vitest": "0.34.6" 32 + "vitest": "0.34.6", 33 + "eslint": "8.50.0" 32 34 } 33 35 }
+2
apps/server/src/checker/checker.test.ts
··· 1 + /* eslint-disable @typescript-eslint/no-unused-vars */ 2 + /* eslint-disable @typescript-eslint/ban-ts-comment */ 1 3 import { expect, it, vi } from "vitest"; 2 4 3 5 // REMINDER: keep it here for the mock
+38 -17
apps/server/src/v1/incident.ts
··· 50 50 51 51 const incidentExtendedSchema = incidentSchema.extend({ 52 52 id: z.number().openapi({ description: "The id of the incident" }), 53 + incident_updates: z 54 + .array(z.number()) 55 + .openapi({ 56 + description: "The ids of the incident updates", 57 + }) 58 + .default([]), 53 59 }); 54 60 const getAllRoute = createRoute({ 55 61 method: "get", ··· 75 81 }, 76 82 }, 77 83 }); 84 + 78 85 incidentApi.openapi(getAllRoute, async (c) => { 79 86 const workspaceId = Number(c.get("workspaceId")); 80 - 81 - const _incidents = await db 82 - .select() 83 - .from(incident) 84 - .where(eq(incident.workspaceId, workspaceId)) 85 - .all(); 87 + const _incidents = await db.query.incident.findMany({ 88 + with: { 89 + incidentUpdates: true, 90 + }, 91 + where: eq(incident.workspaceId, workspaceId), 92 + }); 86 93 87 94 if (!_incidents) return c.jsonT({ code: 404, message: "Not Found" }); 88 95 89 - const data = z.array(incidentExtendedSchema).parse(_incidents); 96 + const data = z.array(incidentExtendedSchema).parse( 97 + _incidents.map((incident) => ({ 98 + ...incident, 99 + incident_updates: incident.incidentUpdates.map((incidentUpdate) => { 100 + return incidentUpdate.id; 101 + }), 102 + })), 103 + ); 90 104 91 105 return c.jsonT(data); 92 106 }); ··· 117 131 }, 118 132 }, 119 133 }); 134 + 120 135 incidentApi.openapi(getRoute, async (c) => { 121 136 const workspaceId = Number(c.get("workspaceId")); 122 137 const { id } = c.req.valid("param"); 123 138 124 139 const incidentId = Number(id); 125 - const _incident = await db 126 - .select() 127 - .from(incident) 128 - .where(eq(incident.id, incidentId)) 129 - .get(); 140 + const _incident = await db.query.incident.findFirst({ 141 + with: { 142 + incidentUpdates: true, 143 + }, 144 + where: and( 145 + eq(incident.workspaceId, workspaceId), 146 + eq(incident.id, incidentId), 147 + ), 148 + }); 130 149 131 150 if (!_incident) return c.jsonT({ code: 404, message: "Not Found" }); 132 - 133 - if (workspaceId !== _incident.workspaceId) 134 - return c.jsonT({ code: 401, message: "Unauthorized" }); 135 - 136 - const data = incidentExtendedSchema.parse(_incident); 151 + console.log(_incident); 152 + const data = incidentExtendedSchema.parse({ 153 + ..._incident, 154 + incident_updates: _incident.incidentUpdates.map( 155 + (incidentUpdate) => incidentUpdate.id, 156 + ), 157 + }); 137 158 138 159 return c.jsonT(data); 139 160 });
+102
apps/server/src/v1/incidentUpdate.ts
··· 1 + import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 + 3 + import { and, db, eq } from "@openstatus/db"; 4 + import { 5 + incident, 6 + incidentStatus, 7 + incidentUpdate, 8 + } from "@openstatus/db/src/schema"; 9 + 10 + import type { Variables } from "."; 11 + import { ErrorSchema } from "./shared"; 12 + 13 + const incidenUpdateApi = new OpenAPIHono<{ Variables: Variables }>(); 14 + 15 + const ParamsSelectSchema = z.object({ 16 + id: z 17 + .string() 18 + .min(1) 19 + .openapi({ 20 + param: { 21 + name: "incidentId", 22 + in: "path", 23 + }, 24 + description: "The id of the update", 25 + example: "1", 26 + }), 27 + }); 28 + 29 + const incidentUpdateSchema = z.object({ 30 + status: z.enum(incidentStatus).openapi({ 31 + description: "The status of the update", 32 + }), 33 + id: z.coerce.string().openapi({ description: "The id of the update" }), 34 + date: z 35 + .preprocess((val) => String(val), z.string()) 36 + .openapi({ 37 + description: "The date of the update in ISO 8601 format", 38 + }), 39 + message: z.string().openapi({ 40 + description: "The message of the update", 41 + }), 42 + }); 43 + 44 + // const incidentUpdateApi = new OpenAPIHono<{ Variables: Variables }>(); 45 + 46 + const getUpdateRoute = createRoute({ 47 + method: "get", 48 + tags: ["incident_update"], 49 + path: "/:id", 50 + request: { 51 + params: ParamsSelectSchema, 52 + }, 53 + responses: { 54 + 200: { 55 + content: { 56 + "application/json": { 57 + schema: z.object({}), 58 + }, 59 + }, 60 + description: "Get all incidents", 61 + }, 62 + 400: { 63 + content: { 64 + "application/json": { 65 + schema: ErrorSchema, 66 + }, 67 + }, 68 + description: "Returns an error", 69 + }, 70 + }, 71 + }); 72 + 73 + incidenUpdateApi.openapi(getUpdateRoute, async (c) => { 74 + const workspaceId = Number(c.get("workspaceId")); 75 + const { id } = c.req.valid("param"); 76 + 77 + const update = await db 78 + .select() 79 + .from(incidentUpdate) 80 + .where(eq(incidentUpdate.id, Number(id))) 81 + .get(); 82 + 83 + if (!update) return c.jsonT({ code: 404, message: "Not Found" }); 84 + 85 + const currentIncident = await db 86 + .select() 87 + .from(incident) 88 + .where( 89 + and( 90 + eq(incident.id, update.incidentId), 91 + eq(incident.workspaceId, workspaceId), 92 + ), 93 + ) 94 + .get(); 95 + if (!currentIncident) 96 + return c.jsonT({ code: 401, message: "Not Authorized" }); 97 + 98 + const data = incidentUpdateSchema.parse(update); 99 + return c.jsonT(data); 100 + }); 101 + 102 + export { incidenUpdateApi as incidenUpdatetApi };
+3
apps/server/src/v1/index.ts
··· 3 3 import type { Plan } from "@openstatus/plans"; 4 4 5 5 import { incidentApi } from "./incident"; 6 + import { incidenUpdatetApi } from "./incidentUpdate"; 6 7 import { middleware } from "./middleware"; 7 8 import { monitorApi } from "./monitor"; 8 9 ··· 27 28 api.use("/*", middleware); 28 29 29 30 api.route("/monitor", monitorApi); 31 + api.route("/incident_update", incidenUpdatetApi); 32 + 30 33 api.route("/incident", incidentApi);
-1
apps/server/src/v1/middleware.ts
··· 10 10 ) { 11 11 const key = c.req.header("x-openstatus-key"); 12 12 if (!key) return c.text("Unauthorized", 401); 13 - 14 13 if (process.env.NODE_ENV === "production") { 15 14 const { error, result } = await verifyKey(key); 16 15
+49 -23
pnpm-lock.yaml
··· 79 79 apps/server: 80 80 dependencies: 81 81 '@hono/sentry': 82 - specifier: ^1.0.0 83 - version: 1.0.0(hono@3.7.6) 82 + specifier: 1.0.0 83 + version: 1.0.0(hono@3.9.1) 84 84 '@hono/zod-openapi': 85 - specifier: 0.7.1 86 - version: 0.7.1(hono@3.7.6)(zod@3.22.2) 85 + specifier: 0.8.3 86 + version: 0.8.3(hono@3.9.1)(zod@3.22.2) 87 87 '@openstatus/db': 88 88 specifier: workspace:* 89 89 version: link:../../packages/db ··· 106 106 specifier: workspace:* 107 107 version: link:../../packages/upstash 108 108 '@t3-oss/env-core': 109 - specifier: 0.7.0 110 - version: 0.7.0(typescript@5.2.2)(zod@3.22.2) 109 + specifier: 0.7.1 110 + version: 0.7.1(typescript@5.2.2)(zod@3.22.2) 111 111 '@unkey/api': 112 - specifier: 0.10.0 113 - version: 0.10.0 112 + specifier: 0.11.0 113 + version: 0.11.0 114 114 hono: 115 - specifier: 3.7.6 116 - version: 3.7.6 115 + specifier: 3.9.1 116 + version: 3.9.1 117 117 nanoid: 118 - specifier: 5.0.1 119 - version: 5.0.1 118 + specifier: 5.0.2 119 + version: 5.0.2 120 120 zod: 121 121 specifier: 3.22.2 122 122 version: 3.22.2 ··· 127 127 dotenv: 128 128 specifier: 16.3.1 129 129 version: 16.3.1 130 + eslint: 131 + specifier: 8.50.0 132 + version: 8.50.0 130 133 vitest: 131 134 specifier: 0.34.6 132 135 version: 0.34.6 ··· 2258 2261 tailwindcss: 3.3.2 2259 2262 dev: false 2260 2263 2261 - /@hono/sentry@1.0.0(hono@3.7.6): 2264 + /@hono/sentry@1.0.0(hono@3.9.1): 2262 2265 resolution: {integrity: sha512-GbPxgpGuasM2zRCSaA77MPWu4KDcuk/EMf7JJykjCvnOTbjmtr7FovNxsvg7xlXCIjZDgLmqBaoJMi3AxbeIAA==} 2263 2266 peerDependencies: 2264 2267 hono: 3.* 2265 2268 dependencies: 2266 - hono: 3.7.6 2269 + hono: 3.9.1 2267 2270 toucan-js: 3.3.0 2268 2271 dev: false 2269 2272 2270 - /@hono/zod-openapi@0.7.1(hono@3.7.6)(zod@3.22.2): 2271 - resolution: {integrity: sha512-zs6AURALRF0A1gkuY5iKmRBT7d0JsLC3xVPDjBdRbJqNV3XQv4i2kprZTRAcI9AjjGApTsZbLZv1pZRCB2viwQ==} 2273 + /@hono/zod-openapi@0.8.3(hono@3.9.1)(zod@3.22.2): 2274 + resolution: {integrity: sha512-TwjSf63miIF3VBqNy4V6EOqZYdTQPeIQQ9Q2HjRWFYH9f2QtBFXziuqIg2Y76gNZ3WDal67NQXmbSErHiFR44g==} 2272 2275 engines: {node: '>=16.0.0'} 2273 2276 peerDependencies: 2274 - hono: '*' 2277 + hono: '>=3.9.0' 2275 2278 zod: 3.* 2276 2279 dependencies: 2277 2280 '@asteasolutions/zod-to-openapi': 5.5.0(zod@3.22.2) 2278 - '@hono/zod-validator': 0.1.9(hono@3.7.6)(zod@3.22.2) 2279 - hono: 3.7.6 2281 + '@hono/zod-validator': 0.1.9(hono@3.9.1)(zod@3.22.2) 2282 + hono: 3.9.1 2280 2283 zod: 3.22.2 2281 2284 dev: false 2282 2285 2283 - /@hono/zod-validator@0.1.9(hono@3.7.6)(zod@3.22.2): 2286 + /@hono/zod-validator@0.1.9(hono@3.9.1)(zod@3.22.2): 2284 2287 resolution: {integrity: sha512-qEG5xagKzyif283ldCKzp+aF9Aebclg0sfrgyRQQNAizmXpicZ3UGduST/Jp+a9bjt3mI+VyEXMftb4rogLxQA==} 2285 2288 peerDependencies: 2286 2289 hono: 3.* 2287 2290 zod: ^3.19.1 2288 2291 dependencies: 2289 - hono: 3.7.6 2292 + hono: 3.9.1 2290 2293 zod: 3.22.2 2291 2294 dev: false 2292 2295 ··· 5262 5265 zod: 3.22.2 5263 5266 dev: false 5264 5267 5268 + /@t3-oss/env-core@0.7.1(typescript@5.2.2)(zod@3.22.2): 5269 + resolution: {integrity: sha512-3+SQt39OlmSaRLqYVFv8uRm1BpFepM5TIiMytRqO9cjH+wB77o6BIJdeyM5h5U4qLBMEzOJWCY4MBaU/rLwbYw==} 5270 + peerDependencies: 5271 + typescript: '>=4.7.2' 5272 + zod: ^3.0.0 5273 + peerDependenciesMeta: 5274 + typescript: 5275 + optional: true 5276 + dependencies: 5277 + typescript: 5.2.2 5278 + zod: 3.22.2 5279 + dev: false 5280 + 5265 5281 /@t3-oss/env-nextjs@0.7.0(typescript@5.2.2)(zod@3.22.2): 5266 5282 resolution: {integrity: sha512-rjQIt6P3tac2eRx4BNQLNaJ+AIb2P8wXw4uFvYbEekqMGShikkUALnX3hUn1twYiGVGHXRm6UbU+LqtjIktuGg==} 5267 5283 peerDependencies: ··· 5991 6007 5992 6008 /@unkey/api@0.10.0: 5993 6009 resolution: {integrity: sha512-ix1XQqZPuBlNkimQniqNjwpqduGb/hBm5LAFRgPfotY97IGUcsKGV3YGl7GAnqFB9wa31OeOT/yeSbEgXiQJWw==} 6010 + dev: false 6011 + 6012 + /@unkey/api@0.11.0: 6013 + resolution: {integrity: sha512-pJRuvRWFAnp6qGryvtI5+J95rtIHUCnTOw0bQTewr2+qFNLv/UimEIlR8Ef6kln/5evc3C94yPjFy59nuCMoWQ==} 5994 6014 dev: false 5995 6015 5996 6016 /@upstash/core-analytics@0.0.6: ··· 9531 9551 react-is: 16.13.1 9532 9552 dev: false 9533 9553 9534 - /hono@3.7.6: 9535 - resolution: {integrity: sha512-nuLNH9+nV6ojXK6b9I0RGZgdMuLTOXeQPs6xsIw/G5iyT8j8m7Nnx8pTxQprmSmCaEYIPv91rCcq45PQNfW68A==} 9554 + /hono@3.9.1: 9555 + resolution: {integrity: sha512-z3nM9CjNZ8PRAH6NNntk4ESKW2POEbGanhK1QpYdQ1MOYRzZPSEE8B5mqw8bYEPa7qIQxX0vtlv7XOxtwFbosg==} 9536 9556 engines: {node: '>=16.0.0'} 9537 9557 dev: false 9538 9558 ··· 11684 11704 11685 11705 /nanoid@5.0.1: 11686 11706 resolution: {integrity: sha512-vWeVtV5Cw68aML/QaZvqN/3QQXc6fBfIieAlu05m7FZW2Dgb+3f0xc0TTxuJW+7u30t7iSDTV/j3kVI0oJqIfQ==} 11707 + engines: {node: ^18 || >=20} 11708 + hasBin: true 11709 + dev: false 11710 + 11711 + /nanoid@5.0.2: 11712 + resolution: {integrity: sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==} 11687 11713 engines: {node: ^18 || >=20} 11688 11714 hasBin: true 11689 11715 dev: false
+11
utils/api-bruno/OpenApi.bru
··· 1 + meta { 2 + name: OpenApi 3 + type: http 4 + seq: 1 5 + } 6 + 7 + get { 8 + url: {{url}}/v1/openapi 9 + body: none 10 + auth: none 11 + }
+5
utils/api-bruno/bruno.json
··· 1 + { 2 + "version": "1", 3 + "name": "OpenStatus", 4 + "type": "collection" 5 + }
+3
utils/api-bruno/environments/local.bru
··· 1 + vars { 2 + url: http://localhost:3000 3 + }
+3
utils/api-bruno/environments/prod.bru
··· 1 + vars { 2 + url: https://api.openstatus.dev 3 + }
+19
utils/api-bruno/incident_update/Get Incident Update.bru
··· 1 + meta { 2 + name: Get Incident Update 3 + type: http 4 + seq: 1 5 + } 6 + 7 + get { 8 + url: {{url}}/v1/incident_update/{{updateId}} 9 + body: none 10 + auth: none 11 + } 12 + 13 + headers { 14 + x-openstatus-key: api-keu 15 + } 16 + 17 + vars:pre-request { 18 + updateId: 32 19 + }
+15
utils/api-bruno/incidents/All incidents.bru
··· 1 + meta { 2 + name: All incidents 3 + type: http 4 + seq: 1 5 + } 6 + 7 + get { 8 + url: {{url}}/v1/incident 9 + body: none 10 + auth: none 11 + } 12 + 13 + headers { 14 + x-openstatus-key: api-key 15 + }
+19
utils/api-bruno/incidents/Get on incident.bru
··· 1 + meta { 2 + name: Get on incident 3 + type: http 4 + seq: 2 5 + } 6 + 7 + get { 8 + url: {{url}}/v1/incident/{{id}} 9 + body: none 10 + auth: none 11 + } 12 + 13 + headers { 14 + x-openstatus-key: api-key 15 + } 16 + 17 + vars:pre-request { 18 + id: 31 19 + }