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
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,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[];
}
18 changes: 15 additions & 3 deletions backend/src/project/project.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 })
Expand All @@ -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[];
}
34 changes: 25 additions & 9 deletions backend/src/project/project.resolver.ts
Original file line number Diff line number Diff line change
@@ -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<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);
}

@Mutation(() => Project)
async upsertProject(
@GetUserIdFromToken() userId: number,
@GetUserIdFromToken() userId: string,
@Args('upsertProjectInput') upsertProjectInput: UpsertProjectInput,
): Promise<Project> {
return this.projectsService.upsertProject(upsertProjectInput, userId);
Expand Down Expand Up @@ -58,4 +62,16 @@ export class ProjectsResolver {
): Promise<boolean> {
return this.projectsService.removePackageFromProject(projectId, packageId);
}

@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) || [];
}
}
66 changes: 43 additions & 23 deletions backend/src/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,44 @@ 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) {
Expand All @@ -57,29 +67,29 @@ export class ProjectService {

async upsertProject(
upsertProjectInput: UpsertProjectInput,
user_id: number,
userId: string,
): Promise<Project> {
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);
}
Expand All @@ -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;
});
Expand All @@ -114,27 +129,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 All @@ -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}`,
Expand All @@ -167,8 +186,9 @@ export class ProjectService {
): Promise<boolean> {
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.`);
}
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
Loading