Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Fixes CI pipeline failures caused by missing Prisma Client generation and TypeScript type safety issues. Added Prisma generation step to CI pipeline, installed missing type dependencies, and resolved 40+ exactOptionalPropertyTypes violations across service layer. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
331 lines
8.7 KiB
TypeScript
331 lines
8.7 KiB
TypeScript
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
import { Prisma } from "@prisma/client";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { ActivityService } from "../activity/activity.service";
|
|
import { TaskStatus, TaskPriority } from "@prisma/client";
|
|
import type { CreateTaskDto, UpdateTaskDto, QueryTasksDto } from "./dto";
|
|
|
|
/**
|
|
* Service for managing tasks
|
|
*/
|
|
@Injectable()
|
|
export class TasksService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly activityService: ActivityService
|
|
) {}
|
|
|
|
/**
|
|
* Create a new task
|
|
*/
|
|
async create(workspaceId: string, userId: string, createTaskDto: CreateTaskDto) {
|
|
const assigneeConnection = createTaskDto.assigneeId
|
|
? { connect: { id: createTaskDto.assigneeId } }
|
|
: undefined;
|
|
|
|
const projectConnection = createTaskDto.projectId
|
|
? { connect: { id: createTaskDto.projectId } }
|
|
: undefined;
|
|
|
|
const parentConnection = createTaskDto.parentId
|
|
? { connect: { id: createTaskDto.parentId } }
|
|
: undefined;
|
|
|
|
const data: Prisma.TaskCreateInput = {
|
|
title: createTaskDto.title,
|
|
description: createTaskDto.description ?? null,
|
|
dueDate: createTaskDto.dueDate ?? null,
|
|
workspace: { connect: { id: workspaceId } },
|
|
creator: { connect: { id: userId } },
|
|
status: createTaskDto.status ?? TaskStatus.NOT_STARTED,
|
|
priority: createTaskDto.priority ?? TaskPriority.MEDIUM,
|
|
sortOrder: createTaskDto.sortOrder ?? 0,
|
|
metadata: createTaskDto.metadata
|
|
? (createTaskDto.metadata as unknown as Prisma.InputJsonValue)
|
|
: {},
|
|
...(assigneeConnection && { assignee: assigneeConnection }),
|
|
...(projectConnection && { project: projectConnection }),
|
|
...(parentConnection && { parent: parentConnection }),
|
|
};
|
|
|
|
// Set completedAt if status is COMPLETED
|
|
if (data.status === TaskStatus.COMPLETED) {
|
|
data.completedAt = new Date();
|
|
}
|
|
|
|
const task = await this.prisma.task.create({
|
|
data,
|
|
include: {
|
|
assignee: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
creator: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
project: {
|
|
select: { id: true, name: true, color: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
// Log activity
|
|
await this.activityService.logTaskCreated(workspaceId, userId, task.id, {
|
|
title: task.title,
|
|
});
|
|
|
|
return task;
|
|
}
|
|
|
|
/**
|
|
* Get paginated tasks with filters
|
|
*/
|
|
async findAll(query: QueryTasksDto) {
|
|
const page = query.page ?? 1;
|
|
const limit = query.limit ?? 50;
|
|
const skip = (page - 1) * limit;
|
|
|
|
// Build where clause
|
|
const where: Prisma.TaskWhereInput = query.workspaceId
|
|
? {
|
|
workspaceId: query.workspaceId,
|
|
}
|
|
: {};
|
|
|
|
if (query.status) {
|
|
where.status = Array.isArray(query.status) ? { in: query.status } : query.status;
|
|
}
|
|
|
|
if (query.priority) {
|
|
where.priority = Array.isArray(query.priority) ? { in: query.priority } : query.priority;
|
|
}
|
|
|
|
if (query.assigneeId) {
|
|
where.assigneeId = query.assigneeId;
|
|
}
|
|
|
|
if (query.projectId) {
|
|
where.projectId = query.projectId;
|
|
}
|
|
|
|
if (query.parentId) {
|
|
where.parentId = query.parentId;
|
|
}
|
|
|
|
if (query.dueDateFrom || query.dueDateTo) {
|
|
where.dueDate = {};
|
|
if (query.dueDateFrom) {
|
|
where.dueDate.gte = query.dueDateFrom;
|
|
}
|
|
if (query.dueDateTo) {
|
|
where.dueDate.lte = query.dueDateTo;
|
|
}
|
|
}
|
|
|
|
// Execute queries in parallel
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.task.findMany({
|
|
where,
|
|
include: {
|
|
assignee: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
creator: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
project: {
|
|
select: { id: true, name: true, color: true },
|
|
},
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
skip,
|
|
take: limit,
|
|
}),
|
|
this.prisma.task.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
data,
|
|
meta: {
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a single task by ID
|
|
*/
|
|
async findOne(id: string, workspaceId: string) {
|
|
const task = await this.prisma.task.findUnique({
|
|
where: {
|
|
id,
|
|
workspaceId,
|
|
},
|
|
include: {
|
|
assignee: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
creator: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
project: {
|
|
select: { id: true, name: true, color: true },
|
|
},
|
|
subtasks: {
|
|
include: {
|
|
assignee: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!task) {
|
|
throw new NotFoundException(`Task with ID ${id} not found`);
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
/**
|
|
* Update a task
|
|
*/
|
|
async update(id: string, workspaceId: string, userId: string, updateTaskDto: UpdateTaskDto) {
|
|
// Verify task exists
|
|
const existingTask = await this.prisma.task.findUnique({
|
|
where: { id, workspaceId },
|
|
});
|
|
|
|
if (!existingTask) {
|
|
throw new NotFoundException(`Task with ID ${id} not found`);
|
|
}
|
|
|
|
// Build update data - only include defined fields
|
|
const data: Prisma.TaskUpdateInput = {};
|
|
|
|
if (updateTaskDto.title !== undefined) {
|
|
data.title = updateTaskDto.title;
|
|
}
|
|
if (updateTaskDto.description !== undefined) {
|
|
data.description = updateTaskDto.description;
|
|
}
|
|
if (updateTaskDto.status !== undefined) {
|
|
data.status = updateTaskDto.status;
|
|
}
|
|
if (updateTaskDto.priority !== undefined) {
|
|
data.priority = updateTaskDto.priority;
|
|
}
|
|
if (updateTaskDto.dueDate !== undefined) {
|
|
data.dueDate = updateTaskDto.dueDate;
|
|
}
|
|
if (updateTaskDto.sortOrder !== undefined) {
|
|
data.sortOrder = updateTaskDto.sortOrder;
|
|
}
|
|
if (updateTaskDto.metadata !== undefined) {
|
|
data.metadata = updateTaskDto.metadata as unknown as Prisma.InputJsonValue;
|
|
}
|
|
if (updateTaskDto.assigneeId !== undefined && updateTaskDto.assigneeId !== null) {
|
|
data.assignee = { connect: { id: updateTaskDto.assigneeId } };
|
|
}
|
|
if (updateTaskDto.projectId !== undefined && updateTaskDto.projectId !== null) {
|
|
data.project = { connect: { id: updateTaskDto.projectId } };
|
|
}
|
|
if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) {
|
|
data.parent = { connect: { id: updateTaskDto.parentId } };
|
|
}
|
|
|
|
// Handle completedAt based on status changes
|
|
if (updateTaskDto.status) {
|
|
if (
|
|
updateTaskDto.status === TaskStatus.COMPLETED &&
|
|
existingTask.status !== TaskStatus.COMPLETED
|
|
) {
|
|
data.completedAt = new Date();
|
|
} else if (
|
|
updateTaskDto.status !== TaskStatus.COMPLETED &&
|
|
existingTask.status === TaskStatus.COMPLETED
|
|
) {
|
|
data.completedAt = null;
|
|
}
|
|
}
|
|
|
|
const task = await this.prisma.task.update({
|
|
where: {
|
|
id,
|
|
workspaceId,
|
|
},
|
|
data,
|
|
include: {
|
|
assignee: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
creator: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
project: {
|
|
select: { id: true, name: true, color: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
// Log activities
|
|
await this.activityService.logTaskUpdated(workspaceId, userId, id, {
|
|
changes: updateTaskDto as Prisma.JsonValue,
|
|
});
|
|
|
|
// Log completion if status changed to COMPLETED
|
|
if (
|
|
updateTaskDto.status === TaskStatus.COMPLETED &&
|
|
existingTask.status !== TaskStatus.COMPLETED
|
|
) {
|
|
await this.activityService.logTaskCompleted(workspaceId, userId, id);
|
|
}
|
|
|
|
// Log assignment if assigneeId changed
|
|
if (
|
|
updateTaskDto.assigneeId !== undefined &&
|
|
updateTaskDto.assigneeId !== existingTask.assigneeId
|
|
) {
|
|
await this.activityService.logTaskAssigned(
|
|
workspaceId,
|
|
userId,
|
|
id,
|
|
updateTaskDto.assigneeId ?? ""
|
|
);
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
/**
|
|
* Delete a task
|
|
*/
|
|
async remove(id: string, workspaceId: string, userId: string) {
|
|
// Verify task exists
|
|
const task = await this.prisma.task.findUnique({
|
|
where: { id, workspaceId },
|
|
});
|
|
|
|
if (!task) {
|
|
throw new NotFoundException(`Task with ID ${id} not found`);
|
|
}
|
|
|
|
await this.prisma.task.delete({
|
|
where: {
|
|
id,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
// Log activity
|
|
await this.activityService.logTaskDeleted(workspaceId, userId, id, {
|
|
title: task.title,
|
|
});
|
|
}
|
|
}
|