kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cd7cada2f86b4e866a15b4323bb8d6d7ab5bba8b 316 lines 8.3 kB view raw
1import { eq } from "drizzle-orm"; 2import { Hono } from "hono"; 3import { describeRoute, resolver, validator } from "hono-openapi"; 4import * as v from "valibot"; 5import db from "../database"; 6import { taskTable, userTable } from "../database/schema"; 7import { publishEvent, subscribeToEvent } from "../events"; 8import { activitySchema } from "../schemas"; 9import { workspaceAccess } from "../utils/workspace-access-middleware"; 10import createActivity from "./controllers/create-activity"; 11import createComment from "./controllers/create-comment"; 12import deleteComment from "./controllers/delete-comment"; 13import getActivities from "./controllers/get-activities"; 14import updateComment from "./controllers/update-comment"; 15 16const activity = new Hono<{ 17 Variables: { 18 userId: string; 19 }; 20}>() 21 .get( 22 "/:taskId", 23 describeRoute({ 24 operationId: "getActivities", 25 tags: ["Activity"], 26 description: "Get all activities for a specific task", 27 responses: { 28 200: { 29 description: "List of activities for the task", 30 content: { 31 "application/json": { schema: resolver(v.array(activitySchema)) }, 32 }, 33 }, 34 }, 35 }), 36 validator("param", v.object({ taskId: v.string() })), 37 workspaceAccess.fromTaskId(), 38 async (c) => { 39 const { taskId } = c.req.valid("param"); 40 const activities = await getActivities(taskId); 41 return c.json(activities); 42 }, 43 ) 44 .post( 45 "/create", 46 describeRoute({ 47 operationId: "createActivity", 48 tags: ["Activity"], 49 description: "Create a new activity (system-generated event)", 50 responses: { 51 200: { 52 description: "Activity created successfully", 53 content: { 54 "application/json": { schema: resolver(activitySchema) }, 55 }, 56 }, 57 }, 58 }), 59 validator( 60 "json", 61 v.object({ 62 taskId: v.string(), 63 userId: v.string(), 64 message: v.nullable(v.string()), 65 type: v.string(), 66 eventData: v.optional(v.nullable(v.record(v.string(), v.unknown()))), 67 }), 68 ), 69 workspaceAccess.fromTaskId(), 70 async (c) => { 71 const { taskId, userId, message, type, eventData } = c.req.valid("json"); 72 const activity = await createActivity( 73 taskId, 74 type, 75 userId, 76 message, 77 eventData, 78 ); 79 return c.json(activity); 80 }, 81 ) 82 .post( 83 "/comment", 84 describeRoute({ 85 operationId: "createComment", 86 tags: ["Activity"], 87 description: "Create a new comment on a task", 88 responses: { 89 200: { 90 description: "Comment created successfully", 91 content: { 92 "application/json": { schema: resolver(activitySchema) }, 93 }, 94 }, 95 }, 96 }), 97 validator( 98 "json", 99 v.object({ 100 taskId: v.string(), 101 comment: v.string(), 102 }), 103 ), 104 workspaceAccess.fromTaskId(), 105 async (c) => { 106 const { taskId, comment } = c.req.valid("json"); 107 const userId = c.get("userId"); 108 const newComment = await createComment(taskId, userId, comment); 109 110 const [user] = await db 111 .select({ name: userTable.name }) 112 .from(userTable) 113 .where(eq(userTable.id, userId)); 114 115 const [task] = await db 116 .select({ projectId: taskTable.projectId }) 117 .from(taskTable) 118 .where(eq(taskTable.id, taskId)); 119 120 if (task) { 121 await publishEvent("task.comment_created", { 122 taskId, 123 userId, 124 comment: `"${user?.name}" commented: ${comment}`, 125 projectId: task.projectId, 126 }); 127 } 128 129 return c.json(newComment); 130 }, 131 ) 132 .put( 133 "/comment", 134 describeRoute({ 135 operationId: "updateComment", 136 tags: ["Activity"], 137 description: "Update an existing comment", 138 responses: { 139 200: { 140 description: "Comment updated successfully", 141 content: { 142 "application/json": { schema: resolver(activitySchema) }, 143 }, 144 }, 145 }, 146 }), 147 validator( 148 "json", 149 v.object({ 150 activityId: v.string(), 151 comment: v.string(), 152 }), 153 ), 154 workspaceAccess.fromActivity("activityId"), 155 async (c) => { 156 const { activityId, comment } = c.req.valid("json"); 157 const userId = c.get("userId"); 158 const updatedComment = await updateComment(userId, activityId, comment); 159 return c.json(updatedComment); 160 }, 161 ) 162 .delete( 163 "/comment", 164 describeRoute({ 165 operationId: "deleteComment", 166 tags: ["Activity"], 167 description: "Delete a comment", 168 responses: { 169 200: { 170 description: "Comment deleted successfully", 171 content: { 172 "application/json": { schema: resolver(activitySchema) }, 173 }, 174 }, 175 }, 176 }), 177 validator( 178 "json", 179 v.object({ 180 activityId: v.string(), 181 }), 182 ), 183 workspaceAccess.fromActivity("activityId"), 184 async (c) => { 185 const { activityId } = c.req.valid("json"); 186 const userId = c.get("userId"); 187 const deletedComment = await deleteComment(userId, activityId); 188 return c.json(deletedComment); 189 }, 190 ); 191 192subscribeToEvent<{ 193 taskId: string; 194 userId: string; 195 type: string; 196 content: string | null; 197}>("task.created", async (data) => { 198 if (!data.userId || !data.taskId || !data.type) { 199 return; 200 } 201 await createActivity(data.taskId, data.type, data.userId, null, {}); 202}); 203 204subscribeToEvent<{ 205 taskId: string; 206 userId: string; 207 type: string; 208 content: string; 209 eventData: { 210 fromProjectId: string; 211 fromProjectName: string; 212 toProjectId: string; 213 toProjectName: string; 214 oldStatus: string; 215 newStatus: string; 216 }; 217}>("task.moved", async (data) => { 218 await createActivity(data.taskId, data.type, data.userId, data.content, { 219 fromProjectId: data.eventData.fromProjectId, 220 fromProjectName: data.eventData.fromProjectName, 221 toProjectId: data.eventData.toProjectId, 222 toProjectName: data.eventData.toProjectName, 223 oldStatus: data.eventData.oldStatus, 224 newStatus: data.eventData.newStatus, 225 }); 226}); 227 228subscribeToEvent<{ 229 taskId: string; 230 userId: string; 231 oldStatus: string; 232 newStatus: string; 233 title: string; 234 assigneeId?: string; 235 type: string; 236}>("task.status_changed", async (data) => { 237 await createActivity(data.taskId, data.type, data.userId, null, { 238 oldStatus: data.oldStatus, 239 newStatus: data.newStatus, 240 }); 241}); 242 243subscribeToEvent<{ 244 taskId: string; 245 userId: string; 246 oldPriority: string; 247 newPriority: string; 248 title: string; 249 type: string; 250}>("task.priority_changed", async (data) => { 251 await createActivity(data.taskId, data.type, data.userId, null, { 252 oldPriority: data.oldPriority, 253 newPriority: data.newPriority, 254 }); 255}); 256 257subscribeToEvent<{ 258 taskId: string; 259 userId: string; 260 title: string; 261 type: string; 262}>("task.unassigned", async (data) => { 263 await createActivity(data.taskId, data.type, data.userId, null, {}); 264}); 265 266subscribeToEvent<{ 267 taskId: string; 268 userId: string; 269 oldAssignee: string | null; 270 newAssignee: string; 271 newAssigneeId: string; 272 title: string; 273 type: string; 274}>("task.assignee_changed", async (data) => { 275 await createActivity(data.taskId, data.type, data.userId, null, { 276 newAssigneeId: data.newAssigneeId, 277 newAssignee: data.newAssignee, 278 isSelfAssigned: data.userId === data.newAssigneeId, 279 }); 280}); 281 282subscribeToEvent<{ 283 taskId: string; 284 userId: string; 285 oldDueDate: Date | null; 286 newDueDate: Date; 287 title: string; 288 type: string; 289}>("task.due_date_changed", async (data) => { 290 await createActivity(data.taskId, data.type, data.userId, null, { 291 oldDueDate: 292 data.oldDueDate instanceof Date 293 ? data.oldDueDate.toISOString() 294 : data.oldDueDate, 295 newDueDate: 296 data.newDueDate instanceof Date 297 ? data.newDueDate.toISOString() 298 : data.newDueDate, 299 }); 300}); 301 302subscribeToEvent<{ 303 taskId: string; 304 userId: string; 305 oldTitle: string; 306 newTitle: string; 307 title: string; 308 type: string; 309}>("task.title_changed", async (data) => { 310 await createActivity(data.taskId, data.type, data.userId, null, { 311 oldTitle: data.oldTitle, 312 newTitle: data.newTitle, 313 }); 314}); 315 316export default activity;