kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { eq } from "drizzle-orm";
2import { Hono } from "hono";
3import { HTTPException } from "hono/http-exception";
4import { describeRoute, resolver, validator } from "hono-openapi";
5import * as v from "valibot";
6import db from "../database";
7import { projectTable, taskRelationTable, taskTable } from "../database/schema";
8import { validateWorkspaceAccess } from "../utils/validate-workspace-access";
9import { workspaceAccess } from "../utils/workspace-access-middleware";
10import createTaskRelation from "./controllers/create-task-relation";
11import deleteTaskRelation from "./controllers/delete-task-relation";
12import getTaskRelations from "./controllers/get-task-relations";
13
14const taskRelationSchema = v.object({
15 id: v.string(),
16 sourceTaskId: v.string(),
17 targetTaskId: v.string(),
18 relationType: v.string(),
19 createdAt: v.date(),
20});
21
22const taskRelation = new Hono<{
23 Variables: {
24 userId: string;
25 };
26}>()
27 .get(
28 "/:taskId",
29 describeRoute({
30 operationId: "getTaskRelations",
31 tags: ["Task Relations"],
32 description: "Get all relations for a task",
33 responses: {
34 200: {
35 description: "Task relations with associated task data",
36 content: {
37 "application/json": { schema: resolver(v.any()) },
38 },
39 },
40 },
41 }),
42 validator("param", v.object({ taskId: v.string() })),
43 workspaceAccess.fromTaskId("taskId"),
44 async (c) => {
45 const { taskId } = c.req.valid("param");
46 const relations = await getTaskRelations(taskId);
47 return c.json(relations);
48 },
49 )
50 .post(
51 "/",
52 describeRoute({
53 operationId: "createTaskRelation",
54 tags: ["Task Relations"],
55 description: "Create a relation between two tasks",
56 responses: {
57 200: {
58 description: "Task relation created successfully",
59 content: {
60 "application/json": { schema: resolver(taskRelationSchema) },
61 },
62 },
63 },
64 }),
65 validator(
66 "json",
67 v.object({
68 sourceTaskId: v.string(),
69 targetTaskId: v.string(),
70 relationType: v.picklist(["subtask", "blocks", "related"]),
71 }),
72 ),
73 async (c, next) => {
74 const userId = c.get("userId");
75 if (!userId) {
76 throw new HTTPException(401, { message: "Unauthorized" });
77 }
78 const { sourceTaskId } = c.req.valid("json");
79 const [task] = await db
80 .select({ workspaceId: projectTable.workspaceId })
81 .from(taskTable)
82 .innerJoin(projectTable, eq(taskTable.projectId, projectTable.id))
83 .where(eq(taskTable.id, sourceTaskId))
84 .limit(1);
85 if (!task) {
86 throw new HTTPException(404, { message: "Source task not found" });
87 }
88 await validateWorkspaceAccess(userId, task.workspaceId);
89 return next();
90 },
91 async (c) => {
92 const { sourceTaskId, targetTaskId, relationType } = c.req.valid("json");
93 const relation = await createTaskRelation({
94 sourceTaskId,
95 targetTaskId,
96 relationType,
97 });
98 return c.json(relation);
99 },
100 )
101 .delete(
102 "/:id",
103 describeRoute({
104 operationId: "deleteTaskRelation",
105 tags: ["Task Relations"],
106 description: "Delete a task relation",
107 responses: {
108 200: {
109 description: "Task relation deleted successfully",
110 content: {
111 "application/json": { schema: resolver(taskRelationSchema) },
112 },
113 },
114 },
115 }),
116 validator("param", v.object({ id: v.string() })),
117 async (c, next) => {
118 const userId = c.get("userId");
119 if (!userId) {
120 throw new HTTPException(401, { message: "Unauthorized" });
121 }
122 const { id } = c.req.valid("param");
123 const [rel] = await db
124 .select({ sourceTaskId: taskRelationTable.sourceTaskId })
125 .from(taskRelationTable)
126 .where(eq(taskRelationTable.id, id))
127 .limit(1);
128 if (!rel) {
129 throw new HTTPException(404, { message: "Task relation not found" });
130 }
131 const [task] = await db
132 .select({ workspaceId: projectTable.workspaceId })
133 .from(taskTable)
134 .innerJoin(projectTable, eq(taskTable.projectId, projectTable.id))
135 .where(eq(taskTable.id, rel.sourceTaskId))
136 .limit(1);
137 if (!task) {
138 throw new HTTPException(404, { message: "Task not found" });
139 }
140 await validateWorkspaceAccess(userId, task.workspaceId);
141 return next();
142 },
143 async (c) => {
144 const { id } = c.req.valid("param");
145 const relation = await deleteTaskRelation(id);
146 return c.json(relation);
147 },
148 );
149
150export default taskRelation;