kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
at main 216 lines 5.8 kB view raw
1import { and, asc, eq, inArray, sql } from "drizzle-orm"; 2import { HTTPException } from "hono/http-exception"; 3import db from "../../database"; 4import { 5 columnTable, 6 externalLinkTable, 7 labelTable, 8 projectTable, 9 taskTable, 10 userTable, 11} from "../../database/schema"; 12 13type GetTasksOptions = { 14 assigneeId?: string; 15 limit?: number; 16 page?: number; 17 priority?: string; 18 status?: string; 19}; 20 21async function getTasks(projectId: string, options: GetTasksOptions = {}) { 22 const project = await db.query.projectTable.findFirst({ 23 where: eq(projectTable.id, projectId), 24 }); 25 26 if (!project) { 27 throw new HTTPException(404, { 28 message: "Project not found", 29 }); 30 } 31 32 const conditions = [eq(taskTable.projectId, projectId)]; 33 34 if (options.status) { 35 conditions.push(eq(taskTable.status, options.status)); 36 } 37 38 if (options.priority) { 39 conditions.push(eq(taskTable.priority, options.priority)); 40 } 41 42 if (options.assigneeId) { 43 conditions.push(eq(taskTable.userId, options.assigneeId)); 44 } 45 46 const whereClause = and(...conditions); 47 const page = options.page && options.page > 0 ? options.page : 1; 48 const limit = 49 options.limit && options.limit > 0 ? Math.min(options.limit, 100) : null; 50 const offset = limit ? (page - 1) * limit : 0; 51 52 const [taskCount] = await db 53 .select({ count: sql<number>`count(*)` }) 54 .from(taskTable) 55 .where(whereClause); 56 57 const taskSelection = { 58 id: taskTable.id, 59 title: taskTable.title, 60 number: taskTable.number, 61 description: taskTable.description, 62 status: taskTable.status, 63 priority: taskTable.priority, 64 dueDate: taskTable.dueDate, 65 position: taskTable.position, 66 createdAt: taskTable.createdAt, 67 userId: taskTable.userId, 68 assigneeName: userTable.name, 69 assigneeId: userTable.id, 70 assigneeImage: userTable.image, 71 projectId: taskTable.projectId, 72 }; 73 74 const paginatedTasks = limit 75 ? await db 76 .select(taskSelection) 77 .from(taskTable) 78 .leftJoin(userTable, eq(taskTable.userId, userTable.id)) 79 .leftJoin(projectTable, eq(taskTable.projectId, projectTable.id)) 80 .where(whereClause) 81 .orderBy(taskTable.position) 82 .limit(limit) 83 .offset(offset) 84 : await db 85 .select(taskSelection) 86 .from(taskTable) 87 .leftJoin(userTable, eq(taskTable.userId, userTable.id)) 88 .leftJoin(projectTable, eq(taskTable.projectId, projectTable.id)) 89 .where(whereClause) 90 .orderBy(taskTable.position); 91 92 const taskIds = paginatedTasks.map((task) => task.id); 93 94 const labelsData = 95 taskIds.length > 0 96 ? await db 97 .select({ 98 id: labelTable.id, 99 name: labelTable.name, 100 color: labelTable.color, 101 taskId: labelTable.taskId, 102 }) 103 .from(labelTable) 104 .where(inArray(labelTable.taskId, taskIds)) 105 : []; 106 107 const externalLinksData = 108 taskIds.length > 0 109 ? await db 110 .select() 111 .from(externalLinkTable) 112 .where(inArray(externalLinkTable.taskId, taskIds)) 113 : []; 114 115 const taskLabelsMap = new Map< 116 string, 117 Array<{ id: string; name: string; color: string }> 118 >(); 119 for (const label of labelsData) { 120 if (label.taskId) { 121 if (!taskLabelsMap.has(label.taskId)) { 122 taskLabelsMap.set(label.taskId, []); 123 } 124 taskLabelsMap.get(label.taskId)?.push({ 125 id: label.id, 126 name: label.name, 127 color: label.color, 128 }); 129 } 130 } 131 132 const taskExternalLinksMap = new Map< 133 string, 134 Array<{ 135 id: string; 136 taskId: string; 137 integrationId: string; 138 resourceType: string; 139 externalId: string; 140 url: string; 141 title: string | null; 142 metadata: Record<string, unknown> | null; 143 }> 144 >(); 145 for (const externalLink of externalLinksData) { 146 if (!taskExternalLinksMap.has(externalLink.taskId)) { 147 taskExternalLinksMap.set(externalLink.taskId, []); 148 } 149 taskExternalLinksMap.get(externalLink.taskId)?.push({ 150 ...externalLink, 151 metadata: externalLink.metadata 152 ? JSON.parse(externalLink.metadata) 153 : null, 154 }); 155 } 156 157 const projectColumns = await db 158 .select() 159 .from(columnTable) 160 .where(eq(columnTable.projectId, projectId)) 161 .orderBy(asc(columnTable.position)); 162 163 const columns = projectColumns.map((column) => ({ 164 id: column.slug, 165 name: column.name, 166 isFinal: column.isFinal, 167 tasks: paginatedTasks 168 .filter((task) => task.status === column.slug) 169 .map((task) => ({ 170 ...task, 171 labels: taskLabelsMap.get(task.id) || [], 172 externalLinks: taskExternalLinksMap.get(task.id) || [], 173 })), 174 })); 175 176 const archivedTasks = paginatedTasks 177 .filter((task) => task.status === "archived") 178 .map((task) => ({ 179 ...task, 180 labels: taskLabelsMap.get(task.id) || [], 181 externalLinks: taskExternalLinksMap.get(task.id) || [], 182 })); 183 184 const plannedTasks = paginatedTasks 185 .filter((task) => task.status === "planned") 186 .map((task) => ({ 187 ...task, 188 labels: taskLabelsMap.get(task.id) || [], 189 externalLinks: taskExternalLinksMap.get(task.id) || [], 190 })); 191 192 return { 193 id: project.id, 194 name: project.name, 195 slug: project.slug, 196 icon: project.icon, 197 description: project.description, 198 isPublic: project.isPublic, 199 workspaceId: project.workspaceId, 200 columns, 201 archivedTasks, 202 plannedTasks, 203 pagination: limit 204 ? { 205 page, 206 limit, 207 total: taskCount?.count ?? 0, 208 totalPages: Math.max(1, Math.ceil((taskCount?.count ?? 0) / limit)), 209 hasNextPage: offset + paginatedTasks.length < (taskCount?.count ?? 0), 210 hasPreviousPage: page > 1, 211 } 212 : undefined, 213 }; 214} 215 216export default getTasks;