Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

๐Ÿš€ create notification api (#716)

authored by

Thibault Le Ouay and committed by
GitHub
3142372c 32c1a466

+295 -9
+2
apps/server/package.json
··· 28 28 "@unkey/api": "0.16.0", 29 29 "hono": "4.0.0", 30 30 "nanoid": "5.0.2", 31 + "validator": "13.11.0", 31 32 "zod": "3.22.2" 32 33 }, 33 34 "devDependencies": { 34 35 "@openstatus/tsconfig": "workspace:*", 36 + "@types/validator": "13.11.6", 35 37 "bun-types": "1.0.11", 36 38 "dotenv": "16.3.1", 37 39 "eslint": "8.50.0"
+4 -2
apps/server/src/v1/index.ts
··· 7 7 import { incidentsApi } from "./incidents"; 8 8 import { middleware } from "./middleware"; 9 9 import { monitorApi } from "./monitor"; 10 + import { notificationApi } from "./notification"; 10 11 import { pageApi } from "./page"; 11 12 import { statusReportApi } from "./statusReport"; 12 13 import { statusReportUpdateApi } from "./statusReportUpdate"; ··· 36 37 */ 37 38 api.use("/*", middleware); 38 39 api.use("/*", logger()); 40 + api.route("/incident", incidentsApi); 39 41 api.route("/monitor", monitorApi); 40 - api.route("/status_report_update", statusReportUpdateApi); 41 - api.route("/incident", incidentsApi); 42 + api.route("/notification", notificationApi); 42 43 api.route("/page", pageApi); 43 44 44 45 api.route("/status_report", statusReportApi); 46 + api.route("/status_report_update", statusReportUpdateApi);
+27
apps/server/src/v1/notification.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { api } from "."; 4 + import { iso8601Regex } from "./test-utils"; 5 + 6 + test.only("Create a notification", async () => { 7 + const data = { 8 + name: "OpenStatus", 9 + provider: "email", 10 + payload: { email: "ping@openstatus.dev" }, 11 + }; 12 + const res = await api.request("/notification", { 13 + method: "POST", 14 + headers: { 15 + "x-openstatus-key": "1", 16 + "content-type": "application/json", 17 + }, 18 + body: JSON.stringify(data), 19 + }); 20 + expect(res.status).toBe(200); 21 + 22 + expect(await res.json()).toMatchObject({ 23 + id: expect.any(Number), 24 + provider: "email", 25 + payload: { email: "ping@openstatus.dev" }, 26 + }); 27 + });
+239
apps/server/src/v1/notification.ts
··· 1 + import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 + import isEmail from "validator/lib/isEmail"; 3 + import isMobilePhone from "validator/lib/isMobilePhone"; 4 + 5 + import { and, db, eq, inArray } from "@openstatus/db"; 6 + import { 7 + monitor, 8 + notification, 9 + NotificationDataSchema, 10 + notificationProvider, 11 + notificationsToMonitors, 12 + page, 13 + selectNotificationSchema, 14 + } from "@openstatus/db/src/schema"; 15 + 16 + import type { Variables } from "."; 17 + import { ErrorSchema } from "./shared"; 18 + 19 + const notificationApi = new OpenAPIHono<{ Variables: Variables }>(); 20 + 21 + const ParamsSchema = z.object({ 22 + id: z 23 + .string() 24 + .min(1) 25 + .openapi({ 26 + param: { 27 + name: "id", 28 + in: "path", 29 + }, 30 + description: "The id of the notification", 31 + example: "1", 32 + }), 33 + }); 34 + 35 + const NotificationSchema = z.object({ 36 + id: z 37 + .number() 38 + .openapi({ description: "The id of the notification", example: 1 }), 39 + name: z.string().openapi({ 40 + description: "The name of the notification", 41 + example: "OpenStatus Discord", 42 + }), 43 + provider: z.enum(notificationProvider).openapi({ 44 + description: "The provider of the notification", 45 + example: "discord", 46 + }), 47 + payload: NotificationDataSchema.openapi({ 48 + description: "The data of the notification", 49 + examples: [ 50 + { email: "ping@openstatus.dev" }, 51 + { discord: "https://discord.com/api/webhooks/{channelId}/xxx..." }, 52 + ], 53 + }), 54 + monitors: z 55 + .array(z.number()) 56 + .openapi({ 57 + description: "The monitors that the notification is linked to", 58 + example: [1, 2], 59 + }) 60 + .nullish(), 61 + }); 62 + 63 + const CreateNotificationSchema = z.object({ 64 + name: z.string().openapi({ 65 + description: "The name of the notification", 66 + example: "OpenStatus Discord", 67 + }), 68 + provider: z.enum(notificationProvider).openapi({ 69 + description: "The provider of the notification", 70 + example: "discord", 71 + }), 72 + payload: NotificationDataSchema.openapi({ 73 + description: "The data of the notification", 74 + examples: [ 75 + { email: "ping@openstatus.dev" }, 76 + { discord: "https://discord.com/api/webhooks/{channelId}/xxx..." }, 77 + ], 78 + }), 79 + monitors: z 80 + .array(z.number()) 81 + .openapi({ 82 + description: "The monitors that the notification is linked to", 83 + example: [1, 2], 84 + }) 85 + .nullish(), 86 + }); 87 + const getRoute = createRoute({ 88 + method: "get", 89 + tags: ["notification"], 90 + description: "Get a notification", 91 + path: "/:id", 92 + request: { 93 + params: ParamsSchema, 94 + }, 95 + responses: { 96 + 200: { 97 + content: { 98 + "application/json": { 99 + schema: NotificationSchema, 100 + }, 101 + }, 102 + description: "Get an Status page", 103 + }, 104 + 400: { 105 + content: { 106 + "application/json": { 107 + schema: ErrorSchema, 108 + }, 109 + }, 110 + description: "Returns an error", 111 + }, 112 + }, 113 + }); 114 + 115 + notificationApi.openapi(getRoute, async (c) => { 116 + const workspaceId = Number(c.get("workspaceId")); 117 + const { id } = c.req.valid("param"); 118 + 119 + const notificationId = Number(id); 120 + const result = await db 121 + .select() 122 + .from(notification) 123 + .where( 124 + and( 125 + eq(page.workspaceId, workspaceId), 126 + eq(notification.id, notificationId), 127 + ), 128 + ) 129 + .get(); 130 + 131 + if (!result) return c.json({ code: 404, message: "Not Found" }, 404); 132 + const linkedMonitors = await db 133 + .select() 134 + .from(notificationsToMonitors) 135 + .where(eq(notificationsToMonitors.notificationId, notificationId)) 136 + .all(); 137 + 138 + const monitorsId = linkedMonitors.map((m) => m.monitorId); 139 + 140 + const data = NotificationSchema.parse({ 141 + ...result, 142 + payload: JSON.parse(result.data || "{}"), 143 + monitors: monitorsId, 144 + }); 145 + 146 + return c.json(data); 147 + }); 148 + 149 + const postRoute = createRoute({ 150 + method: "post", 151 + tags: ["page"], 152 + description: "Create a notification", 153 + path: "/", 154 + request: { 155 + body: { 156 + description: "The notification to create", 157 + content: { 158 + "application/json": { 159 + schema: CreateNotificationSchema, 160 + }, 161 + }, 162 + }, 163 + }, 164 + responses: { 165 + 200: { 166 + content: { 167 + "application/json": { 168 + schema: NotificationSchema, 169 + }, 170 + }, 171 + description: "Return the created notification", 172 + }, 173 + 400: { 174 + content: { 175 + "application/json": { 176 + schema: ErrorSchema, 177 + }, 178 + }, 179 + description: "Returns an error", 180 + }, 181 + }, 182 + }); 183 + 184 + notificationApi.openapi(postRoute, async (c) => { 185 + const workspaceId = Number(c.get("workspaceId")); 186 + const workspacePlan = c.get("workspacePlan"); 187 + const input = c.req.valid("json"); 188 + console.log(input); 189 + if (input.provider === "sms" && workspacePlan.title === "free") { 190 + return c.json({ code: 403, message: "Forbidden" }, 403); 191 + } 192 + 193 + const { payload, monitors, ...rest } = input; 194 + if (monitors) { 195 + const monitorsData = await db 196 + .select() 197 + .from(monitor) 198 + .where( 199 + and( 200 + inArray(monitor.id, monitors), 201 + eq(monitor.workspaceId, workspaceId), 202 + ), 203 + ) 204 + .all(); 205 + if (monitorsData.length !== monitors.length) 206 + return c.json({ code: 400, message: "Monitor not found" }, 400); 207 + } 208 + 209 + const newNotif = await db 210 + .insert(notification) 211 + .values({ 212 + workspaceId, 213 + ...rest, 214 + data: JSON.stringify(payload), 215 + }) 216 + .returning() 217 + .get(); 218 + 219 + console.log(newNotif); 220 + if (monitors) { 221 + for (const monitorId of monitors) { 222 + await db 223 + .insert(notificationsToMonitors) 224 + .values({ notificationId: newNotif.id, monitorId }) 225 + .run(); 226 + } 227 + } 228 + const d = selectNotificationSchema.parse(newNotif); 229 + 230 + const p = NotificationDataSchema.parse(JSON.parse(d.data)); 231 + const data = NotificationSchema.parse({ 232 + ...newNotif, 233 + monitors: monitors, 234 + payload: p, 235 + }); 236 + return c.json(data); 237 + }); 238 + 239 + export { notificationApi };
+2 -2
apps/server/src/v1/page.ts
··· 1 1 import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 2 3 - import { and, desc, eq, inArray, sql } from "@openstatus/db"; 3 + import { and, eq, inArray, sql } from "@openstatus/db"; 4 4 import { db } from "@openstatus/db/src/db"; 5 5 import { 6 6 monitor, ··· 314 314 path: "/", 315 315 request: { 316 316 body: { 317 - description: "The monitor to create", 317 + description: "The status page to create", 318 318 content: { 319 319 "application/json": { 320 320 schema: CreatePageSchema,
+15
packages/db/src/schema/notifications/validation.ts
··· 26 26 export type InsertNotification = z.infer<typeof insertNotificationSchema>; 27 27 export type Notification = z.infer<typeof selectNotificationSchema>; 28 28 export type NotificationProvider = z.infer<typeof notificationProviderSchema>; 29 + 30 + export const NotificationDataSchema = z.union([ 31 + z.object({ 32 + sms: z.string(), 33 + }), 34 + z.object({ 35 + email: z.string().email(), 36 + }), 37 + z.object({ 38 + slack: z.string(), 39 + }), 40 + z.object({ 41 + discord: z.string(), 42 + }), 43 + ]);
+6 -5
pnpm-lock.yaml
··· 88 88 nanoid: 89 89 specifier: 5.0.2 90 90 version: 5.0.2 91 + validator: 92 + specifier: 13.11.0 93 + version: 13.11.0 91 94 zod: 92 95 specifier: 3.22.2 93 96 version: 3.22.2 ··· 95 98 '@openstatus/tsconfig': 96 99 specifier: workspace:* 97 100 version: link:../../packages/tsconfig 101 + '@types/validator': 102 + specifier: 13.11.6 103 + version: 13.11.6 98 104 bun-types: 99 105 specifier: 1.0.11 100 106 version: 1.0.11 ··· 1392 1398 peerDependencies: 1393 1399 '@effect-ts/otel-node': '*' 1394 1400 peerDependenciesMeta: 1395 - '@effect-ts/core': 1396 - optional: true 1397 - '@effect-ts/otel': 1398 - optional: true 1399 1401 '@effect-ts/otel-node': 1400 1402 optional: true 1401 1403 dependencies: ··· 5409 5411 5410 5412 /@types/validator@13.11.6: 5411 5413 resolution: {integrity: sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==} 5412 - dev: false 5413 5414 5414 5415 /@types/ws@8.5.8: 5415 5416 resolution: {integrity: sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==}