makabe.t d08c6c99af Merged PR 681: タスク削除API実装
## 概要
[Task3457: タスク削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3457)

- タスク削除APIとUTを実装しました。

## レビューポイント
- テストケースは適切でしょうか?
- リポジトリでの削除処理は適切でしょうか?
- エラー時のコード使い分けは適切でしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
2024-01-16 07:55:38 +00:00

835 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Logger,
Param,
ParseIntPipe,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import {
ApiResponse,
ApiOperation,
ApiTags,
ApiBearerAuth,
ApiParam,
} 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,
PostDeleteTaskRequest,
PostDeleteTaskResponse,
TasksRequest,
TasksResponse,
} from './types/types';
import {
SortDirection,
TaskListSortableAttribute,
isSortDirection,
isTaskListSortableAttribute,
} from '../../common/types/sort';
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';
import { Roles } from '../../common/types/role';
import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('tasks')
@Controller('tasks')
export class TasksController {
private readonly logger = new Logger(TasksController.name);
constructor(private readonly taskService: TasksService) {}
@ApiResponse({
status: HttpStatus.OK,
type: TasksResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '不正なパラメータ',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'getTasks',
description: '音声ファイル・文字起こしタスク情報をページ指定して取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@Get()
async getTasks(
@Req() req,
@Query() body: TasksRequest,
): Promise<TasksResponse> {
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
const { limit, offset, status } = body;
const paramName = isTaskListSortableAttribute(body.paramName ?? '')
? (body.paramName as TaskListSortableAttribute)
: undefined;
const direction = isSortDirection(body.direction ?? '')
? (body.direction as SortDirection)
: undefined;
const { tasks, total } = await this.taskService.getTasks(
context,
userId,
roles,
offset,
limit,
// statusが指定されていない場合は全てのステータスを取得する
status?.split(','),
paramName,
direction,
);
return { tasks, total, limit, offset };
}
@Get('next')
@ApiResponse({
status: HttpStatus.OK,
type: AudioNextResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '不正なパラメータ',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'getNextAudioFile',
description:
'指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します',
})
@ApiBearerAuth()
@UseGuards(
RoleGuard.requireds({
roles: [USER_ROLES.TYPIST],
}),
)
async getNextAudioFile(
@Req() req: Request,
@Query() param: AudioNextRequest,
): Promise<AudioNextResponse> {
const { endedFileId } = param;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
const nextFileId = await this.taskService.getNextTask(
context,
userId,
endedFileId,
);
return { nextFileId };
}
@Post(':audioFileId/checkout')
@ApiResponse({
status: HttpStatus.OK,
type: ChangeStatusResponse,
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: 'checkout',
description:
'指定した文字起こしタスクをチェックアウトしますステータスをInprogressにします',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST],
}),
)
async checkout(
@Req() req: Request,
@Param() param: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.checkout(context, param.audioFileId, roles, userId);
return {};
}
@Post(':audioFileId/checkin')
@ApiResponse({
status: HttpStatus.OK,
type: ChangeStatusResponse,
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: 'checkin',
description:
'指定した文字起こしタスクをチェックインしますステータスをFinishedにします',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [USER_ROLES.TYPIST],
}),
)
async checkin(
@Req() req: Request,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.checkin(context, audioFileId, userId);
return {};
}
@Post(':audioFileId/cancel')
@ApiResponse({
status: HttpStatus.OK,
type: ChangeStatusResponse,
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: 'cancel',
description:
'指定した文字起こしタスクをキャンセルしますステータスをUploadedにします',
})
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN, USER_ROLES.TYPIST],
}),
)
@ApiBearerAuth()
async cancel(
@Req() req: Request,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.cancel(context, audioFileId, userId, roles);
return {};
}
@Post(':audioFileId/suspend')
@ApiResponse({
status: HttpStatus.OK,
type: ChangeStatusResponse,
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: 'suspend',
description:
'指定した文字起こしタスクを一時中断しますステータスをPendingにします',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [USER_ROLES.TYPIST],
}),
)
async suspend(
@Req() req: Request,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.suspend(context, audioFileId, userId);
return {};
}
@Post(':audioFileId/backup')
@ApiResponse({
status: HttpStatus.OK,
type: ChangeStatusResponse,
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: 'backup',
description:
'指定した文字起こしタスクをバックアップしますステータスをBackupにします',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN],
}),
)
async backup(
@Req() req: Request,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.backup(context, audioFileId, userId);
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: '指定した文字起こしタスクのチェックアウト候補を変更します。',
})
@ApiParam({
name: 'audioFileId',
required: true,
description: 'ODMS Cloud上の音声ファイルID',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN, USER_ROLES.AUTHOR] }),
)
async changeCheckoutPermission(
@Req() req: Request,
//TODO [Task2243] checkoutやcheckinと同じパスパラメータなので記述方法を統一したい
@Param(`audioFileId`, ParseIntPipe)
audioFileId: number,
@Body() body: PostCheckoutPermissionRequest,
): Promise<PostCheckoutPermissionResponse> {
const { assignees } = body;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.changeCheckoutPermission(
context,
audioFileId,
assignees,
userId,
roles,
);
return {};
}
@Post(':audioFileId/delete')
@ApiResponse({
status: HttpStatus.OK,
type: PostDeleteTaskResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '不正なパラメータ',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'deleteTask',
description: '指定した文字起こしタスクを削除します。',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN, USER_ROLES.AUTHOR] }),
)
async deleteTask(
@Req() req: Request,
@Param() params: PostDeleteTaskRequest,
): Promise<PostDeleteTaskResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const ip = retrieveIp(req);
if (!ip) {
throw new HttpException(
makeErrorResponse('E000401'),
HttpStatus.UNAUTHORIZED,
);
}
const requestId = retrieveRequestId(req);
if (!requestId) {
throw new HttpException(
makeErrorResponse('E000501'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.deleteTask(context, userId, audioFileId);
return {};
}
}