Merged PR 136: API実装(ソート条件取得API)

## 概要
[Task1923: API実装(ソート条件取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1923)

- ソート条件取得APIを実装
- テスト追加

## レビューポイント
- ソート条件取得APIのレスポンスのデータは足りているか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-06-07 09:21:20 +00:00
parent 9441049201
commit 942ac30d8f
7 changed files with 299 additions and 17 deletions

View File

@ -398,7 +398,9 @@
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SortCriteriaRequest" }
"schema": {
"$ref": "#/components/schemas/PostSortCriteriaRequest"
}
}
}
},
@ -408,7 +410,51 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SortCriteriaResponse"
"$ref": "#/components/schemas/PostSortCriteriaResponse"
}
}
}
},
"400": {
"description": "不正なパラメータ",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["users"],
"security": [{ "bearer": [] }]
},
"get": {
"operationId": "getSortCcriteria",
"summary": "",
"description": "ログインしているユーザーのタスクソート条件を取得します",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PostSortCriteriaResponse"
}
}
}
@ -1529,7 +1575,7 @@
"prompt"
]
},
"SortCriteriaRequest": {
"PostSortCriteriaRequest": {
"type": "object",
"properties": {
"direction": { "type": "string", "description": "ASC/DESC" },
@ -1540,7 +1586,7 @@
},
"required": ["direction", "paramName"]
},
"SortCriteriaResponse": { "type": "object", "properties": {} },
"PostSortCriteriaResponse": { "type": "object", "properties": {} },
"AudioOptionItem": {
"type": "object",
"properties": {

View File

@ -12,6 +12,10 @@ import { UsersRepositoryService } from '../../../repositories/users/users.reposi
import { UsersService } from '../users.service';
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service';
import {
SortDirection,
TaskListSortableAttribute,
} from '../../../common/types/sort';
export type CryptoMockValue = {
getPublicKey: string | Error;
@ -19,6 +23,7 @@ export type CryptoMockValue = {
export type SortCriteriaRepositoryMockValue = {
updateSortCriteria: SortCriteria | Error;
getSortCriteria: SortCriteria | Error;
};
export type UsersRepositoryMockValue = {
@ -93,15 +98,29 @@ export const makeUsersServiceMock = async (
export const makeSortCriteriaRepositoryMock = (
value: SortCriteriaRepositoryMockValue,
) => {
const { updateSortCriteria } = value;
const { updateSortCriteria, getSortCriteria } = value;
return {
updateSortCriteria:
updateSortCriteria instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(updateSortCriteria)
? jest
.fn<
Promise<void>,
[number, TaskListSortableAttribute, SortDirection]
>()
.mockRejectedValue(updateSortCriteria)
: jest
.fn<Promise<SortCriteria>, []>()
.fn<
Promise<SortCriteria>,
[number, TaskListSortableAttribute, SortDirection]
>()
.mockResolvedValue(updateSortCriteria),
getSortCriteria:
getSortCriteria instanceof Error
? jest.fn<Promise<void>, [number]>().mockRejectedValue(getSortCriteria)
: jest
.fn<Promise<SortCriteria>, [number]>()
.mockResolvedValue(getSortCriteria),
};
};
@ -303,6 +322,7 @@ export const makeDefaultSortCriteriaRepositoryMockValue =
}
return {
updateSortCriteria: sortCriteria,
getSortCriteria: sortCriteria,
};
};

View File

@ -134,7 +134,7 @@ export class GetRelationsResponse {
prompt: boolean;
}
export class SortCriteriaRequest {
export class PostSortCriteriaRequest {
@ApiProperty({ description: 'ASC/DESC' })
@IsIn(['ASC', 'DESC'], { message: 'invalid direction' })
direction: string;
@ -148,4 +148,16 @@ export class SortCriteriaRequest {
paramName: string;
}
export class SortCriteriaResponse {}
export class PostSortCriteriaResponse {}
export class GetSortCriteriaRequest {}
export class GetSortCriteriaResponse {
@ApiProperty({ description: 'ASC/DESC' })
direction: string;
@ApiProperty({
description: `${TASK_LIST_SORTABLE_ATTRIBUTES.join('/')}`,
})
paramName: string;
}

View File

@ -7,6 +7,7 @@ import {
Req,
HttpException,
UseGuards,
Query,
} from '@nestjs/common';
import {
ApiBearerAuth,
@ -29,8 +30,10 @@ import {
GetUsersResponse,
SignupRequest,
SignupResponse,
SortCriteriaRequest,
SortCriteriaResponse,
PostSortCriteriaRequest,
PostSortCriteriaResponse,
GetSortCriteriaRequest,
GetSortCriteriaResponse,
} from './types/types';
import { UsersService } from './users.service';
import jwt from 'jsonwebtoken';
@ -279,7 +282,7 @@ export class UsersController {
@ApiResponse({
status: HttpStatus.OK,
type: SortCriteriaResponse,
type: PostSortCriteriaResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
@ -305,9 +308,9 @@ export class UsersController {
@UseGuards(AuthGuard)
@Post('sort-criteria')
async updateSortCriteria(
@Body() body: SortCriteriaRequest,
@Body() body: PostSortCriteriaRequest,
@Req() req: Request,
): Promise<SortCriteriaResponse> {
): Promise<PostSortCriteriaResponse> {
const { direction, paramName } = body;
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
@ -329,4 +332,40 @@ export class UsersController {
);
return {};
}
@ApiResponse({
status: HttpStatus.OK,
type: GetSortCriteriaResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'getSortCcriteria',
description: 'ログインしているユーザーのタスクソート条件を取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@Get('sort-criteria')
async getSortCriteria(
@Query() query: GetSortCriteriaRequest,
@Req() req: Request,
): Promise<GetSortCriteriaResponse> {
const {} = query;
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const { direction, paramName } = await this.usersService.getSortCriteria(
decodedToken,
);
return { direction, paramName };
}
}

View File

@ -727,7 +727,6 @@ it('ユーザー情報が存在せず、ソート条件を変更できない', a
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
// モックでDBからのユーザ取得がエラーとなる
usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
const service = await makeUsersServiceMock(
@ -764,8 +763,6 @@ it('ソート条件が存在せず、ソート条件を変更できない', asyn
'sort criteria not found',
);
// モックでDBからのユーザ取得を空にする
const service = await makeUsersServiceMock(
cryptoMockValue,
usersRepositoryMockValue,
@ -787,3 +784,100 @@ it('ソート条件が存在せず、ソート条件を変更できない', asyn
),
);
});
it('ソート条件を取得できる', async () => {
const cryptoMockValue = makeDefaultCryptoMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
cryptoMockValue,
usersRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
expect(
await service.getSortCriteria({
role: 'none admin',
userId: 'xxxxxxxxxxxx',
}),
).toEqual({ direction: 'ASC', paramName: 'JOB_NUMBER' });
});
it('ソート条件が存在せず、ソート条件を取得できない', async () => {
const cryptoMockValue = makeDefaultCryptoMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
sortCriteriaRepositoryMockValue.getSortCriteria = new Error(
'sort criteria not found',
);
const service = await makeUsersServiceMock(
cryptoMockValue,
usersRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
await expect(
service.getSortCriteria({
role: 'none admin',
userId: 'xxxxxxxxxxxx',
}),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('DBから取得した値が不正だった場合、エラーとなる', async () => {
const cryptoMockValue = makeDefaultCryptoMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
sortCriteriaRepositoryMockValue.getSortCriteria = {
id: 1,
direction: 'AAA',
parameter: 'BBBBB',
user_id: 1,
};
const service = await makeUsersServiceMock(
cryptoMockValue,
usersRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
await expect(
service.getSortCriteria({
role: 'none admin',
userId: 'xxxxxxxxxxxx',
}),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});

View File

@ -7,6 +7,8 @@ import { AccessToken } from '../../common/token';
import {
TaskListSortableAttribute,
SortDirection,
isTaskListSortableAttribute,
isSortDirection,
} from '../../common/types/sort';
import {
AdB2cService,
@ -386,4 +388,52 @@ export class UsersService {
this.logger.log(`[OUT] ${this.updateSortCriteria.name}`);
}
}
/**
* Gets sort criteria
* @param token
* @returns sort criteria
*/
async getSortCriteria(token: AccessToken): Promise<{
paramName: TaskListSortableAttribute;
direction: SortDirection;
}> {
this.logger.log(`[IN] ${this.getSortCriteria.name}`);
let user: EntityUser;
try {
// ユーザー情報を取得
const sub = token.userId;
user = await this.usersRepository.findUserByExternalId(sub);
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// ユーザーのソート条件を取得
const sortCriteria = await this.sortCriteriaRepository.getSortCriteria(
user.id,
);
const { direction, parameter } = sortCriteria;
//型チェック
if (
!isTaskListSortableAttribute(parameter) ||
!isSortDirection(direction)
) {
throw new Error('The value stored in the DB is invalid.');
}
return { direction, paramName: parameter };
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${this.getSortCriteria.name}`);
}
}
}

View File

@ -45,4 +45,25 @@ export class SortCriteriaRepositoryService {
return persisted;
});
}
/**
* Gets sort criteria
* @param userId
* @returns sort criteria
*/
async getSortCriteria(userId: number): Promise<SortCriteria> {
this.logger.log(` ${this.updateSortCriteria.name}; userId:${userId}`);
const repo = this.dataSource.getRepository(SortCriteria);
const sortCriteria = await repo.findOne({
where: {
user_id: userId,
},
});
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
if (!sortCriteria) {
throw new Error('sort criteria not found ');
}
return sortCriteria;
}
}