kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
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;