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の変更
- なし
## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2024-01-16 07:55:38 +00:00
parent 8793606070
commit d08c6c99af
11 changed files with 918 additions and 10 deletions

View File

@ -66,12 +66,10 @@ const AuthPage: React.FC = (): JSX.Element => {
clearToken();
return;
}
const loginResult = await instance.handleRedirectPromise();
// eslint-disable-next-line
console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
if (loginResult && loginResult.account) {
const { homeAccountId, idTokenClaims } = loginResult.account;
if (idTokenClaims && idTokenClaims.aud) {
@ -85,11 +83,11 @@ const AuthPage: React.FC = (): JSX.Element => {
localStorageKeyforIdToken,
})
);
// トークン取得と設定を行う
navigate("/login");
}
}
// ログインページに遷移し、トークン取得と設定を行う
// 何らかの原因で、loginResultがnullの場合でも、ログイン画面に遷移するログイン画面でトップページに戻る
navigate("/login");
} catch (e) {
// eslint-disable-next-line
console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE `tasks` ADD INDEX `idx_account_id_and_audio_file_id` (account_id,audio_file_id);
-- +migrate Down
ALTER TABLE `tasks` DROP INDEX `idx_account_id_and_audio_file_id`;

View File

@ -158,6 +158,12 @@ export const overrideBlobstorageService = <TService>(
accountId: number,
country: string,
) => Promise<void>;
deleteFile?: (
context: Context,
accountId: number,
country: string,
fileName: string,
) => Promise<void>;
containerExists?: (
context: Context,
accountId: number,
@ -189,6 +195,12 @@ export const overrideBlobstorageService = <TService>(
writable: true,
});
}
if (overrides.deleteFile) {
Object.defineProperty(obj, obj.deleteFile.name, {
value: overrides.deleteFile,
writable: true,
});
}
if (overrides.containerExists) {
Object.defineProperty(obj, obj.containerExists.name, {
value: overrides.containerExists,

View File

@ -828,8 +828,7 @@ export class TasksController {
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
// TODO: Task削除処理を実装する
console.log(audioFileId);
await this.taskService.deleteTask(context, userId, audioFileId);
return {};
}
}

View File

@ -8,6 +8,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
@Module({
imports: [
@ -18,6 +19,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r
AdB2cModule,
NotificationhubModule,
SendGridModule,
BlobstorageModule,
],
providers: [TasksService],
controllers: [TasksController],

View File

@ -14,6 +14,8 @@ import {
createCheckoutPermissions,
createTask,
createUserGroup,
getAudioFile,
getAudioOptionItems,
getCheckoutPermissions,
getTask,
makeTaskTestingModuleWithNotificaiton,
@ -37,6 +39,8 @@ import { createTemplateFile } from '../templates/test/utility';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import { Roles } from '../../common/types/role';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { overrideBlobstorageService } from '../../common/test/overrides';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
describe('TasksService', () => {
it('タスク一覧を取得できるadmin', async () => {
@ -3775,3 +3779,531 @@ describe('getNextTask', () => {
}
});
});
describe('deleteTask', () => {
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
database: ':memory:',
logging: false,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('管理者として、アカウント内のタスクを削除できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const authorId = 'AUTHOR_ID';
const { id: authorUserId } = await makeTestUser(source, {
account_id: account.id,
author_id: 'AUTHOR_ID',
external_id: 'author-user-external-id',
role: USER_ROLES.AUTHOR,
});
const { id: typistUserId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
});
const { taskId, audioFileId } = await createTask(
source,
account.id,
authorUserId,
authorId,
'',
'01',
'00000001',
TASK_STATUS.UPLOADED,
);
await createCheckoutPermissions(source, taskId, typistUserId);
// 作成したデータを確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task?.id).toBe(taskId);
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
expect(task?.audio_file_id).toBe(audioFileId);
expect(audioFile?.id).toBe(audioFileId);
expect(audioFile?.file_name).toBe('x.zip');
expect(audioFile?.author_id).toBe(authorId);
expect(checkoutPermissions.length).toBe(1);
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
expect(optionItems.length).toBe(10);
}
const service = module.get<TasksService>(TasksService);
const blobStorageService =
module.get<BlobstorageService>(BlobstorageService);
const context = makeContext(admin.external_id, 'requestId');
overrideBlobstorageService(service, {
deleteFile: jest.fn(),
});
await service.deleteTask(context, admin.external_id, audioFileId);
// 実行結果が正しいか確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task).toBe(null);
expect(audioFile).toBe(null);
expect(checkoutPermissions.length).toBe(0);
expect(optionItems.length).toBe(0);
// Blob削除メソッドが呼ばれているか確認
expect(blobStorageService.deleteFile).toBeCalledWith(
context,
account.id,
account.country,
'x.zip',
);
}
});
it('Authorとして、自身が追加したタスクを削除できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const authorId = 'AUTHOR_ID';
const { id: authorUserId, external_id: authorExternalId } =
await makeTestUser(source, {
account_id: account.id,
author_id: 'AUTHOR_ID',
external_id: 'author-user-external-id',
role: USER_ROLES.AUTHOR,
});
const { id: typistUserId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
});
const { taskId, audioFileId } = await createTask(
source,
account.id,
authorUserId,
authorId,
'',
'01',
'00000001',
TASK_STATUS.UPLOADED,
);
await createCheckoutPermissions(source, taskId, typistUserId);
// 作成したデータを確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task?.id).toBe(taskId);
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
expect(task?.audio_file_id).toBe(audioFileId);
expect(audioFile?.id).toBe(audioFileId);
expect(audioFile?.file_name).toBe('x.zip');
expect(audioFile?.author_id).toBe(authorId);
expect(checkoutPermissions.length).toBe(1);
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
expect(optionItems.length).toBe(10);
}
const service = module.get<TasksService>(TasksService);
const blobStorageService =
module.get<BlobstorageService>(BlobstorageService);
const context = makeContext(authorExternalId, 'requestId');
overrideBlobstorageService(service, {
deleteFile: jest.fn(),
});
await service.deleteTask(context, authorExternalId, audioFileId);
// 実行結果が正しいか確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task).toBe(null);
expect(audioFile).toBe(null);
expect(checkoutPermissions.length).toBe(0);
expect(optionItems.length).toBe(0);
// Blob削除メソッドが呼ばれているか確認
expect(blobStorageService.deleteFile).toBeCalledWith(
context,
account.id,
account.country,
'x.zip',
);
}
});
it('ステータスがInProgressのタスクを削除しようとした場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const authorId = 'AUTHOR_ID';
const { id: authorUserId, external_id: authorExternalId } =
await makeTestUser(source, {
account_id: account.id,
author_id: 'AUTHOR_ID',
external_id: 'author-user-external-id',
role: USER_ROLES.AUTHOR,
});
const { id: typistUserId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
});
const { taskId, audioFileId } = await createTask(
source,
account.id,
authorUserId,
authorId,
'',
'01',
'00000001',
TASK_STATUS.IN_PROGRESS,
);
await createCheckoutPermissions(source, taskId, typistUserId);
// 作成したデータを確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task?.id).toBe(taskId);
expect(task?.status).toBe(TASK_STATUS.IN_PROGRESS);
expect(task?.audio_file_id).toBe(audioFileId);
expect(audioFile?.id).toBe(audioFileId);
expect(audioFile?.file_name).toBe('x.zip');
expect(audioFile?.author_id).toBe(authorId);
expect(checkoutPermissions.length).toBe(1);
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
expect(optionItems.length).toBe(10);
}
const service = module.get<TasksService>(TasksService);
const context = makeContext(authorExternalId, 'requestId');
overrideBlobstorageService(service, {
// eslint-disable-next-line @typescript-eslint/no-empty-function
deleteFile: async () => {},
});
try {
await service.deleteTask(context, authorExternalId, audioFileId);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010601'));
} else {
fail();
}
}
});
it('Authorが自身が作成したタスク以外を削除しようとした場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const authorId1 = 'AUTHOR_ID1';
const authorId2 = 'AUTHOR_ID2';
const { id: authorUserId1 } = await makeTestUser(source, {
account_id: account.id,
author_id: authorId1,
external_id: 'author-user-external-id1',
role: USER_ROLES.AUTHOR,
});
const { external_id: authorExternalId2 } = await makeTestUser(source, {
account_id: account.id,
author_id: authorId2,
external_id: 'author-user-external-id2',
role: USER_ROLES.AUTHOR,
});
const { id: typistUserId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
});
const { taskId, audioFileId } = await createTask(
source,
account.id,
authorUserId1,
authorId1,
'',
'01',
'00000001',
TASK_STATUS.UPLOADED,
);
await createCheckoutPermissions(source, taskId, typistUserId);
// 作成したデータを確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task?.id).toBe(taskId);
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
expect(task?.audio_file_id).toBe(audioFileId);
expect(audioFile?.id).toBe(audioFileId);
expect(audioFile?.file_name).toBe('x.zip');
expect(audioFile?.author_id).toBe(authorId1);
expect(checkoutPermissions.length).toBe(1);
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
expect(optionItems.length).toBe(10);
}
const service = module.get<TasksService>(TasksService);
const context = makeContext(authorExternalId2, 'requestId');
overrideBlobstorageService(service, {
// eslint-disable-next-line @typescript-eslint/no-empty-function
deleteFile: async () => {},
});
try {
await service.deleteTask(context, authorExternalId2, audioFileId);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010602'));
} else {
fail();
}
}
});
it('削除対象タスクが存在しない場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<TasksService>(TasksService);
const context = makeContext(admin.external_id, 'requestId');
overrideBlobstorageService(service, {
// eslint-disable-next-line @typescript-eslint/no-empty-function
deleteFile: async () => {},
});
try {
await service.deleteTask(context, admin.external_id, 1);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010603'));
} else {
fail();
}
}
});
it('タスクのDB削除に失敗した場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const authorId = 'AUTHOR_ID';
const { id: authorUserId, external_id: authorExternalId } =
await makeTestUser(source, {
account_id: account.id,
author_id: 'AUTHOR_ID',
external_id: 'author-user-external-id',
role: USER_ROLES.AUTHOR,
});
const { id: typistUserId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
});
const { taskId, audioFileId } = await createTask(
source,
account.id,
authorUserId,
authorId,
'',
'01',
'00000001',
TASK_STATUS.UPLOADED,
);
await createCheckoutPermissions(source, taskId, typistUserId);
// 作成したデータを確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task?.id).toBe(taskId);
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
expect(task?.audio_file_id).toBe(audioFileId);
expect(audioFile?.id).toBe(audioFileId);
expect(audioFile?.file_name).toBe('x.zip');
expect(audioFile?.author_id).toBe(authorId);
expect(checkoutPermissions.length).toBe(1);
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
expect(optionItems.length).toBe(10);
}
const service = module.get<TasksService>(TasksService);
const context = makeContext(authorExternalId, 'requestId');
// DBアクセスに失敗するようにする
const tasksRepositoryService = module.get<TasksRepositoryService>(
TasksRepositoryService,
);
tasksRepositoryService.deleteTask = jest
.fn()
.mockRejectedValue('DB failed');
overrideBlobstorageService(service, {
// eslint-disable-next-line @typescript-eslint/no-empty-function
deleteFile: async () => {},
});
try {
await service.deleteTask(context, authorExternalId, audioFileId);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
it('blobストレージからの音声ファイル削除に失敗した場合でも、エラーとならないこと', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const authorId = 'AUTHOR_ID';
const { id: authorUserId } = await makeTestUser(source, {
account_id: account.id,
author_id: 'AUTHOR_ID',
external_id: 'author-user-external-id',
role: USER_ROLES.AUTHOR,
});
const { id: typistUserId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
});
const { taskId, audioFileId } = await createTask(
source,
account.id,
authorUserId,
authorId,
'',
'01',
'00000001',
TASK_STATUS.UPLOADED,
);
await createCheckoutPermissions(source, taskId, typistUserId);
// 作成したデータを確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task?.id).toBe(taskId);
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
expect(task?.audio_file_id).toBe(audioFileId);
expect(audioFile?.id).toBe(audioFileId);
expect(audioFile?.file_name).toBe('x.zip');
expect(audioFile?.author_id).toBe(authorId);
expect(checkoutPermissions.length).toBe(1);
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
expect(optionItems.length).toBe(10);
}
const service = module.get<TasksService>(TasksService);
const context = makeContext(admin.external_id, 'requestId');
overrideBlobstorageService(service, {
deleteFile: async () => {
throw new Error('blob failed');
},
});
await service.deleteTask(context, admin.external_id, audioFileId);
// 実行結果が正しいか確認
{
const task = await getTask(source, taskId);
const audioFile = await getAudioFile(source, audioFileId);
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
const optionItems = await getAudioOptionItems(source, taskId);
expect(task).toBe(null);
expect(audioFile).toBe(null);
expect(checkoutPermissions.length).toBe(0);
expect(optionItems.length).toBe(0);
}
});
});

View File

@ -1,6 +1,6 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { Assignee, Task } from './types/types';
import { Assignee, PostDeleteTaskRequest, Task } from './types/types';
import { Task as TaskEntity } from '../../repositories/tasks/entity/task.entity';
import { createTasks } from './types/convert';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
@ -9,7 +9,12 @@ import {
SortDirection,
TaskListSortableAttribute,
} from '../../common/types/sort';
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
import {
ADMIN_ROLES,
MANUAL_RECOVERY_REQUIRED,
TASK_STATUS,
USER_ROLES,
} from '../../constants';
import {
AdB2cService,
Adb2cTooManyRequestsError,
@ -36,6 +41,7 @@ import { User } from '../../repositories/users/entity/user.entity';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
@Injectable()
export class TasksService {
@ -48,6 +54,7 @@ export class TasksService {
private readonly adB2cService: AdB2cService,
private readonly sendgridService: SendGridService,
private readonly notificationhubService: NotificationhubService,
private readonly blobStorageService: BlobstorageService,
) {}
async getTasks(
@ -848,6 +855,107 @@ export class TasksService {
}
}
/**
*
* @param context
* @param externalId ID
* @param audioFileId audio_file_id
* @returns task
*/
async deleteTask(
context: Context,
externalId: string,
audioFileId: number,
): Promise<void> {
try {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteTask.name
} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
);
// 実行ユーザーの情報を取得する
const user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
const account = user.account;
if (!account) {
throw new Error(`account not found. externalId: ${externalId}`);
}
// 削除対象の音声ファイル情報を取得する
const task = await this.taskRepository.getTaskAndAudioFile(
context,
audioFileId,
user.account_id,
Object.values(TASK_STATUS),
);
const targetFileName = task.file?.file_name;
if (!targetFileName) {
throw new Error(`target file not found. audioFileId: ${audioFileId}`);
}
// DBからタスクと紐づくデータを削除する
await this.taskRepository.deleteTask(context, user.id, audioFileId);
// Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行するため、try-catchで囲む
try {
// BlobStorageから音声ファイルを削除する
await this.blobStorageService.deleteFile(
context,
account.id,
account.country,
targetFileName,
);
} catch (e) {
// Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
this.logger.log(`[${context.getTrackingId()}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete Blob: accountId: ${
account.id
}, fileName: ${targetFileName}`,
);
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case StatusNotMatchError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
case TaskAuthorIdNotMatchError:
throw new HttpException(
makeErrorResponse('E010602'),
HttpStatus.BAD_REQUEST,
);
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteTask.name}`,
);
}
}
// 通知を送信するプライベートメソッド
private async sendNotify(
context: Context,

View File

@ -17,6 +17,7 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service';
import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service';
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
export type TasksRepositoryMockValue = {
getTasksFromAccountId:
@ -92,6 +93,8 @@ export const makeTasksServiceMock = async (
// メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。
case SendGridService:
return {};
case BlobstorageService:
return {};
}
})
.compile();

View File

@ -38,6 +38,7 @@ import {
NotificationhubServiceMockValue,
makeNotificationhubServiceMock,
} from './tasks.service.mock';
import { AudioOptionItem } from '../../../repositories/audio_option_items/entity/audio_option_item.entity';
export const makeTaskTestingModuleWithNotificaiton = async (
datasource: DataSource,
@ -145,6 +146,60 @@ export const createTask = async (
created_at: new Date(),
});
const task = taskIdentifiers.pop() as Task;
await datasource.getRepository(AudioOptionItem).insert([
{
audio_file_id: audioFile.id,
label: 'label01',
value: 'value01',
},
{
audio_file_id: audioFile.id,
label: 'label02',
value: 'value02',
},
{
audio_file_id: audioFile.id,
label: 'label03',
value: 'value03',
},
{
audio_file_id: audioFile.id,
label: 'label04',
value: 'value04',
},
{
audio_file_id: audioFile.id,
label: 'label05',
value: 'value05',
},
{
audio_file_id: audioFile.id,
label: 'label06',
value: 'value06',
},
{
audio_file_id: audioFile.id,
label: 'label07',
value: 'value07',
},
{
audio_file_id: audioFile.id,
label: 'label08',
value: 'value08',
},
{
audio_file_id: audioFile.id,
label: 'label09',
value: 'value09',
},
{
audio_file_id: audioFile.id,
label: 'label10',
value: 'value10',
},
]);
return { taskId: task.id, audioFileId: audioFile.id };
};
/**
@ -229,3 +284,23 @@ export const getCheckoutPermissions = async (
});
return permissions;
};
export const getAudioFile = async (
datasource: DataSource,
audio_file_id: number,
): Promise<AudioFile | null> => {
const audioFile = await datasource.getRepository(AudioFile).findOne({
where: { id: audio_file_id },
});
return audioFile;
};
export const getAudioOptionItems = async (
datasource: DataSource,
audio_file_id: number,
): Promise<AudioOptionItem[]> => {
const audioOptionItems = await datasource
.getRepository(AudioOptionItem)
.find({ where: { audio_file_id: audio_file_id } });
return audioOptionItems;
};

View File

@ -142,6 +142,60 @@ export class BlobstorageService {
}
}
/**
*
* @param context
* @param accountId
* @param country
* @param fileName
* @returns file
*/
async deleteFile(
context: Context,
accountId: number,
country: string,
fileName: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.deleteFile.name} | params: { ` +
`accountId: ${accountId} ` +
`country: ${country} ` +
`fileName: ${fileName} };`,
);
try {
// 国に応じたリージョンでコンテナ名を指定してClientを取得
const containerClient = this.getContainerClient(
context,
accountId,
country,
);
// コンテナ内のBlobパス名を指定してClientを取得
const blobClient = containerClient.getBlobClient(fileName);
const { succeeded, errorCode, date } = await blobClient.deleteIfExists();
this.logger.log(
`[${context.getTrackingId()}] succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`,
);
// 失敗時、Blobが存在しない場合以外はエラーとして例外をスローする
// Blob不在の場合のエラーコードは「BlobNotFound」以下を参照
// https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes
if (!succeeded && errorCode !== 'BlobNotFound') {
throw new Error(
`delete blob failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`,
);
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw e;
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteFile.name}`,
);
}
}
/**
* Containers exists
* @param country

View File

@ -10,7 +10,12 @@ import {
Repository,
} from 'typeorm';
import { Task } from './entity/task.entity';
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
import {
ADMIN_ROLES,
NODE_ENV_TEST,
TASK_STATUS,
USER_ROLES,
} from '../../constants';
import { AudioOptionItem as ParamOptionItem } from '../../features/files/types/types';
import { AudioFile } from '../audio_files/entity/audio_file.entity';
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
@ -1280,6 +1285,121 @@ export class TasksRepositoryService {
);
});
}
/**
* Deletes task
* @param context
* @param userId ID
* @param audioFileId ID
* @returns task
*/
async deleteTask(
context: Context,
userId: number,
audioFileId: number,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const userRepo = entityManager.getRepository(User);
// 削除を行うユーザーとアカウントを取得
const user = await userRepo.findOne({
where: { id: userId },
relations: { account: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
if (!user) {
throw new Error(`user not found. userId:${userId}`);
}
const account = user.account;
if (!account) {
throw new Error(`account not found. userId:${userId}`);
}
// ユーザーがアカウントの管理者であるかを確認
const isAdmin =
account.primary_admin_user_id === userId ||
account.secondary_admin_user_id === userId;
// 削除を行うタスクを取得
const taskRepo = entityManager.getRepository(Task);
const task = await taskRepo.findOne({
where: {
account_id: account.id,
audio_file_id: audioFileId,
},
relations: { file: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
// テスト環境の場合はロックを行わない(sqliteがlockに対応していないため)
lock:
process.env.NODE_ENV !== NODE_ENV_TEST
? { mode: 'pessimistic_write' }
: undefined,
});
if (!task) {
throw new TasksNotFoundError(
`task not found. audio_file_id:${audioFileId}`,
);
}
if (!task.file) {
throw new Error(`audio file not found. audio_file_id:${audioFileId}`);
}
// ユーザーが管理者でない場合は、タスクのAuthorIdとユーザーのAuthorIdが一致するかを確認
if (!isAdmin) {
// ユーザーがAuthorである場合
if (user.role === USER_ROLES.AUTHOR) {
if (task.file.author_id !== user.author_id) {
throw new TaskAuthorIdNotMatchError(
`Task authorId not match. userId:${userId}, authorId:${user.author_id}`,
);
}
} else {
// ユーザーが管理者でもAuthorでもない場合はエラー
throw new Error(`The user is not admin or author. userId:${userId}`);
}
}
// タスクのステータスがInProgressの場合はエラー
if (task.status === TASK_STATUS.IN_PROGRESS) {
throw new StatusNotMatchError(
`task status is InProgress. audio_file_id:${audioFileId}`,
);
}
// タスクに紐づくオプションアイテムを削除
const optionItemRepo = entityManager.getRepository(AudioOptionItem);
await deleteEntity(
optionItemRepo,
{ audio_file_id: task.audio_file_id },
this.isCommentOut,
context,
);
// タスクに紐づくチェックアウト候補を削除
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
await deleteEntity(
checkoutRepo,
{ task_id: task.id },
this.isCommentOut,
context,
);
// タスクを削除
await deleteEntity(
taskRepo,
{ audio_file_id: audioFileId },
this.isCommentOut,
context,
);
// タスクに紐づく音声ファイル情報を削除
const audioFileRepo = entityManager.getRepository(AudioFile);
await deleteEntity(
audioFileRepo,
{ id: audioFileId },
this.isCommentOut,
context,
);
});
}
/**
* workflowに紐づけられているタイピスト