gadget/gadget-code/src/controllers/api/v1/project.ts
2026-05-16 10:48:42 -04:00

262 lines
6.5 KiB
TypeScript

// controllers/api/v1/project.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved
import { Request, Response } from "express";
import { DtpController } from "../../../lib/controller.js";
import { IUser, ProjectStatus } from "@gadget/api";
import projectService from "../../../services/project.js";
export class ProjectApiControllerV1 extends DtpController {
get name(): string {
return "ProjectApiControllerV1";
}
get slug(): string {
return "projectV1";
}
get route(): string {
return "/projects";
}
constructor() {
super();
}
async start(): Promise<void> {
this.router.use(this.requireUser());
this.router.get("/", this.getProjects.bind(this));
this.router.post("/", this.createProject.bind(this));
this.router.post("/pull", this.pullProject.bind(this));
this.router.get("/:projectId", this.getProject.bind(this));
this.router.put("/:projectId", this.updateProject.bind(this));
this.router.delete("/:projectId", this.deleteProject.bind(this));
}
async getProjects(req: Request, res: Response): Promise<void> {
try {
const projects = await projectService.getForUser(req.user);
res.status(200).json({
success: true,
data: projects,
});
} catch (error) {
this.log.error("failed to get projects", { error });
res.status(500).json({
success: false,
message: "failed to get projects",
});
}
}
async createProject(req: Request, res: Response): Promise<void> {
try {
const { name, slug, gitUrl, status } = req.body;
if (!name || !slug) {
res.status(400).json({
success: false,
message: "name and slug are required",
});
return;
}
const project = await projectService.create(req.user, {
name,
slug,
gitUrl,
status: status as ProjectStatus,
});
res.status(201).json({
success: true,
data: project,
});
} catch (error) {
this.log.error("failed to create project", { error });
res.status(500).json({
success: false,
message: "failed to create project",
});
}
}
async pullProject(req: Request, res: Response): Promise<void> {
try {
const { gitUrl } = req.body;
if (!gitUrl || typeof gitUrl !== "string") {
res.status(400).json({
success: false,
message: "gitUrl is required",
});
return;
}
// Validate the URL is parseable
const trimmedUrl = gitUrl.trim();
const isSshFormat = trimmedUrl.includes(":") && !trimmedUrl.startsWith("http") && !trimmedUrl.startsWith("ssh://");
if (!isSshFormat) {
try {
new URL(trimmedUrl);
} catch {
res.status(400).json({
success: false,
message: "invalid git URL format",
});
return;
}
}
const project = await projectService.createFromGitUrl(req.user, trimmedUrl);
res.status(201).json({
success: true,
data: project,
});
} catch (error: any) {
if (error.statusCode === 400) {
res.status(400).json({
success: false,
message: error.message,
});
return;
}
// Handle duplicate gitUrl (unique partial index violation)
if (error.code === 11000) {
res.status(409).json({
success: false,
message: "a project with this git URL already exists",
});
return;
}
this.log.error("failed to pull project", { error });
res.status(500).json({
success: false,
message: "failed to pull project",
});
}
}
async getProject(req: Request, res: Response): Promise<void> {
try {
const id = req.params.projectId as string;
const project = await projectService.findById(id);
if (!project) {
res.status(404).json({
success: false,
message: "project not found",
});
return;
}
const user = project.user as IUser;
if (user._id !== req.user._id) {
res.status(403).json({
success: false,
message: "access denied",
});
return;
}
res.status(200).json({
success: true,
data: project,
});
} catch (error) {
this.log.error("failed to get project", { error });
res.status(500).json({
success: false,
message: "failed to get project",
});
}
}
async updateProject(req: Request, res: Response): Promise<void> {
try {
const id = req.params.projectId as string;
const project = await projectService.findById(id);
if (!project) {
res.status(404).json({
success: false,
message: "project not found",
});
return;
}
const user = project.user as IUser;
if (user._id !== req.user._id) {
res.status(403).json({
success: false,
message: "access denied",
});
return;
}
const { name, slug, gitUrl, status, description, system, skills, tasks } = req.body;
const updated = await projectService.update(project, {
name,
slug,
gitUrl,
status: status as ProjectStatus,
description,
system,
skills,
tasks,
});
res.status(200).json({
success: true,
data: updated,
});
} catch (error) {
this.log.error("failed to update project", { error });
res.status(500).json({
success: false,
message: "failed to update project",
});
}
}
async deleteProject(req: Request, res: Response): Promise<void> {
try {
const id = req.params.projectId as string;
const project = await projectService.findById(id);
if (!project) {
res.status(404).json({
success: false,
message: "project not found",
});
return;
}
const user = project.user as IUser;
if (user._id !== req.user._id) {
res.status(403).json({
success: false,
message: "access denied",
});
return;
}
await projectService.delete(project);
res.status(200).json({
success: true,
message: "project deleted",
});
} catch (error) {
this.log.error("failed to delete project", { error });
res.status(500).json({
success: false,
message: "failed to delete project",
});
}
}
}
export default ProjectApiControllerV1;