diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 017192b..a7b3f86 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -39,6 +39,7 @@ import { LicensesController } from './features/licenses/licenses.controller'; import { CheckoutPermissionsRepositoryModule } from './repositories/checkout_permissions/checkout_permissions.repository.module'; import { UserGroupsRepositoryModule } from './repositories/user_groups/user_groups.repository.module'; import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module'; +import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module'; @Module({ imports: [ @@ -66,6 +67,7 @@ import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_ TasksRepositoryModule, CheckoutPermissionsRepositoryModule, UserGroupsRepositoryModule, + TemplateFilesRepositoryModule, TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ diff --git a/dictation_server/src/features/files/errors/types.ts b/dictation_server/src/features/files/errors/types.ts index 6b20c5b..735e11c 100644 --- a/dictation_server/src/features/files/errors/types.ts +++ b/dictation_server/src/features/files/errors/types.ts @@ -1,2 +1,12 @@ // 音声ファイル不在エラー export class AudioFileNotFoundError extends Error {} +// テンプレートファイル不在エラー +export class TemplateFileNotFoundError extends Error {} +// Account不一致エラー +export class AccountNotMatchError extends Error {} +// Status不一致エラー +export class StatusNotMatchError extends Error {} +// Author不一致エラー +export class AuthorUserNotMatchError extends Error {} +// TypistUser不一致エラー +export class TypistUserNotMatchError extends Error {} diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index a9a402a..5aeb394 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, - Headers, HttpStatus, Post, Query, @@ -225,14 +224,23 @@ export class FilesController { '指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します', }) @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }), + ) async downloadTemplateLocation( - @Headers() headers, + @Req() req: Request, @Query() body: TemplateDownloadLocationRequest, ): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { audioFileId } = body; - // コンテナ作成処理の前にアクセストークンの認証を行う - return { url: '' }; + const token = retrieveAuthorizationToken(req); + const accessToken = jwt.decode(token, { json: true }) as AccessToken; + const url = await this.filesService.publishTemplateFileDownloadSas( + accessToken.userId, + audioFileId, + ); + + return { url }; } } diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 4c71073..7b677bf 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -301,7 +301,7 @@ describe('音声ファイルダウンロードURL取得', () => { it('ダウンロードSASトークンが乗っているURLを取得できる', async () => { const { accountId } = await createAccount(source); - const { externalId, userId } = await createUser( + const { externalId, userId, authorId } = await createUser( source, accountId, 'author-user-external-id', @@ -315,6 +315,9 @@ describe('音声ファイルダウンロードURL取得', () => { accountId, url, 'test.zip', + 'InProgress', + undefined, + authorId, ); const blobParam = makeBlobstorageServiceMockValue(); @@ -329,14 +332,13 @@ describe('音声ファイルダウンロードURL取得', () => { ).toEqual(`${url}?sas-token`); }); - it('Typistの場合、タスクのステータスが[Uploaded,Inprogress,Pending]以外でエラー', async () => { + it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { const { accountId } = await createAccount(source); const { externalId, userId } = await createUser( source, accountId, 'typist-user-external-id', 'typist', - undefined, ); const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`; @@ -345,6 +347,8 @@ describe('音声ファイルダウンロードURL取得', () => { accountId, url, 'test.zip', + 'Finished', + userId, ); const blobParam = makeBlobstorageServiceMockValue(); @@ -354,9 +358,85 @@ describe('音声ファイルダウンロードURL取得', () => { const module = await makeTestingModuleWithBlob(source, blobParam); const service = module.get(FilesService); - expect( - await service.publishAudioFileDownloadSas(externalId, audioFileId), - ).toEqual(`${url}?sas-token`); + await expect( + service.publishAudioFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST), + ); + }); + + it('Typistの場合、自身が担当するタスクでない場合エラー', async () => { + const { accountId } = await createAccount(source); + const { externalId, userId } = await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + ); + const { userId: otherId } = await createUser( + source, + accountId, + 'other-typist-user-external-id', + 'typist', + ); + const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`; + + const { audioFileId } = await createTask( + source, + accountId, + url, + 'test.zip', + 'InProgress', + otherId, + ); + + const blobParam = makeBlobstorageServiceMockValue(); + blobParam.publishDownloadSas = `${url}?sas-token`; + blobParam.fileExists = true; + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + await expect( + service.publishTemplateFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST), + ); + }); + + it('Authorの場合、自身が登録したタスクでない場合エラー', async () => { + const { accountId } = await createAccount(source); + const { externalId, userId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'AUTHOR_ID', + ); + const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`; + + const { audioFileId } = await createTask( + source, + accountId, + url, + 'test.zip', + 'InProgress', + undefined, + 'OTHOR_ID', + ); + + const blobParam = makeBlobstorageServiceMockValue(); + blobParam.publishDownloadSas = `${url}?sas-token`; + blobParam.fileExists = true; + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + await expect( + service.publishAudioFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST), + ); }); it('Taskが存在しない場合はエラーとなる', async () => { @@ -383,7 +463,7 @@ describe('音声ファイルダウンロードURL取得', () => { it('blobストレージにファイルが存在しない場合はエラーとなる', async () => { const { accountId } = await createAccount(source); - const { externalId, userId } = await createUser( + const { externalId, userId, authorId } = await createUser( source, accountId, 'author-user-external-id', @@ -397,6 +477,9 @@ describe('音声ファイルダウンロードURL取得', () => { accountId, url, 'test.zip', + 'InProgress', + undefined, + authorId, ); const blobParam = makeBlobstorageServiceMockValue(); @@ -414,6 +497,225 @@ describe('音声ファイルダウンロードURL取得', () => { }); }); +describe('テンプレートファイルダウンロードURL取得', () => { + let source: DataSource = 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 () => { + await source.destroy(); + source = null; + }); + + it('ダウンロードSASトークンが乗っているURLを取得できる', async () => { + const { accountId } = await createAccount(source); + const { externalId, authorId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'AUTHOR_ID', + ); + const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; + + const { audioFileId } = await createTask( + source, + accountId, + url, + 'test.zip', + 'InProgress', + undefined, + authorId, + ); + + const blobParam = makeBlobstorageServiceMockValue(); + blobParam.publishDownloadSas = `${url}?sas-token`; + blobParam.fileExists = true; + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + expect( + await service.publishTemplateFileDownloadSas(externalId, audioFileId), + ).toEqual(`${url}?sas-token`); + }); + + it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { + const { accountId } = await createAccount(source); + const { externalId, userId } = await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + undefined, + ); + const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; + + const { audioFileId } = await createTask( + source, + accountId, + url, + 'test.zip', + 'Finished', + userId, + ); + + const blobParam = makeBlobstorageServiceMockValue(); + blobParam.publishDownloadSas = `${url}?sas-token`; + blobParam.fileExists = true; + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + await expect( + service.publishTemplateFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST), + ); + }); + + it('Typistの場合、自身が担当するタスクでない場合エラー', async () => { + const { accountId } = await createAccount(source); + const { externalId } = await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + undefined, + ); + const { userId: otherId } = await createUser( + source, + accountId, + 'other-typist-user-external-id', + 'typist', + undefined, + ); + const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; + + const { audioFileId } = await createTask( + source, + accountId, + url, + 'test.zip', + 'InProgress', + otherId, + ); + + const blobParam = makeBlobstorageServiceMockValue(); + blobParam.publishDownloadSas = `${url}?sas-token`; + blobParam.fileExists = true; + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + await expect( + service.publishTemplateFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST), + ); + }); + + it('Authorの場合、自身が登録したタスクでない場合エラー', async () => { + const { accountId } = await createAccount(source); + const { externalId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'AUTHOR_ID', + ); + const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; + + const { audioFileId } = await createTask( + source, + accountId, + url, + 'test.zip', + 'InProgress', + undefined, + 'OTHOR_ID', + ); + + const blobParam = makeBlobstorageServiceMockValue(); + blobParam.publishDownloadSas = `${url}?sas-token`; + blobParam.fileExists = true; + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + await expect( + service.publishTemplateFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST), + ); + }); + + it('Taskが存在しない場合はエラーとなる', async () => { + const { accountId } = await createAccount(source); + const { externalId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'AUTHOR_ID', + ); + + const blobParam = makeBlobstorageServiceMockValue(); + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + await expect( + service.publishTemplateFileDownloadSas(externalId, 1), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST), + ); + }); + + it('blobストレージにファイルが存在しない場合はエラーとなる', async () => { + const { accountId } = await createAccount(source); + const { externalId, authorId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'AUTHOR_ID', + ); + const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/Templates`; + + const { audioFileId } = await createTask( + source, + accountId, + url, + 'test.zip', + 'InProgress', + undefined, + authorId, + ); + + const blobParam = makeBlobstorageServiceMockValue(); + blobParam.publishDownloadSas = `${url}?sas-token`; + blobParam.fileExists = false; + + const module = await makeTestingModuleWithBlob(source, blobParam); + const service = module.get(FilesService); + + await expect( + service.publishTemplateFileDownloadSas(externalId, audioFileId), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST), + ); + }); +}); + const optionItemList = [ { optionItemLabel: 'label_01', diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 86a901c..94b196e 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -5,10 +5,23 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types'; -import { OPTION_ITEM_NUM, USER_ROLES } from '../../constants/index'; +import { + OPTION_ITEM_NUM, + TASK_STATUS, + USER_ROLES, +} from '../../constants/index'; import { User } from '../../repositories/users/entity/user.entity'; -import { AudioFileNotFoundError } from './errors/types'; -import { TasksNotFoundError } from '../../repositories/tasks/errors/types'; +import { + AccountNotMatchError, + AudioFileNotFoundError, + AuthorUserNotMatchError, + StatusNotMatchError, + TemplateFileNotFoundError, +} from './errors/types'; +import { + TasksNotFoundError, + TypistUserNotFoundError, +} from '../../repositories/tasks/errors/types'; @Injectable() export class FilesService { @@ -223,15 +236,18 @@ export class FilesService { ): Promise { //DBから国情報とアカウントID,ユーザーIDを取得する let accountId: number; - let country: string; let userId: number; + let country: string; let isTypist: boolean; + let authorId: string; try { const user = await this.usersRepository.findUserByExternalId(externalId); accountId = user.account.id; userId = user.id; + userId = user.id; country = user.account.country; isTypist = user.role === USER_ROLES.TYPIST; + authorId = user.author_id; } catch (e) { this.logger.error(`error=${e}`); console.log(e); @@ -242,11 +258,16 @@ export class FilesService { } try { - const { file } = await this.tasksRepository.getTaskAndAudioFile( + const status = isTypist + ? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING] + : Object.values(TASK_STATUS); + + const task = await this.tasksRepository.getTaskAndAudioFile( audioFileId, accountId, - isTypist, + status, ); + const file = task.file; // タスクに紐づく音声ファイルだけが消される場合がある。 // その場合はダウンロード不可なので不在エラーとして扱う @@ -256,12 +277,26 @@ export class FilesService { ); } + // ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー + if (!isTypist && task.file.author_id !== authorId) { + throw new AuthorUserNotMatchError( + `task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`, + ); + } + + // ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー + if (isTypist && task.typist_user_id !== userId) { + throw new AuthorUserNotMatchError( + `task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`, + ); + } + const filePath = `${userId}/${file.file_name}`; const isFileExist = await this.blobStorageService.fileExists( accountId, country, - filePath + filePath, ); if (!isFileExist) { @@ -282,6 +317,10 @@ export class FilesService { if (e instanceof Error) { switch (e.constructor) { case TasksNotFoundError: + case AccountNotMatchError: + case StatusNotMatchError: + case AuthorUserNotMatchError: + case TypistUserNotFoundError: throw new HttpException( makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST, @@ -304,4 +343,124 @@ export class FilesService { ); } } + + /** + * 指定したIDの音声ファイルに紐づいた文字起こしテンプレートファイルのダウンロードURLを取得する + * @param externalId + * @param audioFileId + * @returns template file download sas + */ + async publishTemplateFileDownloadSas( + externalId: string, + audioFileId: number, + ): Promise { + //DBから国情報とアカウントID,ユーザーIDを取得する + let accountId: number; + let userId: number; + let country: string; + let isTypist: boolean; + let authorId: string; + try { + const user = await this.usersRepository.findUserByExternalId(externalId); + accountId = user.account.id; + userId = user.id; + country = user.account.country; + isTypist = user.role === USER_ROLES.TYPIST; + authorId = user.author_id; + } catch (e) { + this.logger.error(`error=${e}`); + console.log(e); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + try { + const status = isTypist + ? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING] + : Object.values(TASK_STATUS); + + const task = await this.tasksRepository.getTaskAndAudioFile( + audioFileId, + accountId, + status, + ); + + const template_file = task.template_file; + + // タスクに紐づくテンプレートファイルがない場合がある。 + // その場合はダウンロード不可なので不在エラーとして扱う + if (!template_file) { + throw new TemplateFileNotFoundError( + `Template file is not exists in DB. audio_file_id:${audioFileId}`, + ); + } + + // ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー + if (!isTypist && task.file.author_id !== authorId) { + throw new AuthorUserNotMatchError( + `task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`, + ); + } + + // ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー + if (isTypist && task.typist_user_id !== userId) { + throw new AuthorUserNotMatchError( + `task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`, + ); + } + + const filePath = `Templates/${template_file.file_name}`; + + const isFileExist = await this.blobStorageService.fileExists( + accountId, + country, + filePath, + ); + + if (!isFileExist) { + throw new TemplateFileNotFoundError( + `Template file is not exists in blob storage. audio_file_id:${audioFileId}, url:${template_file.url}, fileName:${template_file.file_name}`, + ); + } + + // SASトークン発行 + const url = await this.blobStorageService.publishDownloadSas( + accountId, + country, + filePath, + ); + return url; + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case TasksNotFoundError: + case AccountNotMatchError: + case StatusNotMatchError: + case AuthorUserNotMatchError: + case TypistUserNotFoundError: + throw new HttpException( + makeErrorResponse('E010603'), + HttpStatus.BAD_REQUEST, + ); + case TemplateFileNotFoundError: + throw new HttpException( + makeErrorResponse('E010701'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index 93df7e1..480becb 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -38,6 +38,7 @@ import { } from './files.service.mock'; import { User } from '../../../repositories/users/entity/user.entity'; import { Account } from '../../../repositories/accounts/entity/account.entity'; +import { TemplateFile } from '../../../repositories/template_files/entity/template_file.entity'; export const createAccount = async ( datasource: DataSource, @@ -65,7 +66,7 @@ export const createUser = async ( external_id: string, role: string, author_id?: string | undefined, -): Promise<{ userId: number; externalId: string }> => { +): Promise<{ userId: number; externalId: string; authorId: string }> => { const { identifiers } = await datasource.getRepository(User).insert({ account_id: accountId, external_id: external_id, @@ -82,14 +83,17 @@ export const createUser = async ( updated_at: new Date(), }); const user = identifiers.pop() as User; - return { userId: user.id, externalId: external_id }; + return { userId: user.id, externalId: external_id, authorId: author_id }; }; export const createTask = async ( datasource: DataSource, account_id: number, url: string, - filename: string, + fileName: string, + status: string, + typist_user_id?: number | undefined, + author_id?: string | undefined, ): Promise<{ audioFileId: number }> => { const { identifiers: audioFileIdentifiers } = await datasource .getRepository(AudioFile) @@ -97,8 +101,8 @@ export const createTask = async ( account_id: account_id, owner_user_id: 1, url: url, - file_name: filename, - author_id: 'AUTHOR_ID', + file_name: fileName, + author_id: author_id ?? 'DEFAULT_ID', work_type_id: 'work_type_id', started_at: new Date(), duration: '100000', @@ -110,19 +114,32 @@ export const createTask = async ( is_encrypted: true, }); const audioFile = audioFileIdentifiers.pop() as AudioFile; - const { identifiers: taskIdentifiers } = await datasource - .getRepository(Task) + const { identifiers: templateFileIdentifiers } = await datasource + .getRepository(TemplateFile) .insert({ - job_number: '00000001', account_id: account_id, - is_job_number_enabled: true, - audio_file_id: audioFile.id, - status: 'Uploaded', - priority: '01', - started_at: new Date().toISOString(), + url: url, + file_name: fileName, + created_by: 'test_runner', created_at: new Date(), + updated_by: 'updater', + updated_at: new Date(), }); + const templateFile = templateFileIdentifiers.pop() as TemplateFile; + await datasource.getRepository(Task).insert({ + job_number: '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(), + }); + return { audioFileId: audioFile.id }; }; diff --git a/dictation_server/src/repositories/tasks/entity/task.entity.ts b/dictation_server/src/repositories/tasks/entity/task.entity.ts index e73cf1a..1252620 100644 --- a/dictation_server/src/repositories/tasks/entity/task.entity.ts +++ b/dictation_server/src/repositories/tasks/entity/task.entity.ts @@ -1,6 +1,7 @@ import { AudioOptionItem } from '../../../repositories/audio_option_items/entity/audio_option_item.entity'; import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity'; import { User } from '../../../repositories/users/entity/user.entity'; +import { TemplateFile } from '../../template_files/entity/template_file.entity'; import { Entity, Column, @@ -8,8 +9,8 @@ import { OneToOne, JoinColumn, OneToMany, + ManyToOne, } from 'typeorm'; -import { TemplateFile } from '../../template_files/entity/template_file.entity'; @Entity({ name: 'tasks' }) export class Task { @@ -45,6 +46,7 @@ export class Task { @OneToOne(() => User, (user) => user.id) @JoinColumn({ name: 'typist_user_id' }) typist_user?: User; + @ManyToOne(() => TemplateFile, (templateFile) => templateFile.id) @JoinColumn({ name: 'template_file_id' }) template_file?: TemplateFile; } diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index caf1ec3..1b9e2db 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -28,31 +28,34 @@ import { TypistUserNotFoundError, } from './errors/types'; import { Roles } from '../../common/types/role'; +import { + AccountNotMatchError, + StatusNotMatchError, +} from '../../features/files/errors/types'; @Injectable() export class TasksRepositoryService { constructor(private dataSource: DataSource) {} + /** * 音声ファイルと紐づいたTaskを取得する * @param audioFileId * @param account_id + * @param status 配列で設定したステータスのタスクのみ取得 * @returns task and audio file */ async getTaskAndAudioFile( audioFileId: number, account_id: number, - isTypist: boolean + status: string[], ): Promise { - const status = isTypist - ? [TASK_STATUS.UPLOADED, TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING] - : [TASK_STATUS.UPLOADED, TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING, TASK_STATUS.FINISHED, TASK_STATUS.BACKUP]; - return await this.dataSource.transaction(async (entityManager) => { const taskRepo = entityManager.getRepository(Task); - // 指定した音声ファイルIDに紐づくTaskの中でAuthorIDが一致するものを取得 + // 指定した音声ファイルIDに紐づくTaskの中でステータスが一致するものを取得 const task = await taskRepo.findOne({ relations: { file: true, + template_file: true, }, where: { audio_file_id: audioFileId, @@ -66,6 +69,19 @@ export class TasksRepositoryService { ); } + // アカウントチェック + if (task.account_id !== account_id) { + throw new AccountNotMatchError( + `task account_id not match. audio_file_id:${audioFileId}, task.account_id:${task.account_id}, account_id:${account_id}`, + ); + } + + // ステータスチェック + if (!status.includes(task.status)) { + throw new StatusNotMatchError( + `Unexpected task status. status:${task.status}`, + ); + } return task; }); } diff --git a/dictation_server/src/repositories/template_files/entity/template_file.entity.ts b/dictation_server/src/repositories/template_files/entity/template_file.entity.ts index ac23433..5c45d2f 100644 --- a/dictation_server/src/repositories/template_files/entity/template_file.entity.ts +++ b/dictation_server/src/repositories/template_files/entity/template_file.entity.ts @@ -1,6 +1,14 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + OneToMany, +} from 'typeorm'; +import { Task } from '../../tasks/entity/task.entity'; -@Entity({ name: 'audio_files' }) +@Entity({ name: 'template_files' }) export class TemplateFile { @PrimaryGeneratedColumn() id: number; @@ -10,4 +18,16 @@ export class TemplateFile { url: string; @Column() file_name: string; + @Column({ nullable: true }) + deleted_at?: Date; + @Column() + created_by: string; + @CreateDateColumn() + created_at: Date; + @Column() + updated_by: string; + @UpdateDateColumn() + updated_at: Date; + @OneToMany(() => Task, (task) => task.template_file) + tasks?: Task[]; }