Add blocking notifications filter and API docs

swadhinbiswas committed 8 months ago
ab0daf0
1 parent 37142e2
commit ab0daf0
4 files changed
+115 -2
▼doc/feature_audit.md
+1-1
183
183
| Feature | Status | Implementation Notes |
184
184
| :--- | :---: | :--- |
185
185
| Smart notifications | ⚠️ | Basic notifications, no AI prioritization |
186
-| Blocking alerts | ⚠️ | Basic via inbox sections |
186
+| Blocking alerts | ⚠️ | Blocking-focused notification feed filter is now available via `GET /notifications?filter=blocking`; prioritization/automation depth remains partial |
187
187
| Daily/weekly digests | ⚠️ | Scheduler is timezone-aware with cron endpoint/tests and user-scoped digest test API (`/user/notification-digests/test`); delivery observability/retry depth remains partial |
188
188
| Mentions & subscriptions | ✅ | Implemented |
189
189
| PR-level discussions | ✅ | Full threaded comments |
190
190
▼src/lib/openapi.ts
+24-0
936
936
},
937
937
},
938
938
},
939
+ "/notifications": {
940
+ get: {
941
+ tags: ["Notifications"],
942
+ summary: "List notifications",
943
+ description: "Returns notifications for the authenticated user. Supports `filter=unread|read|archived|all|blocking`.",
944
+ security: [{ bearerAuth: [] }],
945
+ parameters: [
946
+ {
947
+ name: "filter",
948
+ in: "query",
949
+ required: false,
950
+ schema: {
951
+ type: "string",
952
+ enum: ["unread", "read", "archived", "all", "blocking"],
953
+ },
954
+ },
955
+ ],
956
+ responses: {
957
+ 200: { description: "Notifications returned" },
958
+ 401: { description: "Unauthorized", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
959
+ 500: { description: "Fetch failed", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
960
+ },
961
+ },
962
+ },
939
963
"/repos/{owner}/{repo}/workflow/states": {
940
964
get: {
941
965
tags: ["Workflow"],
942
966
▼src/pages/api/notifications/index.ts
+25-1
3
3
*/
4
4
import { type APIRoute } from 'astro';
5
5
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
6
-import { eq, desc, and } from 'drizzle-orm';
6
+import { eq, desc, and, or } from 'drizzle-orm';
7
7
import { getDatabase , schema } from "@/db";
8
8
import { notifications } from '@/db/schema';
9
9
import { getUserFromRequest } from '@/lib/auth';
10
10
import { success, unauthorized, serverError } from '@/lib/api';
11
11
12
+const BLOCKING_NOTIFICATION_TYPES = [
13
+ "ci_failed",
14
+ "review_request",
15
+ "security_alert",
16
+ "merge_conflict",
17
+ "merge_blocked",
18
+];
19
+
20
+const BLOCKING_NOTIFICATION_REASONS = [
21
+ "ci_failed",
22
+ "review_requested",
23
+ "security_alert",
24
+ "merge_conflict",
25
+ "merge_blocked",
26
+];
27
+
12
28
export const GET: APIRoute = async ({ request, url }) => {
13
29
try {
14
30
const tokenPayload = await getUserFromRequest(request);
29
45
conditions.push(eq(notifications.isArchived, false));
30
46
} else if (filter === 'archived') {
31
47
conditions.push(eq(notifications.isArchived, true));
48
+ } else if (filter === 'blocking') {
49
+ conditions.push(eq(notifications.isArchived, false));
50
+ conditions.push(
51
+ or(
52
+ ...BLOCKING_NOTIFICATION_TYPES.map((type) => eq(notifications.type, type)),
53
+ ...BLOCKING_NOTIFICATION_REASONS.map((reason) => eq(notifications.reason, reason))
54
+ )!
55
+ );
32
56
} else {
33
57
// all - just exclude archived
34
58
conditions.push(eq(notifications.isArchived, false));
35
59
▼tests/unit/notifications-route.test.ts
+65-0
1
+import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+const { getUserFromRequestMock } = vi.hoisted(() => ({
4
+ getUserFromRequestMock: vi.fn(),
5
+}));
6
+
7
+let mockDb: any;
8
+
9
+vi.mock("@/lib/auth", () => ({
10
+ getUserFromRequest: getUserFromRequestMock,
11
+}));
12
+
13
+vi.mock("@/db", async () => {
14
+ const actual = await vi.importActual<typeof import("@/db")>("@/db");
15
+ return {
16
+ ...actual,
17
+ getDatabase: () => mockDb,
18
+ };
19
+});
20
+
21
+import { GET as listNotificationsGet } from "@/pages/api/notifications/index";
22
+
23
+describe("notifications list route", () => {
24
+ beforeEach(() => {
25
+ getUserFromRequestMock.mockReset();
26
+ mockDb = {
27
+ query: {
28
+ notifications: {
29
+ findMany: vi.fn(),
30
+ },
31
+ },
32
+ };
33
+ });
34
+
35
+ it("returns unauthorized when no auth token is present", async () => {
36
+ getUserFromRequestMock.mockResolvedValue(null);
37
+
38
+ const response = await listNotificationsGet({
39
+ request: new Request("http://localhost/api/notifications"),
40
+ url: new URL("http://localhost/api/notifications"),
41
+ } as any);
42
+
43
+ expect(response.status).toBe(401);
44
+ });
45
+
46
+ it("supports filter=blocking and returns unread count", async () => {
47
+ getUserFromRequestMock.mockResolvedValue({ userId: "user-1" });
48
+ mockDb.query.notifications.findMany
49
+ .mockResolvedValueOnce([{ id: "notif-1", type: "ci_failed" }])
50
+ .mockResolvedValueOnce([{ id: "notif-2" }, { id: "notif-3" }]);
51
+
52
+ const response = await listNotificationsGet({
53
+ request: new Request("http://localhost/api/notifications?filter=blocking"),
54
+ url: new URL("http://localhost/api/notifications?filter=blocking"),
55
+ } as any);
56
+
57
+ const body = await response.json();
58
+
59
+ expect(response.status).toBe(200);
60
+ expect(body?.success).toBe(true);
61
+ expect(body?.data?.notifications).toHaveLength(1);
62
+ expect(body?.data?.unreadCount).toBe(2);
63
+ expect(mockDb.query.notifications.findMany).toHaveBeenCalledTimes(2);
64
+ });
65
+});
0
66