tangled
alpha
login
or
join now
openstatus.dev
/
openstatus
5
fork
atom
Openstatus
www.openstatus.dev
5
fork
atom
overview
issues
pulls
pipelines
add maintenance resource routes
Thomas Mol
1 year ago
a4f8a3bf
4ccfd939
+568
-1
11 changed files
expand all
collapse all
unified
split
apps
server
src
routes
v1
index.ts
maintenance
get.test.ts
get.ts
get_all.test.ts
get_all.ts
index.ts
post.test.ts
post.ts
put.test.ts
put.ts
schema.ts
+8
-1
apps/server/src/routes/v1/index.ts
···
10
10
import { incidentsApi } from "./incidents";
11
11
import { monitorsApi } from "./monitors";
12
12
import { notificationsApi } from "./notifications";
13
13
+
import { maintenanceApi } from "./maintenance";
13
14
import { pageSubscribersApi } from "./pageSubscribers";
14
15
import { pagesApi } from "./pages";
15
16
import { statusReportUpdatesApi } from "./statusReportUpdates";
···
72
73
"x-displayName": "Incident",
73
74
},
74
75
{
76
76
+
name: "maintenance",
77
77
+
description: "Maintenance related endpoints",
78
78
+
"x-displayName": "Maintenance",
79
79
+
},
80
80
+
{
75
81
name: "notification",
76
82
description: "Notification related endpoints",
77
83
"x-displayName": "Notification",
···
115
121
"https://openstatus.dev/api/og?title=OpenStatus%20API&description=API%20Reference",
116
122
twitterCard: "summary_large_image",
117
123
},
118
118
-
}),
124
124
+
})
119
125
);
120
126
/**
121
127
* Middlewares
···
130
136
api.route("/status_report", statusReportsApi);
131
137
api.route("/status_report_update", statusReportUpdatesApi);
132
138
api.route("/incident", incidentsApi);
139
139
+
api.route("/maintenance", maintenanceApi);
133
140
api.route("/notification", notificationsApi);
134
141
api.route("/page_subscriber", pageSubscribersApi);
135
142
api.route("/check", checkApi);
+31
apps/server/src/routes/v1/maintenance/get.test.ts
···
1
1
+
import { expect, test } from "bun:test";
2
2
+
import { app } from "@/index";
3
3
+
import { MaintenanceSchema } from "./schema";
4
4
+
5
5
+
test("return the maintenance", async () => {
6
6
+
const res = await app.request("/v1/maintenance/1", {
7
7
+
headers: {
8
8
+
"x-openstatus-key": "1",
9
9
+
},
10
10
+
});
11
11
+
const result = MaintenanceSchema.safeParse(await res.json());
12
12
+
13
13
+
expect(res.status).toBe(200);
14
14
+
expect(result.success).toBe(true);
15
15
+
});
16
16
+
17
17
+
test("no auth key should return 401", async () => {
18
18
+
const res = await app.request("/v1/maintenance/1");
19
19
+
20
20
+
expect(res.status).toBe(401);
21
21
+
});
22
22
+
23
23
+
test("invalid maintenance id should return 404", async () => {
24
24
+
const res = await app.request("/v1/maintenance/999", {
25
25
+
headers: {
26
26
+
"x-openstatus-key": "1",
27
27
+
},
28
28
+
});
29
29
+
30
30
+
expect(res.status).toBe(404);
31
31
+
});
+58
apps/server/src/routes/v1/maintenance/get.ts
···
1
1
+
import { createRoute } from "@hono/zod-openapi";
2
2
+
import { and, db, eq } from "@openstatus/db";
3
3
+
import { maintenance } from "@openstatus/db/src/schema/maintenances";
4
4
+
import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors";
5
5
+
import type { maintenanceApi } from "./index";
6
6
+
import { MaintenanceSchema, ParamsSchema } from "./schema";
7
7
+
8
8
+
const getRoute = createRoute({
9
9
+
method: "get",
10
10
+
tags: ["maintenance"],
11
11
+
summary: "Get a maintenance",
12
12
+
path: "/:id",
13
13
+
request: {
14
14
+
params: ParamsSchema,
15
15
+
},
16
16
+
responses: {
17
17
+
200: {
18
18
+
content: {
19
19
+
"application/json": {
20
20
+
schema: MaintenanceSchema,
21
21
+
},
22
22
+
},
23
23
+
description: "Get a maintenance",
24
24
+
},
25
25
+
...openApiErrorResponses,
26
26
+
},
27
27
+
});
28
28
+
29
29
+
export function registerGetMaintenance(api: typeof maintenanceApi) {
30
30
+
return api.openapi(getRoute, async (c) => {
31
31
+
const workspaceId = c.get("workspace").id;
32
32
+
const { id } = c.req.valid("param");
33
33
+
34
34
+
const _maintenance = await db.query.maintenance.findFirst({
35
35
+
with: {
36
36
+
maintenancesToMonitors: true,
37
37
+
},
38
38
+
where: and(
39
39
+
eq(maintenance.id, Number(id)),
40
40
+
eq(maintenance.workspaceId, workspaceId)
41
41
+
),
42
42
+
});
43
43
+
44
44
+
if (!_maintenance) {
45
45
+
throw new OpenStatusApiError({
46
46
+
code: "NOT_FOUND",
47
47
+
message: `Maintenance ${id} not found`,
48
48
+
});
49
49
+
}
50
50
+
51
51
+
const data = MaintenanceSchema.parse({
52
52
+
..._maintenance,
53
53
+
monitorIds: _maintenance.maintenancesToMonitors.map((m) => m.monitorId),
54
54
+
});
55
55
+
56
56
+
return c.json(data, 200);
57
57
+
});
58
58
+
}
+41
apps/server/src/routes/v1/maintenance/get_all.test.ts
···
1
1
+
import { expect, test } from "bun:test";
2
2
+
import { app } from "@/index";
3
3
+
import { MaintenanceSchema } from "./schema";
4
4
+
5
5
+
test("return all maintenances", async () => {
6
6
+
const res = await app.request("/v1/maintenance", {
7
7
+
method: "GET",
8
8
+
headers: {
9
9
+
"x-openstatus-key": "1",
10
10
+
},
11
11
+
});
12
12
+
13
13
+
const result = MaintenanceSchema.array().safeParse(await res.json());
14
14
+
15
15
+
expect(res.status).toBe(200);
16
16
+
expect(result.success).toBe(true);
17
17
+
expect(result.data?.length).toBeGreaterThan(0);
18
18
+
});
19
19
+
20
20
+
test("return empty maintenances", async () => {
21
21
+
const res = await app.request("/v1/maintenance", {
22
22
+
method: "GET",
23
23
+
headers: {
24
24
+
"x-openstatus-key": "2",
25
25
+
},
26
26
+
});
27
27
+
28
28
+
const result = MaintenanceSchema.array().safeParse(await res.json());
29
29
+
30
30
+
expect(result.success).toBe(true);
31
31
+
expect(res.status).toBe(200);
32
32
+
expect(result.data?.length).toBe(0);
33
33
+
});
34
34
+
35
35
+
test("no auth key should return 401", async () => {
36
36
+
const res = await app.request("/v1/maintenance", {
37
37
+
method: "GET",
38
38
+
});
39
39
+
40
40
+
expect(res.status).toBe(401);
41
41
+
});
+48
apps/server/src/routes/v1/maintenance/get_all.ts
···
1
1
+
import { createRoute } from "@hono/zod-openapi";
2
2
+
import { db, eq, desc } from "@openstatus/db";
3
3
+
import { maintenance } from "@openstatus/db/src/schema/maintenances";
4
4
+
import { openApiErrorResponses } from "@/libs/errors";
5
5
+
import type { maintenanceApi } from "./index";
6
6
+
import { MaintenanceSchema } from "./schema";
7
7
+
8
8
+
const getAllRoute = createRoute({
9
9
+
method: "get",
10
10
+
tags: ["maintenance"],
11
11
+
summary: "List all maintenances",
12
12
+
path: "/",
13
13
+
request: {},
14
14
+
responses: {
15
15
+
200: {
16
16
+
content: {
17
17
+
"application/json": {
18
18
+
schema: MaintenanceSchema.array(),
19
19
+
},
20
20
+
},
21
21
+
description: "Get all maintenances",
22
22
+
},
23
23
+
...openApiErrorResponses,
24
24
+
},
25
25
+
});
26
26
+
27
27
+
export function registerGetAllMaintenances(api: typeof maintenanceApi) {
28
28
+
return api.openapi(getAllRoute, async (c) => {
29
29
+
const workspaceId = c.get("workspace").id;
30
30
+
31
31
+
const _maintenances = await db.query.maintenance.findMany({
32
32
+
with: {
33
33
+
maintenancesToMonitors: true,
34
34
+
},
35
35
+
where: eq(maintenance.workspaceId, workspaceId),
36
36
+
orderBy: desc(maintenance.createdAt)
37
37
+
});
38
38
+
39
39
+
const data = MaintenanceSchema.array().parse(
40
40
+
_maintenances.map((m) => ({
41
41
+
...m,
42
42
+
monitorIds: m.maintenancesToMonitors.map((mtm) => mtm.monitorId),
43
43
+
}))
44
44
+
);
45
45
+
46
46
+
return c.json(data, 200);
47
47
+
});
48
48
+
}
+18
apps/server/src/routes/v1/maintenance/index.ts
···
1
1
+
import { OpenAPIHono } from "@hono/zod-openapi";
2
2
+
import { handleZodError } from "@/libs/errors";
3
3
+
import type { Variables } from "../index";
4
4
+
import { registerGetMaintenance } from "./get";
5
5
+
import { registerGetAllMaintenances } from "./get_all";
6
6
+
import { registerPostMaintenance } from "./post";
7
7
+
import { registerPutMaintenance } from "./put";
8
8
+
9
9
+
const maintenanceApi = new OpenAPIHono<{ Variables: Variables }>({
10
10
+
defaultHook: handleZodError,
11
11
+
});
12
12
+
13
13
+
registerGetAllMaintenances(maintenanceApi);
14
14
+
registerGetMaintenance(maintenanceApi);
15
15
+
registerPostMaintenance(maintenanceApi);
16
16
+
registerPutMaintenance(maintenanceApi);
17
17
+
18
18
+
export { maintenanceApi };
+60
apps/server/src/routes/v1/maintenance/post.test.ts
···
1
1
+
import { expect, test } from "bun:test";
2
2
+
import { app } from "@/index";
3
3
+
import { MaintenanceSchema } from "./schema";
4
4
+
5
5
+
test("create a valid maintenance", async () => {
6
6
+
const from = new Date();
7
7
+
const to = new Date(from.getTime() + 3600000); // 1 hour later
8
8
+
9
9
+
const res = await app.request("/v1/maintenance", {
10
10
+
method: "POST",
11
11
+
headers: {
12
12
+
"x-openstatus-key": "1",
13
13
+
"content-type": "application/json",
14
14
+
},
15
15
+
body: JSON.stringify({
16
16
+
title: "Database Upgrade",
17
17
+
message: "Scheduled database maintenance",
18
18
+
from: from.toISOString(),
19
19
+
to: to.toISOString(),
20
20
+
monitorIds: [1],
21
21
+
pageId: 1,
22
22
+
}),
23
23
+
});
24
24
+
25
25
+
const result = MaintenanceSchema.safeParse(await res.json());
26
26
+
27
27
+
expect(res.status).toBe(200);
28
28
+
expect(result.success).toBe(true);
29
29
+
expect(result.data?.monitorIds?.length).toBe(1);
30
30
+
});
31
31
+
32
32
+
test("create a maintenance with invalid dates should return 400", async () => {
33
33
+
const res = await app.request("/v1/maintenance", {
34
34
+
method: "POST",
35
35
+
headers: {
36
36
+
"x-openstatus-key": "1",
37
37
+
"content-type": "application/json",
38
38
+
},
39
39
+
body: JSON.stringify({
40
40
+
title: "Invalid Maintenance",
41
41
+
message: "Test message",
42
42
+
from: "invalid-date",
43
43
+
to: "invalid-date",
44
44
+
pageId: 1,
45
45
+
}),
46
46
+
});
47
47
+
48
48
+
expect(res.status).toBe(400);
49
49
+
});
50
50
+
51
51
+
test("no auth key should return 401", async () => {
52
52
+
const res = await app.request("/v1/maintenance", {
53
53
+
method: "POST",
54
54
+
headers: {
55
55
+
"content-type": "application/json",
56
56
+
},
57
57
+
});
58
58
+
59
59
+
expect(res.status).toBe(401);
60
60
+
});
+78
apps/server/src/routes/v1/maintenance/post.ts
···
1
1
+
import { createRoute } from "@hono/zod-openapi";
2
2
+
import { db } from "@openstatus/db";
3
3
+
import {
4
4
+
maintenance,
5
5
+
maintenancesToMonitors,
6
6
+
} from "@openstatus/db/src/schema/maintenances";
7
7
+
import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors";
8
8
+
import { trackMiddleware } from "@/libs/middlewares";
9
9
+
import { Events } from "@openstatus/analytics";
10
10
+
import type { maintenanceApi } from "./index";
11
11
+
import { MaintenanceSchema } from "./schema";
12
12
+
13
13
+
const postRoute = createRoute({
14
14
+
method: "post",
15
15
+
tags: ["maintenance"],
16
16
+
summary: "Create a maintenance",
17
17
+
path: "/",
18
18
+
middleware: [trackMiddleware(Events.CreateMaintenance)],
19
19
+
request: {
20
20
+
body: {
21
21
+
content: {
22
22
+
"application/json": {
23
23
+
schema: MaintenanceSchema.omit({ id: true }),
24
24
+
},
25
25
+
},
26
26
+
},
27
27
+
},
28
28
+
responses: {
29
29
+
200: {
30
30
+
content: {
31
31
+
"application/json": {
32
32
+
schema: MaintenanceSchema,
33
33
+
},
34
34
+
},
35
35
+
description: "Create a maintenance",
36
36
+
},
37
37
+
...openApiErrorResponses,
38
38
+
},
39
39
+
});
40
40
+
41
41
+
export function registerPostMaintenance(api: typeof maintenanceApi) {
42
42
+
return api.openapi(postRoute, async (c) => {
43
43
+
const workspaceId = c.get("workspace").id;
44
44
+
const input = c.req.valid("json");
45
45
+
46
46
+
const _maintenance = await db.transaction(async (tx) => {
47
47
+
const newMaintenance = await tx
48
48
+
.insert(maintenance)
49
49
+
.values({
50
50
+
...input,
51
51
+
workspaceId,
52
52
+
})
53
53
+
.returning()
54
54
+
.get();
55
55
+
56
56
+
if (input.monitorIds?.length) {
57
57
+
await tx
58
58
+
.insert(maintenancesToMonitors)
59
59
+
.values(
60
60
+
input.monitorIds.map((monitorId) => ({
61
61
+
maintenanceId: newMaintenance.id,
62
62
+
monitorId,
63
63
+
}))
64
64
+
)
65
65
+
.run();
66
66
+
}
67
67
+
68
68
+
return newMaintenance;
69
69
+
});
70
70
+
71
71
+
const data = MaintenanceSchema.parse({
72
72
+
..._maintenance,
73
73
+
monitorIds: input.monitorIds,
74
74
+
});
75
75
+
76
76
+
return c.json(data, 200);
77
77
+
});
78
78
+
}
+69
apps/server/src/routes/v1/maintenance/put.test.ts
···
1
1
+
import { expect, test } from "bun:test";
2
2
+
import { app } from "@/index";
3
3
+
import { MaintenanceSchema } from "./schema";
4
4
+
5
5
+
test("update the maintenance", async () => {
6
6
+
const res = await app.request("/v1/maintenance/1", {
7
7
+
method: "PUT",
8
8
+
headers: {
9
9
+
"x-openstatus-key": "1",
10
10
+
"Content-Type": "application/json",
11
11
+
},
12
12
+
body: JSON.stringify({
13
13
+
title: "Updated Maintenance",
14
14
+
message: "Updated message",
15
15
+
}),
16
16
+
});
17
17
+
18
18
+
const result = MaintenanceSchema.safeParse(await res.json());
19
19
+
20
20
+
expect(res.status).toBe(200);
21
21
+
expect(result.success).toBe(true);
22
22
+
expect(result.data?.title).toBe("Updated Maintenance");
23
23
+
});
24
24
+
25
25
+
test("update maintenance monitors", async () => {
26
26
+
const res = await app.request("/v1/maintenance/1", {
27
27
+
method: "PUT",
28
28
+
headers: {
29
29
+
"x-openstatus-key": "1",
30
30
+
"Content-Type": "application/json",
31
31
+
},
32
32
+
body: JSON.stringify({
33
33
+
monitorIds: [1, 2],
34
34
+
}),
35
35
+
});
36
36
+
37
37
+
const result = MaintenanceSchema.safeParse(await res.json());
38
38
+
39
39
+
expect(res.status).toBe(200);
40
40
+
expect(result.success).toBe(true);
41
41
+
expect(result.data?.monitorIds?.length).toBe(2);
42
42
+
});
43
43
+
44
44
+
test("invalid maintenance id should return 404", async () => {
45
45
+
const res = await app.request("/v1/maintenance/999", {
46
46
+
method: "PUT",
47
47
+
headers: {
48
48
+
"x-openstatus-key": "1",
49
49
+
"Content-Type": "application/json",
50
50
+
},
51
51
+
body: JSON.stringify({
52
52
+
title: "Not Found",
53
53
+
}),
54
54
+
});
55
55
+
56
56
+
expect(res.status).toBe(404);
57
57
+
});
58
58
+
59
59
+
test("no auth key should return 401", async () => {
60
60
+
const res = await app.request("/v1/maintenance/1", {
61
61
+
method: "PUT",
62
62
+
headers: {
63
63
+
"content-type": "application/json",
64
64
+
},
65
65
+
body: JSON.stringify({}),
66
66
+
});
67
67
+
68
68
+
expect(res.status).toBe(401);
69
69
+
});
+109
apps/server/src/routes/v1/maintenance/put.ts
···
1
1
+
import { createRoute } from "@hono/zod-openapi";
2
2
+
import { and, db, eq } from "@openstatus/db";
3
3
+
import {
4
4
+
maintenance,
5
5
+
maintenancesToMonitors,
6
6
+
} from "@openstatus/db/src/schema/maintenances";
7
7
+
import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors";
8
8
+
import { trackMiddleware } from "@/libs/middlewares";
9
9
+
import { Events } from "@openstatus/analytics";
10
10
+
import type { maintenanceApi } from "./index";
11
11
+
import { MaintenanceSchema, ParamsSchema } from "./schema";
12
12
+
13
13
+
const putRoute = createRoute({
14
14
+
method: "put",
15
15
+
tags: ["maintenance"],
16
16
+
summary: "Update a maintenance",
17
17
+
path: "/:id",
18
18
+
middleware: [trackMiddleware(Events.UpdateMaintenance)],
19
19
+
request: {
20
20
+
params: ParamsSchema,
21
21
+
body: {
22
22
+
content: {
23
23
+
"application/json": {
24
24
+
schema: MaintenanceSchema.omit({ id: true }).partial(),
25
25
+
},
26
26
+
},
27
27
+
},
28
28
+
},
29
29
+
responses: {
30
30
+
200: {
31
31
+
content: {
32
32
+
"application/json": {
33
33
+
schema: MaintenanceSchema,
34
34
+
},
35
35
+
},
36
36
+
description: "Update a maintenance",
37
37
+
},
38
38
+
...openApiErrorResponses,
39
39
+
},
40
40
+
});
41
41
+
42
42
+
export function registerPutMaintenance(api: typeof maintenanceApi) {
43
43
+
return api.openapi(putRoute, async (c) => {
44
44
+
const workspaceId = c.get("workspace").id;
45
45
+
const { id } = c.req.valid("param");
46
46
+
const input = c.req.valid("json");
47
47
+
48
48
+
const _maintenance = await db.query.maintenance.findFirst({
49
49
+
with: {
50
50
+
maintenancesToMonitors: true,
51
51
+
},
52
52
+
where: and(
53
53
+
eq(maintenance.id, Number(id)),
54
54
+
eq(maintenance.workspaceId, workspaceId)
55
55
+
),
56
56
+
});
57
57
+
58
58
+
if (!_maintenance) {
59
59
+
throw new OpenStatusApiError({
60
60
+
code: "NOT_FOUND",
61
61
+
message: `Maintenance ${id} not found`,
62
62
+
});
63
63
+
}
64
64
+
65
65
+
const updatedMaintenance = await db.transaction(async (tx) => {
66
66
+
const updated = await tx
67
67
+
.update(maintenance)
68
68
+
.set({
69
69
+
...input,
70
70
+
updatedAt: new Date(),
71
71
+
})
72
72
+
.where(eq(maintenance.id, Number(id)))
73
73
+
.returning()
74
74
+
.get();
75
75
+
76
76
+
if (input.monitorIds) {
77
77
+
// Delete existing monitor associations
78
78
+
await tx
79
79
+
.delete(maintenancesToMonitors)
80
80
+
.where(eq(maintenancesToMonitors.maintenanceId, Number(id)))
81
81
+
.run();
82
82
+
83
83
+
// Add new monitor associations
84
84
+
if (input.monitorIds.length > 0) {
85
85
+
await tx
86
86
+
.insert(maintenancesToMonitors)
87
87
+
.values(
88
88
+
input.monitorIds.map((monitorId) => ({
89
89
+
maintenanceId: Number(id),
90
90
+
monitorId,
91
91
+
}))
92
92
+
)
93
93
+
.run();
94
94
+
}
95
95
+
}
96
96
+
97
97
+
return updated;
98
98
+
});
99
99
+
100
100
+
const data = MaintenanceSchema.parse({
101
101
+
...updatedMaintenance,
102
102
+
monitorIds:
103
103
+
input.monitorIds ??
104
104
+
_maintenance.maintenancesToMonitors.map((m) => m.monitorId),
105
105
+
});
106
106
+
107
107
+
return c.json(data, 200);
108
108
+
});
109
109
+
}
+48
apps/server/src/routes/v1/maintenance/schema.ts
···
1
1
+
import { z } from "@hono/zod-openapi";
2
2
+
3
3
+
export const ParamsSchema = z.object({
4
4
+
id: z
5
5
+
.string()
6
6
+
.min(1)
7
7
+
.openapi({
8
8
+
param: {
9
9
+
name: "id",
10
10
+
in: "path",
11
11
+
},
12
12
+
description: "The id of the maintenance",
13
13
+
example: "1",
14
14
+
}),
15
15
+
});
16
16
+
17
17
+
export const MaintenanceSchema = z
18
18
+
.object({
19
19
+
id: z.number().openapi({
20
20
+
description: "The id of the maintenance",
21
21
+
example: 1,
22
22
+
}),
23
23
+
title: z.string().openapi({
24
24
+
description: "The title of the maintenance",
25
25
+
example: "Database Upgrade",
26
26
+
}),
27
27
+
message: z.string().openapi({
28
28
+
description: "The message describing the maintenance",
29
29
+
example: "Upgrading database to improve performance",
30
30
+
}),
31
31
+
from: z.coerce.date().openapi({
32
32
+
description: "When the maintenance starts",
33
33
+
}),
34
34
+
to: z.coerce.date().openapi({
35
35
+
description: "When the maintenance ends",
36
36
+
}),
37
37
+
monitorIds: z
38
38
+
.array(z.number())
39
39
+
.optional()
40
40
+
.default([])
41
41
+
.openapi({ description: "IDs of affected monitors" }),
42
42
+
pageId: z.number().openapi({
43
43
+
description: "The id of the status page this maintenance belongs to",
44
44
+
}),
45
45
+
})
46
46
+
.openapi("Maintenance");
47
47
+
48
48
+
export type MaintenanceSchema = z.infer<typeof MaintenanceSchema>;