From 942ac30d8f210d90a7e41560599aa2aeb2377a2f Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Wed, 7 Jun 2023 09:21:20 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20136:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=82=BD=E3=83=BC=E3=83=88=E6=9D=A1=E4=BB=B6=E5=8F=96?= =?UTF-8?q?=E5=BE=97API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [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のスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/api/odms/openapi.json | 54 +++++++++- .../features/users/test/users.service.mock.ts | 26 ++++- .../src/features/users/types/types.ts | 16 ++- .../src/features/users/users.controller.ts | 49 ++++++++- .../src/features/users/users.service.spec.ts | 100 +++++++++++++++++- .../src/features/users/users.service.ts | 50 +++++++++ .../sort_criteria.repository.service.ts | 21 ++++ 7 files changed, 299 insertions(+), 17 deletions(-) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 05e89d6..33e5620 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -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": { diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 465a76f..815c252 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -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, []>().mockRejectedValue(updateSortCriteria) + ? jest + .fn< + Promise, + [number, TaskListSortableAttribute, SortDirection] + >() + .mockRejectedValue(updateSortCriteria) : jest - .fn, []>() + .fn< + Promise, + [number, TaskListSortableAttribute, SortDirection] + >() .mockResolvedValue(updateSortCriteria), + getSortCriteria: + getSortCriteria instanceof Error + ? jest.fn, [number]>().mockRejectedValue(getSortCriteria) + : jest + .fn, [number]>() + .mockResolvedValue(getSortCriteria), }; }; @@ -303,6 +322,7 @@ export const makeDefaultSortCriteriaRepositoryMockValue = } return { updateSortCriteria: sortCriteria, + getSortCriteria: sortCriteria, }; }; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 4aa30e1..c164bf8 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -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; +} diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 5cf3b08..a20bd1f 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -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 { + ): Promise { 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 { + 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 }; + } } diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 7a69a77..68090b0 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -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, + ), + ); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index f4fcf58..ad5c6fd 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -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}`); + } + } } diff --git a/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.service.ts b/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.service.ts index bd7225e..1edb67b 100644 --- a/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.service.ts +++ b/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.service.ts @@ -45,4 +45,25 @@ export class SortCriteriaRepositoryService { return persisted; }); } + /** + * Gets sort criteria + * @param userId + * @returns sort criteria + */ + async getSortCriteria(userId: number): Promise { + 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; + } }