diff --git a/src/canvas/canvas-tag.controller.ts b/src/canvas/canvas-tag.controller.ts new file mode 100644 index 0000000..87a80f6 --- /dev/null +++ b/src/canvas/canvas-tag.controller.ts @@ -0,0 +1,66 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { CanvasTagService } from './canvas-tag.service'; +import { CanvasTagCreateOrUpdateDTO, CanvasTagDTO } from './canvas.interface'; +import { AuthenticatedGuard } from '../auth/guard/authenticated.guard'; +import { Uuid } from '../common/common.interface'; +import {ListFilter, PagedResult} from '../common/pageable.utils'; + +@Controller('/canvas-tag') +export class CanvasTagController { + constructor(private readonly canvasTagService: CanvasTagService) {} + + @Post() + @UseGuards(AuthenticatedGuard) + public async create( + @Body() dto: CanvasTagCreateOrUpdateDTO, + ): Promise { + const tag = await this.canvasTagService.create(dto); + return { + id: tag.id, + name: tag.name, + color: tag.color, + }; + } + + @Get('/:id') + @UseGuards(AuthenticatedGuard) + public async readById(@Param('id') id: Uuid): Promise { + return await this.canvasTagService.readById(id); + } + + @Get('/') + @UseGuards(AuthenticatedGuard) + public async readAll( + @Query() filter: ListFilter, + ): Promise> { + return await this.canvasTagService.readAll(filter); + } + + @Put('/:id') + @UseGuards(AuthenticatedGuard) + public async update( + @Param('id') id: Uuid, + @Body() dto: CanvasTagCreateOrUpdateDTO, + ): Promise { + return await this.canvasTagService.update({ + ...dto, + id, + }); + } + + @Delete('/:id') + @UseGuards(AuthenticatedGuard) + public async delete(@Param('id') id: Uuid) { + await this.canvasTagService.delete({ id }); + } +} diff --git a/src/canvas/canvas-tag.service.ts b/src/canvas/canvas-tag.service.ts new file mode 100644 index 0000000..256d650 --- /dev/null +++ b/src/canvas/canvas-tag.service.ts @@ -0,0 +1,84 @@ +import { ConflictException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CanvasTagEntity } from './entity/canvas-tag.entity'; +import { Repository } from 'typeorm'; +import { + CanvasTagCreateCommand, + CanvasTagDeleteCommand, + CanvasTagUpdateCommand, +} from './canvas.interface'; +import { + ListFilter, + PageableUtils, + PagedResult, +} from '../common/pageable.utils'; +import { Uuid } from '../common/common.interface'; + +@Injectable() +export class CanvasTagService { + constructor( + @InjectRepository(CanvasTagEntity) + private readonly canvasTagRepository: Repository, + ) {} + + public async create( + command: CanvasTagCreateCommand, + ): Promise { + const tagName = command.name?.trim().toUpperCase(); + const existingTag = await this.canvasTagRepository.findOne({ + where: { name: tagName }, + }); + if (existingTag) { + throw new ConflictException(); + } + const tag = new CanvasTagEntity(); + tag.name = tagName; + tag.color = command.color; + await this.canvasTagRepository.save(tag); + return tag; + } + + public async readById(id: Uuid): Promise { + return this.canvasTagRepository.findOne({ + where: { + id, + }, + }); + } + + public async readAll( + filter: ListFilter, + ): Promise> { + return PageableUtils.producePagedResult( + filter, + await this.canvasTagRepository.findAndCount({ order: { name: 'asc' } }), + ); + } + + public async update( + command: CanvasTagUpdateCommand, + ): Promise { + const tagName = command.name?.trim().toUpperCase(); + const tagToBeUpdated = await this.canvasTagRepository.findOne({ + where: { id: command.id }, + }); + const existingTag = await this.canvasTagRepository.findOne({ + where: { name: tagName }, + }); + if (existingTag && existingTag.id != command.id) { + throw new ConflictException(); + } + tagToBeUpdated.name = tagName; + tagToBeUpdated.color = command.color; + await this.canvasTagRepository.save(tagToBeUpdated); + return tagToBeUpdated; + } + + public async delete(command: CanvasTagDeleteCommand) { + try { + await this.canvasTagRepository.delete(command.id); + } catch (e) { + throw new ConflictException(); + } + } +} diff --git a/src/canvas/canvas.controller.ts b/src/canvas/canvas.controller.ts index 7f51a22..18c3b1b 100644 --- a/src/canvas/canvas.controller.ts +++ b/src/canvas/canvas.controller.ts @@ -17,10 +17,11 @@ import { CanvasCreateDTO, CanvasDTO, CanvasMetadataUpdateDTO, + CanvasModifyTagDTO, CanvasStateFilter, } from './canvas.interface'; import { Uuid } from '../common/common.interface'; -import { ListFilter } from '../common/pageable.utils'; +import {ListFilter, PagedResult} from '../common/pageable.utils'; import { AuthenticatedGuard } from '../auth/guard/authenticated.guard'; import { CanvasGuard } from './guard/canvas.guard'; import { Log } from '@algoan/nestjs-logging-interceptor'; @@ -235,14 +236,14 @@ export class CanvasController { * * @param {ListFilter} filter - The filter to apply when retrieving items. * @param req - HTTP request object - * @return {Promise} - A Promise that resolves to the retrieved items. + * @return {Promise} - A Promise that resolves to the retrieved items. */ @Get('/') @UseGuards(AuthenticatedGuard) public async readAll( @Query() filter: ListFilter, @Req() req: Request, - ): Promise { + ): Promise> { return await this.canvasService.readAll(filter, req.user.toString()); } @@ -275,4 +276,34 @@ export class CanvasController { const userId = dto.userId; await this.canvasService.cancelAccess({ canvasId, userId }); } + + /** + * Adds a single tag to a canvas + * @param canvasId + * @param dto - an object containing 'tagId' + */ + @Post('/:id/tags') + @UseGuards(AuthenticatedGuard, CanvasGuard) + public async addTags( + @Param('id') canvasId: Uuid, + @Body() dto: CanvasModifyTagDTO, + ) { + const tagIds = dto.tagIds; + await this.canvasService.addTags({ canvasId, tagIds }); + } + + /** + * Removes a single tag from a canvas + * @param canvasId + * @param dto - an object containing 'tagId' + */ + @Delete('/:id/tags') + @UseGuards(AuthenticatedGuard, CanvasGuard) + public async removeTags( + @Param('id') canvasId: Uuid, + @Body() dto: CanvasModifyTagDTO, + ) { + const tagIds = dto.tagIds; + await this.canvasService.removeTags({ canvasId, tagIds }); + } } diff --git a/src/canvas/canvas.interface.ts b/src/canvas/canvas.interface.ts index 3bf9696..554e1b9 100644 --- a/src/canvas/canvas.interface.ts +++ b/src/canvas/canvas.interface.ts @@ -39,6 +39,16 @@ export interface CancelAccessCommand { userId: Uuid; } +export interface CanvasAddTagCommand { + canvasId: Uuid; + tagIds: [Uuid]; +} + +export interface CanvasRemoveTagCommand { + canvasId: Uuid; + tagIds: [Uuid]; +} + export class CanvasMetadataUpdateDTO { @MinLength(3) @MaxLength(255) @@ -81,3 +91,35 @@ export class CanvasStateFilter { @IsOptional() versionTimestamp?: string; } + +export interface CanvasTagCreateCommand { + name: string; + color?: string; +} + +export interface CanvasTagUpdateCommand { + id: Uuid; + name: string; + color?: string; +} + +export interface CanvasTagDeleteCommand { + id: Uuid; +} + +export class CanvasTagCreateOrUpdateDTO { + @MinLength(3) + @MaxLength(12) + name: string; + @IsOptional() + @MinLength(7) + @MaxLength(7) + color?: string; +} + +export class CanvasModifyTagDTO { + @IsUUID('all', { each: true }) + @IsNotEmpty({ each: true }) + @IsNotEmpty() + tagIds: [Uuid]; +} diff --git a/src/canvas/canvas.module.ts b/src/canvas/canvas.module.ts index 2f6379f..259acad 100644 --- a/src/canvas/canvas.module.ts +++ b/src/canvas/canvas.module.ts @@ -7,6 +7,8 @@ import { CanvasStateEntity } from './entity/canvas-state.entity'; import { CanvasTagEntity } from './entity/canvas-tag.entity'; import { CanvasAccessEntity } from './entity/canvas-access.entity'; import { UserEntity } from '../user/entity/user.entity'; +import { CanvasTagController } from './canvas-tag.controller'; +import { CanvasTagService } from './canvas-tag.service'; @Module({ imports: [ @@ -18,8 +20,8 @@ import { UserEntity } from '../user/entity/user.entity'; UserEntity, ]), ], - controllers: [CanvasController], - providers: [CanvasService], - exports: [CanvasService], + controllers: [CanvasController, CanvasTagController], + providers: [CanvasService, CanvasTagService], + exports: [CanvasService, CanvasTagService], }) export class CanvasModule {} diff --git a/src/canvas/canvas.service.ts b/src/canvas/canvas.service.ts index 5423047..d44332f 100644 --- a/src/canvas/canvas.service.ts +++ b/src/canvas/canvas.service.ts @@ -4,9 +4,11 @@ import { CanvasEntity } from './entity/canvas.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { CancelAccessCommand, + CanvasAddTagCommand, CanvasContentUpdateCommand, CanvasCreateCommand, CanvasMetadataUpdateCommand, + CanvasRemoveTagCommand, CanvasStateFilter, GiveAccessCommand, } from './canvas.interface'; @@ -19,6 +21,7 @@ import { } from '../common/pageable.utils'; import { CanvasAccessEntity } from './entity/canvas-access.entity'; import { UserEntity } from '../user/entity/user.entity'; +import { CanvasTagEntity } from './entity/canvas-tag.entity'; @Injectable() export class CanvasService { @@ -31,6 +34,8 @@ export class CanvasService { private readonly canvasAccessRepository: Repository, @InjectRepository(UserEntity) private readonly userRepository: Repository, + @InjectRepository(CanvasTagEntity) + private readonly canvasTagRepository: Repository, ) {} /** @@ -188,4 +193,41 @@ export class CanvasService { canvas: { id: command.canvasId }, }); } + + public async addTags(command: CanvasAddTagCommand) { + const canvas = await this.canvasRepository.findOne({ + where: { id: command.canvasId }, + relations: { tags: true }, + }); + if (!canvas) { + throw new NotFoundException(); + } + command.tagIds.forEach((tagId) => this.addTag(canvas, tagId)); + await this.canvasRepository.save(canvas); + } + + public async removeTags(command: CanvasRemoveTagCommand) { + const canvas = await this.canvasRepository.findOne({ + where: { id: command.canvasId }, + relations: { tags: true }, + }); + if (!canvas) { + throw new NotFoundException(); + } + canvas.tags = canvas.tags.filter((tag) => !command.tagIds.includes(tag.id)); + await this.canvasRepository.save(canvas); + } + + private async addTag(canvas: CanvasEntity, tagId: Uuid) { + const tag = await this.canvasTagRepository.findOne({ + where: { id: tagId }, + }); + if (!tag) { + throw new NotFoundException(); + } + if (canvas.tags.includes(tag)) { + return; + } + canvas.tags.push(tag); + } } diff --git a/src/canvas/entity/canvas-tag.entity.ts b/src/canvas/entity/canvas-tag.entity.ts index c351e05..bfe7a4e 100644 --- a/src/canvas/entity/canvas-tag.entity.ts +++ b/src/canvas/entity/canvas-tag.entity.ts @@ -5,7 +5,7 @@ export class CanvasTagEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ nullable: false }) + @Column({ nullable: false, unique: true }) name: string; @Column({