Skip to content

Commit 2539cb5

Browse files
authored
Merge pull request #1937 from bcgov/feature/ALCS-2325-create-tags-entity
ALCS-2325 Tag/Category entities and cruds
2 parents c0b806c + ba57f1c commit 2539cb5

16 files changed

+794
-0
lines changed

services/apps/alcs/src/alcs/admin/admin.module.ts

+12
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ import { NoiSubtypeController } from './noi-subtype/noi-subtype.controller';
3232
import { NoiSubtypeService } from './noi-subtype/noi-subtype.service';
3333
import { UnarchiveCardController } from './unarchive-card/unarchive-card.controller';
3434
import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
35+
import { TagCategoryService } from './tag-category/tag-category.service';
36+
import { TagCategoryController } from './tag-category/tag-category.controller';
37+
import { TagCategory } from './tag-category/tag-category.entity';
38+
import { Tag } from './tag/tag.entity';
39+
import { TagController } from './tag/tag.controller';
40+
import { TagService } from './tag/tag.service';
3541

3642
@Module({
3743
imports: [
@@ -42,6 +48,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
4248
NoticeOfIntentSubtype,
4349
ApplicationDecisionConditionType,
4450
Configuration,
51+
TagCategory,
52+
Tag,
4553
]),
4654
ApplicationModule,
4755
NoticeOfIntentModule,
@@ -62,6 +70,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
6270
UnarchiveCardController,
6371
NoiSubtypeController,
6472
ApplicationDecisionMakerController,
73+
TagCategoryController,
74+
TagController,
6575
ApplicationDecisionConditionTypesController,
6676
CardStatusController,
6777
BoardManagementController,
@@ -71,6 +81,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service';
7181
HolidayService,
7282
ApplicationCeoCriterionService,
7383
ApplicationDecisionMakerService,
84+
TagCategoryService,
85+
TagService,
7486
UnarchiveCardService,
7587
NoiSubtypeService,
7688
ApplicationDecisionConditionTypesService,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { TagCategoryController } from './tag-category.controller';
3+
import { TagCategoryService } from './tag-category.service';
4+
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
5+
import { ClsService } from 'nestjs-cls';
6+
import { initTagCategoryMockEntity } from '../../../../test/mocks/mockEntities';
7+
import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes';
8+
import { TagCategory } from './tag-category.entity';
9+
import { TagCategoryDto } from './tag-category.dto';
10+
11+
describe('TagCategoryController', () => {
12+
let controller: TagCategoryController;
13+
let tagCategoryService: DeepMocked<TagCategoryService>;
14+
15+
const mockCategoryTag = initTagCategoryMockEntity();
16+
17+
beforeEach(async () => {
18+
tagCategoryService = createMock<TagCategoryService>();
19+
20+
const module: TestingModule = await Test.createTestingModule({
21+
controllers: [TagCategoryController],
22+
providers: [
23+
{ provide: TagCategoryService, useValue: tagCategoryService },
24+
{
25+
provide: ClsService,
26+
useValue: {},
27+
},
28+
...mockKeyCloakProviders,
29+
],
30+
}).compile();
31+
32+
controller = module.get<TagCategoryController>(TagCategoryController);
33+
});
34+
35+
it('should be defined', () => {
36+
expect(controller).toBeDefined();
37+
});
38+
39+
it('should get a tag category', async () => {
40+
tagCategoryService.fetch.mockResolvedValue([[mockCategoryTag], 1]);
41+
42+
const result = await controller.fetch(0, 0);
43+
expect(tagCategoryService.fetch).toHaveBeenCalledTimes(1);
44+
expect(result.data).toEqual([mockCategoryTag]);
45+
expect(result.total).toEqual(1);
46+
});
47+
48+
it('should create a tag category', async () => {
49+
tagCategoryService.create.mockResolvedValue(mockCategoryTag);
50+
51+
const result = await controller.create(mockCategoryTag);
52+
expect(tagCategoryService.create).toHaveBeenCalledTimes(1);
53+
expect(result).toEqual(mockCategoryTag);
54+
});
55+
56+
it('should update tag category', async () => {
57+
tagCategoryService.update.mockResolvedValue({
58+
...mockCategoryTag,
59+
} as TagCategory);
60+
const categoryToUpdate = {
61+
uuid: mockCategoryTag.uuid,
62+
name: mockCategoryTag.name,
63+
} as TagCategoryDto;
64+
65+
const result = await controller.update(
66+
mockCategoryTag.uuid,
67+
categoryToUpdate,
68+
);
69+
70+
expect(tagCategoryService.update).toHaveBeenCalledTimes(1);
71+
expect(result).toEqual(mockCategoryTag);
72+
});
73+
74+
it('should delete a tag category', async () => {
75+
tagCategoryService.delete.mockResolvedValue(mockCategoryTag);
76+
77+
const result = await controller.delete(mockCategoryTag.uuid);
78+
expect(tagCategoryService.delete).toHaveBeenCalledTimes(1);
79+
expect(result).toEqual(mockCategoryTag);
80+
});
81+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {
2+
Body,
3+
Controller,
4+
Delete,
5+
Get,
6+
Param,
7+
Patch,
8+
Post,
9+
Query,
10+
UseGuards,
11+
} from '@nestjs/common';
12+
import { ApiOAuth2 } from '@nestjs/swagger';
13+
import * as config from 'config';
14+
import { RolesGuard } from '../../../common/authorization/roles-guard.service';
15+
import { UserRoles } from '../../../common/authorization/roles.decorator';
16+
import { AUTH_ROLE } from '../../../common/authorization/roles';
17+
import { TagCategoryDto } from './tag-category.dto';
18+
import { TagCategoryService } from './tag-category.service';
19+
20+
@Controller('tag-category')
21+
@ApiOAuth2(config.get<string[]>('KEYCLOAK.SCOPES'))
22+
@UseGuards(RolesGuard)
23+
export class TagCategoryController {
24+
25+
constructor(private service: TagCategoryService) {}
26+
27+
@Get('/:pageIndex/:itemsPerPage')
28+
@UserRoles(AUTH_ROLE.ADMIN)
29+
async fetch(
30+
@Param('pageIndex') pageIndex: number,
31+
@Param('itemsPerPage') itemsPerPage: number,
32+
@Query('search') search?: string,
33+
) {
34+
const result = await this.service.fetch(pageIndex, itemsPerPage, search);
35+
return { data: result[0], total: result[1] };
36+
}
37+
38+
@Post('')
39+
@UserRoles(AUTH_ROLE.ADMIN)
40+
async create(@Body() createDto: TagCategoryDto) {
41+
return await this.service.create(createDto);
42+
}
43+
44+
@Patch('/:uuid')
45+
@UserRoles(AUTH_ROLE.ADMIN)
46+
async update(@Param('uuid') uuid: string, @Body() updateDto: TagCategoryDto) {
47+
return await this.service.update(uuid, updateDto);
48+
}
49+
50+
@Delete('/:uuid')
51+
@UserRoles(AUTH_ROLE.ADMIN)
52+
async delete(@Param('uuid') uuid: string) {
53+
return await this.service.delete(uuid);
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IsString } from 'class-validator';
2+
3+
export class TagCategoryDto {
4+
@IsString()
5+
uuid: string;
6+
7+
@IsString()
8+
name: string;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { AutoMap } from 'automapper-classes';
2+
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
3+
import { Base } from '../../../common/entities/base.entity';
4+
5+
@Entity({ comment: 'Tag category.' })
6+
export class TagCategory extends Base {
7+
constructor(data?: Partial<TagCategory>) {
8+
super();
9+
if (data) {
10+
Object.assign(this, data);
11+
}
12+
}
13+
14+
@AutoMap()
15+
@PrimaryGeneratedColumn('uuid')
16+
uuid: string;
17+
18+
@AutoMap()
19+
@Column()
20+
name: string;
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { CONFIG_TOKEN } from '@app/common/config/config.module';
2+
import { Test, TestingModule } from '@nestjs/testing';
3+
import { Repository } from 'typeorm';
4+
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
5+
import { TagCategory } from '../tag-category/tag-category.entity';
6+
import { initTagCategoryMockEntity } from '../../../../test/mocks/mockEntities';
7+
import { AutomapperModule } from 'automapper-nestjs';
8+
import { classes } from 'automapper-classes';
9+
import { getRepositoryToken } from '@nestjs/typeorm';
10+
import * as config from 'config';
11+
import { TagCategoryDto } from './tag-category.dto';
12+
import { ServiceValidationException } from '@app/common/exceptions/base.exception';
13+
import { TagCategoryService } from './tag-category.service';
14+
15+
describe('TagCategoryService', () => {
16+
let service: TagCategoryService;
17+
let tagCategoryRepositoryMock: DeepMocked<Repository<TagCategory>>;
18+
let mockTagCategoryEntity;
19+
20+
beforeEach(async () => {
21+
tagCategoryRepositoryMock = createMock<Repository<TagCategory>>();
22+
mockTagCategoryEntity = initTagCategoryMockEntity();
23+
24+
const module: TestingModule = await Test.createTestingModule({
25+
imports: [
26+
AutomapperModule.forRoot({
27+
strategyInitializer: classes(),
28+
}),
29+
],
30+
providers: [
31+
TagCategoryService,
32+
{
33+
provide: getRepositoryToken(TagCategory),
34+
useValue: tagCategoryRepositoryMock,
35+
},
36+
{
37+
provide: CONFIG_TOKEN,
38+
useValue: config,
39+
},
40+
],
41+
}).compile();
42+
43+
service = module.get<TagCategoryService>(TagCategoryService);
44+
tagCategoryRepositoryMock.findOne.mockResolvedValue(mockTagCategoryEntity);
45+
tagCategoryRepositoryMock.findOneOrFail.mockResolvedValue(mockTagCategoryEntity);
46+
tagCategoryRepositoryMock.save.mockResolvedValue(mockTagCategoryEntity);
47+
tagCategoryRepositoryMock = module.get(getRepositoryToken(TagCategory));
48+
});
49+
50+
it('should be defined', () => {
51+
expect(service).toBeDefined();
52+
});
53+
54+
it('should get a tag category', async () => {
55+
expect(await service.getOneOrFail('fake')).toStrictEqual(
56+
mockTagCategoryEntity,
57+
);
58+
});
59+
60+
it('should call save when an tag category is updated', async () => {
61+
const payload: TagCategoryDto = {
62+
uuid: mockTagCategoryEntity.uuid,
63+
name: mockTagCategoryEntity.name,
64+
};
65+
66+
const result = await service.update(mockTagCategoryEntity.uuid, payload);
67+
expect(result).toStrictEqual(mockTagCategoryEntity);
68+
expect(tagCategoryRepositoryMock.save).toHaveBeenCalledTimes(1);
69+
expect(tagCategoryRepositoryMock.save).toHaveBeenCalledWith(
70+
mockTagCategoryEntity,
71+
);
72+
});
73+
74+
it('should fail update if tag category does not exist', async () => {
75+
const payload: TagCategoryDto = {
76+
uuid: mockTagCategoryEntity.uuid,
77+
name: mockTagCategoryEntity.name,
78+
};
79+
80+
tagCategoryRepositoryMock.findOneOrFail.mockRejectedValue(
81+
new ServiceValidationException(
82+
`Tag Category for with ${mockTagCategoryEntity.uuid} not found`,
83+
),
84+
);
85+
86+
await expect(
87+
service.update(mockTagCategoryEntity.uuid, payload),
88+
).rejects.toMatchObject(
89+
new ServiceValidationException(
90+
`Tag Category for with ${mockTagCategoryEntity.uuid} not found`,
91+
),
92+
);
93+
expect(tagCategoryRepositoryMock.save).toBeCalledTimes(0);
94+
});
95+
96+
it('should call save when tag successfully create', async () => {
97+
const payload: TagCategoryDto = {
98+
uuid: mockTagCategoryEntity.uuid,
99+
name: mockTagCategoryEntity.name,
100+
};
101+
102+
await service.create(payload);
103+
104+
expect(tagCategoryRepositoryMock.save).toBeCalledTimes(1);
105+
});
106+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { TagCategory } from './tag-category.entity';
3+
import { FindOptionsWhere, Like, Repository } from 'typeorm';
4+
import { InjectRepository } from '@nestjs/typeorm';
5+
import { TagCategoryDto } from './tag-category.dto';
6+
7+
@Injectable()
8+
export class TagCategoryService {
9+
constructor(
10+
@InjectRepository(TagCategory)
11+
private repository: Repository<TagCategory>,
12+
) {}
13+
14+
async fetch(pageIndex: number, itemsPerPage: number, search?: string) {
15+
let searchExpression: FindOptionsWhere<TagCategory> | undefined = undefined;
16+
17+
if (search) {
18+
searchExpression = {
19+
name: Like(`%${search}%`),
20+
};
21+
}
22+
23+
return (
24+
(await this.repository.findAndCount({
25+
where: searchExpression,
26+
order: { name: 'DESC' },
27+
take: itemsPerPage,
28+
skip: pageIndex * itemsPerPage,
29+
})) || [[], 0]
30+
);
31+
}
32+
33+
async create(dto: TagCategoryDto) {
34+
const newTagCategory = new TagCategory();
35+
newTagCategory.name = dto.name;
36+
return this.repository.save(newTagCategory);
37+
}
38+
39+
async getOneOrFail(uuid: string) {
40+
return await this.repository.findOneOrFail({
41+
where: { uuid },
42+
});
43+
}
44+
45+
async update(uuid: string, updateDto: TagCategoryDto) {
46+
const tagCategory = await this.getOneOrFail(uuid);
47+
tagCategory.name = updateDto.name;
48+
return await this.repository.save(tagCategory);
49+
}
50+
51+
async delete(uuid: string) {
52+
const tagCategory = await this.getOneOrFail(uuid);
53+
54+
return await this.repository.remove(tagCategory);
55+
}
56+
}

0 commit comments

Comments
 (0)