- Backend: Bun + Elysia + Drizzle ORM + PostgreSQL - Frontend: React + Vite + TailwindCSS + Zustand - Auth: better-auth with invite-only system - Features: Tasks, Projects, Sections, Labels, Comments - Hammer API: Dedicated endpoints for AI assistant integration - Unit tests: 24 passing tests - Docker: Compose file for deployment
147 lines
3.4 KiB
TypeScript
147 lines
3.4 KiB
TypeScript
import { Elysia, t } from 'elysia';
|
|
import { db } from '../db';
|
|
import { comments, tasks } from '../db/schema';
|
|
import { eq, and, asc } from 'drizzle-orm';
|
|
import type { User } from '../lib/auth';
|
|
|
|
export const commentRoutes = new Elysia({ prefix: '/comments' })
|
|
// Get comments for a task
|
|
.get('/task/:taskId', async ({ params, user, set }) => {
|
|
// Verify task ownership
|
|
const task = await db.query.tasks.findFirst({
|
|
where: and(
|
|
eq(tasks.id, params.taskId),
|
|
eq(tasks.userId, (user as User).id)
|
|
),
|
|
});
|
|
|
|
if (!task) {
|
|
set.status = 404;
|
|
throw new Error('Task not found');
|
|
}
|
|
|
|
const taskComments = await db.query.comments.findMany({
|
|
where: eq(comments.taskId, params.taskId),
|
|
orderBy: [asc(comments.createdAt)],
|
|
with: {
|
|
user: {
|
|
columns: { id: true, name: true, image: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
return taskComments;
|
|
}, {
|
|
params: t.Object({
|
|
taskId: t.String(),
|
|
}),
|
|
})
|
|
|
|
// Create comment
|
|
.post('/', async ({ body, user }) => {
|
|
const userId = (user as User).id;
|
|
|
|
// Verify task ownership
|
|
const task = await db.query.tasks.findFirst({
|
|
where: and(
|
|
eq(tasks.id, body.taskId),
|
|
eq(tasks.userId, userId)
|
|
),
|
|
});
|
|
|
|
if (!task) {
|
|
throw new Error('Task not found');
|
|
}
|
|
|
|
const [comment] = await db.insert(comments).values({
|
|
taskId: body.taskId,
|
|
userId,
|
|
content: body.content,
|
|
attachments: body.attachments || [],
|
|
}).returning();
|
|
|
|
// Return with user info
|
|
const fullComment = await db.query.comments.findFirst({
|
|
where: eq(comments.id, comment.id),
|
|
with: {
|
|
user: {
|
|
columns: { id: true, name: true, image: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
return fullComment;
|
|
}, {
|
|
body: t.Object({
|
|
taskId: t.String(),
|
|
content: t.String({ minLength: 1 }),
|
|
attachments: t.Optional(t.Array(t.Object({
|
|
name: t.String(),
|
|
url: t.String(),
|
|
type: t.String(),
|
|
}))),
|
|
}),
|
|
})
|
|
|
|
// Update comment
|
|
.patch('/:id', async ({ params, body, user, set }) => {
|
|
const existing = await db.query.comments.findFirst({
|
|
where: and(
|
|
eq(comments.id, params.id),
|
|
eq(comments.userId, (user as User).id)
|
|
),
|
|
});
|
|
|
|
if (!existing) {
|
|
set.status = 404;
|
|
throw new Error('Comment not found');
|
|
}
|
|
|
|
const [updated] = await db
|
|
.update(comments)
|
|
.set({
|
|
content: body.content,
|
|
attachments: body.attachments,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(comments.id, params.id))
|
|
.returning();
|
|
|
|
return updated;
|
|
}, {
|
|
params: t.Object({
|
|
id: t.String(),
|
|
}),
|
|
body: t.Object({
|
|
content: t.Optional(t.String({ minLength: 1 })),
|
|
attachments: t.Optional(t.Array(t.Object({
|
|
name: t.String(),
|
|
url: t.String(),
|
|
type: t.String(),
|
|
}))),
|
|
}),
|
|
})
|
|
|
|
// Delete comment
|
|
.delete('/:id', async ({ params, user, set }) => {
|
|
const existing = await db.query.comments.findFirst({
|
|
where: and(
|
|
eq(comments.id, params.id),
|
|
eq(comments.userId, (user as User).id)
|
|
),
|
|
});
|
|
|
|
if (!existing) {
|
|
set.status = 404;
|
|
throw new Error('Comment not found');
|
|
}
|
|
|
|
await db.delete(comments).where(eq(comments.id, params.id));
|
|
|
|
return { success: true };
|
|
}, {
|
|
params: t.Object({
|
|
id: t.String(),
|
|
}),
|
|
});
|