Openstatus www.openstatus.dev

πŸ“ rename table (#487)

* πŸ“ rename table

* πŸ“ rename table

* πŸ“ rename table

* πŸ“ rename table

* πŸ“ rename table

* πŸ“ rename table

* πŸ“ rename table

* πŸ“ rename table

* πŸ“ rename table

* πŸ“reaname table

* πŸ”₯

* wip: fix zod and replace incident with status report

* fix: wrong status-report url

* fix: tests

* wip: rename

* fix: status

---------

Co-authored-by: mxkaske <maximilian@kaske.org>

authored by

Thibault Le Ouay
mxkaske
and committed by
GitHub
9c64fe2d 920ab4f4

+2336 -1174
-1
apps/server/fly.toml
··· 15 15 force_https = true 16 16 auto_stop_machines = false 17 17 auto_start_machines = false 18 - min_machines_running = 12 19 18 processes = ["app"] 20 19 [http_service.concurrency] 21 20 type = "requests"
+25 -21
apps/server/src/public/status.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { endTime, setMetric, startTime } from "hono/timing"; 3 3 4 - import { db, eq } from "@openstatus/db"; 4 + import { and, db, eq } from "@openstatus/db"; 5 5 import { 6 - incident, 7 6 monitor, 8 - monitorsToIncidents, 9 7 monitorsToPages, 8 + monitorsToStatusReport, 10 9 page, 11 - pagesToIncidents, 10 + pagesToStatusReports, 11 + statusReport, 12 12 } from "@openstatus/db/src/schema"; 13 13 import { getMonitorList, Tinybird } from "@openstatus/tinybird"; 14 14 import { Redis } from "@openstatus/upstash"; ··· 36 36 const { slug } = c.req.param(); 37 37 38 38 const cache = await redis.get(slug); 39 + 39 40 if (cache) { 40 41 setMetric(c, "OpenStatus-Cache", "HIT"); 41 - 42 42 return c.json({ status: cache }); 43 43 } 44 44 45 45 startTime(c, "database"); 46 - // { monitors, pages, monitors_to_pages } 46 + 47 47 const monitorData = await db 48 48 .select() 49 49 .from(monitorsToPages) 50 - .leftJoin(monitor, eq(monitorsToPages.monitorId, monitor.id)) 50 + .leftJoin(monitor, and(eq(monitorsToPages.monitorId, monitor.id))) 51 + .leftJoin( 52 + monitorsToStatusReport, 53 + eq(monitor.id, monitorsToStatusReport.monitorId), 54 + ) 51 55 .leftJoin( 52 - monitorsToIncidents, 53 - eq(monitor.id, monitorsToIncidents.monitorId), 56 + statusReport, 57 + eq(monitorsToStatusReport.statusReportId, statusReport.id), 54 58 ) 55 - .leftJoin(incident, eq(monitorsToIncidents.incidentId, incident.id)) 56 59 .leftJoin(page, eq(monitorsToPages.pageId, page.id)) 57 - .where(eq(page.slug, slug)) 60 + .where(eq(page.slug, slug)) // TODO: query only active monitors 58 61 .all(); 59 62 60 - const pageIncidentData = await db 63 + const pageStatusReportData = await db 61 64 .select() 62 - .from(pagesToIncidents) 63 - .leftJoin(incident, eq(pagesToIncidents.incidentId, incident.id)) 64 - .leftJoin(page, eq(pagesToIncidents.pageId, page.id)) 65 + .from(pagesToStatusReports) 66 + .leftJoin( 67 + statusReport, 68 + eq(pagesToStatusReports.statusReportId, statusReport.id), 69 + ) 70 + .leftJoin(page, eq(pagesToStatusReports.pageId, page.id)) 65 71 .where(eq(page.slug, slug)) 66 72 .all(); 67 73 68 - endTime(c, "database"); 69 - 70 - const isIncident = [...pageIncidentData, ...monitorData].some((data) => { 71 - if (!data.incident) return false; 72 - return !["monitoring", "resolved"].includes(data.incident.status); 74 + const isIncident = [...pageStatusReportData, ...monitorData].some((data) => { 75 + if (!data.status_report) return false; 76 + return !["monitoring", "resolved"].includes(data.status_report.status); 73 77 }); 74 78 75 79 startTime(c, "clickhouse"); ··· 83 87 }), 84 88 ); 85 89 endTime(c, "clickhouse"); 86 - // { ok, count } 90 + 87 91 const data = lastMonitorPings.reduce( 88 92 (prev, curr) => { 89 93 if (curr.status === "fulfilled") {
-39
apps/server/src/v1/incident.test.ts
··· 1 - import { expect, test } from "bun:test"; 2 - 3 - import { api } from "."; 4 - 5 - test("GET one incident update ", async () => { 6 - const res = await api.request("/incident/1", { 7 - headers: { 8 - "x-openstatus-key": "1", 9 - }, 10 - }); 11 - expect(res.status).toBe(200); 12 - expect(await res.json()).toMatchObject({ 13 - id: 1, 14 - status: "investigating", 15 - title: "Test Incident", 16 - incident_updates: expect.any(Array), 17 - }); 18 - }); 19 - 20 - test("create one incident ", async () => { 21 - const res = await api.request("/incident", { 22 - method: "POST", 23 - headers: { 24 - "x-openstatus-key": "1", 25 - "content-type": "application/json", 26 - }, 27 - body: JSON.stringify({ 28 - status: "investigating", 29 - date: "2023-11-08T21:03:13.000Z", 30 - title: "Test Incident", 31 - }), 32 - }); 33 - expect(res.status).toBe(200); 34 - expect(await res.json()).toMatchObject({ 35 - id: expect.any(Number), 36 - status: "investigating", 37 - title: "Test Incident", 38 - }); 39 - });
-342
apps/server/src/v1/incident.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 { incidentUpdateSchema } from "./incidentUpdate"; 11 - import type { Variables } from "./index"; 12 - import { ErrorSchema } from "./shared"; 13 - 14 - const incidentApi = new OpenAPIHono<{ Variables: Variables }>(); 15 - 16 - const ParamsSchema = z.object({ 17 - id: z 18 - .string() 19 - .min(1) 20 - .openapi({ 21 - param: { 22 - name: "id", 23 - in: "path", 24 - }, 25 - description: "The id of the incident", 26 - example: "1", 27 - }), 28 - }); 29 - 30 - const createIncidentUpdateSchema = z.object({ 31 - status: z.enum(incidentStatus).openapi({ 32 - description: "The status of the update", 33 - }), 34 - date: z.string().openapi({ 35 - description: "The date of the update in ISO 8601 format", 36 - }), 37 - message: z.string().openapi({ 38 - description: "The message of the update", 39 - }), 40 - }); 41 - 42 - const incidentSchema = z.object({ 43 - title: z.string().openapi({ 44 - example: "Documenso", 45 - description: "The title of the incident", 46 - }), 47 - status: z.enum(incidentStatus).openapi({ 48 - description: "The current status of the incident", 49 - }), 50 - }); 51 - 52 - const incidentResultSchema = incidentSchema.extend({ 53 - id: z.number().openapi({ description: "The id of the incident" }), 54 - }); 55 - 56 - const incidentExtendedSchema = incidentSchema.extend({ 57 - id: z.number().openapi({ description: "The id of the incident" }), 58 - incident_updates: z 59 - .array(z.number()) 60 - .openapi({ 61 - description: "The ids of the incident updates", 62 - }) 63 - .default([]), 64 - }); 65 - const getAllRoute = createRoute({ 66 - method: "get", 67 - tags: ["incident"], 68 - description: "Get all incidents", 69 - path: "/", 70 - request: {}, 71 - responses: { 72 - 200: { 73 - content: { 74 - "application/json": { 75 - schema: z.array(incidentExtendedSchema), 76 - }, 77 - }, 78 - description: "Get all incidents", 79 - }, 80 - 400: { 81 - content: { 82 - "application/json": { 83 - schema: ErrorSchema, 84 - }, 85 - }, 86 - description: "Returns an error", 87 - }, 88 - }, 89 - }); 90 - 91 - incidentApi.openapi(getAllRoute, async (c) => { 92 - const workspaceId = Number(c.get("workspaceId")); 93 - const _incidents = await db.query.incident.findMany({ 94 - with: { 95 - incidentUpdates: true, 96 - }, 97 - where: eq(incident.workspaceId, workspaceId), 98 - }); 99 - 100 - if (!_incidents) return c.jsonT({ code: 404, message: "Not Found" }); 101 - 102 - const data = z.array(incidentExtendedSchema).parse( 103 - _incidents.map((incident) => ({ 104 - ...incident, 105 - incident_updates: incident.incidentUpdates.map((incidentUpdate) => { 106 - return incidentUpdate.id; 107 - }), 108 - })), 109 - ); 110 - 111 - return c.jsonT(data); 112 - }); 113 - 114 - const getRoute = createRoute({ 115 - method: "get", 116 - tags: ["incident"], 117 - description: "Get an incident", 118 - path: "/:id", 119 - request: { 120 - params: ParamsSchema, 121 - }, 122 - responses: { 123 - 200: { 124 - content: { 125 - "application/json": { 126 - schema: incidentExtendedSchema, 127 - }, 128 - }, 129 - description: "Get all incidents", 130 - }, 131 - 400: { 132 - content: { 133 - "application/json": { 134 - schema: ErrorSchema, 135 - }, 136 - }, 137 - description: "Returns an error", 138 - }, 139 - }, 140 - }); 141 - 142 - incidentApi.openapi(getRoute, async (c) => { 143 - const workspaceId = Number(c.get("workspaceId")); 144 - const { id } = c.req.valid("param"); 145 - 146 - const incidentId = Number(id); 147 - const _incident = await db.query.incident.findFirst({ 148 - with: { 149 - incidentUpdates: true, 150 - }, 151 - where: and( 152 - eq(incident.workspaceId, workspaceId), 153 - eq(incident.id, incidentId), 154 - ), 155 - }); 156 - 157 - if (!_incident) return c.jsonT({ code: 404, message: "Not Found" }); 158 - const data = incidentExtendedSchema.parse({ 159 - ..._incident, 160 - incident_updates: _incident.incidentUpdates.map( 161 - (incidentUpdate) => incidentUpdate.id, 162 - ), 163 - }); 164 - 165 - return c.jsonT(data); 166 - }); 167 - 168 - const postRoute = createRoute({ 169 - method: "post", 170 - tags: ["incident"], 171 - description: "Create an incident", 172 - path: "/", 173 - request: { 174 - body: { 175 - description: "The incident to create", 176 - content: { 177 - "application/json": { 178 - schema: incidentSchema, 179 - }, 180 - }, 181 - }, 182 - }, 183 - responses: { 184 - 200: { 185 - content: { 186 - "application/json": { 187 - schema: incidentExtendedSchema, 188 - }, 189 - }, 190 - description: "Incident created", 191 - }, 192 - 400: { 193 - content: { 194 - "application/json": { 195 - schema: ErrorSchema, 196 - }, 197 - }, 198 - description: "Returns an error", 199 - }, 200 - }, 201 - }); 202 - 203 - incidentApi.openapi(postRoute, async (c) => { 204 - const input = c.req.valid("json"); 205 - const workspaceId = Number(c.get("workspaceId")); 206 - 207 - const _newIncident = await db 208 - .insert(incident) 209 - .values({ 210 - ...input, 211 - workspaceId: workspaceId, 212 - }) 213 - .returning() 214 - .get(); 215 - 216 - const data = incidentExtendedSchema.parse(_newIncident); 217 - 218 - return c.jsonT(data); 219 - }); 220 - 221 - const deleteRoute = createRoute({ 222 - method: "delete", 223 - tags: ["incident"], 224 - description: "Delete an incident", 225 - path: "/:id", 226 - request: { 227 - params: ParamsSchema, 228 - }, 229 - responses: { 230 - 200: { 231 - content: { 232 - "application/json": { 233 - schema: z.object({ 234 - message: z.string().openapi({ 235 - example: "Deleted", 236 - }), 237 - }), 238 - }, 239 - }, 240 - description: "Incident deleted", 241 - }, 242 - 400: { 243 - content: { 244 - "application/json": { 245 - schema: ErrorSchema, 246 - }, 247 - }, 248 - description: "Returns an error", 249 - }, 250 - }, 251 - }); 252 - 253 - incidentApi.openapi(deleteRoute, async (c) => { 254 - const workspaceId = Number(c.get("workspaceId")); 255 - const { id } = c.req.valid("param"); 256 - 257 - const incidentId = Number(id); 258 - const _incident = await db 259 - .select() 260 - .from(incident) 261 - .where(eq(incident.id, incidentId)) 262 - .get(); 263 - 264 - if (!_incident) return c.jsonT({ code: 404, message: "Not Found" }); 265 - 266 - if (workspaceId !== _incident.workspaceId) 267 - return c.jsonT({ code: 401, message: "Unauthorized" }); 268 - 269 - await db.delete(incident).where(eq(incident.id, incidentId)).run(); 270 - return c.jsonT({ message: "Deleted" }); 271 - }); 272 - 273 - const postRouteUpdate = createRoute({ 274 - method: "post", 275 - tags: ["incident"], 276 - path: "/:id/update", 277 - description: "Create an incident update", 278 - request: { 279 - params: ParamsSchema, 280 - body: { 281 - description: "the incident update", 282 - content: { 283 - "application/json": { 284 - schema: createIncidentUpdateSchema, 285 - }, 286 - }, 287 - }, 288 - }, 289 - responses: { 290 - 200: { 291 - content: { 292 - "application/json": { 293 - schema: incidentUpdateSchema, 294 - }, 295 - }, 296 - description: "Incident updated", 297 - }, 298 - 400: { 299 - content: { 300 - "application/json": { 301 - schema: ErrorSchema, 302 - }, 303 - }, 304 - description: "Returns an error", 305 - }, 306 - }, 307 - }); 308 - 309 - incidentApi.openapi(postRouteUpdate, async (c) => { 310 - const input = c.req.valid("json"); 311 - const { id } = c.req.valid("param"); 312 - const workspaceId = Number(c.get("workspaceId")); 313 - 314 - const incidentId = Number(id); 315 - const _incident = await db 316 - .select() 317 - .from(incident) 318 - .where( 319 - and(eq(incident.id, incidentId), eq(incident.workspaceId, workspaceId)), 320 - ) 321 - .get(); 322 - 323 - if (!_incident) return c.jsonT({ code: 401, message: "Not authorized" }); 324 - 325 - const _incidentUpdate = await db 326 - .insert(incidentUpdate) 327 - .values({ 328 - ...input, 329 - date: new Date(input.date), 330 - incidentId: Number(id), 331 - }) 332 - .returning() 333 - .get(); 334 - 335 - const data = incidentUpdateSchema.parse(_incidentUpdate); 336 - 337 - return c.jsonT({ 338 - ...data, 339 - }); 340 - }); 341 - 342 - export { incidentApi };
+5 -5
apps/server/src/v1/incidentUpdate.test.ts apps/server/src/v1/statusReportUpdate.test.ts
··· 2 2 3 3 import { api } from "."; 4 4 5 - test("GET one incident update ", async () => { 6 - const res = await api.request("/incident_update/1", { 5 + test("GET one status report update ", async () => { 6 + const res = await api.request("/status_report_update/1", { 7 7 headers: { 8 8 "x-openstatus-key": "1", 9 9 }, ··· 15 15 }); 16 16 }); 17 17 18 - test("create one incident update ", async () => { 19 - const res = await api.request("/incident_update", { 18 + test("create one status report update ", async () => { 19 + const res = await api.request("/status_report_update", { 20 20 method: "POST", 21 21 headers: { 22 22 "x-openstatus-key": "1", ··· 26 26 status: "investigating", 27 27 date: "2023-11-08T21:03:13.000Z", 28 28 message: "test", 29 - incident_id: 1, 29 + status_report_id: 1, 30 30 }), 31 31 }); 32 32 expect(res.status).toBe(200);
+38 -38
apps/server/src/v1/incidentUpdate.ts apps/server/src/v1/statusReportUpdate.ts
··· 2 2 3 3 import { and, db, eq } from "@openstatus/db"; 4 4 import { 5 - incident, 6 - incidentStatus, 7 - incidentUpdate, 5 + statusReport, 6 + statusReportStatus, 7 + statusReportUpdate, 8 8 } from "@openstatus/db/src/schema"; 9 9 10 10 import type { Variables } from "."; 11 11 import { ErrorSchema } from "./shared"; 12 12 13 - const incidenUpdateApi = new OpenAPIHono<{ Variables: Variables }>(); 13 + const statusReportUpdateApi = new OpenAPIHono<{ Variables: Variables }>(); 14 14 15 15 const ParamsSelectSchema = z.object({ 16 16 id: z ··· 26 26 }), 27 27 }); 28 28 29 - export const incidentUpdateSchema = z.object({ 30 - status: z.enum(incidentStatus).openapi({ 29 + export const statusUpdateSchema = z.object({ 30 + status: z.enum(statusReportStatus).openapi({ 31 31 description: "The status of the update", 32 32 }), 33 33 id: z.coerce.string().openapi({ description: "The id of the update" }), ··· 41 41 }), 42 42 }); 43 43 44 - const createIncidentUpdateSchema = z.object({ 45 - incident_id: z.number().openapi({ 46 - description: "The id of the incident", 44 + const createStatusReportUpdateSchema = z.object({ 45 + status_report_id: z.number().openapi({ 46 + description: "The id of the status report", 47 47 }), 48 - status: z.enum(incidentStatus).openapi({ 48 + status: z.enum(statusReportStatus).openapi({ 49 49 description: "The status of the update", 50 50 }), 51 51 date: z.string().datetime().openapi({ ··· 57 57 }); 58 58 const getUpdateRoute = createRoute({ 59 59 method: "get", 60 - tags: ["incident_update"], 60 + tags: ["status_report_update"], 61 61 path: "/:id", 62 62 request: { 63 63 params: ParamsSelectSchema, ··· 66 66 200: { 67 67 content: { 68 68 "application/json": { 69 - schema: incidentUpdateSchema, 69 + schema: statusUpdateSchema, 70 70 }, 71 71 }, 72 - description: "Get all incidents", 72 + description: "Get all status report updates", 73 73 }, 74 74 400: { 75 75 content: { ··· 82 82 }, 83 83 }); 84 84 85 - incidenUpdateApi.openapi(getUpdateRoute, async (c) => { 85 + statusReportUpdateApi.openapi(getUpdateRoute, async (c) => { 86 86 const workspaceId = Number(c.get("workspaceId")); 87 87 const { id } = c.req.valid("param"); 88 88 89 89 const update = await db 90 90 .select() 91 - .from(incidentUpdate) 92 - .where(eq(incidentUpdate.id, Number(id))) 91 + .from(statusReportUpdate) 92 + .where(eq(statusReportUpdate.id, Number(id))) 93 93 .get(); 94 94 95 95 if (!update) return c.jsonT({ code: 404, message: "Not Found" }); 96 96 97 - const currentIncident = await db 97 + const currentStatusReport = await db 98 98 .select() 99 - .from(incident) 99 + .from(statusReport) 100 100 .where( 101 101 and( 102 - eq(incident.id, update.incidentId), 103 - eq(incident.workspaceId, workspaceId), 102 + eq(statusReport.id, update.statusReportId), 103 + eq(statusReport.workspaceId, workspaceId), 104 104 ), 105 105 ) 106 106 .get(); 107 - if (!currentIncident) 107 + if (!currentStatusReport) 108 108 return c.jsonT({ code: 401, message: "Not Authorized" }); 109 109 110 - const data = incidentUpdateSchema.parse(update); 110 + const data = statusUpdateSchema.parse(update); 111 111 112 112 return c.jsonT(data); 113 113 }); 114 114 115 - const createIncidentUpdate = createRoute({ 115 + const createStatusUpdate = createRoute({ 116 116 method: "post", 117 - tags: ["incident_update"], 117 + tags: ["status_report_update"], 118 118 path: "/", 119 119 request: { 120 120 body: { 121 - description: "the incident update", 121 + description: "the status report update", 122 122 content: { 123 123 "application/json": { 124 - schema: createIncidentUpdateSchema, 124 + schema: createStatusReportUpdateSchema, 125 125 }, 126 126 }, 127 127 }, ··· 130 130 200: { 131 131 content: { 132 132 "application/json": { 133 - schema: incidentUpdateSchema, 133 + schema: statusUpdateSchema, 134 134 }, 135 135 }, 136 - description: "Get all incidents", 136 + description: "Get all status report updates", 137 137 }, 138 138 400: { 139 139 content: { ··· 146 146 }, 147 147 }); 148 148 149 - incidenUpdateApi.openapi(createIncidentUpdate, async (c) => { 149 + statusReportUpdateApi.openapi(createStatusUpdate, async (c) => { 150 150 const workspaceId = Number(c.get("workspaceId")); 151 151 const input = c.req.valid("json"); 152 152 153 - const currentIncident = await db 153 + const _currentStatusReport = await db 154 154 .select() 155 - .from(incident) 155 + .from(statusReport) 156 156 .where( 157 157 and( 158 - eq(incident.id, input.incident_id), 159 - eq(incident.workspaceId, workspaceId), 158 + eq(statusReport.id, input.status_report_id), 159 + eq(statusReport.workspaceId, workspaceId), 160 160 ), 161 161 ) 162 162 .get(); 163 - if (!currentIncident) 163 + if (!_currentStatusReport) 164 164 return c.jsonT({ code: 401, message: "Not Authorized" }); 165 165 166 166 const res = await db 167 - .insert(incidentUpdate) 167 + .insert(statusReportUpdate) 168 168 .values({ 169 169 ...input, 170 170 date: new Date(input.date), 171 - incidentId: input.incident_id, 171 + statusReportId: input.status_report_id, 172 172 }) 173 173 .returning() 174 174 .get(); 175 175 176 - const data = incidentUpdateSchema.parse(res); 176 + const data = statusUpdateSchema.parse(res); 177 177 return c.jsonT(data); 178 178 }); 179 179 180 - export { incidenUpdateApi }; 180 + export { statusReportUpdateApi };
+4 -4
apps/server/src/v1/index.ts
··· 3 3 4 4 import type { Plan } from "@openstatus/plans"; 5 5 6 - import { incidentApi } from "./incident"; 7 - import { incidenUpdateApi } from "./incidentUpdate"; 8 6 import { middleware } from "./middleware"; 9 7 import { monitorApi } from "./monitor"; 8 + import { statusReportApi } from "./statusReport"; 9 + import { statusReportUpdateApi } from "./statusReportUpdate"; 10 10 11 11 export type Variables = { 12 12 workspaceId: string; ··· 29 29 api.use("/*", middleware); 30 30 api.use("/*", logger()); 31 31 api.route("/monitor", monitorApi); 32 - api.route("/incident_update", incidenUpdateApi); 32 + api.route("/status_report_update", statusReportUpdateApi); 33 33 34 - api.route("/incident", incidentApi); 34 + api.route("/status_report", statusReportApi);
+40
apps/server/src/v1/statusReport.test.ts
··· 1 + import { expect, test } from "bun:test"; 2 + 3 + import { api } from "."; 4 + 5 + test("GET one status report", async () => { 6 + const res = await api.request("/status_report/1", { 7 + headers: { 8 + "x-openstatus-key": "1", 9 + }, 10 + }); 11 + expect(res.status).toBe(200); 12 + expect(await res.json()).toMatchObject({ 13 + id: 1, 14 + status: "investigating", 15 + title: "Test Status Report", 16 + // TODO: discuss if we should return `updates` instead of `status_report_updates` 17 + status_report_updates: expect.any(Array), 18 + }); 19 + }); 20 + 21 + test("create one status report", async () => { 22 + const res = await api.request("/status_report", { 23 + method: "POST", 24 + headers: { 25 + "x-openstatus-key": "1", 26 + "content-type": "application/json", 27 + }, 28 + body: JSON.stringify({ 29 + status: "investigating", 30 + date: "2023-11-08T21:03:13.000Z", 31 + title: "Test Status Report", 32 + }), 33 + }); 34 + expect(res.status).toBe(200); 35 + expect(await res.json()).toMatchObject({ 36 + id: expect.any(Number), 37 + status: "investigating", 38 + title: "Test Status Report", 39 + }); 40 + });
+346
apps/server/src/v1/statusReport.ts
··· 1 + import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 + 3 + import { and, db, eq } from "@openstatus/db"; 4 + import { 5 + statusReport, 6 + statusReportStatus, 7 + statusReportUpdate, 8 + } from "@openstatus/db/src/schema"; 9 + 10 + import type { Variables } from "./index"; 11 + import { ErrorSchema } from "./shared"; 12 + import { statusUpdateSchema } from "./statusReportUpdate"; 13 + 14 + const statusReportApi = new OpenAPIHono<{ Variables: Variables }>(); 15 + 16 + const ParamsSchema = z.object({ 17 + id: z 18 + .string() 19 + .min(1) 20 + .openapi({ 21 + param: { 22 + name: "id", 23 + in: "path", 24 + }, 25 + description: "The id of the status report", 26 + example: "1", 27 + }), 28 + }); 29 + 30 + const createStatusReportUpdateSchema = z.object({ 31 + status: z.enum(statusReportStatus).openapi({ 32 + description: "The status of the update", 33 + }), 34 + date: z.string().openapi({ 35 + description: "The date of the update in ISO 8601 format", 36 + }), 37 + message: z.string().openapi({ 38 + description: "The message of the update", 39 + }), 40 + }); 41 + 42 + const statusSchema = z.object({ 43 + title: z.string().openapi({ 44 + example: "Documenso", 45 + description: "The title of the status report", 46 + }), 47 + status: z.enum(statusReportStatus).openapi({ 48 + description: "The current status of the report", 49 + }), 50 + }); 51 + 52 + const statusReportExtendedSchema = statusSchema.extend({ 53 + id: z.number().openapi({ description: "The id of the status report" }), 54 + status_report_updates: z 55 + .array(z.number()) 56 + .openapi({ 57 + description: "The ids of the status report updates", 58 + }) 59 + .default([]), 60 + }); 61 + const getAllRoute = createRoute({ 62 + method: "get", 63 + tags: ["status_report"], 64 + description: "Get all status reports", 65 + path: "/", 66 + request: {}, 67 + responses: { 68 + 200: { 69 + content: { 70 + "application/json": { 71 + schema: z.array(statusReportExtendedSchema), 72 + }, 73 + }, 74 + description: "Get all status reports", 75 + }, 76 + 400: { 77 + content: { 78 + "application/json": { 79 + schema: ErrorSchema, 80 + }, 81 + }, 82 + description: "Returns an error", 83 + }, 84 + }, 85 + }); 86 + 87 + statusReportApi.openapi(getAllRoute, async (c) => { 88 + const workspaceId = Number(c.get("workspaceId")); 89 + const _statusReports = await db.query.statusReport.findMany({ 90 + with: { 91 + statusReportUpdates: true, 92 + }, 93 + where: eq(statusReport.workspaceId, workspaceId), 94 + }); 95 + 96 + if (!_statusReports) return c.jsonT({ code: 404, message: "Not Found" }); 97 + 98 + const data = z.array(statusReportExtendedSchema).parse( 99 + _statusReports.map((statusReport) => ({ 100 + ...statusReport, 101 + status_report_updates: statusReport.statusReportUpdates.map( 102 + (statusReportUpdate) => { 103 + return statusReportUpdate.id; 104 + }, 105 + ), 106 + })), 107 + ); 108 + 109 + return c.jsonT(data); 110 + }); 111 + 112 + const getRoute = createRoute({ 113 + method: "get", 114 + tags: ["status_report"], 115 + description: "Get an status report", 116 + path: "/:id", 117 + request: { 118 + params: ParamsSchema, 119 + }, 120 + responses: { 121 + 200: { 122 + content: { 123 + "application/json": { 124 + schema: statusReportExtendedSchema, 125 + }, 126 + }, 127 + description: "Get all status reports", 128 + }, 129 + 400: { 130 + content: { 131 + "application/json": { 132 + schema: ErrorSchema, 133 + }, 134 + }, 135 + description: "Returns an error", 136 + }, 137 + }, 138 + }); 139 + 140 + statusReportApi.openapi(getRoute, async (c) => { 141 + const workspaceId = Number(c.get("workspaceId")); 142 + const { id } = c.req.valid("param"); 143 + 144 + const statusUpdateId = Number(id); 145 + const _statusUpdate = await db.query.statusReport.findFirst({ 146 + with: { 147 + statusReportUpdates: true, 148 + }, 149 + where: and( 150 + eq(statusReport.workspaceId, workspaceId), 151 + eq(statusReport.id, statusUpdateId), 152 + ), 153 + }); 154 + 155 + if (!_statusUpdate) return c.jsonT({ code: 404, message: "Not Found" }); 156 + const data = statusReportExtendedSchema.parse({ 157 + ..._statusUpdate, 158 + status_report_updates: _statusUpdate.statusReportUpdates.map( 159 + (update) => update.id, 160 + ), 161 + }); 162 + 163 + return c.jsonT(data); 164 + }); 165 + 166 + const postRoute = createRoute({ 167 + method: "post", 168 + tags: ["status_report"], 169 + description: "Create an status report", 170 + path: "/", 171 + request: { 172 + body: { 173 + description: "The status report to create", 174 + content: { 175 + "application/json": { 176 + schema: statusSchema, 177 + }, 178 + }, 179 + }, 180 + }, 181 + responses: { 182 + 200: { 183 + content: { 184 + "application/json": { 185 + schema: statusReportExtendedSchema, 186 + }, 187 + }, 188 + description: "Status report created", 189 + }, 190 + 400: { 191 + content: { 192 + "application/json": { 193 + schema: ErrorSchema, 194 + }, 195 + }, 196 + description: "Returns an error", 197 + }, 198 + }, 199 + }); 200 + 201 + statusReportApi.openapi(postRoute, async (c) => { 202 + const input = c.req.valid("json"); 203 + const workspaceId = Number(c.get("workspaceId")); 204 + 205 + const _newStatusReport = await db 206 + .insert(statusReport) 207 + .values({ 208 + ...input, 209 + workspaceId: workspaceId, 210 + }) 211 + .returning() 212 + .get(); 213 + 214 + const data = statusReportExtendedSchema.parse(_newStatusReport); 215 + 216 + return c.jsonT(data); 217 + }); 218 + 219 + const deleteRoute = createRoute({ 220 + method: "delete", 221 + tags: ["status_report"], 222 + description: "Delete an status report", 223 + path: "/:id", 224 + request: { 225 + params: ParamsSchema, 226 + }, 227 + responses: { 228 + 200: { 229 + content: { 230 + "application/json": { 231 + schema: z.object({ 232 + message: z.string().openapi({ 233 + example: "Deleted", 234 + }), 235 + }), 236 + }, 237 + }, 238 + description: "Status report deleted", 239 + }, 240 + 400: { 241 + content: { 242 + "application/json": { 243 + schema: ErrorSchema, 244 + }, 245 + }, 246 + description: "Returns an error", 247 + }, 248 + }, 249 + }); 250 + 251 + statusReportApi.openapi(deleteRoute, async (c) => { 252 + const workspaceId = Number(c.get("workspaceId")); 253 + const { id } = c.req.valid("param"); 254 + 255 + const statusReportId = Number(id); 256 + const _statusReport = await db 257 + .select() 258 + .from(statusReport) 259 + .where(eq(statusReport.id, statusReportId)) 260 + .get(); 261 + 262 + if (!_statusReport) return c.jsonT({ code: 404, message: "Not Found" }); 263 + 264 + if (workspaceId !== _statusReport.workspaceId) 265 + return c.jsonT({ code: 401, message: "Unauthorized" }); 266 + 267 + await db 268 + .delete(statusReport) 269 + .where(eq(statusReport.id, statusReportId)) 270 + .run(); 271 + return c.jsonT({ message: "Deleted" }); 272 + }); 273 + 274 + const postRouteUpdate = createRoute({ 275 + method: "post", 276 + tags: ["status_report"], 277 + path: "/:id/update", 278 + description: "Create an status report update", 279 + request: { 280 + params: ParamsSchema, 281 + body: { 282 + description: "the status report update", 283 + content: { 284 + "application/json": { 285 + schema: createStatusReportUpdateSchema, 286 + }, 287 + }, 288 + }, 289 + }, 290 + responses: { 291 + 200: { 292 + content: { 293 + "application/json": { 294 + schema: statusUpdateSchema, 295 + }, 296 + }, 297 + description: "Status report updated", 298 + }, 299 + 400: { 300 + content: { 301 + "application/json": { 302 + schema: ErrorSchema, 303 + }, 304 + }, 305 + description: "Returns an error", 306 + }, 307 + }, 308 + }); 309 + 310 + statusReportApi.openapi(postRouteUpdate, async (c) => { 311 + const input = c.req.valid("json"); 312 + const { id } = c.req.valid("param"); 313 + const workspaceId = Number(c.get("workspaceId")); 314 + 315 + const statusReportId = Number(id); 316 + const _statusReport = await db 317 + .select() 318 + .from(statusReport) 319 + .where( 320 + and( 321 + eq(statusReport.id, statusReportId), 322 + eq(statusReport.workspaceId, workspaceId), 323 + ), 324 + ) 325 + .get(); 326 + 327 + if (!_statusReport) return c.jsonT({ code: 401, message: "Not authorized" }); 328 + 329 + const _statusReportUpdate = await db 330 + .insert(statusReportUpdate) 331 + .values({ 332 + ...input, 333 + date: new Date(input.date), 334 + statusReportId: Number(id), 335 + }) 336 + .returning() 337 + .get(); 338 + 339 + const data = statusUpdateSchema.parse(_statusReportUpdate); 340 + 341 + return c.jsonT({ 342 + ...data, 343 + }); 344 + }); 345 + 346 + export { statusReportApi };
+10 -10
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/_components/action-button.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/_components/action-button.tsx
··· 6 6 import { MoreVertical } from "lucide-react"; 7 7 import type * as z from "zod"; 8 8 9 - import { 10 - insertIncidentSchema, 11 - // insertIncidentUpdateSchema, 12 - } from "@openstatus/db/src/schema"; 9 + import { insertStatusReportSchema } from "@openstatus/db/src/schema"; 13 10 import { 14 11 AlertDialog, 15 12 AlertDialogAction, ··· 31 28 import { useToastAction } from "@/hooks/use-toast-action"; 32 29 import { api } from "@/trpc/client"; 33 30 34 - const temporary = insertIncidentSchema.pick({ id: true, workspaceSlug: true }); 31 + const temporary = insertStatusReportSchema.pick({ 32 + id: true, 33 + workspaceSlug: true, 34 + }); 35 35 36 36 type Schema = z.infer<typeof temporary>; 37 37 ··· 41 41 const [alertOpen, setAlertOpen] = React.useState(false); 42 42 const [isPending, startTransition] = React.useTransition(); 43 43 44 - async function deleteIncident() { 44 + async function deeteStatusReport() { 45 45 startTransition(async () => { 46 46 try { 47 47 if (!props.id) return; 48 - await api.incident.deleteIncident.mutate({ id: props.id }); 48 + await api.statusReport.deleteStatusReport.mutate({ id: props.id }); 49 49 toast("deleted"); 50 50 router.refresh(); 51 51 setAlertOpen(false); ··· 68 68 </Button> 69 69 </DropdownMenuTrigger> 70 70 <DropdownMenuContent align="end"> 71 - <Link href={`./incidents/edit?id=${props.id}`}> 71 + <Link href={`./status-reports/edit?id=${props.id}`}> 72 72 <DropdownMenuItem>Edit</DropdownMenuItem> 73 73 </Link> 74 74 <AlertDialogTrigger asChild> ··· 83 83 <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 84 84 <AlertDialogDescription> 85 85 This action cannot be undone. This will permanently delete the 86 - incident. 86 + status report. 87 87 </AlertDialogDescription> 88 88 </AlertDialogHeader> 89 89 <AlertDialogFooter> ··· 91 91 <AlertDialogAction 92 92 onClick={(e) => { 93 93 e.preventDefault(); 94 - deleteIncident(); 94 + deeteStatusReport(); 95 95 }} 96 96 disabled={isPending} 97 97 className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+3 -3
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/_components/delete-incident-update.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/_components/delete-status-update.tsx
··· 21 21 import { useToastAction } from "@/hooks/use-toast-action"; 22 22 import { api } from "@/trpc/client"; 23 23 24 - export function DeleteIncidentUpdateButtonIcon({ id }: { id: number }) { 24 + export function DeleteStatusReportUpdateButtonIcon({ id }: { id: number }) { 25 25 const router = useRouter(); 26 26 const { toast } = useToastAction(); 27 27 const [alertOpen, setAlertOpen] = React.useState(false); ··· 30 30 async function onDelete() { 31 31 startTransition(async () => { 32 32 try { 33 - await api.incident.deleteIncidentUpdate.mutate({ id }); 33 + await api.statusReport.deleteStatusReportUpdate.mutate({ id }); 34 34 toast("deleted"); 35 35 router.refresh(); 36 36 setAlertOpen(false); ··· 56 56 <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 57 57 <AlertDialogDescription> 58 58 This action cannot be undone. This will permanently delete the 59 - monitor. 59 + status report update. 60 60 </AlertDialogDescription> 61 61 </AlertDialogHeader> 62 62 <AlertDialogFooter>
+3 -3
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/_components/empty-state.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/_components/empty-state.tsx
··· 8 8 return ( 9 9 <DefaultEmptyState 10 10 icon="siren" 11 - title="No incidents" 12 - description="Create your first incident" 11 + title="No status reports" 12 + description="Create your first status report" 13 13 action={ 14 14 <Button asChild> 15 - <Link href="./incidents/edit">Create</Link> 15 + <Link href="./status-reports/edit">Create</Link> 16 16 </Button> 17 17 } 18 18 />
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/edit/loading.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/edit/loading.tsx
+15 -10
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/edit/page.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/edit/page.tsx
··· 2 2 import * as z from "zod"; 3 3 4 4 import { Header } from "@/components/dashboard/header"; 5 - import { IncidentForm } from "@/components/forms/incident-form"; 5 + import { StatusReportForm } from "@/components/forms/status-report-form"; 6 6 import { api } from "@/trpc/server"; 7 7 8 8 /** ··· 27 27 28 28 const { id } = search.data; 29 29 30 - const incident = id 31 - ? await api.incident.getIncidentById.query({ 30 + const statusUpdate = id 31 + ? await api.statusReport.getStatusReportById.query({ 32 32 id, 33 33 }) 34 34 : undefined; ··· 39 39 40 40 return ( 41 41 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 42 - <Header title="Incident" description="Upsert your incident." /> 42 + <Header 43 + title="Status Report" 44 + description="Create a public report for your incident" 45 + /> 43 46 <div className="col-span-full"> 44 - <IncidentForm 47 + <StatusReportForm 45 48 monitors={monitors} 46 49 pages={pages} 47 50 defaultValues={ 48 - incident 51 + statusUpdate 49 52 ? // TODO: we should move the mapping to the trpc layer 50 53 // so we don't have to do this in the UI 51 - // it should be something like defaultValues={incident} 54 + // it should be something like defaultValues={statusReport} 52 55 { 53 - ...incident, 54 - monitors: incident?.monitorsToIncidents.map( 56 + ...statusUpdate, 57 + monitors: statusUpdate?.monitorsToStatusReports.map( 55 58 ({ monitorId }) => monitorId, 56 59 ), 57 - pages: incident?.pagesToIncidents.map(({ pageId }) => pageId), 60 + pages: statusUpdate?.pagesToStatusReports.map( 61 + ({ pageId }) => pageId, 62 + ), 58 63 message: "", 59 64 } 60 65 : undefined
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/loading.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/loading.tsx
+18 -18
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/page.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/page.tsx
··· 8 8 import { Header } from "@/components/dashboard/header"; 9 9 import { HelpCallout } from "@/components/dashboard/help-callout"; 10 10 import { Icons } from "@/components/icons"; 11 - import { AffectedMonitors } from "@/components/incidents/affected-monitors"; 12 - import { Events } from "@/components/incidents/events"; 13 - import { StatusBadge } from "@/components/incidents/status-badge"; 11 + import { AffectedMonitors } from "@/components/status-update/affected-monitors"; 12 + import { Events } from "@/components/status-update/events"; 13 + import { StatusBadge } from "@/components/status-update/status-badge"; 14 14 import { statusDict } from "@/data/incidents-dictionary"; 15 15 import { api } from "@/trpc/server"; 16 16 import { ActionButton } from "./_components/action-button"; 17 17 import { EmptyState } from "./_components/empty-state"; 18 18 19 - export default async function IncidentPage({ 19 + export default async function StatusReportsPage({ 20 20 params, 21 21 }: { 22 22 params: { workspaceSlug: string }; 23 23 }) { 24 - const incidents = await api.incident.getIncidentByWorkspace.query(); 24 + const reports = await api.statusReport.getStatusReportByWorkspace.query(); 25 25 return ( 26 26 <div className="grid min-h-full grid-cols-1 grid-rows-[auto,1fr,auto] gap-6 md:grid-cols-1 md:gap-8"> 27 27 <Header 28 - title="Incidents" 29 - description="Overview of all your incidents." 28 + title="Status Reports" 29 + description="Overview of all your status reports and updates." 30 30 actions={ 31 31 <Button asChild> 32 - <Link href="./incidents/edit">Create</Link> 32 + <Link href="./status-reports/edit">Create</Link> 33 33 </Button> 34 34 } 35 35 /> 36 - {Boolean(incidents?.length) ? ( 36 + {Boolean(reports?.length) ? ( 37 37 <div className="col-span-full"> 38 38 <ul role="list" className="grid gap-4 sm:col-span-6"> 39 - {incidents?.map((incident, i) => { 39 + {reports?.map((report, i) => { 40 40 const { label, icon } = 41 - statusDict[incident.status as keyof typeof statusDict]; 42 - const monitors = incident.monitorsToIncidents.map( 41 + statusDict[report.status as keyof typeof statusDict]; 42 + const monitors = report.monitorsToStatusReports.map( 43 43 ({ monitor }) => monitor, 44 44 ); 45 45 return ( 46 46 <li key={i} className="grid gap-2"> 47 47 <time className="text-muted-foreground pl-3 text-xs"> 48 - {formatDistance(new Date(incident.createdAt!), new Date(), { 48 + {formatDistance(new Date(report.createdAt!), new Date(), { 49 49 addSuffix: true, 50 50 })} 51 51 </time> 52 52 <Container 53 53 title={ 54 54 <span className="flex flex-wrap gap-2"> 55 - {incident.title} 56 - <StatusBadge status={incident.status} /> 55 + {report.title} 56 + <StatusBadge status={report.status} /> 57 57 </span> 58 58 } 59 59 actions={[ 60 60 <Button key="status-button" variant="outline" size="sm"> 61 61 <Link 62 - href={`./incidents/update/edit?incidentId=${incident.id}`} 62 + href={`./status-reports/update/edit?id=${report.id}`} 63 63 > 64 64 New Update 65 65 </Link> 66 66 </Button>, 67 - <ActionButton key="action-button" id={incident.id} />, 67 + <ActionButton key="action-button" id={report.id} />, 68 68 ]} 69 69 > 70 70 <div className="grid gap-4"> ··· 82 82 </p> 83 83 {/* Make it ordered by desc and make it toggable if you want the whole history! */} 84 84 <Events 85 - incidentUpdates={incident.incidentUpdates} 85 + statusReportUpdates={report.statusReportUpdates} 86 86 editable 87 87 /> 88 88 </div>
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/update/edit/loading.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/update/edit/loading.tsx
-51
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/update/edit/page.tsx
··· 1 - import { notFound } from "next/navigation"; 2 - import * as z from "zod"; 3 - 4 - import { Header } from "@/components/dashboard/header"; 5 - import { IncidentUpdateForm } from "@/components/forms/incident-update-form"; 6 - import { api } from "@/trpc/server"; 7 - 8 - /** 9 - * allowed URL search params 10 - */ 11 - const searchParamsSchema = z.object({ 12 - id: z.coerce.number().optional(), 13 - incidentId: z.coerce.number(), 14 - }); 15 - 16 - export default async function EditPage({ 17 - params, 18 - searchParams, 19 - }: { 20 - params: { workspaceSlug: string }; 21 - searchParams: { [key: string]: string | string[] | undefined }; 22 - }) { 23 - const search = searchParamsSchema.safeParse(searchParams); 24 - 25 - if (!search.success) { 26 - return notFound(); 27 - } 28 - 29 - const { id, incidentId } = search.data; 30 - 31 - const incidentUpdate = id 32 - ? await api.incident.getIncidentUpdateById.query({ 33 - id, 34 - }) 35 - : undefined; 36 - 37 - return ( 38 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 39 - <Header 40 - title="Incident Update" 41 - description="Upsert your incident update." 42 - /> 43 - <div className="col-span-full"> 44 - <IncidentUpdateForm 45 - incidentId={incidentId} 46 - defaultValues={incidentUpdate || undefined} 47 - /> 48 - </div> 49 - </div> 50 - ); 51 - }
+1 -1
apps/web/src/app/app/(dashboard)/[workspaceSlug]/integrations/page.tsx
··· 6 6 import { Header } from "@/components/dashboard/header"; 7 7 import { api } from "@/trpc/server"; 8 8 9 - export default async function IncidentPage({ 9 + export default async function IntegrationPage({ 10 10 params, 11 11 }: { 12 12 params: { workspaceSlug: string };
+51
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-reports/update/edit/page.tsx
··· 1 + import { notFound } from "next/navigation"; 2 + import * as z from "zod"; 3 + 4 + import { Header } from "@/components/dashboard/header"; 5 + import { StatusReportUpdateForm } from "@/components/forms/status-report-update-form"; 6 + import { api } from "@/trpc/server"; 7 + 8 + /** 9 + * allowed URL search params 10 + */ 11 + const searchParamsSchema = z.object({ 12 + id: z.coerce.number(), 13 + statusUpdate: z.coerce.number().optional(), 14 + }); 15 + 16 + export default async function EditPage({ 17 + params, 18 + searchParams, 19 + }: { 20 + params: { workspaceSlug: string }; 21 + searchParams: { [key: string]: string | string[] | undefined }; 22 + }) { 23 + const search = searchParamsSchema.safeParse(searchParams); 24 + 25 + if (!search.success) { 26 + return notFound(); 27 + } 28 + 29 + const { id, statusUpdate } = search.data; 30 + 31 + const data = statusUpdate 32 + ? await api.statusReport.getStatusReportUpdateById.query({ 33 + id: statusUpdate, 34 + }) 35 + : undefined; 36 + 37 + return ( 38 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 39 + <Header 40 + title="Status Report Update" 41 + description="Create a public update for your incident" 42 + /> 43 + <div className="col-span-full"> 44 + <StatusReportUpdateForm 45 + statusReportId={id} 46 + defaultValues={data || undefined} 47 + /> 48 + </div> 49 + </div> 50 + ); 51 + }
+1
apps/web/src/app/page.tsx
··· 19 19 <Example /> 20 20 <Cards {...cardConfig.monitors} /> 21 21 <Stats /> 22 + {/* TODO: rename to `reports` */} 22 23 <Cards {...cardConfig.incidents} /> 23 24 <Partners /> 24 25 <Cards {...cardConfig.pages} />
+1 -1
apps/web/src/app/status-page/[domain]/incidents/page.tsx
··· 28 28 description={page.description} 29 29 className="text-left" 30 30 /> 31 - <IncidentList incidents={page.incidents} monitors={page.monitors} /> 31 + <IncidentList incidents={page.statusReports} monitors={page.monitors} /> 32 32 </div> 33 33 ); 34 34 }
+7 -3
apps/web/src/app/status-page/[domain]/page.tsx
··· 36 36 } 37 37 38 38 const isEmptyState = !( 39 - Boolean(page.monitors.length) || Boolean(page.incidents.length) 39 + Boolean(page.monitors.length) || Boolean(page.statusReports.length) 40 40 ); 41 41 42 42 return ( ··· 59 59 /> 60 60 ) : ( 61 61 <> 62 - <StatusCheck incidents={page.incidents} monitors={page.monitors} /> 62 + <StatusCheck 63 + statusReports={page.statusReports} 64 + monitors={page.monitors} 65 + /> 63 66 <MonitorList monitors={page.monitors} /> 67 + {/* TODO: rename to StatusReportList */} 64 68 <IncidentList 65 - incidents={page.incidents} 69 + incidents={page.statusReports} 66 70 monitors={page.monitors} 67 71 context="latest" 68 72 />
+27 -23
apps/web/src/components/forms/incident-form.tsx apps/web/src/components/forms/status-report-form.tsx
··· 5 5 import { zodResolver } from "@hookform/resolvers/zod"; 6 6 import { useForm } from "react-hook-form"; 7 7 8 - import type { InsertIncident, Monitor, Page } from "@openstatus/db/src/schema"; 9 8 import { 10 - incidentStatus, 11 - incidentStatusSchema, 12 - insertIncidentSchema, 9 + insertStatusReportSchema, 10 + statusReportStatus, 11 + statusReportStatusSchema, 12 + } from "@openstatus/db/src/schema"; 13 + import type { 14 + InsertStatusReport, 15 + Monitor, 16 + Page, 13 17 } from "@openstatus/db/src/schema"; 14 18 import { 15 19 Accordion, ··· 45 49 import { api } from "@/trpc/client"; 46 50 47 51 interface Props { 48 - defaultValues?: InsertIncident; 52 + defaultValues?: InsertStatusReport; 49 53 monitors?: Monitor[]; 50 54 pages?: Page[]; 51 55 } 52 56 53 - export function IncidentForm({ defaultValues, monitors, pages }: Props) { 54 - const form = useForm<InsertIncident>({ 55 - resolver: zodResolver(insertIncidentSchema), 57 + export function StatusReportForm({ defaultValues, monitors, pages }: Props) { 58 + const form = useForm<InsertStatusReport>({ 59 + resolver: zodResolver(insertStatusReportSchema), 56 60 defaultValues: defaultValues 57 61 ? { 58 62 id: defaultValues.id, ··· 73 77 const [isPending, startTransition] = React.useTransition(); 74 78 const { toast } = useToastAction(); 75 79 76 - const onSubmit = ({ ...props }: InsertIncident) => { 80 + const onSubmit = ({ ...props }: InsertStatusReport) => { 77 81 startTransition(async () => { 78 82 try { 79 83 if (defaultValues) { 80 - await api.incident.updateIncident.mutate({ ...props }); 84 + await api.statusReport.updateStatusReport.mutate({ ...props }); 81 85 } else { 82 - // or use createIncident to create automaticaaly an IncidentUpdate? 83 86 const { message, date, status, ...rest } = props; 84 - const incident = await api.incident.createIncident.mutate({ 85 - status, 86 - message, 87 - ...rest, 88 - }); 87 + const statusReport = await api.statusReport.createStatusReport.mutate( 88 + { 89 + status, 90 + message, 91 + ...rest, 92 + }, 93 + ); 89 94 // include update on creation 90 - if (incident?.id) { 91 - await api.incident.createIncidentUpdate.mutate({ 95 + if (statusReport?.id) { 96 + await api.statusReport.createStatusReportUpdate.mutate({ 92 97 message, 93 98 date, 94 99 status, 95 - incidentId: incident.id, 100 + statusReportId: statusReport.id, 96 101 }); 97 102 } 98 103 } ··· 104 109 }); 105 110 }; 106 111 107 - console.log(form.formState.errors); 108 112 return ( 109 113 <Form {...form}> 110 114 <form ··· 146 150 <FormMessage /> 147 151 <RadioGroup 148 152 onValueChange={(value) => 149 - field.onChange(incidentStatusSchema.parse(value)) 153 + field.onChange(statusReportStatusSchema.parse(value)) 150 154 } // value is a string 151 155 defaultValue={field.value} 152 156 className="grid grid-cols-2 gap-4 sm:grid-cols-4" 153 157 > 154 - {incidentStatus.map((status) => { 158 + {statusReportStatus.map((status) => { 155 159 const { value, label, icon } = statusDict[status]; 156 160 const Icon = Icons[icon]; 157 161 return ( ··· 317 321 <div className="grid gap-4 sm:grid-cols-3"> 318 322 <div className="my-1.5 flex flex-col gap-2"> 319 323 <p className="text-sm font-semibold leading-none"> 320 - Incident Update 324 + Status Update 321 325 </p> 322 326 <p className="text-muted-foreground text-sm"> 323 327 What is actually going wrong?
+18 -15
apps/web/src/components/forms/incident-update-form.tsx apps/web/src/components/forms/status-report-update-form.tsx
··· 5 5 import { zodResolver } from "@hookform/resolvers/zod"; 6 6 import { useForm } from "react-hook-form"; 7 7 8 - import type { InsertIncidentUpdate } from "@openstatus/db/src/schema"; 8 + import type { InsertStatusReportUpdate } from "@openstatus/db/src/schema"; 9 9 import { 10 - incidentStatus, 11 - incidentStatusSchema, 12 - insertIncidentUpdateSchema, 10 + insertStatusReportUpdateSchema, 11 + statusReportStatus, 12 + statusReportStatusSchema, 13 13 } from "@openstatus/db/src/schema"; 14 14 import { 15 15 Button, ··· 38 38 import { api } from "@/trpc/client"; 39 39 40 40 interface Props { 41 - defaultValues?: InsertIncidentUpdate; 42 - incidentId: number; 41 + defaultValues?: InsertStatusReportUpdate; 42 + statusReportId: number; 43 43 } 44 44 45 - export function IncidentUpdateForm({ defaultValues, incidentId }: Props) { 46 - const form = useForm<InsertIncidentUpdate>({ 47 - resolver: zodResolver(insertIncidentUpdateSchema), 45 + export function StatusReportUpdateForm({ 46 + defaultValues, 47 + statusReportId, 48 + }: Props) { 49 + const form = useForm<InsertStatusReportUpdate>({ 50 + resolver: zodResolver(insertStatusReportUpdateSchema), 48 51 defaultValues: { 49 52 id: defaultValues?.id || 0, 50 53 status: defaultValues?.status || "investigating", 51 54 message: defaultValues?.message, 52 55 date: defaultValues?.date || new Date(), 53 - incidentId, 56 + statusReportId, 54 57 }, 55 58 }); 56 59 const router = useRouter(); 57 60 const [isPending, startTransition] = React.useTransition(); 58 61 const { toast } = useToastAction(); 59 62 60 - const onSubmit = ({ ...props }: InsertIncidentUpdate) => { 63 + const onSubmit = ({ ...props }: InsertStatusReportUpdate) => { 61 64 startTransition(async () => { 62 65 try { 63 66 if (defaultValues) { 64 - await api.incident.updateIncidentUpdate.mutate({ ...props }); 67 + await api.statusReport.updateStatusReportUpdate.mutate({ ...props }); 65 68 } else { 66 - await api.incident.createIncidentUpdate.mutate({ ...props }); 69 + await api.statusReport.createStatusReportUpdate.mutate({ ...props }); 67 70 } 68 71 toast("saved"); 69 72 router.refresh(); ··· 100 103 <FormMessage /> 101 104 <RadioGroup 102 105 onValueChange={(value) => 103 - field.onChange(incidentStatusSchema.parse(value)) 106 + field.onChange(statusReportStatusSchema.parse(value)) 104 107 } // value is a string 105 108 defaultValue={field.value} 106 109 className="grid grid-cols-2 gap-4 sm:grid-cols-4" 107 110 > 108 - {incidentStatus.map((status) => { 111 + {statusReportStatus.map((status) => { 109 112 const { value, label, icon } = statusDict[status]; 110 113 const Icon = Icons[icon]; 111 114 return (
+2
apps/web/src/components/icons.tsx
··· 13 13 Laptop, 14 14 LayoutDashboard, 15 15 Link, 16 + Megaphone, 16 17 MessageCircle, 17 18 Minus, 18 19 Moon, ··· 66 67 bell: Bell, 67 68 zap: Zap, 68 69 "alert-triangle": AlertTriangle, 70 + megaphone: Megaphone, 69 71 minus: Minus, 70 72 sun: SunMedium, 71 73 moon: Moon,
apps/web/src/components/incidents/affected-monitors.tsx apps/web/src/components/status-update/affected-monitors.tsx
+9 -9
apps/web/src/components/incidents/events.tsx apps/web/src/components/status-update/events.tsx
··· 5 5 import { format, formatDistance } from "date-fns"; 6 6 import type * as z from "zod"; 7 7 8 - import type { selectIncidentUpdateSchema } from "@openstatus/db/src/schema"; 8 + import type { selectStatusReportUpdateSchema } from "@openstatus/db/src/schema"; 9 9 import { 10 10 Button, 11 11 Tooltip, ··· 14 14 TooltipTrigger, 15 15 } from "@openstatus/ui"; 16 16 17 + import { DeleteStatusReportUpdateButtonIcon } from "@/app/app/(dashboard)/[workspaceSlug]/status-reports/_components/delete-status-update"; 17 18 import { Icons } from "@/components/icons"; 18 19 import { statusDict } from "@/data/incidents-dictionary"; 19 20 import { useProcessor } from "@/hooks/use-preprocessor"; 20 21 import { cn } from "@/lib/utils"; 21 - import { DeleteIncidentUpdateButtonIcon } from "../../app/app/(dashboard)/[workspaceSlug]/incidents/_components/delete-incident-update"; 22 22 23 - type IncidentUpdateProps = z.infer<typeof selectIncidentUpdateSchema>; 23 + type StatusReportUpdateProps = z.infer<typeof selectStatusReportUpdateSchema>; 24 24 25 25 export function Events({ 26 - incidentUpdates, 26 + statusReportUpdates, 27 27 editable = false, 28 28 }: { 29 - incidentUpdates: IncidentUpdateProps[]; 29 + statusReportUpdates: StatusReportUpdateProps[]; 30 30 editable?: boolean; 31 31 }) { 32 32 const [open, toggle] = React.useReducer((open) => !open, false); 33 33 const router = useRouter(); 34 34 35 35 // TODO: make it simpler.. 36 - const sortedArray = incidentUpdates.sort((a, b) => { 36 + const sortedArray = statusReportUpdates.sort((a, b) => { 37 37 const orderA = statusDict[a.status].order; 38 38 const orderB = statusDict[b.status].order; 39 39 return orderB - orderA; ··· 75 75 className="h-7 w-7 p-0" 76 76 onClick={() => { 77 77 router.push( 78 - `./incidents/update/edit?incidentId=${update.incidentId}&id=${update.id}`, 78 + `./status-reports/update/edit?id=${update.statusReportId}&statusUpdate=${update.id}`, 79 79 ); 80 80 }} 81 81 > 82 82 <Icons.pencil className="h-4 w-4" /> 83 83 </Button> 84 - <DeleteIncidentUpdateButtonIcon id={update.id} /> 84 + <DeleteStatusReportUpdateButtonIcon id={update.id} /> 85 85 </div> 86 86 ) : undefined} 87 87 <div className="flex items-center justify-between gap-4"> ··· 106 106 ); 107 107 })} 108 108 109 - {incidentUpdates.length > 1 ? ( 109 + {statusReportUpdates.length > 1 ? ( 110 110 <div className="text-center"> 111 111 <Button variant="ghost" onClick={toggle}> 112 112 {open ? "Close" : "More"}
apps/web/src/components/incidents/status-badge.tsx apps/web/src/components/status-update/status-badge.tsx
+9 -9
apps/web/src/components/status-page/incident-list.tsx
··· 1 1 import type { z } from "zod"; 2 2 3 3 import type { 4 - selectIncidentsPageSchema, 5 4 selectPublicMonitorSchema, 5 + selectStatusReportPageSchema, 6 6 } from "@openstatus/db/src/schema"; 7 7 8 8 import { notEmpty } from "@/lib/utils"; 9 - import { AffectedMonitors } from "../incidents/affected-monitors"; 10 - import { Events } from "../incidents/events"; 11 - import { StatusBadge } from "../incidents/status-badge"; 9 + import { AffectedMonitors } from "../status-update/affected-monitors"; 10 + import { Events } from "../status-update/events"; 11 + import { StatusBadge } from "../status-update/status-badge"; 12 12 13 13 // TODO: change layout - it is too packed with data rn 14 14 ··· 17 17 monitors, 18 18 context = "all", 19 19 }: { 20 - incidents: z.infer<typeof selectIncidentsPageSchema>; 20 + incidents: z.infer<typeof selectStatusReportPageSchema>; 21 21 monitors: z.infer<typeof selectPublicMonitorSchema>[]; 22 22 context?: "all" | "latest"; // latest 7 days 23 23 }) => { ··· 25 25 26 26 function getLastWeeksIncidents() { 27 27 return incidents.filter((incident) => { 28 - return incident.incidentUpdates.some( 28 + return incident.statusReportUpdates.some( 29 29 (update) => update.date.getTime() > lastWeek, 30 30 ); 31 31 }); ··· 45 45 {context === "all" ? "All incidents" : "Latest incidents"} 46 46 </h2> 47 47 {_incidents.map((incident) => { 48 - const affectedMonitors = incident.monitorsToIncidents 48 + const affectedMonitors = incident.monitorsToStatusReport 49 49 .map(({ monitorId }) => { 50 50 const monitor = monitors.find(({ id }) => monitorId === id); 51 51 return monitor || undefined; ··· 63 63 Affected Monitors 64 64 </p> 65 65 <AffectedMonitors 66 - monitors={incident.monitorsToIncidents 66 + monitors={incident.monitorsToStatusReport 67 67 .map(({ monitorId }) => { 68 68 const monitor = monitors.find( 69 69 ({ id }) => monitorId === id, ··· 78 78 <p className="text-muted-foreground mb-2 text-xs"> 79 79 Latest Updates 80 80 </p> 81 - <Events incidentUpdates={incident.incidentUpdates} /> 81 + <Events statusReportUpdates={incident.statusReportUpdates} /> 82 82 </div> 83 83 </div> 84 84 );
+4 -4
apps/web/src/components/status-page/status-check.tsx
··· 2 2 import type { z } from "zod"; 3 3 4 4 import type { 5 - selectIncidentsPageSchema, 6 5 selectPublicMonitorSchema, 6 + selectStatusReportPageSchema, 7 7 } from "@openstatus/db/src/schema"; 8 8 9 9 import { getResponseListData } from "@/lib/tb"; ··· 28 28 }); 29 29 30 30 export async function StatusCheck({ 31 - incidents, 31 + statusReports, 32 32 monitors, 33 33 }: { 34 - incidents: z.infer<typeof selectIncidentsPageSchema>; 34 + statusReports: z.infer<typeof selectStatusReportPageSchema>; 35 35 monitors: z.infer<typeof selectPublicMonitorSchema>[]; 36 36 }) { 37 - const isIncident = incidents.some( 37 + const isIncident = statusReports.some( 38 38 (incident) => !["monitoring", "resolved"].includes(incident.status), 39 39 ); 40 40
+2 -2
apps/web/src/config/features.ts
··· 85 85 }, 86 86 { 87 87 icon: "message-circle", 88 - catchline: "Inform.", 89 - description: "Keep your teams and users updated with incident updates.", 88 + catchline: "Status Reports.", 89 + description: "Keep your teams and users updated with status updates.", 90 90 }, 91 91 { 92 92 icon: "zap",
+6 -6
apps/web/src/config/pages.ts
··· 23 23 icon: "panel-top", 24 24 }, 25 25 { 26 + title: "Status Reports", 27 + description: "War room where you handle the incidents.", 28 + href: "/status-reports", 29 + icon: "megaphone", 30 + }, 31 + { 26 32 title: "Notifications", 27 33 description: "Where you can see all the notifications.", 28 34 href: "/notifications", 29 35 icon: "bell", 30 - }, 31 - { 32 - title: "Incidents", 33 - description: "War room where you handle the incidents.", 34 - href: "/incidents", 35 - icon: "siren", 36 36 }, 37 37 // { 38 38 // title: "Integrations",
-18
fly.toml
··· 1 - # fly.toml app configuration file generated for openstatus-api on 2023-09-13T17:29:05+02:00 2 - # 3 - # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 - # 5 - 6 - app = "openstatus-api" 7 - primary_region = "ams" 8 - 9 - [build] 10 - dockerfile = "apps/server/Dockerfile" 11 - 12 - [http_service] 13 - internal_port = 3000 14 - force_https = true 15 - auto_stop_machines = true 16 - auto_start_machines = true 17 - min_machines_running = 0 18 - processes = ["app"]
+1 -1
packages/api/src/analytics.ts
··· 44 44 }); 45 45 } 46 46 47 - export async function trackNewIncident() {} 47 + export async function trackNewStatusReport() {}
+2 -2
packages/api/src/edge.ts
··· 1 1 import { domainRouter } from "./router/domain"; 2 - import { incidentRouter } from "./router/incident"; 3 2 import { integrationRouter } from "./router/integration"; 4 3 import { monitorRouter } from "./router/monitor"; 5 4 import { notificationRouter } from "./router/notification"; 6 5 import { pageRouter } from "./router/page"; 6 + import { statusReportRouter } from "./router/statusReport"; 7 7 import { userRouter } from "./router/user"; 8 8 import { workspaceRouter } from "./router/workspace"; 9 9 import { createTRPCRouter } from "./trpc"; ··· 13 13 workspace: workspaceRouter, 14 14 monitor: monitorRouter, 15 15 page: pageRouter, 16 - incident: incidentRouter, 16 + statusReport: statusReportRouter, 17 17 domain: domainRouter, 18 18 integration: integrationRouter, 19 19 user: userRouter,
-317
packages/api/src/router/incident.ts
··· 1 - import { z } from "zod"; 2 - 3 - import { and, eq, inArray } from "@openstatus/db"; 4 - import { 5 - incident, 6 - incidentStatusSchema, 7 - incidentUpdate, 8 - insertIncidentSchema, 9 - insertIncidentUpdateSchema, 10 - monitorsToIncidents, 11 - pagesToIncidents, 12 - selectIncidentSchema, 13 - selectIncidentUpdateSchema, 14 - selectMonitorSchema, 15 - } from "@openstatus/db/src/schema"; 16 - 17 - import { createTRPCRouter, protectedProcedure } from "../trpc"; 18 - 19 - export const incidentRouter = createTRPCRouter({ 20 - createIncident: protectedProcedure 21 - .input(insertIncidentSchema) 22 - .mutation(async (opts) => { 23 - const { id, monitors, pages, date, message, ...incidentInput } = 24 - opts.input; 25 - 26 - const newIncident = await opts.ctx.db 27 - .insert(incident) 28 - .values({ 29 - workspaceId: opts.ctx.workspace.id, 30 - ...incidentInput, 31 - }) 32 - .returning() 33 - .get(); 34 - 35 - if (Boolean(monitors.length)) { 36 - await opts.ctx.db 37 - .insert(monitorsToIncidents) 38 - .values( 39 - monitors.map((monitor) => ({ 40 - monitorId: monitor, 41 - incidentId: newIncident.id, 42 - })), 43 - ) 44 - .returning() 45 - .get(); 46 - } 47 - 48 - if (Boolean(pages.length)) { 49 - await opts.ctx.db 50 - .insert(pagesToIncidents) 51 - .values( 52 - pages.map((page) => ({ 53 - pageId: page, 54 - incidentId: newIncident.id, 55 - })), 56 - ) 57 - .returning() 58 - .get(); 59 - } 60 - 61 - return newIncident; 62 - }), 63 - 64 - createIncidentUpdate: protectedProcedure 65 - .input(insertIncidentUpdateSchema) 66 - .mutation(async (opts) => { 67 - // update parent incident with latest status 68 - await opts.ctx.db 69 - .update(incident) 70 - .set({ status: opts.input.status, updatedAt: new Date() }) 71 - .where( 72 - and( 73 - eq(incident.id, opts.input.incidentId), 74 - eq(incident.workspaceId, opts.ctx.workspace.id), 75 - ), 76 - ) 77 - .returning() 78 - .get(); 79 - 80 - const { id, ...incidentUpdateInput } = opts.input; 81 - return await opts.ctx.db 82 - .insert(incidentUpdate) 83 - .values(incidentUpdateInput) 84 - .returning() 85 - .get(); 86 - }), 87 - 88 - updateIncident: protectedProcedure 89 - .input(insertIncidentSchema) 90 - .mutation(async (opts) => { 91 - const { monitors, pages, ...incidentInput } = opts.input; 92 - 93 - if (!incidentInput.id) return; 94 - 95 - const { title, status } = incidentInput; 96 - 97 - const currentIncident = await opts.ctx.db 98 - .update(incident) 99 - .set({ title, status, updatedAt: new Date() }) 100 - .where( 101 - and( 102 - eq(incident.id, incidentInput.id), 103 - eq(incident.workspaceId, opts.ctx.workspace.id), 104 - ), 105 - ) 106 - .returning() 107 - .get(); 108 - 109 - const currentMonitorToIncidents = await opts.ctx.db 110 - .select() 111 - .from(monitorsToIncidents) 112 - .where(eq(monitorsToIncidents.incidentId, currentIncident.id)) 113 - .all(); 114 - 115 - const addedMonitors = monitors.filter( 116 - (x) => 117 - !currentMonitorToIncidents 118 - .map(({ monitorId }) => monitorId) 119 - .includes(x), 120 - ); 121 - 122 - if (Boolean(addedMonitors.length)) { 123 - const values = addedMonitors.map((monitorId) => ({ 124 - monitorId: monitorId, 125 - incidentId: currentIncident.id, 126 - })); 127 - 128 - await opts.ctx.db.insert(monitorsToIncidents).values(values).run(); 129 - } 130 - 131 - const removedMonitors = currentMonitorToIncidents 132 - .map(({ monitorId }) => monitorId) 133 - .filter((x) => !monitors?.includes(x)); 134 - 135 - if (Boolean(removedMonitors.length)) { 136 - await opts.ctx.db 137 - .delete(monitorsToIncidents) 138 - .where( 139 - and( 140 - eq(monitorsToIncidents.incidentId, currentIncident.id), 141 - inArray(monitorsToIncidents.monitorId, removedMonitors), 142 - ), 143 - ) 144 - .run(); 145 - } 146 - 147 - const currentPagesToIncidents = await opts.ctx.db 148 - .select() 149 - .from(pagesToIncidents) 150 - .where(eq(pagesToIncidents.incidentId, currentIncident.id)) 151 - .all(); 152 - 153 - const addedPages = pages?.filter( 154 - (x) => 155 - !currentPagesToIncidents.map(({ pageId }) => pageId)?.includes(x), 156 - ); 157 - 158 - if (Boolean(addedPages.length)) { 159 - const values = addedPages.map((pageId) => ({ 160 - pageId, 161 - incidentId: currentIncident.id, 162 - })); 163 - 164 - await opts.ctx.db.insert(pagesToIncidents).values(values).run(); 165 - } 166 - 167 - const removedPages = currentPagesToIncidents 168 - .map(({ pageId }) => pageId) 169 - .filter((x) => !pages?.includes(x)); 170 - 171 - if (Boolean(removedPages.length)) { 172 - await opts.ctx.db 173 - .delete(pagesToIncidents) 174 - .where( 175 - and( 176 - eq(pagesToIncidents.incidentId, currentIncident.id), 177 - inArray(pagesToIncidents.pageId, removedPages), 178 - ), 179 - ) 180 - .run(); 181 - } 182 - 183 - return currentIncident; 184 - }), 185 - 186 - updateIncidentUpdate: protectedProcedure 187 - .input(insertIncidentUpdateSchema) 188 - .mutation(async (opts) => { 189 - const incidentUpdateInput = opts.input; 190 - 191 - if (!incidentUpdateInput.id) return; 192 - 193 - const currentIncident = await opts.ctx.db 194 - .update(incidentUpdate) 195 - .set(incidentUpdateInput) 196 - .where(eq(incidentUpdate.id, incidentUpdateInput.id)) 197 - .returning() 198 - .get(); 199 - 200 - return currentIncident; 201 - }), 202 - 203 - deleteIncident: protectedProcedure 204 - .input(z.object({ id: z.number() })) 205 - .mutation(async (opts) => { 206 - const incidentToDelete = await opts.ctx.db 207 - .select() 208 - .from(incident) 209 - .where( 210 - and( 211 - eq(incident.id, opts.input.id), 212 - eq(incident.workspaceId, opts.ctx.workspace.id), 213 - ), 214 - ) 215 - .get(); 216 - if (!incidentToDelete) return; 217 - 218 - await opts.ctx.db 219 - .delete(incident) 220 - .where(eq(incident.id, incidentToDelete.id)) 221 - .run(); 222 - }), 223 - 224 - deleteIncidentUpdate: protectedProcedure 225 - .input(z.object({ id: z.number() })) 226 - .mutation(async (opts) => { 227 - const incidentUpdateToDelete = await opts.ctx.db 228 - .select() 229 - .from(incidentUpdate) 230 - .where(and(eq(incidentUpdate.id, opts.input.id))) 231 - // FIXME: check if incident related to workspaceId 232 - // .innerJoin(incident, inArray(incident.workspaceId, workspaceIds)) 233 - .get(); 234 - 235 - if (!incidentUpdateToDelete) return; 236 - 237 - await opts.ctx.db 238 - .delete(incidentUpdate) 239 - .where(eq(incidentUpdate.id, opts.input.id)) 240 - .run(); 241 - }), 242 - 243 - getIncidentById: protectedProcedure 244 - .input(z.object({ id: z.number() })) 245 - .query(async (opts) => { 246 - const selectIncidentSchemaWithRelation = selectIncidentSchema.extend({ 247 - status: incidentStatusSchema.default("investigating"), // TODO: remove! 248 - monitorsToIncidents: z 249 - .array(z.object({ incidentId: z.number(), monitorId: z.number() })) 250 - .default([]), 251 - pagesToIncidents: z 252 - .array(z.object({ incidentId: z.number(), pageId: z.number() })) 253 - .default([]), 254 - incidentUpdates: z.array(selectIncidentUpdateSchema), 255 - date: z.date().default(new Date()), 256 - }); 257 - 258 - const data = await opts.ctx.db.query.incident.findFirst({ 259 - where: and( 260 - eq(incident.id, opts.input.id), 261 - eq(incident.workspaceId, opts.ctx.workspace.id), 262 - ), 263 - with: { 264 - monitorsToIncidents: true, 265 - pagesToIncidents: true, 266 - incidentUpdates: { 267 - orderBy: (incidentUpdate, { desc }) => [ 268 - desc(incidentUpdate.createdAt), 269 - ], 270 - }, 271 - }, 272 - }); 273 - 274 - return selectIncidentSchemaWithRelation.parse(data); 275 - }), 276 - 277 - getIncidentUpdateById: protectedProcedure 278 - .input(z.object({ id: z.number() })) 279 - .query(async (opts) => { 280 - const data = await opts.ctx.db.query.incidentUpdate.findFirst({ 281 - where: and(eq(incidentUpdate.id, opts.input.id)), 282 - }); 283 - return selectIncidentUpdateSchema.parse(data); 284 - }), 285 - 286 - getIncidentByWorkspace: protectedProcedure.query(async (opts) => { 287 - // FIXME: can we get rid of that? 288 - const selectIncidentSchemaWithRelation = selectIncidentSchema.extend({ 289 - status: incidentStatusSchema.default("investigating"), // TODO: remove! 290 - monitorsToIncidents: z 291 - .array( 292 - z.object({ 293 - incidentId: z.number(), 294 - monitorId: z.number(), 295 - monitor: selectMonitorSchema, 296 - }), 297 - ) 298 - .default([]), 299 - incidentUpdates: z.array(selectIncidentUpdateSchema), 300 - }); 301 - 302 - const result = await opts.ctx.db.query.incident.findMany({ 303 - where: eq(incident.workspaceId, opts.ctx.workspace.id), 304 - with: { 305 - monitorsToIncidents: { with: { monitor: true } }, 306 - incidentUpdates: { 307 - orderBy: (incidentUpdate, { desc }) => [ 308 - desc(incidentUpdate.createdAt), 309 - ], 310 - }, 311 - }, 312 - orderBy: (incident, { desc }) => [desc(incident.updatedAt)], 313 - }); 314 - 315 - return z.array(selectIncidentSchemaWithRelation).parse(result); 316 - }), 317 - });
+1 -1
packages/api/src/router/page.test.ts
··· 20 20 customDomain: "", 21 21 description: "hello", 22 22 icon: "https://www.openstatus.dev/favicon.ico", 23 - incidents: [], 23 + statusReports: [], 24 24 monitors: [ 25 25 { 26 26 active: true,
+30 -26
packages/api/src/router/page.ts
··· 3 3 4 4 import { and, eq, inArray, or, sql } from "@openstatus/db"; 5 5 import { 6 - incident, 7 6 insertPageSchema, 8 7 monitor, 9 - monitorsToIncidents, 10 8 monitorsToPages, 9 + monitorsToStatusReport, 11 10 page, 12 - pagesToIncidents, 11 + pagesToStatusReports, 13 12 selectPublicPageSchemaWithRelation, 13 + statusReport, 14 14 } from "@openstatus/db/src/schema"; 15 15 import { allPlans } from "@openstatus/plans"; 16 16 ··· 74 74 ), 75 75 with: { 76 76 monitorsToPages: { with: { monitor: true } }, 77 - // incidents: true 78 77 }, 79 78 }); 80 79 }), ··· 169 168 const monitorsToPagesResult = await opts.ctx.db 170 169 .select() 171 170 .from(monitorsToPages) 172 - .where(eq(monitorsToPages.pageId, result.id)) 171 + .leftJoin(monitor, eq(monitorsToPages.monitorId, monitor.id)) 172 + .where( 173 + // make sur only active monitors are returned! 174 + and(eq(monitorsToPages.pageId, result.id), eq(monitor.active, true)), 175 + ) 173 176 .all(); 174 177 175 178 const monitorsId = monitorsToPagesResult.map( 176 - ({ monitorId }) => monitorId, 179 + ({ monitors_to_pages }) => monitors_to_pages.monitorId, 177 180 ); 178 181 179 - const monitorsToIncidentsResult = 182 + const monitorsToStatusReportResult = 180 183 monitorsId.length > 0 181 184 ? await opts.ctx.db 182 185 .select() 183 - .from(monitorsToIncidents) 184 - .where(inArray(monitorsToIncidents.monitorId, monitorsId)) 186 + .from(monitorsToStatusReport) 187 + .where(inArray(monitorsToStatusReport.monitorId, monitorsId)) 185 188 .all() 186 189 : []; 187 190 188 - const incidentsToPagesResult = await opts.ctx.db 191 + const statusReportsToPagesResult = await opts.ctx.db 189 192 .select() 190 - .from(pagesToIncidents) 191 - .where(eq(pagesToIncidents.pageId, result.id)) 193 + .from(pagesToStatusReports) 194 + .where(eq(pagesToStatusReports.pageId, result.id)) 192 195 .all(); 193 196 194 - const monitorIncidentIds = monitorsToIncidentsResult.map( 195 - ({ incidentId }) => incidentId, 197 + const monitorStatusReportIds = monitorsToStatusReportResult.map( 198 + ({ statusReportId }) => statusReportId, 196 199 ); 197 200 198 - const pageIncidentIds = incidentsToPagesResult.map( 199 - ({ incidentId }) => incidentId, 201 + const pageStatusReportIds = statusReportsToPagesResult.map( 202 + ({ statusReportId }) => statusReportId, 200 203 ); 201 204 202 - const incidentIds = Array.from( 203 - new Set([...monitorIncidentIds, ...pageIncidentIds]), 205 + const statusReportIds = Array.from( 206 + new Set([...monitorStatusReportIds, ...pageStatusReportIds]), 204 207 ); 205 208 206 - const incidents = 207 - incidentIds.length > 0 208 - ? await opts.ctx.db.query.incident.findMany({ 209 - where: or(inArray(incident.id, incidentIds)), 209 + const statusReports = 210 + statusReportIds.length > 0 211 + ? await opts.ctx.db.query.statusReport.findMany({ 212 + where: or(inArray(statusReport.id, statusReportIds)), 210 213 with: { 211 - incidentUpdates: true, 212 - monitorsToIncidents: true, 213 - pagesToIncidents: true, 214 + statusReportUpdates: true, 215 + monitorsToStatusReports: true, 216 + pagesToStatusReports: true, 214 217 }, 215 218 }) 216 219 : []; 217 220 221 + // TODO: monitorsToPagesResult has the result already, no need to query again 218 222 const monitors = 219 223 monitorsId.length > 0 220 224 ? await opts.ctx.db ··· 229 233 return selectPublicPageSchemaWithRelation.parse({ 230 234 ...result, 231 235 monitors, 232 - incidents, 236 + statusReports, 233 237 }); 234 238 }), 235 239
+329
packages/api/src/router/statusReport.ts
··· 1 + import { z } from "zod"; 2 + 3 + import { and, eq, inArray } from "@openstatus/db"; 4 + import { 5 + insertStatusReportSchema, 6 + insertStatusReportUpdateSchema, 7 + monitorsToStatusReport, 8 + pagesToStatusReports, 9 + selectMonitorSchema, 10 + selectStatusReportSchema, 11 + selectStatusReportUpdateSchema, 12 + statusReport, 13 + statusReportStatusSchema, 14 + statusReportUpdate, 15 + } from "@openstatus/db/src/schema"; 16 + 17 + import { createTRPCRouter, protectedProcedure } from "../trpc"; 18 + 19 + export const statusReportRouter = createTRPCRouter({ 20 + createStatusReport: protectedProcedure 21 + .input(insertStatusReportSchema) 22 + .mutation(async (opts) => { 23 + const { id, monitors, pages, date, message, ...statusReportInput } = 24 + opts.input; 25 + 26 + const newStatusReport = await opts.ctx.db 27 + .insert(statusReport) 28 + .values({ 29 + workspaceId: opts.ctx.workspace.id, 30 + ...statusReportInput, 31 + }) 32 + .returning() 33 + .get(); 34 + 35 + if (Boolean(monitors.length)) { 36 + await opts.ctx.db 37 + .insert(monitorsToStatusReport) 38 + .values( 39 + monitors.map((monitor) => ({ 40 + monitorId: monitor, 41 + statusReportId: newStatusReport.id, 42 + })), 43 + ) 44 + .returning() 45 + .get(); 46 + } 47 + 48 + if (Boolean(pages.length)) { 49 + await opts.ctx.db 50 + .insert(pagesToStatusReports) 51 + .values( 52 + pages.map((page) => ({ 53 + pageId: page, 54 + statusReportId: newStatusReport.id, 55 + })), 56 + ) 57 + .returning() 58 + .get(); 59 + } 60 + 61 + return newStatusReport; 62 + }), 63 + 64 + createStatusReportUpdate: protectedProcedure 65 + .input(insertStatusReportUpdateSchema) 66 + .mutation(async (opts) => { 67 + // update parent status report with latest status 68 + await opts.ctx.db 69 + .update(statusReport) 70 + .set({ status: opts.input.status, updatedAt: new Date() }) 71 + .where( 72 + and( 73 + eq(statusReport.id, opts.input.statusReportId), 74 + eq(statusReport.workspaceId, opts.ctx.workspace.id), 75 + ), 76 + ) 77 + .returning() 78 + .get(); 79 + 80 + const { id, ...statusReportUpdateInput } = opts.input; 81 + return await opts.ctx.db 82 + .insert(statusReportUpdate) 83 + .values(statusReportUpdateInput) 84 + .returning() 85 + .get(); 86 + }), 87 + 88 + updateStatusReport: protectedProcedure 89 + .input(insertStatusReportSchema) 90 + .mutation(async (opts) => { 91 + const { monitors, pages, ...statusReportInput } = opts.input; 92 + 93 + if (!statusReportInput.id) return; 94 + 95 + console.log({ pages }); 96 + 97 + const { title, status } = statusReportInput; 98 + 99 + const currentStatusReport = await opts.ctx.db 100 + .update(statusReport) 101 + .set({ title, status, updatedAt: new Date() }) 102 + .where( 103 + and( 104 + eq(statusReport.id, statusReportInput.id), 105 + eq(statusReport.workspaceId, opts.ctx.workspace.id), 106 + ), 107 + ) 108 + .returning() 109 + .get(); 110 + 111 + const currentMonitorsToStatusReport = await opts.ctx.db 112 + .select() 113 + .from(monitorsToStatusReport) 114 + .where( 115 + eq(monitorsToStatusReport.statusReportId, currentStatusReport.id), 116 + ) 117 + .all(); 118 + 119 + const addedMonitors = monitors.filter( 120 + (x) => 121 + !currentMonitorsToStatusReport 122 + .map(({ monitorId }) => monitorId) 123 + .includes(x), 124 + ); 125 + 126 + if (Boolean(addedMonitors.length)) { 127 + const values = addedMonitors.map((monitorId) => ({ 128 + monitorId: monitorId, 129 + statusReportId: currentStatusReport.id, 130 + })); 131 + 132 + await opts.ctx.db.insert(monitorsToStatusReport).values(values).run(); 133 + } 134 + 135 + const removedMonitors = currentMonitorsToStatusReport 136 + .map(({ monitorId }) => monitorId) 137 + .filter((x) => !monitors?.includes(x)); 138 + 139 + if (Boolean(removedMonitors.length)) { 140 + await opts.ctx.db 141 + .delete(monitorsToStatusReport) 142 + .where( 143 + and( 144 + eq(monitorsToStatusReport.statusReportId, currentStatusReport.id), 145 + inArray(monitorsToStatusReport.monitorId, removedMonitors), 146 + ), 147 + ) 148 + .run(); 149 + } 150 + 151 + const currentPagesToStatusReports = await opts.ctx.db 152 + .select() 153 + .from(pagesToStatusReports) 154 + .where(eq(pagesToStatusReports.statusReportId, currentStatusReport.id)) 155 + .all(); 156 + 157 + const addedPages = pages?.filter( 158 + (x) => 159 + !currentPagesToStatusReports.map(({ pageId }) => pageId)?.includes(x), 160 + ); 161 + 162 + if (Boolean(addedPages.length)) { 163 + const values = addedPages.map((pageId) => ({ 164 + pageId, 165 + statusReportId: currentStatusReport.id, 166 + })); 167 + 168 + await opts.ctx.db.insert(pagesToStatusReports).values(values).run(); 169 + } 170 + 171 + const removedPages = currentPagesToStatusReports 172 + .map(({ pageId }) => pageId) 173 + .filter((x) => !pages?.includes(x)); 174 + 175 + console.log({ 176 + currentPagesToStatusReports, 177 + removedPages, 178 + pages, 179 + addedPages, 180 + }); 181 + 182 + if (Boolean(removedPages.length)) { 183 + await opts.ctx.db 184 + .delete(pagesToStatusReports) 185 + .where( 186 + and( 187 + eq(pagesToStatusReports.statusReportId, currentStatusReport.id), 188 + inArray(pagesToStatusReports.pageId, removedPages), 189 + ), 190 + ) 191 + .run(); 192 + } 193 + 194 + return currentStatusReport; 195 + }), 196 + 197 + updateStatusReportUpdate: protectedProcedure 198 + .input(insertStatusReportUpdateSchema) 199 + .mutation(async (opts) => { 200 + const statusReportUpdateInput = opts.input; 201 + 202 + if (!statusReportUpdateInput.id) return; 203 + 204 + const currentStatusReportUpdate = await opts.ctx.db 205 + .update(statusReportUpdate) 206 + .set(statusReportUpdateInput) 207 + .where(eq(statusReportUpdate.id, statusReportUpdateInput.id)) 208 + .returning() 209 + .get(); 210 + 211 + return currentStatusReportUpdate; 212 + }), 213 + 214 + deleteStatusReport: protectedProcedure 215 + .input(z.object({ id: z.number() })) 216 + .mutation(async (opts) => { 217 + const statusReportToDelete = await opts.ctx.db 218 + .select() 219 + .from(statusReport) 220 + .where( 221 + and( 222 + eq(statusReport.id, opts.input.id), 223 + eq(statusReport.workspaceId, opts.ctx.workspace.id), 224 + ), 225 + ) 226 + .get(); 227 + if (!statusReportToDelete) return; 228 + 229 + await opts.ctx.db 230 + .delete(statusReport) 231 + .where(eq(statusReport.id, statusReportToDelete.id)) 232 + .run(); 233 + }), 234 + 235 + deleteStatusReportUpdate: protectedProcedure 236 + .input(z.object({ id: z.number() })) 237 + .mutation(async (opts) => { 238 + const statusReportUpdateToDelete = await opts.ctx.db 239 + .select() 240 + .from(statusReportUpdate) 241 + .where(and(eq(statusReportUpdate.id, opts.input.id))) 242 + .get(); 243 + 244 + if (!statusReportUpdateToDelete) return; 245 + 246 + await opts.ctx.db 247 + .delete(statusReportUpdate) 248 + .where(eq(statusReportUpdate.id, opts.input.id)) 249 + .run(); 250 + }), 251 + 252 + getStatusReportById: protectedProcedure 253 + .input(z.object({ id: z.number() })) 254 + .query(async (opts) => { 255 + const selectStatusReportSchemaWithRelation = 256 + selectStatusReportSchema.extend({ 257 + status: statusReportStatusSchema.default("investigating"), // TODO: remove! 258 + monitorsToStatusReports: z 259 + .array( 260 + z.object({ statusReportId: z.number(), monitorId: z.number() }), 261 + ) 262 + .default([]), 263 + pagesToStatusReports: z 264 + .array(z.object({ statusReportId: z.number(), pageId: z.number() })) 265 + .default([]), 266 + statusReportUpdates: z.array(selectStatusReportUpdateSchema), 267 + date: z.date().default(new Date()), 268 + }); 269 + 270 + const data = await opts.ctx.db.query.statusReport.findFirst({ 271 + where: and( 272 + eq(statusReport.id, opts.input.id), 273 + eq(statusReport.workspaceId, opts.ctx.workspace.id), 274 + ), 275 + with: { 276 + monitorsToStatusReports: true, 277 + pagesToStatusReports: true, 278 + statusReportUpdates: { 279 + orderBy: (statusReportUpdate, { desc }) => [ 280 + desc(statusReportUpdate.createdAt), 281 + ], 282 + }, 283 + }, 284 + }); 285 + 286 + return selectStatusReportSchemaWithRelation.parse(data); 287 + }), 288 + 289 + getStatusReportUpdateById: protectedProcedure 290 + .input(z.object({ id: z.number() })) 291 + .query(async (opts) => { 292 + const data = await opts.ctx.db.query.statusReportUpdate.findFirst({ 293 + where: and(eq(statusReportUpdate.id, opts.input.id)), 294 + }); 295 + return selectStatusReportUpdateSchema.parse(data); 296 + }), 297 + 298 + getStatusReportByWorkspace: protectedProcedure.query(async (opts) => { 299 + // FIXME: can we get rid of that? 300 + const selectStatusSchemaWithRelation = selectStatusReportSchema.extend({ 301 + status: statusReportStatusSchema.default("investigating"), // TODO: remove! 302 + monitorsToStatusReports: z 303 + .array( 304 + z.object({ 305 + statusReportId: z.number(), 306 + monitorId: z.number(), 307 + monitor: selectMonitorSchema, 308 + }), 309 + ) 310 + .default([]), 311 + statusReportUpdates: z.array(selectStatusReportUpdateSchema), 312 + }); 313 + 314 + const result = await opts.ctx.db.query.statusReport.findMany({ 315 + where: eq(statusReport.workspaceId, opts.ctx.workspace.id), 316 + with: { 317 + monitorsToStatusReports: { with: { monitor: true } }, 318 + statusReportUpdates: { 319 + orderBy: (statusReportUpdate, { desc }) => [ 320 + desc(statusReportUpdate.createdAt), 321 + ], 322 + }, 323 + }, 324 + orderBy: (statusReport, { desc }) => [desc(statusReport.updatedAt)], 325 + }); 326 + console.log(result); 327 + return z.array(selectStatusSchemaWithRelation).parse(result); 328 + }), 329 + });
+7
packages/db/drizzle/0011_bright_jazinda.sql
··· 1 + ALTER TABLE `incidents_to_monitors` RENAME TO `status_report_to_monitors`;--> statement-breakpoint 2 + ALTER TABLE `incidents_to_pages` RENAME TO `status_reports_to_pages`;--> statement-breakpoint 3 + ALTER TABLE `incident_update` RENAME TO `status_report_update`;--> statement-breakpoint 4 + ALTER TABLE `incident` RENAME TO `status_report`; --> statement-breakpoint 5 + ALTER TABLE `status_report_to_monitors` RENAME COLUMN `incident_id` TO `status_report_id`;--> statement-breakpoint 6 + ALTER TABLE `status_reports_to_pages` RENAME COLUMN `incident_id` TO `status_report_id`;--> statement-breakpoint 7 + ALTER TABLE `status_report_update` RENAME COLUMN `incident_id` TO `status_report_id`;
+1099
packages/db/drizzle/meta/0011_snapshot.json
··· 1 + { 2 + "version": "5", 3 + "dialect": "sqlite", 4 + "id": "21a5d3a2-6aab-4f67-8385-19d905e1a232", 5 + "prevId": "452e7037-85cf-4d9a-b4e2-375a695d5fa8", 6 + "tables": { 7 + "status_report_to_monitors": { 8 + "name": "status_report_to_monitors", 9 + "columns": { 10 + "monitor_id": { 11 + "name": "monitor_id", 12 + "type": "integer", 13 + "primaryKey": false, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "incident_id": { 18 + "name": "incident_id", 19 + "type": "integer", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + } 24 + }, 25 + "indexes": {}, 26 + "foreignKeys": { 27 + "status_report_to_monitors_monitor_id_monitor_id_fk": { 28 + "name": "status_report_to_monitors_monitor_id_monitor_id_fk", 29 + "tableFrom": "status_report_to_monitors", 30 + "tableTo": "monitor", 31 + "columnsFrom": [ 32 + "monitor_id" 33 + ], 34 + "columnsTo": [ 35 + "id" 36 + ], 37 + "onDelete": "cascade", 38 + "onUpdate": "no action" 39 + }, 40 + "status_report_to_monitors_incident_id_status_report_id_fk": { 41 + "name": "status_report_to_monitors_incident_id_status_report_id_fk", 42 + "tableFrom": "status_report_to_monitors", 43 + "tableTo": "status_report", 44 + "columnsFrom": [ 45 + "incident_id" 46 + ], 47 + "columnsTo": [ 48 + "id" 49 + ], 50 + "onDelete": "cascade", 51 + "onUpdate": "no action" 52 + } 53 + }, 54 + "compositePrimaryKeys": { 55 + "status_report_to_monitors_monitor_id_incident_id_pk": { 56 + "columns": [ 57 + "incident_id", 58 + "monitor_id" 59 + ] 60 + } 61 + }, 62 + "uniqueConstraints": {} 63 + }, 64 + "status_reports_to_pages": { 65 + "name": "status_reports_to_pages", 66 + "columns": { 67 + "page_id": { 68 + "name": "page_id", 69 + "type": "integer", 70 + "primaryKey": false, 71 + "notNull": true, 72 + "autoincrement": false 73 + }, 74 + "incident_id": { 75 + "name": "incident_id", 76 + "type": "integer", 77 + "primaryKey": false, 78 + "notNull": true, 79 + "autoincrement": false 80 + } 81 + }, 82 + "indexes": {}, 83 + "foreignKeys": { 84 + "status_reports_to_pages_page_id_page_id_fk": { 85 + "name": "status_reports_to_pages_page_id_page_id_fk", 86 + "tableFrom": "status_reports_to_pages", 87 + "tableTo": "page", 88 + "columnsFrom": [ 89 + "page_id" 90 + ], 91 + "columnsTo": [ 92 + "id" 93 + ], 94 + "onDelete": "cascade", 95 + "onUpdate": "no action" 96 + }, 97 + "status_reports_to_pages_incident_id_status_report_id_fk": { 98 + "name": "status_reports_to_pages_incident_id_status_report_id_fk", 99 + "tableFrom": "status_reports_to_pages", 100 + "tableTo": "status_report", 101 + "columnsFrom": [ 102 + "incident_id" 103 + ], 104 + "columnsTo": [ 105 + "id" 106 + ], 107 + "onDelete": "cascade", 108 + "onUpdate": "no action" 109 + } 110 + }, 111 + "compositePrimaryKeys": { 112 + "status_reports_to_pages_page_id_incident_id_pk": { 113 + "columns": [ 114 + "incident_id", 115 + "page_id" 116 + ] 117 + } 118 + }, 119 + "uniqueConstraints": {} 120 + }, 121 + "status_report_update": { 122 + "name": "status_report_update", 123 + "columns": { 124 + "id": { 125 + "name": "id", 126 + "type": "integer", 127 + "primaryKey": true, 128 + "notNull": true, 129 + "autoincrement": false 130 + }, 131 + "status": { 132 + "name": "status", 133 + "type": "text(4)", 134 + "primaryKey": false, 135 + "notNull": true, 136 + "autoincrement": false 137 + }, 138 + "date": { 139 + "name": "date", 140 + "type": "integer", 141 + "primaryKey": false, 142 + "notNull": true, 143 + "autoincrement": false 144 + }, 145 + "message": { 146 + "name": "message", 147 + "type": "text", 148 + "primaryKey": false, 149 + "notNull": true, 150 + "autoincrement": false 151 + }, 152 + "incident_id": { 153 + "name": "incident_id", 154 + "type": "integer", 155 + "primaryKey": false, 156 + "notNull": true, 157 + "autoincrement": false 158 + }, 159 + "created_at": { 160 + "name": "created_at", 161 + "type": "integer", 162 + "primaryKey": false, 163 + "notNull": false, 164 + "autoincrement": false, 165 + "default": "(strftime('%s', 'now'))" 166 + }, 167 + "updated_at": { 168 + "name": "updated_at", 169 + "type": "integer", 170 + "primaryKey": false, 171 + "notNull": false, 172 + "autoincrement": false, 173 + "default": "(strftime('%s', 'now'))" 174 + } 175 + }, 176 + "indexes": {}, 177 + "foreignKeys": { 178 + "status_report_update_incident_id_status_report_id_fk": { 179 + "name": "status_report_update_incident_id_status_report_id_fk", 180 + "tableFrom": "status_report_update", 181 + "tableTo": "status_report", 182 + "columnsFrom": [ 183 + "incident_id" 184 + ], 185 + "columnsTo": [ 186 + "id" 187 + ], 188 + "onDelete": "cascade", 189 + "onUpdate": "no action" 190 + } 191 + }, 192 + "compositePrimaryKeys": {}, 193 + "uniqueConstraints": {} 194 + }, 195 + "status_report": { 196 + "name": "status_report", 197 + "columns": { 198 + "id": { 199 + "name": "id", 200 + "type": "integer", 201 + "primaryKey": true, 202 + "notNull": true, 203 + "autoincrement": false 204 + }, 205 + "status": { 206 + "name": "status", 207 + "type": "text", 208 + "primaryKey": false, 209 + "notNull": true, 210 + "autoincrement": false 211 + }, 212 + "title": { 213 + "name": "title", 214 + "type": "text(256)", 215 + "primaryKey": false, 216 + "notNull": true, 217 + "autoincrement": false 218 + }, 219 + "workspace_id": { 220 + "name": "workspace_id", 221 + "type": "integer", 222 + "primaryKey": false, 223 + "notNull": false, 224 + "autoincrement": false 225 + }, 226 + "created_at": { 227 + "name": "created_at", 228 + "type": "integer", 229 + "primaryKey": false, 230 + "notNull": false, 231 + "autoincrement": false, 232 + "default": "(strftime('%s', 'now'))" 233 + }, 234 + "updated_at": { 235 + "name": "updated_at", 236 + "type": "integer", 237 + "primaryKey": false, 238 + "notNull": false, 239 + "autoincrement": false, 240 + "default": "(strftime('%s', 'now'))" 241 + } 242 + }, 243 + "indexes": {}, 244 + "foreignKeys": { 245 + "status_report_workspace_id_workspace_id_fk": { 246 + "name": "status_report_workspace_id_workspace_id_fk", 247 + "tableFrom": "status_report", 248 + "tableTo": "workspace", 249 + "columnsFrom": [ 250 + "workspace_id" 251 + ], 252 + "columnsTo": [ 253 + "id" 254 + ], 255 + "onDelete": "no action", 256 + "onUpdate": "no action" 257 + } 258 + }, 259 + "compositePrimaryKeys": {}, 260 + "uniqueConstraints": {} 261 + }, 262 + "integration": { 263 + "name": "integration", 264 + "columns": { 265 + "id": { 266 + "name": "id", 267 + "type": "integer", 268 + "primaryKey": true, 269 + "notNull": true, 270 + "autoincrement": false 271 + }, 272 + "name": { 273 + "name": "name", 274 + "type": "text(256)", 275 + "primaryKey": false, 276 + "notNull": true, 277 + "autoincrement": false 278 + }, 279 + "workspace_id": { 280 + "name": "workspace_id", 281 + "type": "integer", 282 + "primaryKey": false, 283 + "notNull": false, 284 + "autoincrement": false 285 + }, 286 + "credential": { 287 + "name": "credential", 288 + "type": "text", 289 + "primaryKey": false, 290 + "notNull": false, 291 + "autoincrement": false 292 + }, 293 + "external_id": { 294 + "name": "external_id", 295 + "type": "text", 296 + "primaryKey": false, 297 + "notNull": true, 298 + "autoincrement": false 299 + }, 300 + "created_at": { 301 + "name": "created_at", 302 + "type": "integer", 303 + "primaryKey": false, 304 + "notNull": false, 305 + "autoincrement": false, 306 + "default": "(strftime('%s', 'now'))" 307 + }, 308 + "updated_at": { 309 + "name": "updated_at", 310 + "type": "integer", 311 + "primaryKey": false, 312 + "notNull": false, 313 + "autoincrement": false, 314 + "default": "(strftime('%s', 'now'))" 315 + }, 316 + "data": { 317 + "name": "data", 318 + "type": "text", 319 + "primaryKey": false, 320 + "notNull": true, 321 + "autoincrement": false 322 + } 323 + }, 324 + "indexes": {}, 325 + "foreignKeys": { 326 + "integration_workspace_id_workspace_id_fk": { 327 + "name": "integration_workspace_id_workspace_id_fk", 328 + "tableFrom": "integration", 329 + "tableTo": "workspace", 330 + "columnsFrom": [ 331 + "workspace_id" 332 + ], 333 + "columnsTo": [ 334 + "id" 335 + ], 336 + "onDelete": "no action", 337 + "onUpdate": "no action" 338 + } 339 + }, 340 + "compositePrimaryKeys": {}, 341 + "uniqueConstraints": {} 342 + }, 343 + "page": { 344 + "name": "page", 345 + "columns": { 346 + "id": { 347 + "name": "id", 348 + "type": "integer", 349 + "primaryKey": true, 350 + "notNull": true, 351 + "autoincrement": false 352 + }, 353 + "workspace_id": { 354 + "name": "workspace_id", 355 + "type": "integer", 356 + "primaryKey": false, 357 + "notNull": true, 358 + "autoincrement": false 359 + }, 360 + "title": { 361 + "name": "title", 362 + "type": "text", 363 + "primaryKey": false, 364 + "notNull": true, 365 + "autoincrement": false 366 + }, 367 + "description": { 368 + "name": "description", 369 + "type": "text", 370 + "primaryKey": false, 371 + "notNull": true, 372 + "autoincrement": false 373 + }, 374 + "icon": { 375 + "name": "icon", 376 + "type": "text(256)", 377 + "primaryKey": false, 378 + "notNull": false, 379 + "autoincrement": false, 380 + "default": "''" 381 + }, 382 + "slug": { 383 + "name": "slug", 384 + "type": "text(256)", 385 + "primaryKey": false, 386 + "notNull": true, 387 + "autoincrement": false 388 + }, 389 + "custom_domain": { 390 + "name": "custom_domain", 391 + "type": "text(256)", 392 + "primaryKey": false, 393 + "notNull": true, 394 + "autoincrement": false 395 + }, 396 + "published": { 397 + "name": "published", 398 + "type": "integer", 399 + "primaryKey": false, 400 + "notNull": false, 401 + "autoincrement": false, 402 + "default": false 403 + }, 404 + "created_at": { 405 + "name": "created_at", 406 + "type": "integer", 407 + "primaryKey": false, 408 + "notNull": false, 409 + "autoincrement": false, 410 + "default": "(strftime('%s', 'now'))" 411 + }, 412 + "updated_at": { 413 + "name": "updated_at", 414 + "type": "integer", 415 + "primaryKey": false, 416 + "notNull": false, 417 + "autoincrement": false, 418 + "default": "(strftime('%s', 'now'))" 419 + } 420 + }, 421 + "indexes": { 422 + "page_slug_unique": { 423 + "name": "page_slug_unique", 424 + "columns": [ 425 + "slug" 426 + ], 427 + "isUnique": true 428 + } 429 + }, 430 + "foreignKeys": { 431 + "page_workspace_id_workspace_id_fk": { 432 + "name": "page_workspace_id_workspace_id_fk", 433 + "tableFrom": "page", 434 + "tableTo": "workspace", 435 + "columnsFrom": [ 436 + "workspace_id" 437 + ], 438 + "columnsTo": [ 439 + "id" 440 + ], 441 + "onDelete": "cascade", 442 + "onUpdate": "no action" 443 + } 444 + }, 445 + "compositePrimaryKeys": {}, 446 + "uniqueConstraints": {} 447 + }, 448 + "monitor": { 449 + "name": "monitor", 450 + "columns": { 451 + "id": { 452 + "name": "id", 453 + "type": "integer", 454 + "primaryKey": true, 455 + "notNull": true, 456 + "autoincrement": false 457 + }, 458 + "job_type": { 459 + "name": "job_type", 460 + "type": "text", 461 + "primaryKey": false, 462 + "notNull": true, 463 + "autoincrement": false, 464 + "default": "'other'" 465 + }, 466 + "periodicity": { 467 + "name": "periodicity", 468 + "type": "text", 469 + "primaryKey": false, 470 + "notNull": true, 471 + "autoincrement": false, 472 + "default": "'other'" 473 + }, 474 + "status": { 475 + "name": "status", 476 + "type": "text", 477 + "primaryKey": false, 478 + "notNull": true, 479 + "autoincrement": false, 480 + "default": "'active'" 481 + }, 482 + "active": { 483 + "name": "active", 484 + "type": "integer", 485 + "primaryKey": false, 486 + "notNull": false, 487 + "autoincrement": false, 488 + "default": false 489 + }, 490 + "regions": { 491 + "name": "regions", 492 + "type": "text", 493 + "primaryKey": false, 494 + "notNull": true, 495 + "autoincrement": false, 496 + "default": "''" 497 + }, 498 + "url": { 499 + "name": "url", 500 + "type": "text(2048)", 501 + "primaryKey": false, 502 + "notNull": true, 503 + "autoincrement": false 504 + }, 505 + "name": { 506 + "name": "name", 507 + "type": "text(256)", 508 + "primaryKey": false, 509 + "notNull": true, 510 + "autoincrement": false, 511 + "default": "''" 512 + }, 513 + "description": { 514 + "name": "description", 515 + "type": "text", 516 + "primaryKey": false, 517 + "notNull": true, 518 + "autoincrement": false, 519 + "default": "''" 520 + }, 521 + "headers": { 522 + "name": "headers", 523 + "type": "text", 524 + "primaryKey": false, 525 + "notNull": false, 526 + "autoincrement": false, 527 + "default": "''" 528 + }, 529 + "body": { 530 + "name": "body", 531 + "type": "text", 532 + "primaryKey": false, 533 + "notNull": false, 534 + "autoincrement": false, 535 + "default": "''" 536 + }, 537 + "method": { 538 + "name": "method", 539 + "type": "text", 540 + "primaryKey": false, 541 + "notNull": false, 542 + "autoincrement": false, 543 + "default": "'GET'" 544 + }, 545 + "workspace_id": { 546 + "name": "workspace_id", 547 + "type": "integer", 548 + "primaryKey": false, 549 + "notNull": false, 550 + "autoincrement": false 551 + }, 552 + "created_at": { 553 + "name": "created_at", 554 + "type": "integer", 555 + "primaryKey": false, 556 + "notNull": false, 557 + "autoincrement": false, 558 + "default": "(strftime('%s', 'now'))" 559 + }, 560 + "updated_at": { 561 + "name": "updated_at", 562 + "type": "integer", 563 + "primaryKey": false, 564 + "notNull": false, 565 + "autoincrement": false, 566 + "default": "(strftime('%s', 'now'))" 567 + } 568 + }, 569 + "indexes": {}, 570 + "foreignKeys": { 571 + "monitor_workspace_id_workspace_id_fk": { 572 + "name": "monitor_workspace_id_workspace_id_fk", 573 + "tableFrom": "monitor", 574 + "tableTo": "workspace", 575 + "columnsFrom": [ 576 + "workspace_id" 577 + ], 578 + "columnsTo": [ 579 + "id" 580 + ], 581 + "onDelete": "no action", 582 + "onUpdate": "no action" 583 + } 584 + }, 585 + "compositePrimaryKeys": {}, 586 + "uniqueConstraints": {} 587 + }, 588 + "monitors_to_pages": { 589 + "name": "monitors_to_pages", 590 + "columns": { 591 + "monitor_id": { 592 + "name": "monitor_id", 593 + "type": "integer", 594 + "primaryKey": false, 595 + "notNull": true, 596 + "autoincrement": false 597 + }, 598 + "page_id": { 599 + "name": "page_id", 600 + "type": "integer", 601 + "primaryKey": false, 602 + "notNull": true, 603 + "autoincrement": false 604 + } 605 + }, 606 + "indexes": {}, 607 + "foreignKeys": { 608 + "monitors_to_pages_monitor_id_monitor_id_fk": { 609 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 610 + "tableFrom": "monitors_to_pages", 611 + "tableTo": "monitor", 612 + "columnsFrom": [ 613 + "monitor_id" 614 + ], 615 + "columnsTo": [ 616 + "id" 617 + ], 618 + "onDelete": "cascade", 619 + "onUpdate": "no action" 620 + }, 621 + "monitors_to_pages_page_id_page_id_fk": { 622 + "name": "monitors_to_pages_page_id_page_id_fk", 623 + "tableFrom": "monitors_to_pages", 624 + "tableTo": "page", 625 + "columnsFrom": [ 626 + "page_id" 627 + ], 628 + "columnsTo": [ 629 + "id" 630 + ], 631 + "onDelete": "cascade", 632 + "onUpdate": "no action" 633 + } 634 + }, 635 + "compositePrimaryKeys": { 636 + "monitors_to_pages_monitor_id_page_id_pk": { 637 + "columns": [ 638 + "monitor_id", 639 + "page_id" 640 + ] 641 + } 642 + }, 643 + "uniqueConstraints": {} 644 + }, 645 + "user": { 646 + "name": "user", 647 + "columns": { 648 + "id": { 649 + "name": "id", 650 + "type": "integer", 651 + "primaryKey": true, 652 + "notNull": true, 653 + "autoincrement": false 654 + }, 655 + "tenant_id": { 656 + "name": "tenant_id", 657 + "type": "text(256)", 658 + "primaryKey": false, 659 + "notNull": false, 660 + "autoincrement": false 661 + }, 662 + "first_name": { 663 + "name": "first_name", 664 + "type": "text", 665 + "primaryKey": false, 666 + "notNull": false, 667 + "autoincrement": false, 668 + "default": "''" 669 + }, 670 + "last_name": { 671 + "name": "last_name", 672 + "type": "text", 673 + "primaryKey": false, 674 + "notNull": false, 675 + "autoincrement": false, 676 + "default": "''" 677 + }, 678 + "email": { 679 + "name": "email", 680 + "type": "text", 681 + "primaryKey": false, 682 + "notNull": false, 683 + "autoincrement": false, 684 + "default": "''" 685 + }, 686 + "photo_url": { 687 + "name": "photo_url", 688 + "type": "text", 689 + "primaryKey": false, 690 + "notNull": false, 691 + "autoincrement": false, 692 + "default": "''" 693 + }, 694 + "created_at": { 695 + "name": "created_at", 696 + "type": "integer", 697 + "primaryKey": false, 698 + "notNull": false, 699 + "autoincrement": false, 700 + "default": "(strftime('%s', 'now'))" 701 + }, 702 + "updated_at": { 703 + "name": "updated_at", 704 + "type": "integer", 705 + "primaryKey": false, 706 + "notNull": false, 707 + "autoincrement": false, 708 + "default": "(strftime('%s', 'now'))" 709 + } 710 + }, 711 + "indexes": { 712 + "user_tenant_id_unique": { 713 + "name": "user_tenant_id_unique", 714 + "columns": [ 715 + "tenant_id" 716 + ], 717 + "isUnique": true 718 + } 719 + }, 720 + "foreignKeys": {}, 721 + "compositePrimaryKeys": {}, 722 + "uniqueConstraints": {} 723 + }, 724 + "users_to_workspaces": { 725 + "name": "users_to_workspaces", 726 + "columns": { 727 + "user_id": { 728 + "name": "user_id", 729 + "type": "integer", 730 + "primaryKey": false, 731 + "notNull": true, 732 + "autoincrement": false 733 + }, 734 + "workspace_id": { 735 + "name": "workspace_id", 736 + "type": "integer", 737 + "primaryKey": false, 738 + "notNull": true, 739 + "autoincrement": false 740 + } 741 + }, 742 + "indexes": {}, 743 + "foreignKeys": { 744 + "users_to_workspaces_user_id_user_id_fk": { 745 + "name": "users_to_workspaces_user_id_user_id_fk", 746 + "tableFrom": "users_to_workspaces", 747 + "tableTo": "user", 748 + "columnsFrom": [ 749 + "user_id" 750 + ], 751 + "columnsTo": [ 752 + "id" 753 + ], 754 + "onDelete": "no action", 755 + "onUpdate": "no action" 756 + }, 757 + "users_to_workspaces_workspace_id_workspace_id_fk": { 758 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 759 + "tableFrom": "users_to_workspaces", 760 + "tableTo": "workspace", 761 + "columnsFrom": [ 762 + "workspace_id" 763 + ], 764 + "columnsTo": [ 765 + "id" 766 + ], 767 + "onDelete": "no action", 768 + "onUpdate": "no action" 769 + } 770 + }, 771 + "compositePrimaryKeys": { 772 + "users_to_workspaces_user_id_workspace_id_pk": { 773 + "columns": [ 774 + "user_id", 775 + "workspace_id" 776 + ] 777 + } 778 + }, 779 + "uniqueConstraints": {} 780 + }, 781 + "workspace": { 782 + "name": "workspace", 783 + "columns": { 784 + "id": { 785 + "name": "id", 786 + "type": "integer", 787 + "primaryKey": true, 788 + "notNull": true, 789 + "autoincrement": false 790 + }, 791 + "slug": { 792 + "name": "slug", 793 + "type": "text", 794 + "primaryKey": false, 795 + "notNull": true, 796 + "autoincrement": false 797 + }, 798 + "name": { 799 + "name": "name", 800 + "type": "text", 801 + "primaryKey": false, 802 + "notNull": false, 803 + "autoincrement": false 804 + }, 805 + "stripe_id": { 806 + "name": "stripe_id", 807 + "type": "text(256)", 808 + "primaryKey": false, 809 + "notNull": false, 810 + "autoincrement": false 811 + }, 812 + "subscription_id": { 813 + "name": "subscription_id", 814 + "type": "text", 815 + "primaryKey": false, 816 + "notNull": false, 817 + "autoincrement": false 818 + }, 819 + "plan": { 820 + "name": "plan", 821 + "type": "text", 822 + "primaryKey": false, 823 + "notNull": false, 824 + "autoincrement": false 825 + }, 826 + "ends_at": { 827 + "name": "ends_at", 828 + "type": "integer", 829 + "primaryKey": false, 830 + "notNull": false, 831 + "autoincrement": false 832 + }, 833 + "paid_until": { 834 + "name": "paid_until", 835 + "type": "integer", 836 + "primaryKey": false, 837 + "notNull": false, 838 + "autoincrement": false 839 + }, 840 + "created_at": { 841 + "name": "created_at", 842 + "type": "integer", 843 + "primaryKey": false, 844 + "notNull": false, 845 + "autoincrement": false, 846 + "default": "(strftime('%s', 'now'))" 847 + }, 848 + "updated_at": { 849 + "name": "updated_at", 850 + "type": "integer", 851 + "primaryKey": false, 852 + "notNull": false, 853 + "autoincrement": false, 854 + "default": "(strftime('%s', 'now'))" 855 + } 856 + }, 857 + "indexes": { 858 + "workspace_slug_unique": { 859 + "name": "workspace_slug_unique", 860 + "columns": [ 861 + "slug" 862 + ], 863 + "isUnique": true 864 + }, 865 + "workspace_stripe_id_unique": { 866 + "name": "workspace_stripe_id_unique", 867 + "columns": [ 868 + "stripe_id" 869 + ], 870 + "isUnique": true 871 + } 872 + }, 873 + "foreignKeys": {}, 874 + "compositePrimaryKeys": {}, 875 + "uniqueConstraints": {} 876 + }, 877 + "notification": { 878 + "name": "notification", 879 + "columns": { 880 + "id": { 881 + "name": "id", 882 + "type": "integer", 883 + "primaryKey": true, 884 + "notNull": true, 885 + "autoincrement": false 886 + }, 887 + "name": { 888 + "name": "name", 889 + "type": "text", 890 + "primaryKey": false, 891 + "notNull": true, 892 + "autoincrement": false 893 + }, 894 + "provider": { 895 + "name": "provider", 896 + "type": "text", 897 + "primaryKey": false, 898 + "notNull": true, 899 + "autoincrement": false 900 + }, 901 + "data": { 902 + "name": "data", 903 + "type": "text", 904 + "primaryKey": false, 905 + "notNull": false, 906 + "autoincrement": false, 907 + "default": "'{}'" 908 + }, 909 + "workspace_id": { 910 + "name": "workspace_id", 911 + "type": "integer", 912 + "primaryKey": false, 913 + "notNull": false, 914 + "autoincrement": false 915 + }, 916 + "created_at": { 917 + "name": "created_at", 918 + "type": "integer", 919 + "primaryKey": false, 920 + "notNull": false, 921 + "autoincrement": false, 922 + "default": "(strftime('%s', 'now'))" 923 + }, 924 + "updated_at": { 925 + "name": "updated_at", 926 + "type": "integer", 927 + "primaryKey": false, 928 + "notNull": false, 929 + "autoincrement": false, 930 + "default": "(strftime('%s', 'now'))" 931 + } 932 + }, 933 + "indexes": {}, 934 + "foreignKeys": { 935 + "notification_workspace_id_workspace_id_fk": { 936 + "name": "notification_workspace_id_workspace_id_fk", 937 + "tableFrom": "notification", 938 + "tableTo": "workspace", 939 + "columnsFrom": [ 940 + "workspace_id" 941 + ], 942 + "columnsTo": [ 943 + "id" 944 + ], 945 + "onDelete": "no action", 946 + "onUpdate": "no action" 947 + } 948 + }, 949 + "compositePrimaryKeys": {}, 950 + "uniqueConstraints": {} 951 + }, 952 + "notifications_to_monitors": { 953 + "name": "notifications_to_monitors", 954 + "columns": { 955 + "monitor_id": { 956 + "name": "monitor_id", 957 + "type": "integer", 958 + "primaryKey": false, 959 + "notNull": true, 960 + "autoincrement": false 961 + }, 962 + "notification_id": { 963 + "name": "notification_id", 964 + "type": "integer", 965 + "primaryKey": false, 966 + "notNull": true, 967 + "autoincrement": false 968 + } 969 + }, 970 + "indexes": {}, 971 + "foreignKeys": { 972 + "notifications_to_monitors_monitor_id_monitor_id_fk": { 973 + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", 974 + "tableFrom": "notifications_to_monitors", 975 + "tableTo": "monitor", 976 + "columnsFrom": [ 977 + "monitor_id" 978 + ], 979 + "columnsTo": [ 980 + "id" 981 + ], 982 + "onDelete": "cascade", 983 + "onUpdate": "no action" 984 + }, 985 + "notifications_to_monitors_notification_id_notification_id_fk": { 986 + "name": "notifications_to_monitors_notification_id_notification_id_fk", 987 + "tableFrom": "notifications_to_monitors", 988 + "tableTo": "notification", 989 + "columnsFrom": [ 990 + "notification_id" 991 + ], 992 + "columnsTo": [ 993 + "id" 994 + ], 995 + "onDelete": "cascade", 996 + "onUpdate": "no action" 997 + } 998 + }, 999 + "compositePrimaryKeys": { 1000 + "notifications_to_monitors_monitor_id_notification_id_pk": { 1001 + "columns": [ 1002 + "monitor_id", 1003 + "notification_id" 1004 + ] 1005 + } 1006 + }, 1007 + "uniqueConstraints": {} 1008 + }, 1009 + "monitor_status": { 1010 + "name": "monitor_status", 1011 + "columns": { 1012 + "monitor_id": { 1013 + "name": "monitor_id", 1014 + "type": "integer", 1015 + "primaryKey": false, 1016 + "notNull": true, 1017 + "autoincrement": false 1018 + }, 1019 + "region": { 1020 + "name": "region", 1021 + "type": "text", 1022 + "primaryKey": false, 1023 + "notNull": true, 1024 + "autoincrement": false, 1025 + "default": "''" 1026 + }, 1027 + "status": { 1028 + "name": "status", 1029 + "type": "text", 1030 + "primaryKey": false, 1031 + "notNull": true, 1032 + "autoincrement": false, 1033 + "default": "'active'" 1034 + }, 1035 + "created_at": { 1036 + "name": "created_at", 1037 + "type": "integer", 1038 + "primaryKey": false, 1039 + "notNull": false, 1040 + "autoincrement": false, 1041 + "default": "(strftime('%s', 'now'))" 1042 + }, 1043 + "updated_at": { 1044 + "name": "updated_at", 1045 + "type": "integer", 1046 + "primaryKey": false, 1047 + "notNull": false, 1048 + "autoincrement": false, 1049 + "default": "(strftime('%s', 'now'))" 1050 + } 1051 + }, 1052 + "indexes": { 1053 + "monitor_status_idx": { 1054 + "name": "monitor_status_idx", 1055 + "columns": [ 1056 + "monitor_id", 1057 + "region" 1058 + ], 1059 + "isUnique": false 1060 + } 1061 + }, 1062 + "foreignKeys": { 1063 + "monitor_status_monitor_id_monitor_id_fk": { 1064 + "name": "monitor_status_monitor_id_monitor_id_fk", 1065 + "tableFrom": "monitor_status", 1066 + "tableTo": "monitor", 1067 + "columnsFrom": [ 1068 + "monitor_id" 1069 + ], 1070 + "columnsTo": [ 1071 + "id" 1072 + ], 1073 + "onDelete": "cascade", 1074 + "onUpdate": "no action" 1075 + } 1076 + }, 1077 + "compositePrimaryKeys": { 1078 + "monitor_status_monitor_id_region_pk": { 1079 + "columns": [ 1080 + "monitor_id", 1081 + "region" 1082 + ] 1083 + } 1084 + }, 1085 + "uniqueConstraints": {} 1086 + } 1087 + }, 1088 + "enums": {}, 1089 + "_meta": { 1090 + "schemas": {}, 1091 + "tables": { 1092 + "\"incidents_to_monitors\"": "\"status_report_to_monitors\"", 1093 + "\"incidents_to_pages\"": "\"status_reports_to_pages\"", 1094 + "\"incident_update\"": "\"status_report_update\"", 1095 + "\"incident\"": "\"status_report\"" 1096 + }, 1097 + "columns": {} 1098 + } 1099 + }
+8 -1
packages/db/drizzle/meta/_journal.json
··· 78 78 "when": 1700586221141, 79 79 "tag": "0010_lame_songbird", 80 80 "breakpoints": true 81 + }, 82 + { 83 + "idx": 11, 84 + "version": "5", 85 + "when": 1701100570578, 86 + "tag": "0011_bright_jazinda", 87 + "breakpoints": true 81 88 } 82 89 ] 83 - } 90 + }
-126
packages/db/src/schema/incidents/incident.ts
··· 1 - import { relations, sql } from "drizzle-orm"; 2 - import { 3 - integer, 4 - primaryKey, 5 - sqliteTable, 6 - text, 7 - } from "drizzle-orm/sqlite-core"; 8 - 9 - import { monitor } from "../monitors"; 10 - import { page } from "../pages"; 11 - import { workspace } from "../workspaces"; 12 - 13 - export const incidentStatus = [ 14 - "investigating", 15 - "identified", 16 - "monitoring", 17 - "resolved", 18 - ] as const; 19 - 20 - export const incident = sqliteTable("incident", { 21 - id: integer("id").primaryKey(), 22 - status: text("status", { enum: incidentStatus }).notNull(), 23 - title: text("title", { length: 256 }).notNull(), 24 - 25 - workspaceId: integer("workspace_id").references(() => workspace.id), 26 - 27 - createdAt: integer("created_at", { mode: "timestamp" }).default( 28 - sql`(strftime('%s', 'now'))`, 29 - ), 30 - updatedAt: integer("updated_at", { mode: "timestamp" }).default( 31 - sql`(strftime('%s', 'now'))`, 32 - ), 33 - }); 34 - 35 - export const incidentUpdate = sqliteTable("incident_update", { 36 - id: integer("id").primaryKey(), 37 - 38 - status: text("status", incidentStatus).notNull(), 39 - date: integer("date", { mode: "timestamp" }).notNull(), 40 - message: text("message").notNull(), 41 - 42 - incidentId: integer("incident_id") 43 - .references(() => incident.id, { onDelete: "cascade" }) 44 - .notNull(), 45 - createdAt: integer("created_at", { mode: "timestamp" }).default( 46 - sql`(strftime('%s', 'now'))`, 47 - ), 48 - updatedAt: integer("updated_at", { mode: "timestamp" }).default( 49 - sql`(strftime('%s', 'now'))`, 50 - ), 51 - }); 52 - 53 - export const incidentRelations = relations(incident, ({ one, many }) => ({ 54 - monitorsToIncidents: many(monitorsToIncidents), 55 - pagesToIncidents: many(pagesToIncidents), 56 - incidentUpdates: many(incidentUpdate), 57 - workspace: one(workspace, { 58 - fields: [incident.workspaceId], 59 - references: [workspace.id], 60 - }), 61 - })); 62 - 63 - export const incidentUpdateRelations = relations(incidentUpdate, ({ one }) => ({ 64 - incident: one(incident, { 65 - fields: [incidentUpdate.incidentId], 66 - references: [incident.id], 67 - }), 68 - })); 69 - 70 - export const monitorsToIncidents = sqliteTable( 71 - "incidents_to_monitors", 72 - { 73 - monitorId: integer("monitor_id") 74 - .notNull() 75 - .references(() => monitor.id, { onDelete: "cascade" }), 76 - incidentId: integer("incident_id") 77 - .notNull() 78 - .references(() => incident.id, { onDelete: "cascade" }), 79 - }, 80 - (t) => ({ 81 - pk: primaryKey(t.monitorId, t.incidentId), 82 - }), 83 - ); 84 - 85 - export const monitorsToIncidentsRelations = relations( 86 - monitorsToIncidents, 87 - ({ one }) => ({ 88 - monitor: one(monitor, { 89 - fields: [monitorsToIncidents.monitorId], 90 - references: [monitor.id], 91 - }), 92 - incident: one(incident, { 93 - fields: [monitorsToIncidents.incidentId], 94 - references: [incident.id], 95 - }), 96 - }), 97 - ); 98 - 99 - export const pagesToIncidents = sqliteTable( 100 - "incidents_to_pages", 101 - { 102 - pageId: integer("page_id") 103 - .notNull() 104 - .references(() => page.id, { onDelete: "cascade" }), 105 - incidentId: integer("incident_id") 106 - .notNull() 107 - .references(() => incident.id, { onDelete: "cascade" }), 108 - }, 109 - (t) => ({ 110 - pk: primaryKey(t.pageId, t.incidentId), 111 - }), 112 - ); 113 - 114 - export const pagesToIncidentsRelations = relations( 115 - pagesToIncidents, 116 - ({ one }) => ({ 117 - page: one(page, { 118 - fields: [pagesToIncidents.pageId], 119 - references: [page.id], 120 - }), 121 - incident: one(incident, { 122 - fields: [pagesToIncidents.incidentId], 123 - references: [incident.id], 124 - }), 125 - }), 126 - );
+1 -1
packages/db/src/schema/incidents/index.ts packages/db/src/schema/status_reports/index.ts
··· 1 - export * from "./incident"; 1 + export * from "./status_reports"; 2 2 export * from "./validation"; 3 3 export type * from "./validation";
-41
packages/db/src/schema/incidents/validation.ts
··· 1 - import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 2 - import * as z from "zod"; 3 - 4 - import { incident, incidentStatus, incidentUpdate } from "./incident"; 5 - 6 - export const incidentStatusSchema = z.enum(incidentStatus); 7 - 8 - export const insertIncidentUpdateSchema = createInsertSchema(incidentUpdate, { 9 - status: incidentStatusSchema, 10 - }); 11 - 12 - export const insertIncidentSchema = createInsertSchema(incident, { 13 - status: incidentStatusSchema, 14 - }) 15 - .extend({ 16 - date: z.date().optional().default(new Date()), 17 - /** 18 - * relationship to monitors and pages 19 - */ 20 - monitors: z.number().array().optional().default([]), 21 - pages: z.number().array().optional().default([]), 22 - }) 23 - .extend({ 24 - /** 25 - * message for the `InsertIncidentUpdate` 26 - */ 27 - message: z.string(), 28 - }); 29 - 30 - export const selectIncidentSchema = createSelectSchema(incident, { 31 - status: incidentStatusSchema, 32 - }); 33 - 34 - export const selectIncidentUpdateSchema = createSelectSchema(incidentUpdate, { 35 - status: incidentStatusSchema, 36 - }); 37 - 38 - export type InsertIncident = z.infer<typeof insertIncidentSchema>; 39 - export type Incident = z.infer<typeof selectIncidentSchema>; 40 - export type InsertIncidentUpdate = z.infer<typeof insertIncidentUpdateSchema>; 41 - export type IncidentUpdate = z.infer<typeof selectIncidentUpdateSchema>;
+1 -1
packages/db/src/schema/index.ts
··· 1 - export * from "./incidents"; 1 + export * from "./status_reports"; 2 2 export * from "./integration"; 3 3 export * from "./pages"; 4 4 export * from "./monitors";
+2 -2
packages/db/src/schema/monitors/monitor.ts
··· 6 6 text, 7 7 } from "drizzle-orm/sqlite-core"; 8 8 9 - import { monitorsToIncidents } from "../incidents"; 10 9 import { notificationsToMonitors } from "../notifications"; 11 10 import { page } from "../pages"; 11 + import { monitorsToStatusReport } from "../status_reports"; 12 12 import { workspace } from "../workspaces"; 13 13 import { 14 14 monitorJobTypes, ··· 50 50 51 51 export const monitorRelation = relations(monitor, ({ one, many }) => ({ 52 52 monitorsToPages: many(monitorsToPages), 53 - monitorsToIncidents: many(monitorsToIncidents), 53 + monitorsToStatusReports: many(monitorsToStatusReport), 54 54 workspace: one(workspace, { 55 55 fields: [monitor.workspaceId], 56 56 references: [workspace.id],
+2 -2
packages/db/src/schema/pages/page.ts
··· 1 1 import { relations, sql } from "drizzle-orm"; 2 2 import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 3 3 4 - import { pagesToIncidents } from "../incidents"; 5 4 import { monitorsToPages } from "../monitors"; 5 + import { pagesToStatusReports } from "../status_reports"; 6 6 import { workspace } from "../workspaces"; 7 7 8 8 export const page = sqliteTable("page", { ··· 29 29 30 30 export const pageRelations = relations(page, ({ many, one }) => ({ 31 31 monitorsToPages: many(monitorsToPages), 32 - pagesToIncidents: many(pagesToIncidents), 32 + pagesToStatusReports: many(pagesToStatusReports), 33 33 workspace: one(workspace, { 34 34 fields: [page.workspaceId], 35 35 references: [workspace.id],
+14 -9
packages/db/src/schema/shared.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { selectIncidentSchema, selectIncidentUpdateSchema } from "./incidents"; 4 3 import { selectMonitorSchema } from "./monitors"; 5 4 import { selectPageSchema } from "./pages"; 5 + import { 6 + selectStatusReportSchema, 7 + selectStatusReportUpdateSchema, 8 + } from "./status_reports"; 6 9 7 - export const selectIncidentsPageSchema = z.array( 8 - selectIncidentSchema.extend({ 9 - incidentUpdates: z.array(selectIncidentUpdateSchema), 10 - monitorsToIncidents: z.array( 11 - z.object({ monitorId: z.number(), incidentId: z.number() }), 12 - ), 10 + // FIXME: delete this file! 11 + 12 + export const selectStatusReportPageSchema = z.array( 13 + selectStatusReportSchema.extend({ 14 + statusReportUpdates: z.array(selectStatusReportUpdateSchema).default([]), 15 + monitorsToStatusReport: z 16 + .array(z.object({ monitorId: z.number(), statusReportId: z.number() })) 17 + .default([]), 13 18 }), 14 19 ); 15 20 export const selectPageSchemaWithRelation = selectPageSchema.extend({ 16 21 monitors: z.array(selectMonitorSchema), 17 - incidents: selectIncidentsPageSchema, 22 + statusReports: selectStatusReportPageSchema, 18 23 }); 19 24 20 25 export const selectPublicMonitorSchema = selectMonitorSchema.omit({ ··· 27 32 export const selectPublicPageSchemaWithRelation = selectPageSchema 28 33 .extend({ 29 34 monitors: z.array(selectPublicMonitorSchema), 30 - incidents: selectIncidentsPageSchema, 35 + statusReports: selectStatusReportPageSchema, 31 36 }) 32 37 .omit({ 33 38 workspaceId: true,
+132
packages/db/src/schema/status_reports/status_reports.ts
··· 1 + import { relations, sql } from "drizzle-orm"; 2 + import { 3 + integer, 4 + primaryKey, 5 + sqliteTable, 6 + text, 7 + } from "drizzle-orm/sqlite-core"; 8 + 9 + import { monitor } from "../monitors"; 10 + import { page } from "../pages"; 11 + import { workspace } from "../workspaces"; 12 + 13 + export const statusReportStatus = [ 14 + "investigating", 15 + "identified", 16 + "monitoring", 17 + "resolved", 18 + ] as const; 19 + 20 + export const statusReport = sqliteTable("status_report", { 21 + id: integer("id").primaryKey(), 22 + status: text("status", { enum: statusReportStatus }).notNull(), 23 + title: text("title", { length: 256 }).notNull(), 24 + 25 + workspaceId: integer("workspace_id").references(() => workspace.id), 26 + 27 + createdAt: integer("created_at", { mode: "timestamp" }).default( 28 + sql`(strftime('%s', 'now'))`, 29 + ), 30 + updatedAt: integer("updated_at", { mode: "timestamp" }).default( 31 + sql`(strftime('%s', 'now'))`, 32 + ), 33 + }); 34 + 35 + export const statusReportUpdate = sqliteTable("status_report_update", { 36 + id: integer("id").primaryKey(), 37 + 38 + status: text("status", statusReportStatus).notNull(), 39 + date: integer("date", { mode: "timestamp" }).notNull(), 40 + message: text("message").notNull(), 41 + 42 + statusReportId: integer("status_report_id") 43 + .references(() => statusReport.id, { onDelete: "cascade" }) 44 + .notNull(), 45 + createdAt: integer("created_at", { mode: "timestamp" }).default( 46 + sql`(strftime('%s', 'now'))`, 47 + ), 48 + updatedAt: integer("updated_at", { mode: "timestamp" }).default( 49 + sql`(strftime('%s', 'now'))`, 50 + ), 51 + }); 52 + 53 + export const StatusReportRelations = relations( 54 + statusReport, 55 + ({ one, many }) => ({ 56 + monitorsToStatusReports: many(monitorsToStatusReport), 57 + pagesToStatusReports: many(pagesToStatusReports), 58 + statusReportUpdates: many(statusReportUpdate), 59 + workspace: one(workspace, { 60 + fields: [statusReport.workspaceId], 61 + references: [workspace.id], 62 + }), 63 + }), 64 + ); 65 + 66 + export const statusReportUpdateRelations = relations( 67 + statusReportUpdate, 68 + ({ one }) => ({ 69 + statusReport: one(statusReport, { 70 + fields: [statusReportUpdate.statusReportId], 71 + references: [statusReport.id], 72 + }), 73 + }), 74 + ); 75 + 76 + export const monitorsToStatusReport = sqliteTable( 77 + "status_report_to_monitors", 78 + { 79 + monitorId: integer("monitor_id") 80 + .notNull() 81 + .references(() => monitor.id, { onDelete: "cascade" }), 82 + statusReportId: integer("status_report_id") 83 + .notNull() 84 + .references(() => statusReport.id, { onDelete: "cascade" }), 85 + }, 86 + (t) => ({ 87 + pk: primaryKey(t.monitorId, t.statusReportId), 88 + }), 89 + ); 90 + 91 + export const monitorsToStatusReportRelations = relations( 92 + monitorsToStatusReport, 93 + ({ one }) => ({ 94 + monitor: one(monitor, { 95 + fields: [monitorsToStatusReport.monitorId], 96 + references: [monitor.id], 97 + }), 98 + statusReport: one(statusReport, { 99 + fields: [monitorsToStatusReport.statusReportId], 100 + references: [statusReport.id], 101 + }), 102 + }), 103 + ); 104 + 105 + export const pagesToStatusReports = sqliteTable( 106 + "status_reports_to_pages", 107 + { 108 + pageId: integer("page_id") 109 + .notNull() 110 + .references(() => page.id, { onDelete: "cascade" }), 111 + statusReportId: integer("status_report_id") 112 + .notNull() 113 + .references(() => statusReport.id, { onDelete: "cascade" }), 114 + }, 115 + (t) => ({ 116 + pk: primaryKey(t.pageId, t.statusReportId), 117 + }), 118 + ); 119 + 120 + export const pagesToStatusReportsRelations = relations( 121 + pagesToStatusReports, 122 + ({ one }) => ({ 123 + page: one(page, { 124 + fields: [pagesToStatusReports.pageId], 125 + references: [page.id], 126 + }), 127 + statusReport: one(statusReport, { 128 + fields: [pagesToStatusReports.statusReportId], 129 + references: [statusReport.id], 130 + }), 131 + }), 132 + );
+53
packages/db/src/schema/status_reports/validation.ts
··· 1 + import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 2 + import * as z from "zod"; 3 + 4 + import { 5 + statusReport, 6 + statusReportStatus, 7 + statusReportUpdate, 8 + } from "./status_reports"; 9 + 10 + export const statusReportStatusSchema = z.enum(statusReportStatus); 11 + 12 + export const insertStatusReportUpdateSchema = createInsertSchema( 13 + statusReportUpdate, 14 + { 15 + status: statusReportStatusSchema, 16 + }, 17 + ); 18 + 19 + export const insertStatusReportSchema = createInsertSchema(statusReport, { 20 + status: statusReportStatusSchema, 21 + }) 22 + .extend({ 23 + date: z.date().optional().default(new Date()), 24 + /** 25 + * relationship to monitors and pages 26 + */ 27 + monitors: z.number().array().optional().default([]), 28 + pages: z.number().array().optional().default([]), 29 + }) 30 + .extend({ 31 + /** 32 + * message for the `InsertIncidentUpdate` 33 + */ 34 + message: z.string(), 35 + }); 36 + 37 + export const selectStatusReportSchema = createSelectSchema(statusReport, { 38 + status: statusReportStatusSchema, 39 + }); 40 + 41 + export const selectStatusReportUpdateSchema = createSelectSchema( 42 + statusReportUpdate, 43 + { 44 + status: statusReportStatusSchema, 45 + }, 46 + ); 47 + 48 + export type InsertStatusReport = z.infer<typeof insertStatusReportSchema>; 49 + export type StatusReport = z.infer<typeof selectStatusReportSchema>; 50 + export type InsertStatusReportUpdate = z.infer< 51 + typeof insertStatusReportUpdateSchema 52 + >; 53 + export type StatusReportUpdate = z.infer<typeof selectStatusReportUpdateSchema>;
+6 -6
packages/db/src/seed.mts
··· 5 5 6 6 import { env } from "../env.mjs"; 7 7 import { 8 - incident, 9 - incidentUpdate, 10 8 monitor, 11 9 monitorsToPages, 12 10 notification, 13 11 notificationsToMonitors, 14 12 page, 13 + statusReport, 14 + statusReportUpdate, 15 15 user, 16 16 usersToWorkspaces, 17 17 workspace, ··· 122 122 .run(); 123 123 124 124 await db 125 - .insert(incident) 125 + .insert(statusReport) 126 126 .values({ 127 127 id: 1, 128 128 workspaceId: 1, 129 - title: "Test Incident", 129 + title: "Test Status Report", 130 130 status: "investigating", 131 131 updatedAt: new Date(), 132 132 }) 133 133 .run(); 134 134 135 135 await db 136 - .insert(incidentUpdate) 136 + .insert(statusReportUpdate) 137 137 .values({ 138 138 id: 1, 139 - incidentId: 1, 139 + statusReportId: 1, 140 140 status: "investigating", 141 141 message: "test", 142 142 date: new Date(),
+1 -1
packages/emails/index.ts
··· 18 18 export const resend = new Resend(env.RESEND_API_KEY); 19 19 20 20 export interface Emails { 21 - react: ReactElement; 21 + react: JSX.Element; 22 22 subject: "Welcome to OpenStatus.dev πŸ‘‹"; 23 23 to: string[]; 24 24 from: "Thibault Le Ouay Ducasse <thibault@openstatus.dev>";
+2 -1
packages/tinybird/src/audit-log/base-validation.ts
··· 43 43 export const targetTypeSchema = z.enum([ 44 44 "monitor", 45 45 "page", 46 - "incident", 46 + // "incident", // has been removed from the schema 47 + "status-report", 47 48 "user", 48 49 "notification", 49 50 "organization",