From 42bc458632f6a6e42e609b76bc26d30270728412 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Wed, 14 Jun 2023 07:55:22 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20155:=20API=20IF=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task1930: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1930) - Typist一覧取得API、TypistGroup一覧取得API、チェックアウト候補変更APIの3本のAPIIFを実装しました。 - 既存のTask系APIで使用していた Typist クラスの名前をAssigneeに変更しました - npm run formatでフォーマットした内容も含まれています。 ## レビューポイント - IFはラフスケッチで合意した内容に沿っているか - 既存のTask系APIのTypistクラスの名前変更に関して、修正抜け漏れはないか - チェックアウト候補変更APIのリクエスト/レスポンスのクラス名は適切か ## UIの変更 - なし ## 動作確認状況 - ローカルでPostmanにてAPIIFの値が返ることを確認しました ## 補足 - 特になし --- dictation_server/src/api/odms/openapi.json | 252 +++++++++++++++++- .../src/common/guards/role/roleguards.spec.ts | 4 +- .../features/accounts/accounts.controller.ts | 80 ++++++ .../src/features/accounts/types/types.ts | 30 +++ .../src/features/files/files.controller.ts | 3 +- .../features/licenses/licenses.controller.ts | 4 +- .../src/features/tasks/tasks.controller.ts | 55 ++++ .../src/features/tasks/types/convert.ts | 26 +- .../src/features/tasks/types/types.ts | 19 +- 9 files changed, 449 insertions(+), 24 deletions(-) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 69a0544..aedfdbe 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -272,6 +272,98 @@ ] } }, + "/accounts/typists": { + "get": { + "operationId": "getTypists", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のタイピスト一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTypistsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "tags": ["accounts"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/accounts/typist-groups": { + "get": { + "operationId": "getTypistGroups", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTypistGroupsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "tags": ["accounts"], + "security": [ + { + "bearer": [] + } + ] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -1527,6 +1619,91 @@ ] } }, + "/tasks/{audioFileId}/checkout-permission": { + "post": { + "operationId": "changeCheckoutPermission", + "summary": "", + "description": "指定した文字起こしタスクのチェックアウト候補を変更します。", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCheckoutPermissionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCheckoutPermissionResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ(タスクのステータス不正、指定ユーザー不正など)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "tags": ["tasks"], + "security": [ + { + "bearer": [] + } + ] + } + }, "/licenses/orders": { "post": { "operationId": "createOrders", @@ -1857,6 +2034,58 @@ }, "required": ["account"] }, + "Typist": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "TypistのユーザーID" + }, + "name": { + "type": "string", + "description": "Typistのユーザー名" + } + }, + "required": ["id", "name"] + }, + "GetTypistsResponse": { + "type": "object", + "properties": { + "typists": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Typist" + } + } + }, + "required": ["typists"] + }, + "TypistGroup": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "TypistGroupのID" + }, + "name": { + "type": "string", + "description": "TypistGroup名" + } + }, + "required": ["id", "name"] + }, + "GetTypistGroupsResponse": { + "type": "object", + "properties": { + "typistGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypistGroup" + } + } + }, + "required": ["typistGroups"] + }, "ConfirmRequest": { "type": "object", "properties": { @@ -2230,7 +2459,7 @@ }, "required": ["url"] }, - "Typist": { + "Assignee": { "type": "object", "properties": { "typistUserId": { @@ -2322,7 +2551,7 @@ "description": "割り当てられたユーザー", "allOf": [ { - "$ref": "#/components/schemas/Typist" + "$ref": "#/components/schemas/Assignee" } ] }, @@ -2330,7 +2559,7 @@ "description": "文字起こしに着手できる(チェックアウト可能な)、タスクにアサインされているグループ/個人の一覧", "type": "array", "items": { - "$ref": "#/components/schemas/Typist" + "$ref": "#/components/schemas/Assignee" } }, "status": { @@ -2407,6 +2636,23 @@ "type": "object", "properties": {} }, + "PostCheckoutPermissionRequest": { + "type": "object", + "properties": { + "assignees": { + "description": "文字起こしに着手可能(チェックアウト可能)にしたい、グループ個人の一覧", + "type": "array", + "items": { + "$ref": "#/components/schemas/Assignee" + } + } + }, + "required": ["assignees"] + }, + "PostCheckoutPermissionResponse": { + "type": "object", + "properties": {} + }, "CreateOrdersRequest": { "type": "object", "properties": { diff --git a/dictation_server/src/common/guards/role/roleguards.spec.ts b/dictation_server/src/common/guards/role/roleguards.spec.ts index ef6fc99..690b40b 100644 --- a/dictation_server/src/common/guards/role/roleguards.spec.ts +++ b/dictation_server/src/common/guards/role/roleguards.spec.ts @@ -12,7 +12,9 @@ describe('RoleGuard', () => { expect(guards.checkRole('author admin')).toBeTruthy(); }); it('author OR adminの許可Roleが設定時、その許可roleを含むroleを持つ場合、許可される', () => { - const guards = RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN] }); + const guards = RoleGuard.requireds({ + roles: [USER_ROLES.AUTHOR, ADMIN_ROLES.ADMIN], + }); // authorが許可リスト([authorまたはadmin])に含まれるので許可 expect(guards.checkRole('author')).toBeTruthy(); // adminが許可リスト([authorまたはadmin])に含まれるので許可 diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 6e09a8f..15e1cba 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -23,6 +23,8 @@ import { GetLicenseSummaryRequest, GetLicenseSummaryResponse, GetMyAccountResponse, + GetTypistGroupsResponse, + GetTypistsResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -194,4 +196,82 @@ export class AccountsController { }, }; } + + @ApiResponse({ + status: HttpStatus.OK, + type: GetTypistsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'getTypists', + description: + 'ログインしているユーザーのアカウント配下のタイピスト一覧を取得します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @Get('typists') + async getTypists(@Req() req: Request): Promise { + console.log(req.header('Authorization')); + return { + typists: [ + { + id: 1, + name: 'AAA', + }, + { + id: 2, + name: 'BBB', + }, + ], + }; + } + + @ApiResponse({ + status: HttpStatus.OK, + type: GetTypistGroupsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'getTypistGroups', + description: + 'ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @Get('typist-groups') + async getTypistGroups(@Req() req: Request): Promise { + console.log(req.header('Authorization')); + return { + typistGroups: [ + { + id: 1, + name: 'GroupA', + }, + { + id: 2, + name: 'GroupB', + }, + ], + }; + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 416af83..642e391 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -82,3 +82,33 @@ export class GetMyAccountResponse { @ApiProperty({ type: Account }) account: Account; } + +export class Typist { + @ApiProperty({ + description: 'TypistのユーザーID', + }) + id: number; + + @ApiProperty({ description: 'Typistのユーザー名' }) + name: string; +} + +export class GetTypistsResponse { + @ApiProperty({ type: [Typist] }) + typists: Typist[]; +} + +export class TypistGroup { + @ApiProperty({ + description: 'TypistGroupのID', + }) + id: number; + + @ApiProperty({ description: 'TypistGroup名' }) + name: string; +} + +export class GetTypistGroupsResponse { + @ApiProperty({ type: [TypistGroup] }) + typistGroups: TypistGroup[]; +} diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index 8efd206..6ebe4b9 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -141,8 +141,9 @@ export class FilesController { @Headers('authorization') authorization: string, // クエリパラメータ AudioUploadLocationRequest は空であるため内部で使用しない。 // 使用しないことを宣言するために先頭にプレフィックス_(アンダースコア)をつけている + // eslint-disable-next-line @typescript-eslint/no-unused-vars @Query() _query: AudioUploadLocationRequest, - ): Promise { + ): Promise { const token = authorization.substring( 'Bearer '.length, authorization.length, diff --git a/dictation_server/src/features/licenses/licenses.controller.ts b/dictation_server/src/features/licenses/licenses.controller.ts index ef6a95a..9f7018e 100644 --- a/dictation_server/src/features/licenses/licenses.controller.ts +++ b/dictation_server/src/features/licenses/licenses.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, - HttpException, HttpStatus, Post, Req, @@ -9,11 +8,10 @@ import { } from '@nestjs/common'; import { ApiResponse, - ApiTags, + ApiTags, ApiOperation, ApiBearerAuth, } from '@nestjs/swagger'; -import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { ErrorResponse } from '../../common/error/types/types'; import { LicensesService } from './licenses.service'; import { CreateOrdersResponse, CreateOrdersRequest } from './types/types'; diff --git a/dictation_server/src/features/tasks/tasks.controller.ts b/dictation_server/src/features/tasks/tasks.controller.ts index eaaeab6..baa338a 100644 --- a/dictation_server/src/features/tasks/tasks.controller.ts +++ b/dictation_server/src/features/tasks/tasks.controller.ts @@ -1,4 +1,5 @@ import { + Body, Controller, Get, Headers, @@ -16,12 +17,15 @@ import { ApiBearerAuth, } from '@nestjs/swagger'; import { ErrorResponse } from '../../common/error/types/types'; +import { Request } from 'express'; import { TasksService } from './tasks.service'; import { AudioNextRequest, AudioNextResponse, ChangeStatusRequest, ChangeStatusResponse, + PostCheckoutPermissionRequest, + PostCheckoutPermissionResponse, TasksRequest, TasksResponse, } from './types/types'; @@ -33,6 +37,8 @@ import jwt from 'jsonwebtoken'; import { retrieveAuthorizationToken } from '../../common/http/helper'; import { AccessToken } from '../../common/token'; import { AuthGuard } from '../../common/guards/auth/authguards'; +import { RoleGuard } from '../../common/guards/role/roleguards'; +import { ADMIN_ROLES, USER_ROLES } from '../../constants'; @ApiTags('tasks') @Controller('tasks') @@ -380,4 +386,53 @@ export class TasksController { return {}; } + + @Post(':audioFileId/checkout-permission') + @ApiResponse({ + status: HttpStatus.OK, + type: PostCheckoutPermissionResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + '不正なパラメータ(タスクのステータス不正、指定ユーザー不正など)', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: '指定したIDの音声ファイルが存在しない', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'changeCheckoutPermission', + description: '指定した文字起こしタスクのチェックアウト候補を変更します。', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN, USER_ROLES.AUTHOR] }), + ) + async changeCheckoutPermission( + @Req() req: Request, + @Param(`audioFileId`) audioFileId: number, + @Body() body: PostCheckoutPermissionRequest, + ): Promise { + const { assignees } = body; + console.log(req.header('Authorization')); + console.log(audioFileId); + console.log(assignees); + + return {}; + } } diff --git a/dictation_server/src/features/tasks/types/convert.ts b/dictation_server/src/features/tasks/types/convert.ts index 0356d75..12a6351 100644 --- a/dictation_server/src/features/tasks/types/convert.ts +++ b/dictation_server/src/features/tasks/types/convert.ts @@ -3,7 +3,7 @@ import { User as UserEntity } from '../../../repositories/users/entity/user.enti import { UserGroup as UserGroupEntity } from '../../../repositories/user_groups/entity/user_group.entity'; import { CheckoutPermission as CheckoutPermissionEntity } from '../../../repositories/checkout_permissions/entity/checkout_permission.entity'; import { AudioOptionItem as AudioOptionItemEntity } from '../../../repositories/audio_option_items/entity/audio_option_item.entity'; -import { Task, Typist } from './types'; +import { Task, Assignee } from './types'; import { AudioOptionItem } from '../../files/types/types'; // Repository側のDTOからTaskオブジェクトの一覧を構築する @@ -41,8 +41,8 @@ const createTask = ( const assignees = createAssignees(permissions); // RepositoryDTO => ControllerDTOに変換 - const typist: Typist = - typist_user != null ? convertUserToTypist(typist_user) : undefined; + const typist: Assignee = + typist_user != null ? convertUserToAssignee(typist_user) : undefined; return { audioFileId: task.audio_file_id, @@ -81,15 +81,17 @@ const createAudioOptionItems = ( }); }; -// Repository側のDTOからAudioOptionItemオブジェクトを構築する -const createAssignees = (permissions: CheckoutPermissionEntity[]): Typist[] => { - return permissions.flatMap((x): Typist[] => { +// Repository側のDTOからAssigneeオブジェクトを構築する +const createAssignees = ( + permissions: CheckoutPermissionEntity[], +): Assignee[] => { + return permissions.flatMap((x): Assignee[] => { if (x.user != null) { - return [convertUserToTypist(x.user)]; + return [convertUserToAssignee(x.user)]; } if (x.user_group != null) { - return [convertUserGroupToTypist(x.user_group)]; + return [convertUserGroupToAssignee(x.user_group)]; } // JOINしようとしたがUserが存在しなかったというケースはSkipする @@ -97,16 +99,16 @@ const createAssignees = (permissions: CheckoutPermissionEntity[]): Typist[] => { }); }; -// RepositoryDTOのUserからTypistオブジェクトを生成します -const convertUserToTypist = (user: UserEntity): Typist => { +// RepositoryDTOのUserからAssigneeオブジェクトを生成します +const convertUserToAssignee = (user: UserEntity): Assignee => { return { typistUserId: user.id, typistName: `USER_${user?.external_id}`, // XXX Azure AD B2Cから取得した名前を入れる }; }; -// RepositoryDTOのUserGroupからTypistオブジェクトを生成します -const convertUserGroupToTypist = (userGroup: UserGroupEntity): Typist => { +// RepositoryDTOのUserGroupからAssigneeオブジェクトを生成します +const convertUserGroupToAssignee = (userGroup: UserGroupEntity): Assignee => { return { typistGroupId: userGroup.id, typistName: userGroup.name, diff --git a/dictation_server/src/features/tasks/types/types.ts b/dictation_server/src/features/tasks/types/types.ts index fcaa5b1..a738634 100644 --- a/dictation_server/src/features/tasks/types/types.ts +++ b/dictation_server/src/features/tasks/types/types.ts @@ -60,7 +60,7 @@ export class TasksRequest { paramName?: string; } -export class Typist { +export class Assignee { @ApiProperty({ required: false, description: 'TypistID(TypistIDかTypistGroupIDのどちらかに値が入る)', @@ -128,13 +128,13 @@ export class Task { required: false, description: '割り当てられたユーザー', }) - typist?: Typist | undefined; + typist?: Assignee | undefined; @ApiProperty({ - type: [Typist], + type: [Assignee], description: '文字起こしに着手できる(チェックアウト可能な)、タスクにアサインされているグループ/個人の一覧', }) - assignees: Typist[]; + assignees: Assignee[]; @ApiProperty({ description: '音声ファイルのファイルステータス Uploaded / Pending / InProgress / Finished / Backup', @@ -186,3 +186,14 @@ export class ChangeStatusRequest { } export class ChangeStatusResponse {} + +export class PostCheckoutPermissionRequest { + @ApiProperty({ + type: [Assignee], + description: + '文字起こしに着手可能(チェックアウト可能)にしたい、グループ個人の一覧', + }) + assignees: Assignee[]; +} + +export class PostCheckoutPermissionResponse {}