kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { Hono } from "hono";
2import { describeRoute, resolver, validator } from "hono-openapi";
3import * as v from "valibot";
4import { projectSchema } from "../schemas";
5import { workspaceAccess } from "../utils/workspace-access-middleware";
6import archiveProjectCtrl from "./controllers/archive-project";
7import createProjectCtrl from "./controllers/create-project";
8import deleteProjectCtrl from "./controllers/delete-project";
9import getProjectCtrl from "./controllers/get-project";
10import getProjectsCtrl from "./controllers/get-projects";
11import unarchiveProjectCtrl from "./controllers/unarchive-project";
12import updateProjectCtrl from "./controllers/update-project";
13
14const project = new Hono<{
15 Variables: {
16 userId: string;
17 workspaceId: string;
18 };
19}>()
20 .get(
21 "/",
22 describeRoute({
23 operationId: "listProjects",
24 tags: ["Projects"],
25 description: "Get all projects in a workspace",
26 responses: {
27 200: {
28 description: "List of projects with statistics",
29 content: {
30 "application/json": { schema: resolver(v.array(projectSchema)) },
31 },
32 },
33 },
34 }),
35 validator(
36 "query",
37 v.object({
38 workspaceId: v.string(),
39 includeArchived: v.optional(v.string()),
40 }),
41 ),
42 workspaceAccess.fromQuery(),
43 async (c) => {
44 const workspaceId = c.get("workspaceId");
45 const { includeArchived } = c.req.valid("query");
46 const projects = await getProjectsCtrl(
47 workspaceId,
48 includeArchived === "true",
49 );
50 return c.json(projects);
51 },
52 )
53 .post(
54 "/",
55 describeRoute({
56 operationId: "createProject",
57 tags: ["Projects"],
58 description: "Create a new project in a workspace",
59 responses: {
60 200: {
61 description: "Project created successfully",
62 content: {
63 "application/json": { schema: resolver(projectSchema) },
64 },
65 },
66 },
67 }),
68 validator(
69 "json",
70 v.object({
71 name: v.string(),
72 workspaceId: v.string(),
73 icon: v.string(),
74 slug: v.string(),
75 }),
76 ),
77 workspaceAccess.fromBody(),
78 async (c) => {
79 const { name, icon, slug } = c.req.valid("json");
80 const workspaceId = c.get("workspaceId");
81 const newProject = await createProjectCtrl(workspaceId, name, icon, slug);
82 return c.json(newProject);
83 },
84 )
85 .get(
86 "/:id",
87 describeRoute({
88 operationId: "getProject",
89 tags: ["Projects"],
90 description: "Get a specific project by ID",
91 responses: {
92 200: {
93 description: "Project details",
94 content: {
95 "application/json": { schema: resolver(projectSchema) },
96 },
97 },
98 },
99 }),
100 validator("param", v.object({ id: v.string() })),
101 workspaceAccess.fromProject(),
102 async (c) => {
103 const { id } = c.req.valid("param");
104 const workspaceId = c.get("workspaceId");
105 const projectData = await getProjectCtrl(id, workspaceId);
106 return c.json(projectData);
107 },
108 )
109 .put(
110 "/:id",
111 describeRoute({
112 operationId: "updateProject",
113 tags: ["Projects"],
114 description: "Update an existing project",
115 responses: {
116 200: {
117 description: "Project updated successfully",
118 content: {
119 "application/json": { schema: resolver(projectSchema) },
120 },
121 },
122 },
123 }),
124 validator("param", v.object({ id: v.string() })),
125 validator(
126 "json",
127 v.object({
128 name: v.string(),
129 icon: v.string(),
130 slug: v.string(),
131 description: v.string(),
132 isPublic: v.boolean(),
133 }),
134 ),
135 workspaceAccess.fromProject(),
136 async (c) => {
137 const { id } = c.req.valid("param");
138 const { name, icon, slug, description, isPublic } = c.req.valid("json");
139 const workspaceId = c.get("workspaceId");
140 const updatedProject = await updateProjectCtrl(
141 id,
142 name,
143 icon,
144 slug,
145 description,
146 isPublic,
147 workspaceId,
148 );
149 return c.json(updatedProject);
150 },
151 )
152 .delete(
153 "/:id",
154 describeRoute({
155 operationId: "deleteProject",
156 tags: ["Projects"],
157 description: "Delete a project by ID",
158 responses: {
159 200: {
160 description: "Project deleted successfully",
161 content: {
162 "application/json": { schema: resolver(projectSchema) },
163 },
164 },
165 },
166 }),
167 validator("param", v.object({ id: v.string() })),
168 workspaceAccess.fromProject(),
169 async (c) => {
170 const { id } = c.req.valid("param");
171 const workspaceId = c.get("workspaceId");
172 const deletedProject = await deleteProjectCtrl(id, workspaceId);
173 return c.json(deletedProject);
174 },
175 )
176 .put(
177 "/:id/archive",
178 describeRoute({
179 operationId: "archiveProject",
180 tags: ["Projects"],
181 description: "Archive a project by ID",
182 responses: {
183 200: {
184 description: "Project archived successfully",
185 content: {
186 "application/json": { schema: resolver(projectSchema) },
187 },
188 },
189 },
190 }),
191 validator("param", v.object({ id: v.string() })),
192 workspaceAccess.fromProject(),
193 async (c) => {
194 const { id } = c.req.valid("param");
195 const workspaceId = c.get("workspaceId");
196 const archivedProject = await archiveProjectCtrl(id, workspaceId);
197 return c.json(archivedProject);
198 },
199 )
200 .put(
201 "/:id/unarchive",
202 describeRoute({
203 operationId: "unarchiveProject",
204 tags: ["Projects"],
205 description: "Unarchive a project by ID",
206 responses: {
207 200: {
208 description: "Project unarchived successfully",
209 content: {
210 "application/json": { schema: resolver(projectSchema) },
211 },
212 },
213 },
214 }),
215 validator("param", v.object({ id: v.string() })),
216 workspaceAccess.fromProject(),
217 async (c) => {
218 const { id } = c.req.valid("param");
219 const workspaceId = c.get("workspaceId");
220 const unarchivedProject = await unarchiveProjectCtrl(id, workspaceId);
221 return c.json(unarchivedProject);
222 },
223 );
224
225export default project;