Files
stack/apps/api/src/projects/projects.service.ts
Jason Woltje c221b63d14
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix: Resolve CI typecheck failures and improve type safety
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>
2026-01-30 20:39:03 -06:00

243 lines
6.2 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 { ProjectStatus } from "@prisma/client";
import type { CreateProjectDto, UpdateProjectDto, QueryProjectsDto } from "./dto";
/**
* Service for managing projects
*/
@Injectable()
export class ProjectsService {
constructor(
private readonly prisma: PrismaService,
private readonly activityService: ActivityService
) {}
/**
* Create a new project
*/
async create(workspaceId: string, userId: string, createProjectDto: CreateProjectDto) {
const data: Prisma.ProjectCreateInput = {
name: createProjectDto.name,
description: createProjectDto.description ?? null,
color: createProjectDto.color ?? null,
startDate: createProjectDto.startDate ?? null,
endDate: createProjectDto.endDate ?? null,
workspace: { connect: { id: workspaceId } },
creator: { connect: { id: userId } },
status: createProjectDto.status ?? ProjectStatus.PLANNING,
metadata: createProjectDto.metadata
? (createProjectDto.metadata as unknown as Prisma.InputJsonValue)
: {},
};
const project = await this.prisma.project.create({
data,
include: {
creator: {
select: { id: true, name: true, email: true },
},
_count: {
select: { tasks: true, events: true },
},
},
});
// Log activity
await this.activityService.logProjectCreated(workspaceId, userId, project.id, {
name: project.name,
});
return project;
}
/**
* Get paginated projects with filters
*/
async findAll(query: QueryProjectsDto) {
const page = query.page ?? 1;
const limit = query.limit ?? 50;
const skip = (page - 1) * limit;
// Build where clause
const where: Prisma.ProjectWhereInput = query.workspaceId
? {
workspaceId: query.workspaceId,
}
: {};
if (query.status) {
where.status = query.status;
}
if (query.startDateFrom || query.startDateTo) {
where.startDate = {};
if (query.startDateFrom) {
where.startDate.gte = query.startDateFrom;
}
if (query.startDateTo) {
where.startDate.lte = query.startDateTo;
}
}
// Execute queries in parallel
const [data, total] = await Promise.all([
this.prisma.project.findMany({
where,
include: {
creator: {
select: { id: true, name: true, email: true },
},
_count: {
select: { tasks: true, events: true },
},
},
orderBy: {
createdAt: "desc",
},
skip,
take: limit,
}),
this.prisma.project.count({ where }),
]);
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
/**
* Get a single project by ID
*/
async findOne(id: string, workspaceId: string) {
const project = await this.prisma.project.findUnique({
where: {
id,
workspaceId,
},
include: {
creator: {
select: { id: true, name: true, email: true },
},
tasks: {
select: {
id: true,
title: true,
status: true,
priority: true,
dueDate: true,
},
orderBy: { sortOrder: "asc" },
},
events: {
select: {
id: true,
title: true,
startTime: true,
endTime: true,
},
orderBy: { startTime: "asc" },
},
_count: {
select: { tasks: true, events: true },
},
},
});
if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`);
}
return project;
}
/**
* Update a project
*/
async update(
id: string,
workspaceId: string,
userId: string,
updateProjectDto: UpdateProjectDto
) {
// Verify project exists
const existingProject = await this.prisma.project.findUnique({
where: { id, workspaceId },
});
if (!existingProject) {
throw new NotFoundException(`Project with ID ${id} not found`);
}
// Build update data, only including defined fields
const updateData: Prisma.ProjectUpdateInput = {};
if (updateProjectDto.name !== undefined) updateData.name = updateProjectDto.name;
if (updateProjectDto.description !== undefined)
updateData.description = updateProjectDto.description;
if (updateProjectDto.status !== undefined) updateData.status = updateProjectDto.status;
if (updateProjectDto.startDate !== undefined) updateData.startDate = updateProjectDto.startDate;
if (updateProjectDto.endDate !== undefined) updateData.endDate = updateProjectDto.endDate;
if (updateProjectDto.color !== undefined) updateData.color = updateProjectDto.color;
if (updateProjectDto.metadata !== undefined) {
updateData.metadata = updateProjectDto.metadata as unknown as Prisma.InputJsonValue;
}
const project = await this.prisma.project.update({
where: {
id,
workspaceId,
},
data: updateData,
include: {
creator: {
select: { id: true, name: true, email: true },
},
_count: {
select: { tasks: true, events: true },
},
},
});
// Log activity
await this.activityService.logProjectUpdated(workspaceId, userId, id, {
changes: updateProjectDto as Prisma.JsonValue,
});
return project;
}
/**
* Delete a project
*/
async remove(id: string, workspaceId: string, userId: string) {
// Verify project exists
const project = await this.prisma.project.findUnique({
where: { id, workspaceId },
});
if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`);
}
await this.prisma.project.delete({
where: {
id,
workspaceId,
},
});
// Log activity
await this.activityService.logProjectDeleted(workspaceId, userId, id, {
name: project.name,
});
}
}