kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import type { InferSelectModel } from "drizzle-orm";
2import { and, eq } from "drizzle-orm";
3import db from "../../../database";
4import {
5 columnTable,
6 integrationTable,
7 taskTable,
8} from "../../../database/schema";
9
10export type TaskRow = InferSelectModel<typeof taskTable>;
11
12export type UpdateTaskStatusResult =
13 | { applied: false }
14 | { applied: true; before: TaskRow; after: TaskRow };
15
16const NON_COLUMN_STATUSES = new Set(["planned", "archived"]);
17
18export async function findTaskByNumber(projectId: string, taskNumber: number) {
19 return db.query.taskTable.findFirst({
20 where: and(
21 eq(taskTable.projectId, projectId),
22 eq(taskTable.number, taskNumber),
23 ),
24 });
25}
26
27export async function findTaskById(taskId: string) {
28 return db.query.taskTable.findFirst({
29 where: eq(taskTable.id, taskId),
30 });
31}
32
33export async function updateTaskStatus(
34 taskId: string,
35 newStatus: string,
36): Promise<UpdateTaskStatusResult> {
37 const task = await db.query.taskTable.findFirst({
38 where: eq(taskTable.id, taskId),
39 });
40
41 if (!task) {
42 return { applied: false };
43 }
44
45 let columnId: string | null = null;
46
47 const column = await db.query.columnTable.findFirst({
48 where: and(
49 eq(columnTable.projectId, task.projectId),
50 eq(columnTable.slug, newStatus),
51 ),
52 });
53
54 if (column) {
55 columnId = column.id;
56 } else if (!NON_COLUMN_STATUSES.has(newStatus)) {
57 console.warn(
58 `[GitHub] Skipping status update for task ${taskId}: column "${newStatus}" not found in project ${task.projectId}`,
59 );
60 return { applied: false };
61 }
62
63 await db
64 .update(taskTable)
65 .set({ status: newStatus, columnId })
66 .where(eq(taskTable.id, taskId));
67
68 const after = await db.query.taskTable.findFirst({
69 where: eq(taskTable.id, taskId),
70 });
71
72 if (!after) {
73 return { applied: false };
74 }
75
76 return { applied: true, before: task, after };
77}
78
79export async function isTaskInFinalState(task: {
80 projectId: string;
81 status: string;
82 columnId: string | null;
83}): Promise<boolean> {
84 if (task.columnId) {
85 const columnById = await db.query.columnTable.findFirst({
86 where: and(
87 eq(columnTable.id, task.columnId),
88 eq(columnTable.projectId, task.projectId),
89 ),
90 });
91
92 if (columnById) {
93 return columnById.isFinal;
94 }
95 }
96
97 const columnByStatus = await db.query.columnTable.findFirst({
98 where: and(
99 eq(columnTable.projectId, task.projectId),
100 eq(columnTable.slug, task.status),
101 ),
102 });
103
104 if (columnByStatus) {
105 return columnByStatus.isFinal;
106 }
107
108 return task.status === "done";
109}
110
111export async function getIntegrationWithProject(integrationId: string) {
112 return db.query.integrationTable.findFirst({
113 where: eq(integrationTable.id, integrationId),
114 with: {
115 project: true,
116 },
117 });
118}
119
120export async function findIntegrationByRepo(owner: string, repo: string) {
121 const integrations = await findAllIntegrationsByRepo(owner, repo);
122 return integrations[0] || null;
123}
124
125export async function findAllIntegrationsByRepo(owner: string, repo: string) {
126 const integrations = await db.query.integrationTable.findMany({
127 where: and(
128 eq(integrationTable.type, "github"),
129 eq(integrationTable.isActive, true),
130 ),
131 with: {
132 project: true,
133 },
134 });
135
136 return integrations.filter((integration) => {
137 try {
138 const config = JSON.parse(integration.config);
139 return config.repositoryOwner === owner && config.repositoryName === repo;
140 } catch {
141 return false;
142 }
143 });
144}