Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions backend/src/build-system/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
27 changes: 25 additions & 2 deletions backend/src/chat/chat.model.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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;
}
Expand Down
14 changes: 12 additions & 2 deletions backend/src/project/dto/project.input.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
// 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
*/
@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()
Expand Down
18 changes: 15 additions & 3 deletions backend/src/project/project.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 })
Expand All @@ -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[];
}
32 changes: 24 additions & 8 deletions backend/src/project/project.resolver.ts
Original file line number Diff line number Diff line change
@@ -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<Project[]> {
async getProjects(@GetUserIdFromToken() userId: string): Promise<Project[]> {
return this.projectsService.getProjectsByUser(userId);
}

// @GetAuthToken() token: string
@Query(() => Project)
@UseGuards(ProjectGuard)
async getProjectDetails(
@Args('projectId') projectId: string,
): Promise<Project> {
async getProject(@Args('projectId') projectId: string): Promise<Project> {
return this.projectsService.getProjectById(projectId);
}

Expand All @@ -48,4 +52,16 @@ export class ProjectsResolver {
): Promise<boolean> {
return this.projectsService.isValidProject(userId, input);
}

@ResolveField('user', () => User)
async getUser(@Parent() project: Project): Promise<User> {
const { user } = await this.projectsService.getProjectById(project.id);
return user;
}

@ResolveField('chats', () => [Chat])
async getChats(@Parent() project: Project): Promise<Chat[]> {
const { chats } = await this.projectsService.getProjectById(project.id);
return chats?.filter((chat) => !chat.isDeleted) || [];
}
}
36 changes: 23 additions & 13 deletions backend/src/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,42 +34,51 @@ export class ProjectService {
private projectPackagesRepository: Repository<ProjectPackages>,
) {}

async getProjectsByUser(userId: number): Promise<Project[]> {
async getProjectsByUser(userId: string): Promise<Project[]> {
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;
}

async getProjectById(projectId: string): Promise<Project> {
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) {
throw new NotFoundException(`Project with ID ${projectId} not found.`);
}
return project;
}

// staring build the project
async createProject(
input: CreateProjectInput,
Expand Down Expand Up @@ -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);
Expand All @@ -123,7 +131,6 @@ export class ProjectService {
throw new InternalServerErrorException('Error creating the project.');
}
}

private async transformInputToProjectPackages(
inputPackages: ProjectPackage[],
): Promise<ProjectPackages[]> {
Expand Down Expand Up @@ -175,27 +182,30 @@ export class ProjectService {
async deleteProject(projectId: string): Promise<boolean> {
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.');
Expand Down
17 changes: 13 additions & 4 deletions backend/src/user/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,7 +16,7 @@ import {
@Entity()
@ObjectType()
export class User extends SystemBaseModel {
@PrimaryGeneratedColumn()
@PrimaryGeneratedColumn('uuid')
id: string;

@Field()
Expand All @@ -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',
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion frontend/src/app/(main)/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export default function Home() {
</div>
);
}

// Render the main layout
return (
<ResizablePanelGroup direction="horizontal" className="h-full w-full">
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/app/api/project/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 = {};

Expand All @@ -68,7 +66,6 @@ async function fetchFileStructure(projectId) {

return tree;
}

function convertTreeToComplexTree(tree, parentId = 'root') {
const items = {};

Expand Down
Loading
Loading