Merged PR 867: 音声ファイル名変更API実装

## 概要
[Task4052: 音声ファイル名変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4052)

- ファイル名変更APIとそのUTを実装しました。

## レビューポイント
- リポジトリ実装のチェック内容とその順序は適切でしょうか?
- テスト項目は適切でしょうか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - UT実行
    - ローカル実行

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
makabe.t 2024-04-15 06:52:07 +00:00
parent f209c7359e
commit e6d27d7810
11 changed files with 750 additions and 53 deletions

View File

@ -89,4 +89,6 @@ export const ErrorCodes = [
'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない)
'E019001', // パートナーアカウント取得不可エラー(階層構造が不正)
'E020001', // パートナーアカウント変更エラー(変更条件を満たしていない)
'E021001', // 音声ファイル名変更不可エラー(権限不足)
'E021002', // 音声ファイル名変更不可エラー(同名ファイルが存在)
] as const;

View File

@ -79,4 +79,6 @@ export const errors: Errors = {
E018001: 'Partner account delete failed Error: not satisfied conditions',
E019001: 'Partner account get failed Error: hierarchy mismatch',
E020001: 'Partner account change failed Error: not satisfied conditions',
E021001: 'Audio file name change failed Error: insufficient permissions',
E021002: 'Audio file name change failed Error: same file name exists',
};

View File

@ -88,20 +88,22 @@ describe('valdation FileRenameRequest', () => {
const errors = await validate(valdationObject);
expect(errors.length).toBe(1);
});
it('音声ファイル名が50文字の場合、リクエストに成功する', async () => {
it('音声ファイル名が64文字の場合、リクエストに成功する', async () => {
const request = new FileRenameRequest();
request.audioFileId = 1;
request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5';
request.fileName =
'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5ABCDEFGHI6ABCD';
const valdationObject = plainToClass(FileRenameRequest, request);
const errors = await validate(valdationObject);
expect(errors.length).toBe(0);
});
it('音声ファイル名が51文字の場合、リクエストが失敗する', async () => {
it('音声ファイル名が65文字の場合、リクエストが失敗する', async () => {
const request = new FileRenameRequest();
request.audioFileId = 1;
request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5A';
request.fileName =
'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5ABCDEFGHI6ABCDE';
const valdationObject = plainToClass(FileRenameRequest, request);

View File

@ -602,7 +602,7 @@ export class FilesController {
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
// TODO: ファイル名変更処理を実装する
await this.filesService.fileRename(context, userId, audioFileId, fileName);
return {};
}
}

View File

@ -28,6 +28,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r
SendGridModule,
AdB2cModule,
AccountsRepositoryModule,
AudioFilesRepositoryModule,
],
providers: [FilesService],
controllers: [FilesController],

View File

@ -35,7 +35,11 @@ import {
import { createWorktype } from '../accounts/test/utility';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import { getCheckoutPermissions, getTask } from '../tasks/test/utility';
import {
createCheckoutPermissions,
getAudioFile,
getCheckoutPermissions,
} from '../tasks/test/utility';
import { DateWithZeroTime } from '../licenses/types/types';
import {
LICENSE_ALLOCATED_STATUS,
@ -601,7 +605,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
// 音声ファイルの録音者のユーザー
const { author_id: authorAuthorId } = await makeTestUser(source, {
await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
@ -724,16 +728,13 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
// 音声ファイルの録音者のユーザー
const {
external_id: authorExternalId,
id: authorUserId,
author_id: authorAuthorId,
} = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const { external_id: authorExternalId, author_id: authorAuthorId } =
await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const blobParam = makeBlobstorageServiceMockValue();
const notificationParam = makeDefaultNotificationhubServiceMockValue();
@ -1126,16 +1127,13 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
it('日付フォーマットが不正な場合、エラーを返却する', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: authorExternalId,
id: authorUserId,
author_id: authorAuthorId,
} = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const { external_id: authorExternalId, author_id: authorAuthorId } =
await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const blobParam = makeBlobstorageServiceMockValue();
const notificationParam = makeDefaultNotificationhubServiceMockValue();
@ -1173,16 +1171,13 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
it('オプションアイテムが10個ない場合、エラーを返却する', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: authorExternalId,
id: authorUserId,
author_id: authorAuthorId,
} = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const { external_id: authorExternalId, author_id: authorAuthorId } =
await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const blobParam = makeBlobstorageServiceMockValue();
const notificationParam = makeDefaultNotificationhubServiceMockValue();
@ -2199,3 +2194,408 @@ const optionItemList = [
optionItemValue: 'value_10',
},
];
describe('fileRename', () => {
let source: DataSource | null = null;
beforeAll(async () => {
if (source == null) {
source = await (async () => {
const s = new DataSource({
type: 'mysql',
host: 'test_mysql_db',
port: 3306,
username: 'user',
password: 'password',
database: 'odms',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
logger: new TestLogger('none'),
logging: true,
});
return await s.initialize();
})();
}
});
beforeEach(async () => {
if (source) {
await truncateAllTable(source);
}
});
afterAll(async () => {
await source?.destroy();
source = null;
});
it('ファイル名を変更できる(管理者)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const context = makeContext(admin.external_id, 'requestId');
const oldFileName = 'old.zip';
const task = await createTask(
source,
account.id,
'https://blob.url/account-1',
oldFileName,
TASK_STATUS.UPLOADED,
);
// 事前にDBを確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(oldFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
const newFileName = 'new.zip';
await service.fileRename(
context,
admin.external_id,
task.audioFileId,
newFileName,
);
//実行結果を確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(newFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
});
it('ファイル名を変更できるAuthor', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const { external_id: authorExternalId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'author-user-external-id',
role: USER_ROLES.AUTHOR,
author_id: 'AUTHOR_ID',
});
const context = makeContext(authorExternalId, 'requestId');
const oldFileName = 'old.zip';
const task = await createTask(
source,
account.id,
'https://blob.url/account-1',
oldFileName,
TASK_STATUS.UPLOADED,
undefined,
'AUTHOR_ID',
);
// 事前にDBを確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(oldFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
const newFileName = 'new.zip';
await service.fileRename(
context,
authorExternalId,
task.audioFileId,
newFileName,
);
//実行結果を確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(newFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
});
it('ファイル名を変更できるTypist', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const { external_id: typistExternalId, id: typistId } = await makeTestUser(
source,
{
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
},
);
const context = makeContext(typistExternalId, 'requestId');
const oldFileName = 'old.zip';
const task = await createTask(
source,
account.id,
'https://blob.url/account-1',
oldFileName,
TASK_STATUS.UPLOADED,
);
await createCheckoutPermissions(source, task.taskId, typistId);
// 事前にDBを確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(oldFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
const newFileName = 'new.zip';
await service.fileRename(
context,
typistExternalId,
task.audioFileId,
newFileName,
);
//実行結果を確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(newFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
});
it('ユーザーが管理者でなくRoleがNoneの場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const { external_id: noneExternalId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'none-user-external-id',
role: USER_ROLES.NONE,
});
const context = makeContext(noneExternalId, 'requestId');
const oldFileName = 'old.zip';
const task = await createTask(
source,
account.id,
'https://blob.url/account-1',
oldFileName,
TASK_STATUS.UPLOADED,
);
// 事前にDBを確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(oldFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
const newFileName = 'new.zip';
try {
await service.fileRename(
context,
noneExternalId,
task.audioFileId,
newFileName,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E021001'));
} else {
fail();
}
}
});
it('Authorがファイル名変更をするときユーザーのAuthorIDとタスクのAuthorIDが異なる場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const { external_id: authorExternalId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'author-user-external-id',
role: USER_ROLES.AUTHOR,
author_id: 'AUTHOR_ID',
});
const context = makeContext(authorExternalId, 'requestId');
const oldFileName = 'old.zip';
const task = await createTask(
source,
account.id,
'https://blob.url/account-1',
oldFileName,
TASK_STATUS.UPLOADED,
undefined,
'AUTHOR_ID_XXX',
);
// 事前にDBを確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(oldFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
const newFileName = 'new.zip';
try {
await service.fileRename(
context,
authorExternalId,
task.audioFileId,
newFileName,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E021001'));
} else {
fail();
}
}
});
it('Typistがファイル名変更をするときユーザーがタスクのチェックアウト候補でない場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account } = await makeTestAccount(source, { tier: 5 });
const { external_id: typistExternalId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'typist-user-external-id',
role: USER_ROLES.TYPIST,
});
const context = makeContext(typistExternalId, 'requestId');
const oldFileName = 'old.zip';
const task = await createTask(
source,
account.id,
'https://blob.url/account-1',
oldFileName,
TASK_STATUS.UPLOADED,
);
// 事前にDBを確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(oldFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
}
const newFileName = 'new.zip';
try {
await service.fileRename(
context,
typistExternalId,
task.audioFileId,
newFileName,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E021001'));
} else {
fail();
}
}
});
it('変更するファイル名がすでに存在する場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const context = makeContext(admin.external_id, 'requestId');
const oldFileName = 'old.zip';
const task = await createTask(
source,
account.id,
'https://blob.url/account-1',
oldFileName,
TASK_STATUS.UPLOADED,
undefined,
undefined,
undefined,
undefined,
'00000001',
);
const alreadyExistFileName = 'already.zip';
const alreadyExistTask = await createTask(
source,
account.id,
'https://blob.url/account-1',
alreadyExistFileName,
TASK_STATUS.UPLOADED,
undefined,
undefined,
undefined,
undefined,
'00000002',
);
// 事前にDBを確認
{
const audioFile = await getAudioFile(source, task.audioFileId);
expect(audioFile?.file_name).toBe(oldFileName);
expect(audioFile?.raw_file_name).toBe(oldFileName);
const alreadyExistAudioFile = await getAudioFile(
source,
alreadyExistTask.audioFileId,
);
expect(alreadyExistAudioFile?.file_name).toBe(alreadyExistFileName);
}
try {
await service.fileRename(
context,
admin.external_id,
task.audioFileId,
alreadyExistFileName,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E021002'));
} else {
fail();
}
}
});
});

View File

@ -43,6 +43,12 @@ import { AccountsRepositoryService } from '../../repositories/accounts/accounts.
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { AudioFilesRepositoryService } from '../../repositories/audio_files/audio_files.repository.service';
import {
CheckoutPermissionNotFoundError,
FileNameAlreadyExistsError,
RoleNotMatchError,
} from '../../repositories/audio_files/errors/types';
@Injectable()
export class FilesService {
@ -59,6 +65,7 @@ export class FilesService {
private readonly notificationhubService: NotificationhubService,
private readonly licensesRepository: LicensesRepositoryService,
private readonly sendGridService: SendGridService,
private readonly audioFilesRepositoryService: AudioFilesRepositoryService,
) {}
/**
@ -911,4 +918,73 @@ export class FilesService {
);
}
}
/**
*
* @param context
* @param externalId
* @param audioFileId
* @param fileName
* @returns rename
*/
async fileRename(
context: Context,
externalId: string,
audioFileId: number,
fileName: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.fileRename.name
} | params: { externalId: ${externalId}, audioFileId: ${audioFileId}, fileName: ${fileName} };`,
);
try {
// ユーザー取得
const { account_id: accountId, id: userId } =
await this.usersRepository.findUserByExternalId(context, externalId);
// 音声ファイルの表示ファイル名を変更
await this.audioFilesRepositoryService.renameAudioFile(
context,
accountId,
userId,
audioFileId,
fileName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
case RoleNotMatchError:
case AuthorUserNotMatchError:
case CheckoutPermissionNotFoundError:
throw new HttpException(
makeErrorResponse('E021001'),
HttpStatus.BAD_REQUEST,
);
case FileNameAlreadyExistsError:
throw new HttpException(
makeErrorResponse('E021002'),
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.fileRename.name}`,
);
}
}
}

View File

@ -56,7 +56,7 @@ export const createTask = async (
owner_user_id?: number | undefined,
fileSize?: number | undefined,
jobNumber?: string | undefined,
): Promise<{ audioFileId: number }> => {
): Promise<{ audioFileId: number; taskId: number }> => {
const { identifiers: audioFileIdentifiers } = await datasource
.getRepository(AudioFile)
.insert({
@ -64,6 +64,7 @@ export const createTask = async (
owner_user_id: owner_user_id ?? 1,
url: url,
file_name: fileName,
raw_file_name: fileName,
author_id: author_id ?? 'DEFAULT_ID',
work_type_id: 'work_type_id',
started_at: new Date(),
@ -89,20 +90,23 @@ export const createTask = async (
});
const templateFile = templateFileIdentifiers.pop() as TemplateFile;
await datasource.getRepository(Task).insert({
job_number: jobNumber ?? '00000001',
account_id: account_id,
is_job_number_enabled: true,
audio_file_id: audioFile.id,
template_file_id: templateFile.id,
typist_user_id: typist_user_id,
status: status,
priority: '01',
started_at: new Date().toISOString(),
created_at: new Date(),
});
const { identifiers: TaskIdentifiers } = await datasource
.getRepository(Task)
.insert({
job_number: jobNumber ?? '00000001',
account_id: account_id,
is_job_number_enabled: true,
audio_file_id: audioFile.id,
template_file_id: templateFile.id,
typist_user_id: typist_user_id,
status: status,
priority: '01',
started_at: new Date().toISOString(),
created_at: new Date(),
});
const task = TaskIdentifiers.pop() as Task;
return { audioFileId: audioFile.id };
return { audioFileId: audioFile.id, taskId: task.id };
};
export const getTaskFromJobNumber = async (

View File

@ -151,7 +151,7 @@ export class FileRenameRequest {
audioFileId: number;
@ApiProperty({ description: '変更するファイル名' })
@IsString()
@MaxLength(50)
@MaxLength(64)
@MinLength(1)
fileName: string;
}

View File

@ -1,7 +1,182 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { DataSource, In, Not } from 'typeorm';
import { AudioFile } from './entity/audio_file.entity';
import { Task } from '../tasks/entity/task.entity';
import { User } from '../users/entity/user.entity';
import { USER_ROLES } from '../../constants';
import { UserGroupMember } from '../user_groups/entity/user_group_member.entity';
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
import { Context } from '../../common/log';
import { updateEntity } from '../../common/repository';
import {
CheckoutPermissionNotFoundError,
FileNameAlreadyExistsError,
RoleNotMatchError,
TasksNotFoundError,
} from './errors/types';
import { AuthorUserNotMatchError } from '../../features/files/errors/types';
@Injectable()
export class AudioFilesRepositoryService {
//クエリログにコメントを出力するかどうか
private readonly isCommentOut = process.env.STAGE !== 'local';
constructor(private dataSource: DataSource) {}
/**
*
* @param context
* @param accountId
* @param userId
* @param audioFileId
* @param fileName
* @returns audio file
*/
async renameAudioFile(
context: Context,
accountId: number,
userId: number,
audioFileId: number,
fileName: string,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
// 実行ユーザーの情報を取得
const userRepo = entityManager.getRepository(User);
const user = await userRepo.findOne({
where: { account_id: accountId, id: userId },
relations: { account: true },
lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
if (!user) {
throw new Error(
`user not found. account_id: ${accountId}, user_id: ${userId}`,
);
}
const account = user.account;
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
if (!account) {
throw new Error(`account not found. account_id: ${accountId}`);
}
// ユーザーがアカウントの管理者であるかどうか
const isAdmin =
account.primary_admin_user_id === userId ||
account.secondary_admin_user_id === userId;
// ユーザーがTypistである場合は、ユーザーの所属するグループを取得しておく
let groupIds: number[] = [];
if (user.role === USER_ROLES.TYPIST) {
const groupMemberRepo = entityManager.getRepository(UserGroupMember);
// ユーザーの所属するすべてのグループを列挙
const groups = await groupMemberRepo.find({
relations: { user: true },
where: { user_id: userId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// ユーザーの所属するすべてのグループIDを列挙
groupIds = groups.map((member) => member.user_group_id);
}
// リクエストの音声ファイル名と同じファイル名の音声ファイルの一覧を取得
const audioFileRepo = entityManager.getRepository(AudioFile);
const audioFiles = await audioFileRepo.find({
where: {
account_id: accountId,
id: Not(audioFileId),
file_name: fileName,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
// ファイル名が重複している場合はエラー
if (audioFiles.length !== 0) {
throw new FileNameAlreadyExistsError(
`The file name already exists. accountId: ${accountId} file_name: ${fileName}`,
);
}
// タスク情報を取得
const taskRepo = entityManager.getRepository(Task);
const task = await taskRepo.findOne({
where: { account_id: accountId, audio_file_id: audioFileId },
relations: { file: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (!task) {
throw new TasksNotFoundError(
`task not found. account_id: ${accountId} audio_file_id: ${audioFileId}`,
);
}
// 音声ファイル情報を取得
const audioFile = task.file;
if (!audioFile) {
throw new TasksNotFoundError(
`audioFile not found. audio_file_id: ${audioFileId}`,
);
}
if (isAdmin) {
// 管理者の場合は、ファイル名を変更できる
await updateEntity(
audioFileRepo,
{ id: audioFileId },
{ file_name: fileName },
this.isCommentOut,
context,
);
return;
}
// ユーザーが管理者でない場合は、ロールに応じた権限を確認
if (user.role === USER_ROLES.NONE) {
// NONEの場合はエラー
throw new RoleNotMatchError(
`The user does not have the required role. userId: ${userId}. role: ${user.role}`,
);
}
if (user.role === USER_ROLES.AUTHOR) {
// ユーザーがAuthorである場合は、音声ファイルのAuthorIDが一致するか確認
if (audioFile.author_id !== user.author_id) {
throw new AuthorUserNotMatchError(
`The user is not the author of the audio file. audioFileId: ${audioFileId}, userId: ${userId}`,
);
}
}
if (user.role === USER_ROLES.TYPIST) {
// ユーザーがTypistである場合は、チェックアウト権限を確認
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
// ユーザーに対するチェックアウト権限、またはユーザーの所属するユーザーグループのチェックアウト権限を取得
const checkoutPermissions = await checkoutRepo.find({
where: [
{ task_id: task.id, user_id: userId }, // ユーザーがチェックアウト可能である
{ task_id: task.id, user_group_id: In(groupIds) }, // ユーザーの所属するユーザーグループがチェックアウト可能である
],
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// チェックアウト権限がない場合はエラー
if (checkoutPermissions.length === 0) {
throw new CheckoutPermissionNotFoundError(
`The user does not have checkout permission. taskId: ${task.id}, userId: ${userId}`,
);
}
}
// ファイル名を変更
await updateEntity(
audioFileRepo,
{ id: audioFileId },
{ file_name: fileName },
this.isCommentOut,
context,
);
});
}
}

View File

@ -0,0 +1,35 @@
// タスク未発見エラー
export class TasksNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'TasksNotFoundError';
}
}
// ファイル名変更権限ロール不一致エラー
export class RoleNotMatchError extends Error {
constructor(message: string) {
super(message);
this.name = 'RoleNotMatchError';
}
}
// タスクAuthorID不一致エラー
export class TaskAuthorIdNotMatchError extends Error {
constructor(message: string) {
super(message);
this.name = 'TaskAuthorIdNotMatchError';
}
}
// チェックアウト権限未発見エラー
export class CheckoutPermissionNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'CheckoutPermissionNotFoundError';
}
}
// 同名ファイルエラー
export class FileNameAlreadyExistsError extends Error {
constructor(message: string) {
super(message);
this.name = 'StatusNotMatchError';
}
}