Merged PR 396: API実装(オプションアイテム取得)

## 概要
[Task2592: API実装(オプションアイテム取得)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2592)

- オプションアイテム取得APIとテストを実装しました。

## レビューポイント
- リポジトリの取得ロジックは想定通りか
- テストケースは適切か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-09-11 08:31:03 +00:00
parent d8d5789f5a
commit 2dcb1c1f84
11 changed files with 268 additions and 81 deletions

View File

@ -41,7 +41,6 @@ import { UserGroupsRepositoryModule } from './repositories/user_groups/user_grou
import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module';
import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module';
import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module';
import { OptionItemsRepositoryModule } from './repositories/option_items/option_items.repository.module';
@Module({
imports: [
@ -97,7 +96,6 @@ import { OptionItemsRepositoryModule } from './repositories/option_items/option_
AuthGuardsModule,
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
OptionItemsRepositoryModule,
],
controllers: [
HealthController,

View File

@ -30,7 +30,6 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat
import { FilesService } from '../../features/files/files.service';
import { LicensesService } from '../../features/licenses/licenses.service';
import { TasksService } from '../../features/tasks/tasks.service';
import { OptionItemsRepositoryModule } from '../../repositories/option_items/option_items.repository.module';
export const makeTestingModule = async (
datasource: DataSource,
@ -66,7 +65,6 @@ export const makeTestingModule = async (
AuthGuardsModule,
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
OptionItemsRepositoryModule,
],
providers: [
AuthService,

View File

@ -815,63 +815,13 @@ export class AccountsController {
const context = makeContext(userId);
console.log('id', id);
console.log(context.trackingId);
const optionItems = await this.accountService.getOptionItems(
context,
userId,
id,
);
return {
optionItems: [
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
{
itemLabel: '',
defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initialValue: '',
},
],
};
return optionItems;
}
@Post('/worktypes/:id/option-items')

View File

@ -16,6 +16,7 @@ import {
createLicense,
createLicenseOrder,
createLicenseSetExpiryDateAndStatus,
createOptionItems,
createWorktype,
getOptionItems,
getSortCriteria,
@ -1505,6 +1506,7 @@ describe('AccountsService', () => {
makeDefaultLicensesRepositoryMockValue();
const worktypesRepositoryMockValue =
makeDefaultWorktypesRepositoryMockValue();
const service = await makeAccountsServiceMock(
accountsRepositoryMockValue,
usersRepositoryMockValue,
@ -3863,6 +3865,135 @@ describe('updateWorktype', () => {
});
});
describe('getOptionItems', () => {
let source: DataSource = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
database: ':memory:',
logging: false,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
await source.destroy();
source = null;
});
it('指定WorktypeIDに紐づいたOptionItemを取得できる', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = await createWorktype(source, account.id, 'worktype1');
const optionItems = await createOptionItems(source, worktype.id);
//作成したデータを確認
{
expect(optionItems.length).toBe(10);
expect(optionItems[0].item_label).toBe('');
expect(optionItems[0].default_value_type).toBe(
OPTION_ITEM_VALUE_TYPE.DEFAULT,
);
expect(optionItems[0].initial_value).toBe('');
}
const resOptionItems = await service.getOptionItems(
context,
admin.external_id,
worktype.id,
);
//実行結果を確認
{
expect(resOptionItems.optionItems.length).toBe(10);
expect(resOptionItems.optionItems[0].itemLabel).toBe('');
expect(resOptionItems.optionItems[0].defaultValueType).toBe(
OPTION_ITEM_VALUE_TYPE.DEFAULT,
);
expect(resOptionItems.optionItems[0].initialValue).toBe('');
}
});
it('WorktypeIDが存在しない場合、400エラーとなること', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = await createWorktype(source, account.id, 'worktype1');
const optionItems = await createOptionItems(source, worktype.id);
//作成したデータを確認
{
expect(optionItems.length).toBe(10);
expect(optionItems[0].item_label).toBe('');
expect(optionItems[0].default_value_type).toBe(
OPTION_ITEM_VALUE_TYPE.DEFAULT,
);
expect(optionItems[0].initial_value).toBe('');
}
try {
await service.getOptionItems(context, admin.external_id, 999);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E011003'));
} else {
fail();
}
}
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = await createWorktype(source, account.id, 'worktype1');
const optionItems = await createOptionItems(source, worktype.id);
//作成したデータを確認
{
expect(optionItems.length).toBe(10);
expect(optionItems[0].item_label).toBe('');
expect(optionItems[0].default_value_type).toBe(
OPTION_ITEM_VALUE_TYPE.DEFAULT,
);
expect(optionItems[0].initial_value).toBe('');
}
//DBアクセスに失敗するようにする
const worktypesService = module.get<WorktypesRepositoryService>(
WorktypesRepositoryService,
);
worktypesService.getOptionItems = jest.fn().mockRejectedValue('DB failed');
try {
await service.getWorktypes(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});
describe('ライセンス発行キャンセル', () => {
let source: DataSource = null;
beforeEach(async () => {

View File

@ -23,6 +23,7 @@ import {
GetMyAccountResponse,
GetTypistGroupResponse,
GetWorktypesResponse,
GetOptionItemsResponse,
GetPartnersResponse,
} from './types/types';
import {
@ -1314,6 +1315,69 @@ export class AccountsService {
}
}
/**
*
* @param context
* @param externalId
* @param id Worktypeの内部ID
* @returns option items
*/
async getOptionItems(
context: Context,
externalId: string,
id: number,
): Promise<GetOptionItemsResponse> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getOptionItems.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// オプションアイテム一覧を取得する
const optionItems = await this.worktypesRepository.getOptionItems(
accountId,
id,
);
return {
optionItems: optionItems.map((x) => ({
itemLabel: x.item_label,
defaultValueType: x.default_value_type,
initialValue: x.initial_value,
})),
};
} catch (e) {
this.logger.error(e);
if (e instanceof Error) {
switch (e.constructor) {
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getOptionItems.name}`,
);
}
}
/**
*
* @param context

View File

@ -7,7 +7,8 @@ import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_cr
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
import { OptionItem } from '../../../repositories/option_items/entity/option_item.entity';
import { OptionItem } from '../../../repositories/worktypes/entity/option_item.entity';
import { OPTION_ITEM_VALUE_TYPE } from '../../../constants';
/**
* ユーティリティ: すべてのソート条件を取得する
@ -153,6 +154,35 @@ export const getWorktypes = async (
});
};
// オプションアイテムを作成する
export const createOptionItems = async (
datasource: DataSource,
worktypeId: number,
): Promise<OptionItem[]> => {
const optionItems = [];
for (let i = 0; i < 10; i++) {
optionItems.push({
worktype_id: worktypeId,
item_label: '',
default_value_type: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initial_value: '',
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
}
await datasource.getRepository(OptionItem).insert(optionItems);
const items = datasource
.getRepository(OptionItem)
.find({ where: { worktype_id: worktypeId } });
return items;
};
// オプションアイテムを取得する
export const getOptionItems = async (
datasource: DataSource,

View File

@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OptionItem } from './entity/option_item.entity';
import { OptionItemsRepositoryService } from './option_items.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([OptionItem])],
providers: [OptionItemsRepositoryService],
exports: [OptionItemsRepositoryService],
})
export class OptionItemsRepositoryModule {}

View File

@ -1,7 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class OptionItemsRepositoryService {
constructor(private dataSource: DataSource) {}
}

View File

@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Worktype } from './entity/worktype.entity';
import { WorktypesRepositoryService } from './worktypes.repository.service';
import { OptionItem } from './entity/option_item.entity';
@Module({
imports: [TypeOrmModule.forFeature([Worktype])],
imports: [TypeOrmModule.forFeature([Worktype, OptionItem])],
providers: [WorktypesRepositoryService],
exports: [WorktypesRepositoryService],
})

View File

@ -11,7 +11,7 @@ import {
WorktypeIdMaxCountError,
WorktypeIdNotFoundError,
} from './errors/types';
import { OptionItem } from '../option_items/entity/option_item.entity';
import { OptionItem } from './entity/option_item.entity';
@Injectable()
export class WorktypesRepositoryService {
@ -135,4 +135,37 @@ export class WorktypesRepositoryService {
await worktypeRepo.save(worktype);
});
}
/**
*
* @param accountId
* @param worktypeId worktypeの内部ID
* @returns option items
*/
async getOptionItems(
accountId: number,
worktypeId: number,
): Promise<OptionItem[]> {
return await this.dataSource.transaction(async (entityManager) => {
const repoWorktype = entityManager.getRepository(Worktype);
const repoOptionItem = entityManager.getRepository(OptionItem);
const worktype = await repoWorktype.findOne({
where: { account_id: accountId, id: worktypeId },
});
// ワークタイプが存在しない場合はエラー
if (!worktype) {
throw new WorktypeIdNotFoundError(
`Worktype is not found. id: ${worktypeId}`,
);
}
const optionItems = await repoOptionItem.find({
where: { worktype_id: worktypeId },
});
return optionItems;
});
}
}