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 237 lines 5.8 kB view raw
1import { and, eq, inArray } from "drizzle-orm"; 2import { HTTPException } from "hono/http-exception"; 3import db from "../../database"; 4import { 5 columnTable, 6 labelTable, 7 projectTable, 8 taskTable, 9 workspaceUserTable, 10} from "../../database/schema"; 11import { 12 assertValidPriority, 13 assertValidTaskStatus, 14} from "../validate-task-fields"; 15 16type BulkOperation = 17 | "updateStatus" 18 | "updatePriority" 19 | "updateAssignee" 20 | "delete" 21 | "addLabel" 22 | "removeLabel" 23 | "updateDueDate"; 24 25async function bulkUpdateTasks({ 26 taskIds, 27 operation, 28 value, 29 userId, 30}: { 31 taskIds: string[]; 32 operation: BulkOperation; 33 value?: string | null; 34 userId: string; 35}) { 36 const tasks = await db 37 .select({ 38 id: taskTable.id, 39 projectId: taskTable.projectId, 40 workspaceId: projectTable.workspaceId, 41 }) 42 .from(taskTable) 43 .innerJoin(projectTable, eq(taskTable.projectId, projectTable.id)) 44 .where(inArray(taskTable.id, taskIds)); 45 46 if (tasks.length === 0) { 47 throw new HTTPException(404, { 48 message: "No tasks found", 49 }); 50 } 51 52 const workspaceIds = [...new Set(tasks.map((t) => t.workspaceId))]; 53 54 if (workspaceIds.length > 1) { 55 throw new HTTPException(400, { 56 message: "All tasks must belong to the same workspace", 57 }); 58 } 59 60 const workspaceId = workspaceIds[0]; 61 62 if (!workspaceId) { 63 throw new HTTPException(400, { 64 message: "Could not determine workspace", 65 }); 66 } 67 68 const [membership] = await db 69 .select({ id: workspaceUserTable.id }) 70 .from(workspaceUserTable) 71 .where( 72 and( 73 eq(workspaceUserTable.userId, userId), 74 eq(workspaceUserTable.workspaceId, workspaceId), 75 ), 76 ) 77 .limit(1); 78 79 if (!membership) { 80 throw new HTTPException(403, { 81 message: "You don't have access to this workspace", 82 }); 83 } 84 85 const foundIds = tasks.map((t) => t.id); 86 let updatedCount = 0; 87 88 switch (operation) { 89 case "updateStatus": { 90 if (!value) { 91 throw new HTTPException(400, { message: "Status value is required" }); 92 } 93 const projectIds = [...new Set(tasks.map((t) => t.projectId))]; 94 95 for (const projectId of projectIds) { 96 await assertValidTaskStatus(value, projectId); 97 98 const column = await db.query.columnTable.findFirst({ 99 where: and( 100 eq(columnTable.projectId, projectId), 101 eq(columnTable.slug, value), 102 ), 103 }); 104 105 const projectTaskIds = tasks 106 .filter((t) => t.projectId === projectId) 107 .map((t) => t.id); 108 109 const result = await db 110 .update(taskTable) 111 .set({ status: value, columnId: column?.id ?? null }) 112 .where(inArray(taskTable.id, projectTaskIds)); 113 114 updatedCount += result.rowCount ?? projectTaskIds.length; 115 } 116 break; 117 } 118 119 case "updatePriority": { 120 if (!value) { 121 throw new HTTPException(400, { message: "Priority value is required" }); 122 } 123 assertValidPriority(value); 124 125 const result = await db 126 .update(taskTable) 127 .set({ priority: value }) 128 .where(inArray(taskTable.id, foundIds)); 129 130 updatedCount = result.rowCount ?? foundIds.length; 131 break; 132 } 133 134 case "updateAssignee": { 135 const result = await db 136 .update(taskTable) 137 .set({ userId: value || null }) 138 .where(inArray(taskTable.id, foundIds)); 139 140 updatedCount = result.rowCount ?? foundIds.length; 141 break; 142 } 143 144 case "delete": { 145 const result = await db 146 .delete(taskTable) 147 .where(inArray(taskTable.id, foundIds)); 148 149 updatedCount = result.rowCount ?? foundIds.length; 150 break; 151 } 152 153 case "addLabel": { 154 if (!value) { 155 throw new HTTPException(400, { message: "Label ID is required" }); 156 } 157 158 const label = await db.query.labelTable.findFirst({ 159 where: eq(labelTable.id, value), 160 }); 161 162 if (!label) { 163 throw new HTTPException(404, { message: "Label not found" }); 164 } 165 166 for (const task of tasks) { 167 const existingAssignment = await db.query.labelTable.findFirst({ 168 where: and( 169 eq(labelTable.name, label.name), 170 eq(labelTable.taskId, task.id), 171 ), 172 }); 173 174 if (!existingAssignment) { 175 await db 176 .insert(labelTable) 177 .values({ 178 name: label.name, 179 color: label.color, 180 workspaceId: workspaceId, 181 taskId: task.id, 182 }) 183 .onConflictDoNothing({ 184 target: [labelTable.taskId, labelTable.name], 185 }); 186 updatedCount++; 187 } 188 } 189 break; 190 } 191 192 case "removeLabel": { 193 if (!value) { 194 throw new HTTPException(400, { message: "Label ID is required" }); 195 } 196 const result = await db 197 .update(labelTable) 198 .set({ taskId: null }) 199 .where( 200 and(eq(labelTable.id, value), inArray(labelTable.taskId, foundIds)), 201 ); 202 203 updatedCount = result.rowCount ?? foundIds.length; 204 break; 205 } 206 207 case "updateDueDate": { 208 let parsedDate: Date | null = null; 209 if (value) { 210 parsedDate = new Date(value); 211 if (Number.isNaN(parsedDate.getTime())) { 212 throw new HTTPException(400, { 213 message: `Invalid date value "${value}"`, 214 }); 215 } 216 } 217 218 const result = await db 219 .update(taskTable) 220 .set({ dueDate: parsedDate }) 221 .where(inArray(taskTable.id, foundIds)); 222 223 updatedCount = result.rowCount ?? foundIds.length; 224 break; 225 } 226 227 default: { 228 throw new HTTPException(400, { 229 message: `Unknown operation "${operation}"`, 230 }); 231 } 232 } 233 234 return { success: true, updatedCount }; 235} 236 237export default bulkUpdateTasks;