Add blocking notifications filter and API docs
swadhinbiswas committed 8 months ago
ab0daf0
1 parent 37142e2
commit ab0daf0
4 files changed
+115 -2
Showing 4 changed files+115-2
▼doc/feature_audit.md
+1-1
| @@ -183,7 +183,7 @@ | ||
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,6 +936,30 @@ export const openApiSpec = { | ||
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,12 +3,28 @@ | ||
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,6 +45,14 @@ export const GET: APIRoute = async ({ request, url }) => { | ||
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
| @@ -0,0 +1,65 @@ | ||
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 | |