287 lines
13 KiB
JavaScript
287 lines
13 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const express_1 = require("express");
|
|
const asyncHandler_1 = require("../utils/asyncHandler");
|
|
const db_1 = require("../db");
|
|
const schema_1 = require("../db/schema");
|
|
const drizzle_orm_1 = require("drizzle-orm");
|
|
const auth_1 = require("../utils/auth");
|
|
const errorHandler_1 = require("../middleware/errorHandler");
|
|
const validation_1 = require("../utils/validation");
|
|
const zod_1 = require("zod");
|
|
const router = (0, express_1.Router)();
|
|
router.use(auth_1.authMiddleware);
|
|
function applyTaskFilters(query, userId, filters) {
|
|
const conditions = [(0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)];
|
|
if (filters.categoryId) {
|
|
conditions.push((0, drizzle_orm_1.eq)(schema_1.tasks.categoryId, filters.categoryId));
|
|
}
|
|
if (filters.completed !== undefined) {
|
|
conditions.push((0, drizzle_orm_1.eq)(schema_1.tasks.completed, filters.completed));
|
|
}
|
|
if (filters.dueBefore) {
|
|
conditions.push((0, drizzle_orm_1.lte)(schema_1.tasks.dueDate, filters.dueBefore));
|
|
}
|
|
if (filters.dueAfter) {
|
|
conditions.push((0, drizzle_orm_1.gte)(schema_1.tasks.dueDate, filters.dueAfter));
|
|
}
|
|
if (filters.priority) {
|
|
conditions.push((0, drizzle_orm_1.eq)(schema_1.tasks.priority, filters.priority));
|
|
}
|
|
return query.where((0, drizzle_orm_1.and)(...conditions));
|
|
}
|
|
function applyTaskSorting(query, sortBy = 'dueDate', sortOrder = 'asc') {
|
|
const orderFn = sortOrder === 'desc' ? drizzle_orm_1.desc : drizzle_orm_1.asc;
|
|
const columnMap = {
|
|
dueDate: schema_1.tasks.dueDate,
|
|
priority: schema_1.tasks.priority,
|
|
createdAt: schema_1.tasks.createdAt,
|
|
title: schema_1.tasks.title,
|
|
};
|
|
return query.orderBy(orderFn(columnMap[sortBy] || schema_1.tasks.dueDate));
|
|
}
|
|
router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
|
const filters = validation_1.taskQuerySchema.parse(req.query);
|
|
const userId = req.user.userId;
|
|
let query = db_1.db.select().from(schema_1.tasks);
|
|
query = applyTaskFilters(query, userId, filters);
|
|
query = applyTaskSorting(query, filters.sortBy, filters.sortOrder);
|
|
const limit = filters.limit ?? 50;
|
|
const offset = filters.offset ?? 0;
|
|
query = query.limit(limit).offset(offset);
|
|
const results = await query;
|
|
// Fetch subtasks for each task
|
|
const taskIds = results.map((t) => t.id);
|
|
let taskSubtasks = [];
|
|
if (taskIds.length > 0) {
|
|
taskSubtasks = await db_1.db
|
|
.select()
|
|
.from(schema_1.subtasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId), (0, drizzle_orm_1.inArray)(schema_1.subtasks.taskId, taskIds)))
|
|
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
|
}
|
|
const subtasksByTask = taskSubtasks.reduce((acc, st) => {
|
|
if (!acc[st.taskId])
|
|
acc[st.taskId] = [];
|
|
acc[st.taskId].push(st);
|
|
return acc;
|
|
}, {});
|
|
const tasksWithSubtasks = results.map((task) => ({
|
|
...task,
|
|
subtasks: subtasksByTask[task.id] || [],
|
|
}));
|
|
// Get total count
|
|
const countQuery = db_1.db.select({ count: (0, drizzle_orm_1.sql) `count(*)` }).from(schema_1.tasks);
|
|
const countResult = await applyTaskFilters(countQuery, userId, filters);
|
|
const total = Number(countResult[0]?.count ?? 0);
|
|
res.json({ tasks: tasksWithSubtasks, total, limit, offset });
|
|
}));
|
|
router.get('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
|
const userId = req.user.userId;
|
|
const task = await db_1.db
|
|
.select()
|
|
.from(schema_1.tasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
|
.limit(1);
|
|
if (task.length === 0) {
|
|
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
|
}
|
|
const taskSubtasks = await db_1.db
|
|
.select()
|
|
.from(schema_1.subtasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
|
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
|
res.json({ ...task[0], subtasks: taskSubtasks });
|
|
}));
|
|
router.post('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
|
const data = validation_1.taskCreateSchema.parse(req.body);
|
|
const userId = req.user.userId;
|
|
const now = Date.now();
|
|
// Verify category exists and belongs to user (optional - tasks may be uncategorized)
|
|
if (data.categoryId) {
|
|
const cat = await db_1.db
|
|
.select()
|
|
.from(schema_1.categories)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
|
.limit(1);
|
|
if (cat.length === 0) {
|
|
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
|
}
|
|
}
|
|
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
const newTask = {
|
|
id: taskId,
|
|
userId,
|
|
categoryId: data.categoryId,
|
|
title: data.title,
|
|
description: data.description ?? '',
|
|
priority: data.priority ?? 'none',
|
|
completed: false,
|
|
dueDate: data.dueDate ?? 0,
|
|
dueTime: data.dueTime ?? '',
|
|
endTime: data.endTime ?? '',
|
|
allDay: data.allDay ?? false,
|
|
assigneeId: data.assigneeId ?? null,
|
|
reminder: data.reminder ?? 'none',
|
|
reminders: data.reminders ?? '',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
await db_1.db.insert(schema_1.tasks).values(newTask);
|
|
// Create subtasks if provided
|
|
if (data.subtasks && data.subtasks.length > 0) {
|
|
const subtaskValues = data.subtasks.map((st, index) => ({
|
|
id: `sub_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}${index}`,
|
|
userId,
|
|
taskId,
|
|
title: st.title,
|
|
completed: false,
|
|
order: index,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}));
|
|
await db_1.db.insert(schema_1.subtasks).values(subtaskValues);
|
|
}
|
|
const createdSubtasks = await db_1.db
|
|
.select()
|
|
.from(schema_1.subtasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, taskId), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
|
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
|
res.status(201).json({ ...newTask, subtasks: createdSubtasks });
|
|
}));
|
|
router.patch('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
|
const data = validation_1.taskUpdateSchema.parse(req.body);
|
|
const userId = req.user.userId;
|
|
const existing = await db_1.db
|
|
.select()
|
|
.from(schema_1.tasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
|
.limit(1);
|
|
if (existing.length === 0) {
|
|
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
|
}
|
|
// Verify category if provided
|
|
if (data.categoryId) {
|
|
const cat = await db_1.db
|
|
.select()
|
|
.from(schema_1.categories)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
|
.limit(1);
|
|
if (cat.length === 0) {
|
|
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
|
}
|
|
}
|
|
// Prevent completing tasks with future due dates
|
|
if (data.completed === true) {
|
|
const effectiveDueDate = data.dueDate ?? existing[0].dueDate;
|
|
if (!(0, validation_1.canCompleteTask)(effectiveDueDate)) {
|
|
throw new errorHandler_1.AppError('VALIDATION_ERROR', 'Cannot mark a task as completed if its due date is in the future', 400);
|
|
}
|
|
}
|
|
const now = Date.now();
|
|
const updated = await db_1.db
|
|
.update(schema_1.tasks)
|
|
.set({ ...data, updatedAt: now })
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
|
.returning();
|
|
const taskSubtasks = await db_1.db
|
|
.select()
|
|
.from(schema_1.subtasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
|
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
|
res.json({ ...updated[0], subtasks: taskSubtasks });
|
|
}));
|
|
router.delete('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
|
const userId = req.user.userId;
|
|
const existing = await db_1.db
|
|
.select()
|
|
.from(schema_1.tasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
|
.limit(1);
|
|
if (existing.length === 0) {
|
|
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
|
}
|
|
await db_1.db
|
|
.delete(schema_1.tasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
|
res.status(204).send();
|
|
}));
|
|
router.post('/batch', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
|
const { operations } = zod_1.z.object({
|
|
operations: zod_1.z.array(zod_1.z.union([
|
|
zod_1.z.object({ type: zod_1.z.literal('create'), data: validation_1.taskCreateSchema }),
|
|
zod_1.z.object({ type: zod_1.z.literal('update'), id: zod_1.z.string(), data: validation_1.taskUpdateSchema }),
|
|
zod_1.z.object({ type: zod_1.z.literal('delete'), id: zod_1.z.string() }),
|
|
])),
|
|
}).parse(req.body);
|
|
const userId = req.user.userId;
|
|
const results = [];
|
|
for (const op of operations) {
|
|
try {
|
|
if (op.type === 'create') {
|
|
if (op.data.categoryId) {
|
|
const cat = await db_1.db
|
|
.select()
|
|
.from(schema_1.categories)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, op.data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
|
.limit(1);
|
|
if (cat.length === 0)
|
|
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
|
}
|
|
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
const now = Date.now();
|
|
await db_1.db.insert(schema_1.tasks).values({
|
|
id: taskId,
|
|
userId,
|
|
...op.data,
|
|
completed: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
results.push({ id: taskId, success: true });
|
|
}
|
|
else if (op.type === 'update') {
|
|
if (op.data.completed === true) {
|
|
let effectiveDueDate;
|
|
if (op.data.dueDate !== undefined) {
|
|
effectiveDueDate = op.data.dueDate;
|
|
}
|
|
else {
|
|
const existingTask = await db_1.db
|
|
.select({ dueDate: schema_1.tasks.dueDate })
|
|
.from(schema_1.tasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, op.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
|
.limit(1);
|
|
if (existingTask.length === 0)
|
|
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
|
effectiveDueDate = existingTask[0].dueDate;
|
|
}
|
|
if (!(0, validation_1.canCompleteTask)(effectiveDueDate)) {
|
|
throw new errorHandler_1.AppError('VALIDATION_ERROR', 'Cannot mark a task as completed if its due date is in the future', 400);
|
|
}
|
|
}
|
|
await db_1.db
|
|
.update(schema_1.tasks)
|
|
.set({ ...op.data, updatedAt: Date.now() })
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, op.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
|
results.push({ id: op.id, success: true });
|
|
}
|
|
else if (op.type === 'delete') {
|
|
await db_1.db
|
|
.delete(schema_1.tasks)
|
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, op.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
|
results.push({ id: op.id, success: true });
|
|
}
|
|
}
|
|
catch (error) {
|
|
results.push({
|
|
id: 'id' in op ? op.id : 'unknown',
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
}
|
|
res.json({ results });
|
|
}));
|
|
exports.default = router;
|
|
//# sourceMappingURL=tasks.js.map
|