From 927e0d71a82a612b06b2fcf7ce635e9159f5e380 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Mon, 3 Feb 2025 13:41:01 -0600 Subject: [PATCH 1/2] feat: enhance chat and project models with relationships and UUIDs --- backend/src/chat/chat.model.ts | 27 +++++++++++++++++++++++++-- backend/src/project/project.model.ts | 18 +++++++++++++++--- backend/src/user/user.model.ts | 17 +++++++++++++---- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/backend/src/chat/chat.model.ts b/backend/src/chat/chat.model.ts index 78da6062..c7a08649 100644 --- a/backend/src/chat/chat.model.ts +++ b/backend/src/chat/chat.model.ts @@ -1,8 +1,15 @@ import { Field, ObjectType, ID, registerEnumType } from '@nestjs/graphql'; -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { Message } from 'src/chat/message.model'; import { SystemBaseModel } from 'src/system-base-model/system-base.model'; import { User } from 'src/user/user.model'; +import { Project } from 'src/project/project.model'; export enum StreamStatus { STREAMING = 'streaming', @@ -41,7 +48,23 @@ export class Chat extends SystemBaseModel { }) messages: Message[]; - @ManyToOne(() => User, (user) => user.chats) + @Field(() => ID) + @Column() + projectId: string; + + @ManyToOne(() => Project, (project) => project.chats, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'project_id' }) + @Field(() => Project) + project: Project; + + @ManyToOne(() => User, (user) => user.chats, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'user_id' }) @Field(() => User) user: User; } diff --git a/backend/src/project/project.model.ts b/backend/src/project/project.model.ts index f3f59791..19f329f6 100644 --- a/backend/src/project/project.model.ts +++ b/backend/src/project/project.model.ts @@ -10,12 +10,13 @@ import { } from 'typeorm'; import { User } from 'src/user/user.model'; import { ProjectPackages } from './project-packages.model'; +import { Chat } from 'src/chat/chat.model'; @Entity() @ObjectType() export class Project extends SystemBaseModel { @Field(() => ID) - @PrimaryGeneratedColumn() + @PrimaryGeneratedColumn('uuid') id: string; @Field() @@ -28,10 +29,14 @@ export class Project extends SystemBaseModel { @Field(() => ID) @Column() - userId: number; + userId: string; - @ManyToOne(() => User) + @ManyToOne(() => User, (user) => user.projects, { + onDelete: 'CASCADE', + nullable: false, + }) @JoinColumn({ name: 'user_id' }) + @Field(() => User) user: User; @Field(() => [ProjectPackages], { nullable: true }) @@ -41,4 +46,11 @@ export class Project extends SystemBaseModel { { cascade: true }, ) projectPackages: ProjectPackages[]; + + @Field(() => [Chat], { nullable: true }) + @OneToMany(() => Chat, (chat) => chat.project, { + cascade: true, + eager: false, + }) + chats: Chat[]; } diff --git a/backend/src/user/user.model.ts b/backend/src/user/user.model.ts index fa3c91e4..209065b4 100644 --- a/backend/src/user/user.model.ts +++ b/backend/src/user/user.model.ts @@ -3,6 +3,7 @@ import { IsEmail } from 'class-validator'; import { Role } from 'src/auth/role/role.model'; import { SystemBaseModel } from 'src/system-base-model/system-base.model'; import { Chat } from 'src/chat/chat.model'; +import { Project } from 'src/project/project.model'; import { Entity, PrimaryGeneratedColumn, @@ -15,7 +16,7 @@ import { @Entity() @ObjectType() export class User extends SystemBaseModel { - @PrimaryGeneratedColumn() + @PrimaryGeneratedColumn('uuid') id: string; @Field() @@ -32,12 +33,20 @@ export class User extends SystemBaseModel { @Field(() => [Chat]) @OneToMany(() => Chat, (chat) => chat.user, { - cascade: true, // Automatically save related chats - lazy: true, // Load chats only when accessed - onDelete: 'CASCADE', // Delete chats when user is deleted + cascade: true, + lazy: true, + onDelete: 'CASCADE', }) chats: Chat[]; + @Field(() => [Project]) + @OneToMany(() => Project, (project) => project.user, { + cascade: true, + lazy: true, + onDelete: 'CASCADE', + }) + projects: Project[]; + @ManyToMany(() => Role) @JoinTable({ name: 'user_roles', From 1d886a69b427d41c9da68a0ed17a98980b847272 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Mon, 3 Feb 2025 13:41:07 -0600 Subject: [PATCH 2/2] feat: enhance project input validation and add user and chat relationships in resolvers --- backend/src/project/dto/project.input.ts | 14 ++++- backend/src/project/project.resolver.ts | 34 ++++++++---- backend/src/project/project.service.ts | 66 +++++++++++++++--------- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/backend/src/project/dto/project.input.ts b/backend/src/project/dto/project.input.ts index 572a4e21..67bc6153 100644 --- a/backend/src/project/dto/project.input.ts +++ b/backend/src/project/dto/project.input.ts @@ -1,16 +1,26 @@ // DTOs for Project APIs import { InputType, Field, ID } from '@nestjs/graphql'; +import { IsNotEmpty, IsString, IsUUID, IsOptional } from 'class-validator'; @InputType() export class UpsertProjectInput { @Field() + @IsNotEmpty() + @IsString() projectName: string; + @Field() + @IsNotEmpty() + @IsString() path: string; @Field(() => ID, { nullable: true }) - projectId: string; + @IsOptional() + @IsUUID() + projectId?: string; @Field(() => [String], { nullable: true }) - projectPackages: string[]; + @IsOptional() + @IsString({ each: true }) + projectPackages?: string[]; } diff --git a/backend/src/project/project.resolver.ts b/backend/src/project/project.resolver.ts index 44005829..357e1db6 100644 --- a/backend/src/project/project.resolver.ts +++ b/backend/src/project/project.resolver.ts @@ -1,35 +1,39 @@ // GraphQL Resolvers for Project APIs -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { + Args, + Mutation, + Query, + Resolver, + ResolveField, + Parent, +} from '@nestjs/graphql'; import { ProjectService } from './project.service'; import { Project } from './project.model'; import { UpsertProjectInput } from './dto/project.input'; import { UseGuards } from '@nestjs/common'; import { ProjectGuard } from '../guard/project.guard'; import { GetUserIdFromToken } from '../decorator/get-auth-token.decorator'; +import { User } from '../user/user.model'; +import { Chat } from '../chat/chat.model'; @Resolver(() => Project) export class ProjectsResolver { constructor(private readonly projectsService: ProjectService) {} @Query(() => [Project]) - async getUserProjects( - @GetUserIdFromToken() userId: number, - ): Promise { + async getProjects(@GetUserIdFromToken() userId: string): Promise { return this.projectsService.getProjectsByUser(userId); } - // @GetAuthToken() token: string @Query(() => Project) @UseGuards(ProjectGuard) - async getProjectDetails( - @Args('projectId') projectId: string, - ): Promise { + async getProject(@Args('projectId') projectId: string): Promise { return this.projectsService.getProjectById(projectId); } @Mutation(() => Project) async upsertProject( - @GetUserIdFromToken() userId: number, + @GetUserIdFromToken() userId: string, @Args('upsertProjectInput') upsertProjectInput: UpsertProjectInput, ): Promise { return this.projectsService.upsertProject(upsertProjectInput, userId); @@ -58,4 +62,16 @@ export class ProjectsResolver { ): Promise { return this.projectsService.removePackageFromProject(projectId, packageId); } + + @ResolveField('user', () => User) + async getUser(@Parent() project: Project): Promise { + const { user } = await this.projectsService.getProjectById(project.id); + return user; + } + + @ResolveField('chats', () => [Chat]) + async getChats(@Parent() project: Project): Promise { + const { chats } = await this.projectsService.getProjectById(project.id); + return chats?.filter((chat) => !chat.isDeleted) || []; + } } diff --git a/backend/src/project/project.service.ts b/backend/src/project/project.service.ts index a1b4c087..01437398 100644 --- a/backend/src/project/project.service.ts +++ b/backend/src/project/project.service.ts @@ -19,21 +19,27 @@ export class ProjectService { private projectPackagesRepository: Repository, ) {} - async getProjectsByUser(userId: number): Promise { + async getProjectsByUser(userId: string): Promise { const projects = await this.projectsRepository.find({ - where: { userId: userId, isDeleted: false }, - relations: ['projectPackages'], + where: { userId, isDeleted: false }, + relations: ['projectPackages', 'chats'], }); + if (projects && projects.length > 0) { projects.forEach((project) => { + // Filter deleted packages project.projectPackages = project.projectPackages.filter( (pkg) => !pkg.isDeleted, ); + // Filter deleted chats + if (project.chats) { + project.chats = project.chats.filter((chat) => !chat.isDeleted); + } }); } if (!projects || projects.length === 0) { - throw new NotFoundException(`User with ID ${userId} have no project.`); + throw new NotFoundException(`User with ID ${userId} has no projects.`); } return projects; } @@ -41,12 +47,16 @@ export class ProjectService { async getProjectById(projectId: string): Promise { const project = await this.projectsRepository.findOne({ where: { id: projectId, isDeleted: false }, - relations: ['projectPackages'], + relations: ['projectPackages', 'chats', 'user'], }); + if (project) { project.projectPackages = project.projectPackages.filter( (pkg) => !pkg.isDeleted, ); + if (project.chats) { + project.chats = project.chats.filter((chat) => !chat.isDeleted); + } } if (!project) { @@ -57,29 +67,29 @@ export class ProjectService { async upsertProject( upsertProjectInput: UpsertProjectInput, - user_id: number, + userId: string, ): Promise { const { projectId, projectName, path, projectPackages } = upsertProjectInput; let project; if (projectId) { - // only extract the project match the user id project = await this.projectsRepository.findOne({ - where: { id: projectId, isDeleted: false, userId: user_id }, + where: { id: projectId, isDeleted: false, userId }, + relations: ['projectPackages', 'chats'], }); } if (project) { // Update existing project - if (projectName) project.project_name = projectName; + if (projectName) project.projectName = projectName; if (path) project.path = path; } else { // Create a new project if it does not exist project = this.projectsRepository.create({ - projectName: projectName, + projectName, path, - userId: user_id, + userId, }); project = await this.projectsRepository.save(project); } @@ -95,17 +105,22 @@ export class ProjectService { await this.projectPackagesRepository.save(newPackages); } - // Return the updated or created project with all packages + // Return the updated or created project with all relations return await this.projectsRepository .findOne({ where: { id: project.id, isDeleted: false }, - relations: ['projectPackages'], + relations: ['projectPackages', 'chats', 'user'], }) .then((project) => { - if (project && project.projectPackages) { - project.projectPackages = project.projectPackages.filter( - (pkg) => !pkg.isDeleted, - ); + if (project) { + if (project.projectPackages) { + project.projectPackages = project.projectPackages.filter( + (pkg) => !pkg.isDeleted, + ); + } + if (project.chats) { + project.chats = project.chats.filter((chat) => !chat.isDeleted); + } } return project; }); @@ -114,27 +129,30 @@ export class ProjectService { async deleteProject(projectId: string): Promise { const project = await this.projectsRepository.findOne({ where: { id: projectId }, + relations: ['projectPackages', 'chats'], }); + if (!project) { throw new NotFoundException(`Project with ID ${projectId} not found.`); } try { - // Perform a soft delete by updating is_active and is_deleted fields + // Soft delete the project project.isActive = false; project.isDeleted = true; await this.projectsRepository.save(project); - // Perform a soft delete for related project packages - const projectPackages = project.projectPackages; - if (projectPackages && projectPackages.length > 0) { - for (const pkg of projectPackages) { + // Soft delete related project packages + if (project.projectPackages?.length > 0) { + for (const pkg of project.projectPackages) { pkg.isActive = false; pkg.isDeleted = true; await this.projectPackagesRepository.save(pkg); } } + // Note: Related chats will be automatically handled by the CASCADE setting + return true; } catch (error) { throw new InternalServerErrorException('Error deleting the project.'); @@ -148,6 +166,7 @@ export class ProjectService { const packageToRemove = await this.projectPackagesRepository.findOne({ where: { id: packageId, project: { id: projectId } }, }); + if (!packageToRemove) { throw new NotFoundException( `Package with ID ${packageId} not found for Project ID ${projectId}`, @@ -167,8 +186,9 @@ export class ProjectService { ): Promise { const project = await this.projectsRepository.findOne({ where: { id: projectId, isDeleted: false }, - relations: ['projectPackages'], + relations: ['projectPackages', 'chats'], }); + if (!project) { throw new NotFoundException(`Project with ID ${projectId} not found.`); }