Skip to content

Commit

Permalink
feat(backend): adding project relationship with user and chat (#114)
Browse files Browse the repository at this point in the history
Still working on this
  • Loading branch information
Sma1lboy authored Feb 5, 2025
1 parent a81d446 commit af3495a
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 43 deletions.
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

0 comments on commit af3495a

Please sign in to comment.