kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { Hono } from "hono";
2import { describeRoute, resolver, validator } from "hono-openapi";
3import * as v from "valibot";
4import { subscribeToEvent } from "../events";
5import { notificationSchema } from "../schemas";
6import clearNotifications from "./controllers/clear-notifications";
7import createNotification from "./controllers/create-notification";
8import getNotifications from "./controllers/get-notifications";
9import markAllNotificationsAsRead from "./controllers/mark-all-notifications-as-read";
10import markAsRead from "./controllers/mark-notification-as-read";
11
12const bulkResultSchema = v.object({
13 success: v.boolean(),
14 count: v.optional(v.number()),
15});
16
17const notification = new Hono<{
18 Variables: {
19 userId: string;
20 };
21}>()
22 .get(
23 "/",
24 describeRoute({
25 operationId: "listNotifications",
26 tags: ["Notifications"],
27 description: "Get all notifications for the current user",
28 responses: {
29 200: {
30 description: "List of notifications",
31 content: {
32 "application/json": {
33 schema: resolver(v.array(notificationSchema)),
34 },
35 },
36 },
37 },
38 }),
39 async (c) => {
40 const userId = c.get("userId");
41 const notifications = await getNotifications(userId);
42 return c.json(notifications);
43 },
44 )
45 .post(
46 "/",
47 describeRoute({
48 operationId: "createNotification",
49 tags: ["Notifications"],
50 description: "Create a new notification for a user",
51 responses: {
52 200: {
53 description: "Notification created successfully",
54 content: {
55 "application/json": { schema: resolver(notificationSchema) },
56 },
57 },
58 },
59 }),
60 validator(
61 "json",
62 v.object({
63 title: v.optional(v.nullable(v.string())),
64 message: v.optional(v.nullable(v.string())),
65 type: v.string(),
66 eventData: v.optional(v.nullable(v.record(v.string(), v.unknown()))),
67 relatedEntityId: v.optional(v.string()),
68 relatedEntityType: v.optional(v.string()),
69 }),
70 ),
71 async (c) => {
72 const {
73 title,
74 message,
75 type,
76 eventData,
77 relatedEntityId,
78 relatedEntityType,
79 } = c.req.valid("json");
80 const userId = c.get("userId");
81 const notification = await createNotification({
82 userId,
83 title,
84 content: message,
85 type,
86 eventData,
87 resourceId: relatedEntityId,
88 resourceType: relatedEntityType,
89 });
90 return c.json(notification);
91 },
92 )
93 .patch(
94 "/:id/read",
95 describeRoute({
96 operationId: "markNotificationAsRead",
97 tags: ["Notifications"],
98 description: "Mark a specific notification as read",
99 responses: {
100 200: {
101 description: "Notification marked as read",
102 content: {
103 "application/json": { schema: resolver(notificationSchema) },
104 },
105 },
106 },
107 }),
108 validator("param", v.object({ id: v.string() })),
109 async (c) => {
110 const { id } = c.req.valid("param");
111 const userId = c.get("userId");
112 const notification = await markAsRead(id, userId);
113 return c.json(notification);
114 },
115 )
116 .patch(
117 "/read-all",
118 describeRoute({
119 operationId: "markAllNotificationsAsRead",
120 tags: ["Notifications"],
121 description: "Mark all notifications as read for the current user",
122 responses: {
123 200: {
124 description: "All notifications marked as read",
125 content: {
126 "application/json": { schema: resolver(bulkResultSchema) },
127 },
128 },
129 },
130 }),
131 async (c) => {
132 const userId = c.get("userId");
133 const result = await markAllNotificationsAsRead(userId);
134 return c.json(result);
135 },
136 )
137 .delete(
138 "/clear-all",
139 describeRoute({
140 operationId: "clearAllNotifications",
141 tags: ["Notifications"],
142 description: "Clear all notifications for the current user",
143 responses: {
144 200: {
145 description: "All notifications cleared",
146 content: {
147 "application/json": { schema: resolver(bulkResultSchema) },
148 },
149 },
150 },
151 }),
152 async (c) => {
153 const userId = c.get("userId");
154 const result = await clearNotifications(userId);
155 return c.json(result);
156 },
157 );
158
159subscribeToEvent<{
160 taskId: string;
161 userId: string;
162 title: string;
163 projectId: string;
164}>("task.created", async (data) => {
165 if (data.userId) {
166 await createNotification({
167 userId: data.userId,
168 type: "task_created",
169 eventData: {
170 taskTitle: data.title,
171 },
172 resourceId: data.taskId,
173 resourceType: "task",
174 });
175 }
176});
177
178subscribeToEvent<{
179 workspaceId: string;
180 workspaceName: string;
181 ownerEmail: string;
182 ownerId?: string;
183}>("workspace.created", async (data) => {
184 if (data.ownerId) {
185 await createNotification({
186 userId: data.ownerId,
187 type: "workspace_created",
188 eventData: {
189 workspaceName: data.workspaceName,
190 },
191 resourceId: data.workspaceId,
192 resourceType: "workspace",
193 });
194 }
195});
196
197subscribeToEvent<{
198 taskId: string;
199 userId: string;
200 oldStatus: string;
201 newStatus: string;
202 title: string;
203 assigneeId?: string;
204}>("task.status_changed", async (data) => {
205 if (data.assigneeId && data.assigneeId !== data.userId) {
206 await createNotification({
207 userId: data.assigneeId,
208 type: "task_status_changed",
209 eventData: {
210 taskTitle: data.title,
211 oldStatus: data.oldStatus,
212 newStatus: data.newStatus,
213 },
214 resourceId: data.taskId,
215 resourceType: "task",
216 });
217 }
218});
219
220subscribeToEvent<{
221 taskId: string;
222 userId: string;
223 oldAssignee: string | null;
224 newAssignee: string;
225 newAssigneeId: string;
226 title: string;
227}>("task.assignee_changed", async (data) => {
228 if (data.newAssigneeId) {
229 await createNotification({
230 userId: data.newAssigneeId,
231 type: "task_assignee_changed",
232 eventData: {
233 taskTitle: data.title,
234 },
235 resourceId: data.taskId,
236 resourceType: "task",
237 });
238 }
239});
240
241subscribeToEvent<{
242 timeEntryId: string;
243 taskId: string;
244 userId: string;
245 taskOwnerId?: string;
246 taskTitle?: string;
247}>("time-entry.created", async (data) => {
248 if (data.taskOwnerId && data.taskOwnerId !== data.userId) {
249 await createNotification({
250 userId: data.taskOwnerId,
251 type: "time_entry_created",
252 eventData: {
253 taskTitle: data.taskTitle ?? null,
254 },
255 resourceId: data.taskId,
256 resourceType: "task",
257 });
258 }
259});
260
261export default notification;