From 7db21ada090fdf62f47cb8dacb34c7457e4b7499 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Wed, 23 Oct 2024 17:12:28 -0700 Subject: [PATCH] ALCS-2325 Tag/Category entities and cruds --- .../apps/alcs/src/alcs/admin/admin.module.ts | 12 +++ .../tag-category.controller.spec.ts | 18 ++++ .../tag-category/tag-category.controller.ts | 55 ++++++++++++ .../admin/tag-category/tag-category.dto.ts | 9 ++ .../admin/tag-category/tag-category.entity.ts | 21 +++++ .../tag-category/tag-category.service.spec.ts | 18 ++++ .../tag-category/tag-category.service.ts | 56 ++++++++++++ .../src/alcs/admin/tag/tag.controller.spec.ts | 18 ++++ .../alcs/src/alcs/admin/tag/tag.controller.ts | 54 ++++++++++++ .../apps/alcs/src/alcs/admin/tag/tag.dto.ts | 13 +++ .../alcs/src/alcs/admin/tag/tag.entity.ts | 29 +++++++ .../src/alcs/admin/tag/tag.service.spec.ts | 18 ++++ .../alcs/src/alcs/admin/tag/tag.service.ts | 86 +++++++++++++++++++ .../1729713891477-create_tag_category.ts | 16 ++++ .../migrations/1729723237404-create_tag.ts | 18 ++++ 15 files changed, 441 insertions(+) create mode 100644 services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag-category/tag-category.dto.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag-category/tag-category.entity.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag/tag.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag/tag.controller.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag/tag.dto.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag/tag.entity.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag/tag.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/admin/tag/tag.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1729713891477-create_tag_category.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1729723237404-create_tag.ts diff --git a/services/apps/alcs/src/alcs/admin/admin.module.ts b/services/apps/alcs/src/alcs/admin/admin.module.ts index 819a9103a..393c6098d 100644 --- a/services/apps/alcs/src/alcs/admin/admin.module.ts +++ b/services/apps/alcs/src/alcs/admin/admin.module.ts @@ -32,6 +32,12 @@ import { NoiSubtypeController } from './noi-subtype/noi-subtype.controller'; import { NoiSubtypeService } from './noi-subtype/noi-subtype.service'; import { UnarchiveCardController } from './unarchive-card/unarchive-card.controller'; import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; +import { TagCategoryService } from './tag-category/tag-category.service'; +import { TagCategoryController } from './tag-category/tag-category.controller'; +import { TagCategory } from './tag-category/tag-category.entity'; +import { Tag } from './tag/tag.entity'; +import { TagController } from './tag/tag.controller'; +import { TagService } from './tag/tag.service'; @Module({ imports: [ @@ -42,6 +48,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; NoticeOfIntentSubtype, ApplicationDecisionConditionType, Configuration, + TagCategory, + Tag, ]), ApplicationModule, NoticeOfIntentModule, @@ -62,6 +70,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; UnarchiveCardController, NoiSubtypeController, ApplicationDecisionMakerController, + TagCategoryController, + TagController, ApplicationDecisionConditionTypesController, CardStatusController, BoardManagementController, @@ -71,6 +81,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; HolidayService, ApplicationCeoCriterionService, ApplicationDecisionMakerService, + TagCategoryService, + TagService, UnarchiveCardService, NoiSubtypeService, ApplicationDecisionConditionTypesService, diff --git a/services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.spec.ts b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.spec.ts new file mode 100644 index 000000000..1b3c8d09f --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TagCategoryController } from './tag-category.controller'; + +describe('TagCategoryController', () => { + let controller: TagCategoryController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [TagCategoryController], + }).compile(); + + controller = module.get(TagCategoryController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.ts b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.ts new file mode 100644 index 000000000..50ad1b72d --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.controller.ts @@ -0,0 +1,55 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { AUTH_ROLE } from '../../../common/authorization/roles'; +import { TagCategoryDto } from './tag-category.dto'; +import { TagCategoryService } from './tag-category.service'; + +@Controller('tag-category') +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(RolesGuard) +export class TagCategoryController { + + constructor(private service: TagCategoryService) {} + + @Get('/:pageIndex/:itemsPerPage') + @UserRoles(AUTH_ROLE.ADMIN) + async fetch( + @Param('pageIndex') pageIndex: number, + @Param('itemsPerPage') itemsPerPage: number, + @Query('search') search?: string, + ) { + const result = await this.service.fetch(pageIndex, itemsPerPage, search); + return { data: result[0], total: result[1] }; + } + + @Post('') + @UserRoles(AUTH_ROLE.ADMIN) + async create(@Body() createDto: TagCategoryDto) { + return await this.service.create(createDto); + } + + @Put('/:uuid') + @UserRoles(AUTH_ROLE.ADMIN) + async update(@Param('uuid') uuid: string, @Body() updateDto: TagCategoryDto) { + return await this.service.update(uuid, updateDto); + } + + @Delete('/:uuid') + @UserRoles(AUTH_ROLE.ADMIN) + async delete(@Param('uuid') uuid: string) { + return await this.service.delete(uuid); + } +} diff --git a/services/apps/alcs/src/alcs/admin/tag-category/tag-category.dto.ts b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.dto.ts new file mode 100644 index 000000000..b39ffc388 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.dto.ts @@ -0,0 +1,9 @@ +import { IsString } from 'class-validator'; + +export class TagCategoryDto { + @IsString() + uuid: string; + + @IsString() + name: string; +} diff --git a/services/apps/alcs/src/alcs/admin/tag-category/tag-category.entity.ts b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.entity.ts new file mode 100644 index 000000000..f866e2a80 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.entity.ts @@ -0,0 +1,21 @@ +import { AutoMap } from 'automapper-classes'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; + +@Entity({ comment: 'Tag category.' }) +export class TagCategory extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @PrimaryGeneratedColumn('uuid') + uuid: string; + + @AutoMap() + @Column() + name: string; +} diff --git a/services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.spec.ts b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.spec.ts new file mode 100644 index 000000000..ad4b9cb17 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TagCategoryService } from './tag-category.service'; + +describe('TagCategoryService', () => { + let service: TagCategoryService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [TagCategoryService], + }).compile(); + + service = module.get(TagCategoryService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.ts b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.ts new file mode 100644 index 000000000..913568d92 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag-category/tag-category.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { TagCategory } from './tag-category.entity'; +import { FindOptionsWhere, Like, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { TagCategoryDto } from './tag-category.dto'; + +@Injectable() +export class TagCategoryService { + constructor( + @InjectRepository(TagCategory) + private repository: Repository, + ) {} + + async fetch(pageIndex: number, itemsPerPage: number, search?: string) { + let searchExpression: FindOptionsWhere | undefined = undefined; + + if (search) { + searchExpression = { + name: Like(`%${search}%`), + }; + } + + return ( + (await this.repository.findAndCount({ + where: searchExpression, + order: { name: 'DESC' }, + take: itemsPerPage, + skip: pageIndex * itemsPerPage, + })) || [[], 0] + ); + } + + async create(dto: TagCategoryDto) { + const newTagCategory = new TagCategory(); + newTagCategory.name = dto.name; + return this.repository.save(newTagCategory); + } + + async getOneOrFail(uuid: string) { + return await this.repository.findOneOrFail({ + where: { uuid }, + }); + } + + async update(uuid: string, updateDto: TagCategoryDto) { + const tagCategory = await this.getOneOrFail(uuid); + tagCategory.name = updateDto.name; + return await this.repository.save(tagCategory); + } + + async delete(uuid: string) { + const tagCategory = await this.getOneOrFail(uuid); + + return await this.repository.remove(tagCategory); + } +} diff --git a/services/apps/alcs/src/alcs/admin/tag/tag.controller.spec.ts b/services/apps/alcs/src/alcs/admin/tag/tag.controller.spec.ts new file mode 100644 index 000000000..3ac7b0a1c --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag/tag.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TagController } from './tag.controller'; + +describe('TagController', () => { + let controller: TagController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [TagController], + }).compile(); + + controller = module.get(TagController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/alcs/admin/tag/tag.controller.ts b/services/apps/alcs/src/alcs/admin/tag/tag.controller.ts new file mode 100644 index 000000000..093317c09 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag/tag.controller.ts @@ -0,0 +1,54 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { TagService } from './tag.service'; +import { AUTH_ROLE } from 'apps/alcs/src/common/authorization/roles'; +import { TagDto } from './tag.dto'; + +@Controller('tag') +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(RolesGuard) +export class TagController { + constructor(private service: TagService) {} + + @Get('/:pageIndex/:itemsPerPage') + @UserRoles(AUTH_ROLE.ADMIN) + async fetch( + @Param('pageIndex') pageIndex: number, + @Param('itemsPerPage') itemsPerPage: number, + @Query('search') search?: string, + ) { + const result = await this.service.fetch(pageIndex, itemsPerPage, search); + return { data: result[0], total: result[1] }; + } + + @Post('') + @UserRoles(AUTH_ROLE.ADMIN) + async create(@Body() createDto: TagDto) { + return await this.service.create(createDto); + } + + @Put('/:uuid') + @UserRoles(AUTH_ROLE.ADMIN) + async update(@Param('uuid') uuid: string, @Body() updateDto: TagDto) { + return await this.service.update(uuid, updateDto); + } + + @Delete('/:uuid') + @UserRoles(AUTH_ROLE.ADMIN) + async delete(@Param('uuid') uuid: string) { + return await this.service.delete(uuid); + } +} diff --git a/services/apps/alcs/src/alcs/admin/tag/tag.dto.ts b/services/apps/alcs/src/alcs/admin/tag/tag.dto.ts new file mode 100644 index 000000000..d24bef19d --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag/tag.dto.ts @@ -0,0 +1,13 @@ +import { IsBoolean, IsObject, IsString } from 'class-validator'; +import { TagCategoryDto } from '../tag-category/tag-category.dto'; + +export class TagDto { + @IsString() + name: string; + + @IsBoolean() + isActive: boolean; + + @IsObject() + category: TagCategoryDto; +} diff --git a/services/apps/alcs/src/alcs/admin/tag/tag.entity.ts b/services/apps/alcs/src/alcs/admin/tag/tag.entity.ts new file mode 100644 index 000000000..b65067199 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag/tag.entity.ts @@ -0,0 +1,29 @@ +import { AutoMap } from 'automapper-classes'; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; +import { TagCategory } from '../tag-category/tag-category.entity'; + +@Entity({ comment: 'Tag.' }) +export class Tag extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @PrimaryGeneratedColumn('uuid') + uuid: string; + + @AutoMap() + @Column() + name: string; + + @AutoMap() + @Column({ default: true }) + isActive: boolean; + + @ManyToOne(() => TagCategory) + category: TagCategory; +} diff --git a/services/apps/alcs/src/alcs/admin/tag/tag.service.spec.ts b/services/apps/alcs/src/alcs/admin/tag/tag.service.spec.ts new file mode 100644 index 000000000..252a25b36 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag/tag.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TagService } from './tag.service'; + +describe('TagCategoryService', () => { + let service: TagService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [TagService], + }).compile(); + + service = module.get(TagService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/alcs/admin/tag/tag.service.ts b/services/apps/alcs/src/alcs/admin/tag/tag.service.ts new file mode 100644 index 000000000..cb1cb894c --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/tag/tag.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@nestjs/common'; +import { FindOptionsWhere, Like, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Tag } from './tag.entity'; +import { TagDto } from './tag.dto'; +import { TagCategory } from '../tag-category/tag-category.entity'; +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; + +@Injectable() +export class TagService { + constructor( + @InjectRepository(Tag) + private repository: Repository, + @InjectRepository(TagCategory) + private categoryRepository: Repository, + ) {} + + async fetch(pageIndex: number, itemsPerPage: number, search?: string) { + let searchExpression: FindOptionsWhere | undefined = undefined; + + if (search) { + searchExpression = { + name: Like(`%${search}%`), + }; + } + + return ( + (await this.repository.findAndCount({ + where: searchExpression, + relations: { + category: true, + }, + order: { name: 'DESC' }, + take: itemsPerPage, + skip: pageIndex * itemsPerPage, + })) || [[], 0] + ); + } + + async create(dto: TagDto) { + const category = await this.categoryRepository.findOne({ + where: { + uuid: dto.category.uuid, + }, + }); + + if (!category) { + throw new ServiceValidationException('Provided category does not exist'); + } + + const newTag = new Tag(); + newTag.name = dto.name; + newTag.isActive = dto.isActive; + newTag.category = category; + return this.repository.save(newTag); + } + + async getOneOrFail(uuid: string) { + return await this.repository.findOneOrFail({ + where: { uuid }, + }); + } + + async update(uuid: string, updateDto: TagDto) { + const category = await this.categoryRepository.findOne({ + where: { + uuid: updateDto.category.uuid, + }, + }); + + if (!category) { + throw new ServiceValidationException('Provided category does not exist'); + } + const tag = await this.getOneOrFail(uuid); + tag.name = updateDto.name; + tag.isActive = updateDto.isActive; + tag.category = category; + return await this.repository.save(tag); + } + + async delete(uuid: string) { + const tag = await this.getOneOrFail(uuid); + + return await this.repository.remove(tag); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1729713891477-create_tag_category.ts b/services/apps/alcs/src/providers/typeorm/migrations/1729713891477-create_tag_category.ts new file mode 100644 index 000000000..04d6fb392 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1729713891477-create_tag_category.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateTagCategory1729713891477 implements MigrationInterface { + name = 'CreateTagCategory1729713891477' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "alcs"."tag_category" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "name" character varying NOT NULL, CONSTRAINT "PK_ff4d55f591b098af8bc6213835c" PRIMARY KEY ("uuid"))`); + await queryRunner.query(`COMMENT ON TABLE "alcs"."tag_category" IS 'Tag category.'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`COMMENT ON TABLE "alcs"."tag_category" IS NULL`); + await queryRunner.query(`DROP TABLE "alcs"."tag_category"`); + } + +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1729723237404-create_tag.ts b/services/apps/alcs/src/providers/typeorm/migrations/1729723237404-create_tag.ts new file mode 100644 index 000000000..1341565c2 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1729723237404-create_tag.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateTag1729723237404 implements MigrationInterface { + name = 'CreateTag1729723237404' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "alcs"."tag" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "name" character varying NOT NULL, "is_active" boolean NOT NULL DEFAULT true, "category_uuid" uuid, CONSTRAINT "PK_d70de2c1e1a3b52adb904028ea2" PRIMARY KEY ("uuid"))`); + await queryRunner.query(`COMMENT ON TABLE "alcs"."tag" IS 'Tag.'`); + await queryRunner.query(`ALTER TABLE "alcs"."tag" ADD CONSTRAINT "FK_ff4d55f591b098af8bc6213835c" FOREIGN KEY ("category_uuid") REFERENCES "alcs"."tag_category"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "alcs"."tag" DROP CONSTRAINT "FK_ff4d55f591b098af8bc6213835c"`); + await queryRunner.query(`COMMENT ON TABLE "alcs"."tag" IS NULL`); + await queryRunner.query(`DROP TABLE "alcs"."tag"`); + } + +}