Skip to content

Commit

Permalink
Merge pull request #1957 from bcgov/bugfix/ALCS-2353-tag-and-categori…
Browse files Browse the repository at this point in the history
…es-name-check

ALCS-2353 Constraints end error handling
  • Loading branch information
fbarreta authored Nov 6, 2024
2 parents e2a99b8 + 42c3e4d commit 665d376
Show file tree
Hide file tree
Showing 17 changed files with 86 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ <h4>{{ isEdit ? 'Edit' : 'Create New' }} Category</h4>
<div class="full-width">
<mat-form-field class="description" appearance="outline">
<mat-label>Name</mat-label>
<input required matInput id="name" [(ngModel)]="name" name="name" (ngModelChange)="onChange()" />
<input required matInput id="name" [(ngModel)]="name" name="name" (ngModelChange)="onChange()" [formControl]="nameControl" />
</mat-form-field>
<div class="warning-section">
<div class="warning" *ngIf="showNameWarning">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -63,6 +65,6 @@ export class TagCategoryDialogComponent {

private showWarning() {
this.showNameWarning = true;
this.name = '';
this.nameControl.setErrors({"invalid": true});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h4>{{ isEdit ? 'Edit' : 'Create New' }} Tag</h4>
<div>
<mat-form-field class="dialog-field" appearance="outline">
<mat-label>Name</mat-label>
<input required matInput id="name" [(ngModel)]="name" name="name" (ngModelChange)="onChange()" />
<input required matInput id="name" [(ngModel)]="name" name="name" (ngModelChange)="onChange()" [formControl]="nameControl" />
</mat-form-field>
</div>
<div class="dialog-field">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -25,6 +26,7 @@ export class TagDialogComponent implements OnInit {
isLoading = false;
isEdit = false;
showNameWarning = false;
nameControl = new FormControl();

categories: TagCategoryDto[] = [];

Expand Down Expand Up @@ -97,6 +99,6 @@ export class TagDialogComponent implements OnInit {

private showWarning() {
this.showNameWarning = true;
this.name = '';
this.nameControl.setErrors({"invalid": true});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export class TagCategoryService {
return await firstValueFrom(this.http.post<TagCategoryDto>(`${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');
Expand All @@ -65,8 +65,8 @@ export class TagCategoryService {
return await firstValueFrom(this.http.patch<TagCategoryDto>(`${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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
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';
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<string[]>('KEYCLOAK.SCOPES'))
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export class TagCategory extends Base {
uuid: string;

@AutoMap()
@Column({ unique: true })
@Column()
name: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('TagCategoryService', () => {
}).compile();

service = module.get<TagCategoryService>(TagCategoryService);
tagCategoryRepositoryMock.find.mockResolvedValue([]);
tagCategoryRepositoryMock.findOne.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock.findOneOrFail.mockResolvedValue(mockTagCategoryEntity);
tagCategoryRepositoryMock.save.mockResolvedValue(mockTagCategoryEntity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -58,12 +65,22 @@ 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) {
const associatedTags = await this.tagRepository.find({ where: { category: { uuid: tagCategory.uuid } } });

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;
}
}
1 change: 1 addition & 0 deletions services/apps/alcs/src/alcs/tag/tag.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('TagController', () => {

it('should create a tag', async () => {
const dto: TagDto = {
uuid: mockTag.uuid,
name: mockTag.name,
category: mockTag.category
? {
Expand Down
48 changes: 4 additions & 44 deletions services/apps/alcs/src/alcs/tag/tag.controller.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
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';
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<string[]>('KEYCLOAK.SCOPES'))
Expand All @@ -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);
}
}
3 changes: 3 additions & 0 deletions services/apps/alcs/src/alcs/tag/tag.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions services/apps/alcs/src/alcs/tag/tag.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class Tag extends Base {
uuid: string;

@AutoMap()
@Column({ unique: true })
@Column()
name: string;

@AutoMap()
Expand All @@ -33,7 +33,7 @@ export class Tag extends Base {

@ManyToMany(() => Application, (application) => application.tags)
applications: Application[];

@ManyToMany(() => NoticeOfIntent, (noticeOfIntent) => noticeOfIntent.tags)
noticeOfIntents: NoticeOfIntent[];
}
3 changes: 3 additions & 0 deletions services/apps/alcs/src/alcs/tag/tag.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 665d376

Please sign in to comment.