added project service

This commit is contained in:
Rob Colbert 2026-04-28 16:18:21 -04:00
parent 2129ff798b
commit f900ecb3dd
5 changed files with 181 additions and 1 deletions

View File

@ -55,6 +55,7 @@
"react-router-dom": "^7.14.2",
"rotating-file-stream": "^3.2.6",
"serve-favicon": "^2.5.1",
"slug": "^11.0.1",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"uikit": "^3.23.11",
@ -81,6 +82,7 @@
"@types/numeral": "^2.0.5",
"@types/react": "^19.2.14",
"@types/serve-favicon": "^2.5.7",
"@types/slug": "^5.0.9",
"@types/uikit": "^3.14.5",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^6.0.1",

View File

@ -4,11 +4,12 @@
import { Types, Schema, model } from "mongoose";
import { IProject } from "@gadget/api";
import { ProjectStatus, IProject } from "@gadget/api";
export const ProjectSchema = new Schema<IProject>({
createdAt: { type: Date, default: Date.now, required: true },
user: { type: Types.ObjectId, required: true, index: 1, ref: "User" },
status: { type: String, enum: ProjectStatus, required: true },
name: { type: String, default: "New Project", required: true },
slug: {
type: String,

View File

@ -0,0 +1,153 @@
// services/project.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// 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<void> {
this.log.info("service started");
}
async stop(): Promise<void> {
this.log.info("service stopped");
}
async create(user: IUser, definition: IProjectDefinition): Promise<IProject> {
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<IProject> {
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.toObject ? project.toObject() : project,
new: newProject.toObject(),
});
return newProject;
}
async setStatus(project: IProject, status: ProjectStatus): Promise<IProject> {
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.toHexString(),
slug: project.slug,
},
old: project.status,
new: newProject.status,
});
return newProject;
}
async getForUser(user: IUser): Promise<IProject[]> {
const projects = await Project.find({ user: user._id })
.sort({ name: 1 })
.populate(this.populateProject);
return projects;
}
}
export default new ProjectService();

View File

@ -5,9 +5,16 @@
import { Document, Types } from "mongoose";
import type { IUser } from "./user.js";
export enum ProjectStatus {
Active = "active",
Inactive = "inactive",
Archived = "archived",
}
export interface IProject extends Document {
createdAt: Date;
user: IUser | Types.ObjectId;
status: ProjectStatus;
name: string;
slug: string;
gitUrl?: string;

View File

@ -112,6 +112,9 @@ importers:
serve-favicon:
specifier: ^2.5.1
version: 2.5.1
slug:
specifier: ^11.0.1
version: 11.0.1
socket.io:
specifier: ^4.8.3
version: 4.8.3
@ -185,6 +188,9 @@ importers:
'@types/serve-favicon':
specifier: ^2.5.7
version: 2.5.7
'@types/slug':
specifier: ^5.0.9
version: 5.0.9
'@types/uikit':
specifier: ^3.14.5
version: 3.23.0
@ -1159,6 +1165,9 @@ packages:
'@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
'@types/slug@5.0.9':
resolution: {integrity: sha512-6Yp8BSplP35Esa/wOG1wLNKiqXevpQTEF/RcL/NV6BBQaMmZh4YlDwCgrrFSoUE4xAGvnKd5c+lkQJmPrBAzfQ==}
'@types/uikit@3.23.0':
resolution: {integrity: sha512-GTn8/K+f4AjFxtLqRKWzjaVKckQp/rHcl20IO09salh2VjyFb9CqeFugL95skO6qbbJLil3PE1MmNajrSj5gMg==}
@ -2792,6 +2801,10 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
slug@11.0.1:
resolution: {integrity: sha512-VrM060OM/E7rdLQSnp6JHrzFfJFmqQBp0+TMhZStnEB8PfNliaZ9UWYjTHGHLUFVJorZ8TjVd/aKvIxHWU2O7g==}
hasBin: true
socket.io-adapter@2.5.6:
resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==}
@ -3829,6 +3842,8 @@ snapshots:
'@types/http-errors': 2.0.5
'@types/node': 25.6.0
'@types/slug@5.0.9': {}
'@types/uikit@3.23.0': {}
'@types/uuid@10.0.0': {}
@ -5615,6 +5630,8 @@ snapshots:
slash@3.0.0: {}
slug@11.0.1: {}
socket.io-adapter@2.5.6:
dependencies:
debug: 4.4.3