Merged PR 155: API IF実装

## 概要
[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の値が返ることを確認しました

## 補足
- 特になし
This commit is contained in:
Kentaro Fukunaga 2023-06-14 07:55:22 +00:00
parent bc442e1a2e
commit 42bc458632
9 changed files with 449 additions and 24 deletions

View File

@ -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": {

View File

@ -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])に含まれるので許可

View File

@ -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<GetTypistsResponse> {
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<GetTypistGroupsResponse> {
console.log(req.header('Authorization'));
return {
typistGroups: [
{
id: 1,
name: 'GroupA',
},
{
id: 2,
name: 'GroupB',
},
],
};
}
}

View File

@ -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[];
}

View File

@ -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<AudioUploadLocationResponse> {
): Promise<AudioUploadLocationResponse> {
const token = authorization.substring(
'Bearer '.length,
authorization.length,

View File

@ -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';

View File

@ -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<PostCheckoutPermissionResponse> {
const { assignees } = body;
console.log(req.header('Authorization'));
console.log(audioFileId);
console.log(assignees);
return {};
}
}

View File

@ -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,

View File

@ -60,7 +60,7 @@ export class TasksRequest {
paramName?: string;
}
export class Typist {
export class Assignee {
@ApiProperty({
required: false,
description: 'TypistIDTypistIDか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 {}