Blame · Line-by-line history
notify.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 3ef4c9d | 1 | /** |
| 2 | * Notifications + audit log helpers. | |
| 3 | * Swallows DB failures so notifications never break the primary request path. | |
| 4 | */ | |
| 5 | ||
| 6 | import { db } from "../db"; | |
| 7 | import { notifications, auditLog } from "../db/schema"; | |
| 8 | ||
| 9 | export type NotificationKind = | |
| 10 | | "mention" | |
| 11 | | "review_requested" | |
| 12 | | "pr_opened" | |
| 13 | | "pr_merged" | |
| 14 | | "pr_closed" | |
| 15 | | "issue_opened" | |
| 16 | | "issue_closed" | |
| 17 | | "assigned" | |
| 18 | | "ai_review" | |
| 19 | | "gate_failed" | |
| 20 | | "gate_repaired" | |
| 21 | | "gate_passed" | |
| 22 | | "security_alert" | |
| 23 | | "deploy_success" | |
| 24 | | "deploy_failed" | |
| 25 | | "release_published" | |
| 26 | | "repo_archived"; | |
| 27 | ||
| 28 | export async function notify( | |
| 29 | userId: string, | |
| 30 | opts: { | |
| 31 | kind: NotificationKind; | |
| 32 | title: string; | |
| 33 | body?: string; | |
| 34 | url?: string; | |
| 35 | repositoryId?: string; | |
| 36 | } | |
| 37 | ): Promise<void> { | |
| 38 | try { | |
| 39 | await db.insert(notifications).values({ | |
| 40 | userId, | |
| 41 | kind: opts.kind, | |
| 42 | title: opts.title, | |
| 43 | body: opts.body, | |
| 44 | url: opts.url, | |
| 45 | repositoryId: opts.repositoryId, | |
| 46 | }); | |
| 47 | } catch (err) { | |
| 48 | console.error("[notify] failed:", err); | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | export async function notifyMany( | |
| 53 | userIds: string[], | |
| 54 | opts: { | |
| 55 | kind: NotificationKind; | |
| 56 | title: string; | |
| 57 | body?: string; | |
| 58 | url?: string; | |
| 59 | repositoryId?: string; | |
| 60 | } | |
| 61 | ): Promise<void> { | |
| 62 | const unique = Array.from(new Set(userIds)); | |
| 63 | if (unique.length === 0) return; | |
| 64 | try { | |
| 65 | await db.insert(notifications).values( | |
| 66 | unique.map((userId) => ({ | |
| 67 | userId, | |
| 68 | kind: opts.kind, | |
| 69 | title: opts.title, | |
| 70 | body: opts.body, | |
| 71 | url: opts.url, | |
| 72 | repositoryId: opts.repositoryId, | |
| 73 | })) | |
| 74 | ); | |
| 75 | } catch (err) { | |
| 76 | console.error("[notify] batch failed:", err); | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 80 | export async function audit(opts: { | |
| 81 | userId?: string | null; | |
| 82 | repositoryId?: string | null; | |
| 83 | action: string; | |
| 84 | targetType?: string; | |
| 85 | targetId?: string; | |
| 86 | ip?: string; | |
| 87 | userAgent?: string; | |
| 88 | metadata?: Record<string, unknown>; | |
| 89 | }): Promise<void> { | |
| 90 | try { | |
| 91 | await db.insert(auditLog).values({ | |
| 92 | userId: opts.userId ?? null, | |
| 93 | repositoryId: opts.repositoryId ?? null, | |
| 94 | action: opts.action, | |
| 95 | targetType: opts.targetType, | |
| 96 | targetId: opts.targetId, | |
| 97 | ip: opts.ip, | |
| 98 | userAgent: opts.userAgent, | |
| 99 | metadata: opts.metadata ? JSON.stringify(opts.metadata) : null, | |
| 100 | }); | |
| 101 | } catch (err) { | |
| 102 | // Audit must never break the primary flow | |
| 103 | console.error("[audit] failed:", err); | |
| 104 | } | |
| 105 | } |