diff --git a/dictation_server/db/migrations/059-add_tasks_template_index.sql b/dictation_server/db/migrations/059-add_tasks_template_index.sql new file mode 100644 index 0000000..17a871c --- /dev/null +++ b/dictation_server/db/migrations/059-add_tasks_template_index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `tasks` ADD INDEX `idx_tasks_template_file_id` (template_file_id); + +-- +migrate Down +ALTER TABLE `tasks` DROP INDEX `idx_tasks_template_file_id`; \ No newline at end of file diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index b6ad2af..a789ab7 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -79,4 +79,7 @@ export const ErrorCodes = [ 'E015001', // タイピストグループ削除エラー(削除しようとしたタイピストグループがすでに削除済みだった) 'E015002', // タイピストグループ削除エラー(削除しようとしたタイピストグループがWorkflowのTypist候補として指定されていた) 'E015003', // タイピストグループ削除エラー(削除しようとしたタイピストグループがチェックアウト可能なタスクが存在した) + 'E016001', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) + 'E016002', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) + 'E016003', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 20a176f..793cdf5 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -68,4 +68,8 @@ export const errors: Errors = { E015001: 'Typist Group delete failed Error: already deleted', E015002: 'Typist Group delete failed Error: workflow assigned', E015003: 'Typist Group delete failed Error: checkout permission existed', + E016001: 'Template file delete failed Error: already deleted', + E016002: 'Template file delete failed Error: workflow assigned', + E016003: + 'Template file delete failed Error: not finished task has template file', }; diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 1d8efc9..d9e6d8f 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -273,6 +273,16 @@ export const getTask = async ( return task; }; +export const getTasks = async ( + datasource: DataSource, + account_id: number, +): Promise => { + const tasks = await datasource.getRepository(Task).find({ + where: { account_id: account_id }, + }); + return tasks; +}; + export const getCheckoutPermissions = async ( datasource: DataSource, task_id: number, diff --git a/dictation_server/src/features/templates/templates.controller.ts b/dictation_server/src/features/templates/templates.controller.ts index f4dce06..e98bcdf 100644 --- a/dictation_server/src/features/templates/templates.controller.ts +++ b/dictation_server/src/features/templates/templates.controller.ts @@ -18,7 +18,11 @@ import { import jwt from 'jsonwebtoken'; import { AccessToken } from '../../common/token'; import { ErrorResponse } from '../../common/error/types/types'; -import { DeleteTemplateRequestParam, DeleteTemplateResponse, GetTemplatesResponse } from './types/types'; +import { + DeleteTemplateRequestParam, + DeleteTemplateResponse, + GetTemplatesResponse, +} from './types/types'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES } from '../../constants'; @@ -132,7 +136,7 @@ export class TemplatesController { RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), ) @Post(':templateFileId/delete') - async deleteTypistGroup( + async deleteTemplateFile( @Req() req: Request, @Param() param: DeleteTemplateRequestParam, ): Promise { @@ -174,7 +178,7 @@ export class TemplatesController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: service層呼び出し + await this.templatesService.deleteTemplate(context, userId, templateFileId); return {}; } diff --git a/dictation_server/src/features/templates/templates.module.ts b/dictation_server/src/features/templates/templates.module.ts index ac14580..fb93de7 100644 --- a/dictation_server/src/features/templates/templates.module.ts +++ b/dictation_server/src/features/templates/templates.module.ts @@ -3,9 +3,14 @@ import { TemplatesController } from './templates.controller'; import { TemplatesService } from './templates.service'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { TemplateFilesRepositoryModule } from '../../repositories/template_files/template_files.repository.module'; +import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; @Module({ - imports: [UsersRepositoryModule, TemplateFilesRepositoryModule], + imports: [ + UsersRepositoryModule, + TemplateFilesRepositoryModule, + BlobstorageModule, + ], providers: [TemplatesService], controllers: [TemplatesController], }) diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index 8ef101f..46dadf7 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -1,13 +1,26 @@ import { DataSource } from 'typeorm'; import { makeTestingModule } from '../../common/test/modules'; import { TemplatesService } from './templates.service'; -import { createTemplateFile } from './test/utility'; -import { makeTestAccount } from '../../common/test/utility'; +import { + createTemplateFile, + getTemplateFiles, + updateTaskTemplateFile, +} from './test/utility'; +import { makeTestAccount, makeTestUser } from '../../common/test/utility'; import { makeContext } from '../../common/log'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; +import { overrideBlobstorageService } from '../../common/test/overrides'; +import { TASK_STATUS, USER_ROLES } from '../../constants'; +import { createTask, getTasks } from '../tasks/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflow, +} from '../workflows/test/utility'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; describe('getTemplates', () => { let source: DataSource | null = null; @@ -129,3 +142,355 @@ describe('getTemplates', () => { } }); }); + +describe('deleteTemplate', () => { + 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が行われるため注意 + }); + 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(TemplatesService); + const blobStorageService = + module.get(BlobstorageService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + const template2 = await createTemplateFile( + source, + account.id, + 'test2', + 'https://url2/test2', + ); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000001', + TASK_STATUS.FINISHED, + ); + await updateTaskTemplateFile(source, taskId1, template1.id); + + const { taskId: taskId2 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000002', + TASK_STATUS.BACKUP, + ); + await updateTaskTemplateFile(source, taskId2, template1.id); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(2); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + expect(templates[1].id).toBe(template2.id); + expect(templates[1].file_name).toBe(template2.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(2); + expect(tasks[0].template_file_id).toBe(template1.id); + expect(tasks[1].template_file_id).toBe(template1.id); + } + + await service.deleteTemplate(context, admin.external_id, template1.id); + + //実行結果を確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template2.id); + expect(templates[0].file_name).toBe(template2.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(2); + expect(tasks[0].template_file_id).toBe(null); + expect(tasks[1].template_file_id).toBe(null); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'Templates/test1', + ); + } + }); + + it('指定したテンプレートファイルが存在しない場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, 9999); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016001')); + } else { + fail(); + } + } + }); + + it('指定したテンプレートファイルがルーティングルールに紐づく場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const { id: typistId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.TYPIST, + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + const { id: workflowId } = await createWorkflow( + source, + account.id, + authorId, + undefined, + template1.id, + ); + await createWorkflowTypist(source, workflowId, typistId); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + + const workflow = await getWorkflow(source, account.id, workflowId); + expect(workflow?.template_id).toBe(template1.id); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016002')); + } else { + fail(); + } + } + }); + + it('指定したテンプレートファイルが未完了のタスクに紐づく場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await updateTaskTemplateFile(source, taskId1, template1.id); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(1); + expect(tasks[0].template_file_id).toBe(template1.id); + expect(tasks[0].status).toBe(TASK_STATUS.UPLOADED); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + } + + //DBアクセスに失敗するようにする + const templateFilesRepositoryService = + module.get( + TemplateFilesRepositoryService, + ); + templateFilesRepositoryService.getTemplateFiles = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/templates/templates.service.ts b/dictation_server/src/features/templates/templates.service.ts index ae36afe..36b2549 100644 --- a/dictation_server/src/features/templates/templates.service.ts +++ b/dictation_server/src/features/templates/templates.service.ts @@ -4,6 +4,13 @@ import { TemplateFile } from './types/types'; import { Context } from '../../common/log'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; +import { + NotFinishedTaskHasTemplateDeleteFailedError, + TemplateFileNotExistError, + WorkflowHasTemplateDeleteFailedError, +} from '../../repositories/template_files/errors/types'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; +import { MANUAL_RECOVERY_REQUIRED } from '../../constants'; @Injectable() export class TemplatesService { @@ -11,6 +18,7 @@ export class TemplatesService { constructor( private readonly usersRepository: UsersRepositoryService, private readonly templateFilesRepository: TemplateFilesRepositoryService, + private readonly blobStorageService: BlobstorageService, ) {} /** @@ -55,4 +63,103 @@ export class TemplatesService { ); } } + + /** + * アカウント内の指定されたテンプレートファイルを削除する + * @param context + * @param externalId + * @param templateFileId + * @returns template + */ + async deleteTemplate( + context: Context, + externalId: string, + templateFileId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTemplate.name + } | params: { externalId: ${externalId}, templateFileId: ${templateFileId} };`, + ); + + try { + const { account } = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + if (!account) { + throw new Error(`account not found. externalId: ${externalId}`); + } + + // テンプレートファイルの取得 + const templateFile = await this.templateFilesRepository.getTemplateFile( + context, + account.id, + templateFileId, + ); + + // DBからのテンプレートファイルの削除 + await this.templateFilesRepository.deleteTemplateFile( + context, + account.id, + templateFileId, + ); + + try { + // Blob Storageからのテンプレートファイルの削除 + await this.blobStorageService.deleteFile( + context, + account.id, + account.country, + `Templates/${templateFile.file_name}`, + ); + } 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: ${templateFile.file_name}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + // 指定されたIDのテンプレートファイルが存在しない + case TemplateFileNotExistError: + throw new HttpException( + makeErrorResponse('E016001'), + HttpStatus.BAD_REQUEST, + ); + // 指定されたIDのテンプレートファイルがルーティングルールに設定されている + case WorkflowHasTemplateDeleteFailedError: + throw new HttpException( + makeErrorResponse('E016002'), + HttpStatus.BAD_REQUEST, + ); + // 指定されたIDのテンプレートファイルが未完了タスクに紐づいている + case NotFinishedTaskHasTemplateDeleteFailedError: + throw new HttpException( + makeErrorResponse('E016003'), + 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.deleteTemplate.name}`, + ); + } + } } diff --git a/dictation_server/src/features/templates/test/utility.ts b/dictation_server/src/features/templates/test/utility.ts index 224b536..1358df1 100644 --- a/dictation_server/src/features/templates/test/utility.ts +++ b/dictation_server/src/features/templates/test/utility.ts @@ -1,5 +1,6 @@ import { DataSource } from 'typeorm'; import { TemplateFile } from '../../../repositories/template_files/entity/template_file.entity'; +import { Task } from '../../../repositories/tasks/entity/task.entity'; export const createTemplateFile = async ( datasource: DataSource, @@ -41,3 +42,18 @@ export const getTemplateFiles = async ( }); return templates; }; + +export const updateTaskTemplateFile = async ( + datasource: DataSource, + taskId: number, + templateFileId: number, +): Promise => { + await datasource.getRepository(Task).update( + { id: taskId }, + { + template_file_id: templateFileId, + updated_by: 'updater', + updated_at: new Date(), + }, + ); +}; diff --git a/dictation_server/src/repositories/template_files/errors/types.ts b/dictation_server/src/repositories/template_files/errors/types.ts index d1db4d7..9cff27f 100644 --- a/dictation_server/src/repositories/template_files/errors/types.ts +++ b/dictation_server/src/repositories/template_files/errors/types.ts @@ -5,3 +5,17 @@ export class TemplateFileNotExistError extends Error { this.name = 'TemplateFileNotExistError'; } } + +export class WorkflowHasTemplateDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorkflowHasTemplateDeleteFailedError'; + } +} + +export class NotFinishedTaskHasTemplateDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'NotFinishedTaskHasTemplateDeleteFailedError'; + } +} diff --git a/dictation_server/src/repositories/template_files/template_files.repository.service.ts b/dictation_server/src/repositories/template_files/template_files.repository.service.ts index 5fc805b..c1ec96c 100644 --- a/dictation_server/src/repositories/template_files/template_files.repository.service.ts +++ b/dictation_server/src/repositories/template_files/template_files.repository.service.ts @@ -1,8 +1,20 @@ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, In } from 'typeorm'; import { TemplateFile } from './entity/template_file.entity'; -import { insertEntity, updateEntity } from '../../common/repository'; +import { + deleteEntity, + insertEntity, + updateEntity, +} from '../../common/repository'; import { Context } from '../../common/log'; +import { + NotFinishedTaskHasTemplateDeleteFailedError, + TemplateFileNotExistError, + WorkflowHasTemplateDeleteFailedError, +} from './errors/types'; +import { Workflow } from '../workflows/entity/workflow.entity'; +import { Task } from '../tasks/entity/task.entity'; +import { TASK_STATUS } from '../../constants'; @Injectable() export class TemplateFilesRepositoryService { @@ -32,6 +44,36 @@ export class TemplateFilesRepositoryService { }); } + /** + * アカウント内のIDで指定されたテンプレートファイルを取得する + * @param context + * @param accountId + * @param templateFileId + * @returns template file + */ + async getTemplateFile( + context: Context, + accountId: number, + templateFileId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const templateFilesRepo = entityManager.getRepository(TemplateFile); + + const template = await templateFilesRepo.findOne({ + where: { account_id: accountId, id: templateFileId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (!template) { + throw new TemplateFileNotExistError( + `template file not found. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + return template; + }); + } + /** * アカウント内にテンプレートファイルを追加(すでに同名ファイルがあれば更新)する * @param accountId @@ -79,4 +121,92 @@ export class TemplateFilesRepositoryService { } }); } + + /** + * アカウント内にある指定されたテンプレートファイルを削除する + * @param accountId + * @param fileName + * @param url + * @returns template file + */ + async deleteTemplateFile( + context: Context, + accountId: number, + templateFileId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const workflowRepo = entityManager.getRepository(Workflow); + // テンプレートファイルがワークフローで使用されているか確認 + const workflow = await workflowRepo.findOne({ + where: { + account_id: accountId, + template_id: templateFileId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ワークフローで使用されている場合はエラー + if (workflow) { + throw new WorkflowHasTemplateDeleteFailedError( + `workflow has template file. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + const templateFilesRepo = entityManager.getRepository(TemplateFile); + // アカウント内に指定IDファイルがあるか確認 + const template = await templateFilesRepo.findOne({ + where: { account_id: accountId, id: templateFileId }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ファイルが存在しない場合はエラー + if (!template) { + throw new TemplateFileNotExistError( + `template file not found. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + const taskRepo = entityManager.getRepository(Task); + // テンプレートファイルが未完了タスクで使用されているか確認 + const templateUsedTasks = await taskRepo.findOne({ + where: { + account_id: accountId, + template_file_id: templateFileId, + status: In([ + TASK_STATUS.UPLOADED, + TASK_STATUS.IN_PROGRESS, + TASK_STATUS.PENDING, + ]), + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 未完了のタスクでテンプレートファイルが使用されている場合はエラー + if (templateUsedTasks) { + throw new NotFinishedTaskHasTemplateDeleteFailedError( + `not finished task has template file. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + // テンプレートファイルの削除 + await deleteEntity( + templateFilesRepo, + { id: templateFileId }, + this.isCommentOut, + context, + ); + + // 完了済みのタスクからテンプレートファイルの紐づけを解除 + await updateEntity( + taskRepo, + { template_file_id: templateFileId }, + { template_file_id: null }, + this.isCommentOut, + context, + ); + }); + } }