// services/project.ts // Copyright (C) 2026 Robert Colbert // All Rights Reserved import slug from "slug"; import { MongooseBaseQueryOptions, PopulateOptions } from "mongoose"; import { IProject, IUser, ProjectStatus } from "@gadget/api"; import Project from "@/models/project.js"; import { DtpService } from "../lib/service.js"; export interface IProjectDefinition { name: string; slug: string; gitUrl?: string; status?: ProjectStatus; } class ProjectService extends DtpService { private populateProject: PopulateOptions[]; get name(): string { return "ProjectService"; } get slug(): string { return "svc:project"; } constructor() { super(); this.populateProject = [ { path: "user", select: "-passwordSalt -password", }, ]; } async start(): Promise { this.log.info("service started"); } async stop(): Promise { this.log.info("service stopped"); } async create(user: IUser, definition: IProjectDefinition): Promise { const NOW = new Date(); const project = new Project(); project.createdAt = NOW; project.user = user._id; project.status = ProjectStatus.Active; project.name = definition.name; project.slug = slug(definition.slug.trim().toLowerCase()); if (definition.gitUrl) { project.gitUrl = definition.gitUrl; } this.log.info("creating project", { name: project.name }); await project.save(); return project.populate(this.populateProject); } async update( project: IProject, definition: IProjectDefinition, ): Promise { const update: MongooseBaseQueryOptions = { $set: {}, $unset: {} }; if (definition.status) { if (definition.status !== project.status) { update.$set.status = definition.status; } } if (definition.name) { if (definition.name !== project.name) { update.$set.name = definition.name; } } if (definition.slug) { if (definition.slug !== project.slug) { update.$set.slug = slug(definition.slug.trim().toLowerCase()); } } if (definition.gitUrl) { if (definition.gitUrl !== project.gitUrl) { update.$set.gitUrl = definition.gitUrl; } } else { update.$unset.gitUrl = 1; } const newProject = await Project.findOneAndUpdate( { _id: project._id }, update, { new: true, populate: this.populateProject }, ); if (!newProject) { const error = new Error("project has been removed"); error.statusCode = 404; throw error; } this.log.info("project updated", { old: project, new: newProject.toObject(), }); return newProject; } async setStatus(project: IProject, status: ProjectStatus): Promise { const newProject = await Project.findOneAndUpdate( { _id: project._id }, { $set: { status } }, { new: true, populate: this.populateProject }, ); if (!newProject) { const error = new Error("project has been removed"); error.statusCode = 404; throw error; } this.log.info("project status updated", { project: { _id: project._id, slug: project.slug, }, old: project.status, new: newProject.status, }); return newProject; } async getForUser(user: IUser): Promise { const projects = await Project.find({ user: user._id }) .sort({ name: 1 }) .populate(this.populateProject); return projects; } async findById(id: string): Promise { return Project.findById(id).populate(this.populateProject); } async findBySlug(slug: string, user: IUser): Promise { return Project.findOne({ slug, user: user._id }).populate( this.populateProject, ); } async delete(project: IProject): Promise { await Project.deleteOne({ _id: project._id }); } } export default new ProjectService();