diff --git a/backend/src/build-system/context.ts b/backend/src/build-system/context.ts index 5fa56427..725b2d23 100644 --- a/backend/src/build-system/context.ts +++ b/backend/src/build-system/context.ts @@ -107,12 +107,11 @@ export class BuilderContext { this.globalContext.set('databaseType', sequence.databaseType || 'SQLite'); const projectUUIDPath = - new Date().toISOString().slice(0, 18).replaceAll(/:/g, '-') + - '-' + - uuidv4(); + new Date().toISOString().slice(0, 18).replaceAll(/:/g, '-') + + '-' + + uuidv4(); this.globalContext.set('projectUUID', projectUUIDPath); - if (process.env.DEBUG) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); this.logFolder = path.join( 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/dto/project.input.ts b/backend/src/project/dto/project.input.ts index 9f39fa3d..8e3568f3 100644 --- a/backend/src/project/dto/project.input.ts +++ b/backend/src/project/dto/project.input.ts @@ -1,5 +1,6 @@ // DTOs for Project APIs import { InputType, Field, ID } from '@nestjs/graphql'; +import { IsNotEmpty, IsString, IsUUID, IsOptional } from 'class-validator'; /** * @deprecated We don't need project upsert @@ -7,15 +8,24 @@ import { InputType, Field, ID } from '@nestjs/graphql'; @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[]; } @InputType() diff --git a/backend/src/project/project.model.ts b/backend/src/project/project.model.ts index 599a675d..11f36406 100644 --- a/backend/src/project/project.model.ts +++ b/backend/src/project/project.model.ts @@ -11,12 +11,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() @@ -29,10 +30,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 }) @@ -52,4 +57,11 @@ export class Project extends SystemBaseModel { }, }) projectPackages: ProjectPackages[]; + + @Field(() => [Chat], { nullable: true }) + @OneToMany(() => Chat, (chat) => chat.project, { + cascade: true, + eager: false, + }) + chats: Chat[]; } diff --git a/backend/src/project/project.resolver.ts b/backend/src/project/project.resolver.ts index d9f4b7f6..50b97ad3 100644 --- a/backend/src/project/project.resolver.ts +++ b/backend/src/project/project.resolver.ts @@ -1,29 +1,33 @@ // 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 { CreateProjectInput, IsValidProjectInput } 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); } @@ -48,4 +52,16 @@ export class ProjectsResolver { ): Promise { return this.projectsService.isValidProject(userId, input); } + + @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 cc70180e..f23fabb9 100644 --- a/backend/src/project/project.service.ts +++ b/backend/src/project/project.service.ts @@ -34,21 +34,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; } @@ -56,12 +62,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) { @@ -69,7 +79,6 @@ export class ProjectService { } return project; } - // staring build the project async createProject( input: CreateProjectInput, @@ -98,7 +107,6 @@ export class ProjectService { input.projectName = response; this.logger.debug(`Generated project name: ${input.projectName}`); } - // Build project sequence and get project path const sequence = buildProjectSequenceByProject(input); const context = new BuilderContext(sequence, sequence.id); @@ -123,7 +131,6 @@ export class ProjectService { throw new InternalServerErrorException('Error creating the project.'); } } - private async transformInputToProjectPackages( inputPackages: ProjectPackage[], ): Promise { @@ -175,27 +182,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.'); 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', diff --git a/frontend/package.json b/frontend/package.json index dd2a8ec6..6e20e337 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "generate:watch": "graphql-codegen --watch" }, "dependencies": { + "codefox-common": "workspace:*", "@apollo/client": "^3.11.8", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", diff --git a/frontend/src/app/(main)/Home.tsx b/frontend/src/app/(main)/Home.tsx index 9b2d079e..3323b532 100644 --- a/frontend/src/app/(main)/Home.tsx +++ b/frontend/src/app/(main)/Home.tsx @@ -99,7 +99,6 @@ export default function Home() { ); } - // Render the main layout return ( diff --git a/frontend/src/app/api/project/route.ts b/frontend/src/app/api/project/route.ts index 07a4ad27..8db41882 100644 --- a/frontend/src/app/api/project/route.ts +++ b/frontend/src/app/api/project/route.ts @@ -24,7 +24,6 @@ export async function GET(req: Request) { async function fetchFileStructure(projectId) { const reader = FileReader.getInstance(); const res = await reader.getAllPaths(projectId); - if (!res || res.length === 0) { return { root: { @@ -42,7 +41,6 @@ async function fetchFileStructure(projectId) { const cleanedPaths = res.map((path) => path.replace(projectPrefix, '')); const fileRegex = /\.[a-z0-9]+$/i; - function buildTree(paths) { const tree = {}; @@ -68,7 +66,6 @@ async function fetchFileStructure(projectId) { return tree; } - function convertTreeToComplexTree(tree, parentId = 'root') { const items = {}; diff --git a/frontend/src/components/code-engine/code-engine.tsx b/frontend/src/components/code-engine/code-engine.tsx index 3cc62e3a..5d03f403 100644 --- a/frontend/src/components/code-engine/code-engine.tsx +++ b/frontend/src/components/code-engine/code-engine.tsx @@ -1,5 +1,4 @@ 'use client'; - import { Button } from '@/components/ui/button'; import Editor from '@monaco-editor/react'; import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; @@ -36,7 +35,6 @@ export function CodeEngine() { const [activeTab, setActiveTab] = useState<'preview' | 'code' | 'console'>( 'code' ); - // Callback: Handle editor mount const handleEditorMount = (editorInstance) => { editorRef.current = editorInstance; @@ -114,7 +112,6 @@ export function CodeEngine() { setCode(value); setSaving(true); }; - // Responsive toolbar component for header tabs and buttons const ResponsiveToolbar = () => { const containerRef = useRef(null); @@ -306,7 +303,6 @@ export function CodeEngine() { ); } - // SaveChangesBar component for showing unsaved changes status const SaveChangesBar = ({ saving, onSave, onReset }) => { return (