diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.ts b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.ts
index e6ef70d08..8ade8f8cb 100644
--- a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.ts
+++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.ts
@@ -2,6 +2,7 @@ import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TagCategoryDto } from '../../../../../services/tag/tag-category/tag-category.dto';
import { TagCategoryService } from '../../../../../services/tag/tag-category/tag-category.service';
+import { FormControl } from '@angular/forms';
@Component({
selector: 'app-tag-category-dialog',
@@ -15,6 +16,7 @@ export class TagCategoryDialogComponent {
isLoading = false;
isEdit = false;
showNameWarning = false;
+ nameControl = new FormControl();
constructor(
@Inject(MAT_DIALOG_DATA) public data: TagCategoryDto | undefined,
@@ -63,6 +65,6 @@ export class TagCategoryDialogComponent {
private showWarning() {
this.showNameWarning = true;
- this.name = '';
+ this.nameControl.setErrors({"invalid": true});
}
}
diff --git a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.html b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.html
index c5a734706..ca9a9bb41 100644
--- a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.html
+++ b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.html
@@ -8,7 +8,7 @@
{{ isEdit ? 'Edit' : 'Create New' }} Tag
Name
-
+
diff --git a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.ts b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.ts
index b03786f32..b99a2f481 100644
--- a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.ts
+++ b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.ts
@@ -5,6 +5,7 @@ import { TagService } from '../../../../services/tag/tag.service';
import { TagCategoryService } from '../../../../services/tag/tag-category/tag-category.service';
import { TagCategoryDto } from 'src/app/services/tag/tag-category/tag-category.dto';
import { Subject, takeUntil } from 'rxjs';
+import { FormControl } from '@angular/forms';
@Component({
selector: 'app-tag-dialog',
@@ -25,6 +26,7 @@ export class TagDialogComponent implements OnInit {
isLoading = false;
isEdit = false;
showNameWarning = false;
+ nameControl = new FormControl();
categories: TagCategoryDto[] = [];
@@ -97,6 +99,6 @@ export class TagDialogComponent implements OnInit {
private showWarning() {
this.showNameWarning = true;
- this.name = '';
+ this.nameControl.setErrors({"invalid": true});
}
}
diff --git a/alcs-frontend/src/app/services/tag/tag-category/tag-category.service.ts b/alcs-frontend/src/app/services/tag/tag-category/tag-category.service.ts
index 43449f1fc..c9eab99cb 100644
--- a/alcs-frontend/src/app/services/tag/tag-category/tag-category.service.ts
+++ b/alcs-frontend/src/app/services/tag/tag-category/tag-category.service.ts
@@ -50,8 +50,8 @@ export class TagCategoryService {
return await firstValueFrom(this.http.post(`${this.url}`, createDto));
} catch (e) {
const res = e as HttpErrorResponse;
- if (res.error.statusCode === HttpStatusCode.Conflict && res.error.message.includes('duplicate key')) {
- throw e as HttpErrorResponse;
+ if (res.error.statusCode === HttpStatusCode.Conflict) {
+ throw res;
} else {
console.error(e);
this.toastService.showErrorToast('Failed to create tag category');
@@ -65,8 +65,8 @@ export class TagCategoryService {
return await firstValueFrom(this.http.patch(`${this.url}/${uuid}`, updateDto));
} catch (e) {
const res = e as HttpErrorResponse;
- if (res.error.statusCode === HttpStatusCode.Conflict && res.error.message.includes('duplicate key')) {
- throw e as HttpErrorResponse;
+ if (res.error.statusCode === HttpStatusCode.Conflict) {
+ throw res;
} else {
console.error(e);
this.toastService.showErrorToast('Failed to update tag category');
diff --git a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.spec.ts b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.spec.ts
index 6025f707a..c0910e17c 100644
--- a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.spec.ts
+++ b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.spec.ts
@@ -7,6 +7,7 @@ import { initTagCategoryMockEntity } from '../../../../test/mocks/mockEntities';
import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes';
import { TagCategory } from './tag-category.entity';
import { TagCategoryDto } from './tag-category.dto';
+import { UpdateResult } from 'typeorm';
describe('TagCategoryController', () => {
let controller: TagCategoryController;
@@ -62,20 +63,17 @@ describe('TagCategoryController', () => {
name: mockCategoryTag.name,
} as TagCategoryDto;
- const result = await controller.update(
- mockCategoryTag.uuid,
- categoryToUpdate,
- );
+ const result = await controller.update(mockCategoryTag.uuid, categoryToUpdate);
expect(tagCategoryService.update).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockCategoryTag);
});
it('should delete a tag category', async () => {
- tagCategoryService.delete.mockResolvedValue(mockCategoryTag);
+ tagCategoryService.delete.mockResolvedValue({} as UpdateResult);
const result = await controller.delete(mockCategoryTag.uuid);
expect(tagCategoryService.delete).toHaveBeenCalledTimes(1);
- expect(result).toEqual(mockCategoryTag);
+ expect(result).toBeDefined();
});
});
diff --git a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.ts b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.ts
index 263097760..cf9a887f2 100644
--- a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.ts
+++ b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.controller.ts
@@ -1,16 +1,4 @@
-import {
- Body,
- Controller,
- Delete,
- Get,
- HttpException,
- HttpStatus,
- Param,
- Patch,
- Post,
- Query,
- UseGuards,
-} from '@nestjs/common';
+import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiOAuth2 } from '@nestjs/swagger';
import * as config from 'config';
import { RolesGuard } from '../../../common/authorization/roles-guard.service';
@@ -18,7 +6,6 @@ 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';
-import { QueryFailedError } from 'typeorm';
@Controller('tag-category')
@ApiOAuth2(config.get('KEYCLOAK.SCOPES'))
@@ -40,45 +27,18 @@ export class TagCategoryController {
@Post('')
@UserRoles(AUTH_ROLE.ADMIN)
async create(@Body() createDto: TagCategoryDto) {
- try {
- return await this.service.create(createDto);
- } catch (e) {
- if (e.constructor === QueryFailedError) {
- const msg = (e as QueryFailedError).message;
- throw new HttpException(msg, HttpStatus.CONFLICT);
- } else {
- throw e;
- }
- }
+ return await this.service.create(createDto);
}
@Patch('/:uuid')
@UserRoles(AUTH_ROLE.ADMIN)
async update(@Param('uuid') uuid: string, @Body() updateDto: TagCategoryDto) {
- try {
- return await this.service.update(uuid, updateDto);
- } catch (e) {
- if (e.constructor === QueryFailedError) {
- const msg = (e as QueryFailedError).message;
- throw new HttpException(msg, HttpStatus.CONFLICT);
- } else {
- throw e;
- }
- }
+ return await this.service.update(uuid, updateDto);
}
@Delete('/:uuid')
@UserRoles(AUTH_ROLE.ADMIN)
async delete(@Param('uuid') uuid: string) {
- try {
- return await this.service.delete(uuid);
- } catch (e) {
- if (e.constructor === QueryFailedError) {
- const msg = (e as QueryFailedError).message;
- throw new HttpException(msg, HttpStatus.CONFLICT);
- } else {
- throw e;
- }
- }
+ return await this.service.delete(uuid);
}
}
diff --git a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.entity.ts b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.entity.ts
index cd50e163e..f866e2a80 100644
--- a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.entity.ts
+++ b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.entity.ts
@@ -16,6 +16,6 @@ export class TagCategory extends Base {
uuid: string;
@AutoMap()
- @Column({ unique: true })
+ @Column()
name: string;
}
diff --git a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.spec.ts b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.spec.ts
index 9616a274e..fb0b496d2 100644
--- a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.spec.ts
+++ b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.spec.ts
@@ -48,6 +48,7 @@ describe('TagCategoryService', () => {
}).compile();
service = module.get(TagCategoryService);
+ tagCategoryRepositoryMock.find.mockResolvedValue([]);
tagCategoryRepositoryMock.findOne.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock.findOneOrFail.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock.save.mockResolvedValue(mockTagCategoryEntity);
diff --git a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.ts b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.ts
index 7f977ff78..37aaa79d8 100644
--- a/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.ts
+++ b/services/apps/alcs/src/alcs/tag/tag-category/tag-category.service.ts
@@ -35,6 +35,9 @@ export class TagCategoryService {
}
async create(dto: TagCategoryDto) {
+ if (await this.hasName(dto)) {
+ throw new ServiceConflictException('There is already a category with this name. Unable to create.');
+ }
const newTagCategory = new TagCategory();
newTagCategory.name = dto.name;
return this.repository.save(newTagCategory);
@@ -47,6 +50,10 @@ export class TagCategoryService {
}
async update(uuid: string, updateDto: TagCategoryDto) {
+ updateDto.uuid = uuid;
+ if (await this.hasName(updateDto)) {
+ throw new ServiceConflictException('There is already a category with this name. Unable to update.');
+ }
const tagCategory = await this.getOneOrFail(uuid);
tagCategory.name = updateDto.name;
return await this.repository.save(tagCategory);
@@ -58,7 +65,7 @@ export class TagCategoryService {
if (await this.isAssociated(tagCategory)) {
throw new ServiceConflictException('Category is associated with tags. Unable to delete.');
}
- return await this.repository.remove(tagCategory);
+ return await this.repository.softDelete(uuid);
}
async isAssociated(tagCategory: TagCategory) {
@@ -66,4 +73,14 @@ export class TagCategoryService {
return associatedTags.length > 0;
}
+
+ async hasName(tag: TagCategoryDto) {
+ let tags = await this.repository.find({
+ where: { name: tag.name },
+ });
+ if (tag.uuid) {
+ tags = tags.filter((t) => t.uuid !== tag.uuid);
+ }
+ return tags.length > 0;
+ }
}
diff --git a/services/apps/alcs/src/alcs/tag/tag.controller.spec.ts b/services/apps/alcs/src/alcs/tag/tag.controller.spec.ts
index 8042769a4..7191b00ad 100644
--- a/services/apps/alcs/src/alcs/tag/tag.controller.spec.ts
+++ b/services/apps/alcs/src/alcs/tag/tag.controller.spec.ts
@@ -48,6 +48,7 @@ describe('TagController', () => {
it('should create a tag', async () => {
const dto: TagDto = {
+ uuid: mockTag.uuid,
name: mockTag.name,
category: mockTag.category
? {
diff --git a/services/apps/alcs/src/alcs/tag/tag.controller.ts b/services/apps/alcs/src/alcs/tag/tag.controller.ts
index 27cc01ff7..a29446a9b 100644
--- a/services/apps/alcs/src/alcs/tag/tag.controller.ts
+++ b/services/apps/alcs/src/alcs/tag/tag.controller.ts
@@ -1,16 +1,4 @@
-import {
- Body,
- Controller,
- Delete,
- Get,
- HttpException,
- HttpStatus,
- Param,
- Patch,
- Post,
- Query,
- UseGuards,
-} from '@nestjs/common';
+import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiOAuth2 } from '@nestjs/swagger';
import * as config from 'config';
import { RolesGuard } from '../../common/authorization/roles-guard.service';
@@ -18,7 +6,6 @@ import { UserRoles } from '../../common/authorization/roles.decorator';
import { TagService } from './tag.service';
import { AUTH_ROLE } from '../../common/authorization/roles';
import { TagDto } from './tag.dto';
-import { QueryFailedError } from 'typeorm';
@Controller('tag')
@ApiOAuth2(config.get('KEYCLOAK.SCOPES'))
@@ -40,45 +27,18 @@ export class TagController {
@Post('')
@UserRoles(AUTH_ROLE.ADMIN)
async create(@Body() createDto: TagDto) {
- try {
- return await this.service.create(createDto);
- } catch (e) {
- if (e.constructor === QueryFailedError) {
- const msg = (e as QueryFailedError).message;
- throw new HttpException(msg, HttpStatus.CONFLICT);
- } else {
- throw e;
- }
- }
+ return await this.service.create(createDto);
}
@Patch('/:uuid')
@UserRoles(AUTH_ROLE.ADMIN)
async update(@Param('uuid') uuid: string, @Body() updateDto: TagDto) {
- try {
- return await this.service.update(uuid, updateDto);
- } catch (e) {
- if (e.constructor === QueryFailedError) {
- const msg = (e as QueryFailedError).message;
- throw new HttpException(msg, HttpStatus.CONFLICT);
- } else {
- throw e;
- }
- }
+ return await this.service.update(uuid, updateDto);
}
@Delete('/:uuid')
@UserRoles(AUTH_ROLE.ADMIN)
async delete(@Param('uuid') uuid: string) {
- try {
- return await this.service.delete(uuid);
- } catch (e) {
- if (e.constructor === QueryFailedError) {
- const msg = (e as QueryFailedError).message;
- throw new HttpException(msg, HttpStatus.CONFLICT);
- } else {
- throw e;
- }
- }
+ return await this.service.delete(uuid);
}
}
diff --git a/services/apps/alcs/src/alcs/tag/tag.dto.ts b/services/apps/alcs/src/alcs/tag/tag.dto.ts
index 8088d8dc6..75ce8d342 100644
--- a/services/apps/alcs/src/alcs/tag/tag.dto.ts
+++ b/services/apps/alcs/src/alcs/tag/tag.dto.ts
@@ -2,6 +2,9 @@ import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator';
import { TagCategoryDto } from './tag-category/tag-category.dto';
export class TagDto {
+ @IsString()
+ uuid: string;
+
@IsString()
name: string;
diff --git a/services/apps/alcs/src/alcs/tag/tag.entity.ts b/services/apps/alcs/src/alcs/tag/tag.entity.ts
index 80f1d2620..071901c6a 100644
--- a/services/apps/alcs/src/alcs/tag/tag.entity.ts
+++ b/services/apps/alcs/src/alcs/tag/tag.entity.ts
@@ -19,7 +19,7 @@ export class Tag extends Base {
uuid: string;
@AutoMap()
- @Column({ unique: true })
+ @Column()
name: string;
@AutoMap()
@@ -33,7 +33,7 @@ export class Tag extends Base {
@ManyToMany(() => Application, (application) => application.tags)
applications: Application[];
-
+
@ManyToMany(() => NoticeOfIntent, (noticeOfIntent) => noticeOfIntent.tags)
noticeOfIntents: NoticeOfIntent[];
}
diff --git a/services/apps/alcs/src/alcs/tag/tag.service.spec.ts b/services/apps/alcs/src/alcs/tag/tag.service.spec.ts
index b853208c7..60a49469e 100644
--- a/services/apps/alcs/src/alcs/tag/tag.service.spec.ts
+++ b/services/apps/alcs/src/alcs/tag/tag.service.spec.ts
@@ -52,6 +52,7 @@ describe('TagCategoryService', () => {
tagRepositoryMock.findOneOrFail.mockResolvedValue(mockTagEntity);
tagRepositoryMock.findOne.mockResolvedValue(mockTagEntity);
+ tagRepositoryMock.find.mockResolvedValue([]);
tagRepositoryMock.save.mockResolvedValue(mockTagEntity);
tagCategoryRepositoryMock.findOne.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock = module.get(getRepositoryToken(TagCategory));
@@ -67,6 +68,7 @@ describe('TagCategoryService', () => {
it('should call save when an Tag is updated', async () => {
const payload: TagDto = {
+ uuid: mockTagEntity.uuid,
name: mockTagEntity.name,
isActive: mockTagEntity.isActive,
category: mockTagCategoryEntity,
@@ -80,6 +82,7 @@ describe('TagCategoryService', () => {
it('should call save when tag successfully create', async () => {
const payload: TagDto = {
+ uuid: mockTagEntity.uuid,
name: mockTagEntity.name,
isActive: mockTagEntity.isActive,
category: mockTagCategoryEntity,
diff --git a/services/apps/alcs/src/alcs/tag/tag.service.ts b/services/apps/alcs/src/alcs/tag/tag.service.ts
index ff3f13c2c..5563c5878 100644
--- a/services/apps/alcs/src/alcs/tag/tag.service.ts
+++ b/services/apps/alcs/src/alcs/tag/tag.service.ts
@@ -38,6 +38,9 @@ export class TagService {
}
async create(dto: TagDto) {
+ if (await this.hasName(dto)) {
+ throw new ServiceConflictException('There is already a tag with this name. Unable to create.');
+ }
const category = dto.category
? await this.categoryRepository.findOne({
where: {
@@ -60,6 +63,10 @@ export class TagService {
}
async update(uuid: string, updateDto: TagDto) {
+ updateDto.uuid = uuid;
+ if (await this.hasName(updateDto)) {
+ throw new ServiceConflictException('There is already a tag with this name. Unable to update.');
+ }
const category = updateDto.category
? await this.categoryRepository.findOne({
where: {
@@ -93,4 +100,14 @@ export class TagService {
return (tag.applications && tag.applications.length > 0) || (tag.noticeOfIntents && tag.noticeOfIntents.length > 0);
}
+
+ async hasName(tag: TagDto) {
+ let tags = await this.repository.find({
+ where: { name: tag.name },
+ });
+ if (tag.uuid) {
+ tags = tags.filter((t) => t.uuid !== tag.uuid);
+ }
+ return tags.length > 0;
+ }
}
diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1730856145155-remove_unique_constraints.ts b/services/apps/alcs/src/providers/typeorm/migrations/1730856145155-remove_unique_constraints.ts
new file mode 100644
index 000000000..5a0326f70
--- /dev/null
+++ b/services/apps/alcs/src/providers/typeorm/migrations/1730856145155-remove_unique_constraints.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class RemoveUniqueConstraints1730856145155 implements MigrationInterface {
+ name = 'RemoveUniqueConstraints1730856145155'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "alcs"."tag_category" DROP CONSTRAINT "UQ_f48a9fe1f705a7c2a60856d395a"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."tag" DROP CONSTRAINT "UQ_6a9775008add570dc3e5a0bab7b"`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "alcs"."tag" ADD CONSTRAINT "UQ_6a9775008add570dc3e5a0bab7b" UNIQUE ("name")`);
+ await queryRunner.query(`ALTER TABLE "alcs"."tag_category" ADD CONSTRAINT "UQ_f48a9fe1f705a7c2a60856d395a" UNIQUE ("name")`);
+ }
+
+}