feat: add tags module with CRUD operations and DTOs
Introduced a new `TagsModule` with support for creating, updating, and managing tags. Implemented DTOs (`CreateTagDto`, `UpdateTagDto`) for validation and structure. Added `TagsService` and `TagsController` with APIs for tags-to-project and tags-to-person associations.
This commit is contained in:
parent
0249d62951
commit
63458333ca
124
backend/src/modules/tags/controllers/tags.controller.ts
Normal file
124
backend/src/modules/tags/controllers/tags.controller.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Put,
|
||||||
|
UseGuards,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TagsService } from '../services/tags.service';
|
||||||
|
import { CreateTagDto } from '../dto/create-tag.dto';
|
||||||
|
import { UpdateTagDto } from '../dto/update-tag.dto';
|
||||||
|
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
@Controller('tags')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class TagsController {
|
||||||
|
constructor(private readonly tagsService: TagsService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new tag
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
create(@Body() createTagDto: CreateTagDto) {
|
||||||
|
return this.tagsService.create(createTagDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags or filter by type
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
findAll(@Query('type') type?: 'PROJECT' | 'PERSON') {
|
||||||
|
if (type) {
|
||||||
|
return this.tagsService.findByType(type);
|
||||||
|
}
|
||||||
|
return this.tagsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a tag by ID
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.tagsService.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a tag
|
||||||
|
*/
|
||||||
|
@Put(':id')
|
||||||
|
update(@Param('id') id: string, @Body() updateTagDto: UpdateTagDto) {
|
||||||
|
return this.tagsService.update(id, updateTagDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a tag
|
||||||
|
*/
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.tagsService.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a tag to a person
|
||||||
|
*/
|
||||||
|
@Post('persons/:personId/tags/:tagId')
|
||||||
|
addTagToPerson(
|
||||||
|
@Param('personId') personId: string,
|
||||||
|
@Param('tagId') tagId: string,
|
||||||
|
) {
|
||||||
|
return this.tagsService.addTagToPerson(tagId, personId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a tag from a person
|
||||||
|
*/
|
||||||
|
@Delete('persons/:personId/tags/:tagId')
|
||||||
|
removeTagFromPerson(
|
||||||
|
@Param('personId') personId: string,
|
||||||
|
@Param('tagId') tagId: string,
|
||||||
|
) {
|
||||||
|
return this.tagsService.removeTagFromPerson(tagId, personId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags for a person
|
||||||
|
*/
|
||||||
|
@Get('persons/:personId/tags')
|
||||||
|
getTagsForPerson(@Param('personId') personId: string) {
|
||||||
|
return this.tagsService.getTagsForPerson(personId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a tag to a project
|
||||||
|
*/
|
||||||
|
@Post('projects/:projectId/tags/:tagId')
|
||||||
|
addTagToProject(
|
||||||
|
@Param('projectId') projectId: string,
|
||||||
|
@Param('tagId') tagId: string,
|
||||||
|
) {
|
||||||
|
return this.tagsService.addTagToProject(tagId, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a tag from a project
|
||||||
|
*/
|
||||||
|
@Delete('projects/:projectId/tags/:tagId')
|
||||||
|
removeTagFromProject(
|
||||||
|
@Param('projectId') projectId: string,
|
||||||
|
@Param('tagId') tagId: string,
|
||||||
|
) {
|
||||||
|
return this.tagsService.removeTagFromProject(tagId, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags for a project
|
||||||
|
*/
|
||||||
|
@Get('projects/:projectId/tags')
|
||||||
|
getTagsForProject(@Param('projectId') projectId: string) {
|
||||||
|
return this.tagsService.getTagsForProject(projectId);
|
||||||
|
}
|
||||||
|
}
|
32
backend/src/modules/tags/dto/create-tag.dto.ts
Normal file
32
backend/src/modules/tags/dto/create-tag.dto.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { IsNotEmpty, IsString, IsEnum, Matches } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for creating a new tag
|
||||||
|
*/
|
||||||
|
export class CreateTagDto {
|
||||||
|
/**
|
||||||
|
* The name of the tag
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color of the tag (hex format)
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||||
|
message: 'Color must be a valid hex color code (e.g., #FF5733)',
|
||||||
|
})
|
||||||
|
color: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the tag (PROJECT or PERSON)
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(['PROJECT', 'PERSON'], {
|
||||||
|
message: 'Type must be either PROJECT or PERSON',
|
||||||
|
})
|
||||||
|
type: 'PROJECT' | 'PERSON';
|
||||||
|
}
|
32
backend/src/modules/tags/dto/update-tag.dto.ts
Normal file
32
backend/src/modules/tags/dto/update-tag.dto.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { IsString, IsEnum, Matches, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for updating an existing tag
|
||||||
|
*/
|
||||||
|
export class UpdateTagDto {
|
||||||
|
/**
|
||||||
|
* The name of the tag
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color of the tag (hex format)
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||||
|
message: 'Color must be a valid hex color code (e.g., #FF5733)',
|
||||||
|
})
|
||||||
|
color?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the tag (PROJECT or PERSON)
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['PROJECT', 'PERSON'], {
|
||||||
|
message: 'Type must be either PROJECT or PERSON',
|
||||||
|
})
|
||||||
|
type?: 'PROJECT' | 'PERSON';
|
||||||
|
}
|
277
backend/src/modules/tags/services/tags.service.ts
Normal file
277
backend/src/modules/tags/services/tags.service.ts
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { DRIZZLE } from '../../../database/database.module';
|
||||||
|
import * as schema from '../../../database/schema';
|
||||||
|
import { CreateTagDto } from '../dto/create-tag.dto';
|
||||||
|
import { UpdateTagDto } from '../dto/update-tag.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TagsService {
|
||||||
|
constructor(@Inject(DRIZZLE) private readonly db: any) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new tag
|
||||||
|
*/
|
||||||
|
async create(createTagDto: CreateTagDto) {
|
||||||
|
const [tag] = await this.db
|
||||||
|
.insert(schema.tags)
|
||||||
|
.values({
|
||||||
|
...createTagDto,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all tags
|
||||||
|
*/
|
||||||
|
async findAll() {
|
||||||
|
return this.db.select().from(schema.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find tags by type
|
||||||
|
*/
|
||||||
|
async findByType(type: 'PROJECT' | 'PERSON') {
|
||||||
|
return this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.tags)
|
||||||
|
.where(eq(schema.tags.type, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a tag by ID
|
||||||
|
*/
|
||||||
|
async findById(id: string) {
|
||||||
|
const [tag] = await this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.tags)
|
||||||
|
.where(eq(schema.tags.id, id));
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
throw new NotFoundException(`Tag with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a tag
|
||||||
|
*/
|
||||||
|
async update(id: string, updateTagDto: UpdateTagDto) {
|
||||||
|
const [tag] = await this.db
|
||||||
|
.update(schema.tags)
|
||||||
|
.set({
|
||||||
|
...updateTagDto,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.tags.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
throw new NotFoundException(`Tag with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a tag
|
||||||
|
*/
|
||||||
|
async remove(id: string) {
|
||||||
|
const [tag] = await this.db
|
||||||
|
.delete(schema.tags)
|
||||||
|
.where(eq(schema.tags.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
throw new NotFoundException(`Tag with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a tag to a person
|
||||||
|
*/
|
||||||
|
async addTagToPerson(tagId: string, personId: string) {
|
||||||
|
// Check if the tag exists and is of type PERSON
|
||||||
|
const tag = await this.findById(tagId);
|
||||||
|
if (tag.type !== 'PERSON') {
|
||||||
|
throw new Error(`Tag with ID ${tagId} is not of type PERSON`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the person exists
|
||||||
|
const [person] = await this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.persons)
|
||||||
|
.where(eq(schema.persons.id, personId));
|
||||||
|
|
||||||
|
if (!person) {
|
||||||
|
throw new NotFoundException(`Person with ID ${personId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the tag is already associated with the person
|
||||||
|
const [existingRelation] = await this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.personToTag)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.personToTag.personId, personId),
|
||||||
|
eq(schema.personToTag.tagId, tagId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingRelation) {
|
||||||
|
return existingRelation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the tag to the person
|
||||||
|
const [relation] = await this.db
|
||||||
|
.insert(schema.personToTag)
|
||||||
|
.values({
|
||||||
|
personId,
|
||||||
|
tagId,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a tag from a person
|
||||||
|
*/
|
||||||
|
async removeTagFromPerson(tagId: string, personId: string) {
|
||||||
|
const [relation] = await this.db
|
||||||
|
.delete(schema.personToTag)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.personToTag.personId, personId),
|
||||||
|
eq(schema.personToTag.tagId, tagId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!relation) {
|
||||||
|
throw new NotFoundException(`Tag with ID ${tagId} is not associated with person with ID ${personId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a tag to a project
|
||||||
|
*/
|
||||||
|
async addTagToProject(tagId: string, projectId: string) {
|
||||||
|
// Check if the tag exists and is of type PROJECT
|
||||||
|
const tag = await this.findById(tagId);
|
||||||
|
if (tag.type !== 'PROJECT') {
|
||||||
|
throw new Error(`Tag with ID ${tagId} is not of type PROJECT`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the project exists
|
||||||
|
const [project] = await this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.projects)
|
||||||
|
.where(eq(schema.projects.id, projectId));
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the tag is already associated with the project
|
||||||
|
const [existingRelation] = await this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.projectToTag)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.projectToTag.projectId, projectId),
|
||||||
|
eq(schema.projectToTag.tagId, tagId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingRelation) {
|
||||||
|
return existingRelation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the tag to the project
|
||||||
|
const [relation] = await this.db
|
||||||
|
.insert(schema.projectToTag)
|
||||||
|
.values({
|
||||||
|
projectId,
|
||||||
|
tagId,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a tag from a project
|
||||||
|
*/
|
||||||
|
async removeTagFromProject(tagId: string, projectId: string) {
|
||||||
|
const [relation] = await this.db
|
||||||
|
.delete(schema.projectToTag)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.projectToTag.projectId, projectId),
|
||||||
|
eq(schema.projectToTag.tagId, tagId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!relation) {
|
||||||
|
throw new NotFoundException(`Tag with ID ${tagId} is not associated with project with ID ${projectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags for a person
|
||||||
|
*/
|
||||||
|
async getTagsForPerson(personId: string) {
|
||||||
|
// Check if the person exists
|
||||||
|
const [person] = await this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.persons)
|
||||||
|
.where(eq(schema.persons.id, personId));
|
||||||
|
|
||||||
|
if (!person) {
|
||||||
|
throw new NotFoundException(`Person with ID ${personId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all tags for the person
|
||||||
|
return this.db
|
||||||
|
.select({
|
||||||
|
tag: schema.tags,
|
||||||
|
})
|
||||||
|
.from(schema.personToTag)
|
||||||
|
.innerJoin(schema.tags, eq(schema.personToTag.tagId, schema.tags.id))
|
||||||
|
.where(eq(schema.personToTag.personId, personId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tags for a project
|
||||||
|
*/
|
||||||
|
async getTagsForProject(projectId: string) {
|
||||||
|
// Check if the project exists
|
||||||
|
const [project] = await this.db
|
||||||
|
.select()
|
||||||
|
.from(schema.projects)
|
||||||
|
.where(eq(schema.projects.id, projectId));
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all tags for the project
|
||||||
|
return this.db
|
||||||
|
.select({
|
||||||
|
tag: schema.tags,
|
||||||
|
})
|
||||||
|
.from(schema.projectToTag)
|
||||||
|
.innerJoin(schema.tags, eq(schema.projectToTag.tagId, schema.tags.id))
|
||||||
|
.where(eq(schema.projectToTag.projectId, projectId));
|
||||||
|
}
|
||||||
|
}
|
10
backend/src/modules/tags/tags.module.ts
Normal file
10
backend/src/modules/tags/tags.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TagsController } from './controllers/tags.controller';
|
||||||
|
import { TagsService } from './services/tags.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [TagsController],
|
||||||
|
providers: [TagsService],
|
||||||
|
exports: [TagsService],
|
||||||
|
})
|
||||||
|
export class TagsModule {}
|
Loading…
x
Reference in New Issue
Block a user