From ecc44e58e0abf405fa803744e288975da9e2a808 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 25 Sep 2023 07:50:19 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20438:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E5=AE=8C=E4=BA=86API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2655: API実装(テンプレートファイルアップロード完了API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2655) - テンプレートファイルのアップロード完了APIとテストを実装しました。 ## レビューポイント - テストケースは適切か - 保存時のリポジトリ処理は適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../src/features/files/files.controller.ts | 9 +- .../src/features/files/files.module.ts | 2 + .../src/features/files/files.service.spec.ts | 135 ++++++++++++++++++ .../src/features/files/files.service.ts | 50 +++++++ .../features/files/test/files.service.mock.ts | 3 + .../src/features/templates/test/utility.ts | 12 ++ .../entity/template_file.entity.ts | 8 +- .../template_files.repository.service.ts | 36 +++++ 8 files changed, 245 insertions(+), 10 deletions(-) diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index 09b9c5e..d188e94 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -336,13 +336,10 @@ export class FilesController { ): Promise { const { name, url } = body; const token = retrieveAuthorizationToken(req); - const accessToken = jwt.decode(token, { json: true }) as AccessToken; - - const context = makeContext(accessToken.userId); - console.log(context.trackingId); - console.log(name); - console.log(url); + const { userId } = jwt.decode(token, { json: true }) as AccessToken; + const context = makeContext(userId); + await this.filesService.templateUploadFinished(context, userId, url, name); return {}; } } diff --git a/dictation_server/src/features/files/files.module.ts b/dictation_server/src/features/files/files.module.ts index c69967f..7054ac8 100644 --- a/dictation_server/src/features/files/files.module.ts +++ b/dictation_server/src/features/files/files.module.ts @@ -6,6 +6,7 @@ import { AudioFilesRepositoryModule } from '../../repositories/audio_files/audio import { AudioOptionItemsRepositoryModule } from '../../repositories/audio_option_items/audio_option_items.repository.module'; import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository.module'; import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; +import { TemplateFilesRepositoryModule } from '../../repositories/template_files/template_files.repository.module'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module AudioOptionItemsRepositoryModule, TasksRepositoryModule, BlobstorageModule, + TemplateFilesRepositoryModule, ], providers: [FilesService], controllers: [FilesController], diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 0fb797c..2ea8e73 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -17,6 +17,11 @@ import { } from '../../common/test/utility'; import { makeTestingModule } from '../../common/test/modules'; import { overrideBlobstorageService } from '../../common/test/overrides'; +import { + createTemplateFile, + getTemplateFiles, +} from '../templates/test/utility'; +import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; describe('音声ファイルアップロードURL取得', () => { it('アップロードSASトークンが乗っているURLを返却する', async () => { @@ -881,6 +886,136 @@ describe('publishTemplateFileUploadSas', () => { }); }); +describe('templateUploadFinished', () => { + 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('アップロード完了後のテンプレートファイル情報をDBに保存できる(新規追加)', async () => { + const module = await makeTestingModule(source); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id); + + const fileName = 'test.docs'; + const url = `https://blob.url/account-${account.id}/Templates`; + + // 事前にDBを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(0); + } + + await service.templateUploadFinished( + context, + admin.external_id, + url, + fileName, + ); + + //実行結果を確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].file_name).toBe(fileName); + expect(templates[0].url).toBe(url); + } + }); + + it('アップロード完了後のテンプレートファイル情報をDBに保存できる(更新)', async () => { + const module = await makeTestingModule(source); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id); + + const fileName = 'test.docs'; + const url = `https://blob.url/account-${account.id}/Templates`; + + await createTemplateFile(source, account.id, fileName, url); + + // 事前にDBを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].file_name).toBe(fileName); + expect(templates[0].url).toBe(url); + } + + const updateUrl = `https://blob.update.url/account-${account.id}/Templates`; + + await service.templateUploadFinished( + context, + admin.external_id, + updateUrl, + fileName, + ); + + //実行結果を確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].file_name).toBe(fileName); + expect(templates[0].url).toBe(updateUrl); + } + }); + + it('DBへの保存に失敗した場合はエラーとなる', async () => { + const module = await makeTestingModule(source); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id); + + const fileName = 'test.docs'; + const url = `https://blob.url/account-${account.id}/Templates`; + + // 事前にDBを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(0); + } + + //DBアクセスに失敗するようにする + const templatesService = module.get( + TemplateFilesRepositoryService, + ); + templatesService.upsertTemplateFile = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.templateUploadFinished( + context, + admin.external_id, + url, + fileName, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + 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 befe6b7..27fd5c3 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -23,6 +23,7 @@ import { TypistUserNotFoundError, } from '../../repositories/tasks/errors/types'; import { Context } from '../../common/log'; +import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; @Injectable() export class FilesService { @@ -31,6 +32,7 @@ export class FilesService { private readonly usersRepository: UsersRepositoryService, private readonly tasksRepository: TasksRepositoryService, private readonly tasksRepositoryService: TasksRepositoryService, + private readonly templateFilesRepository: TemplateFilesRepositoryService, private readonly blobStorageService: BlobstorageService, ) {} @@ -584,4 +586,52 @@ export class FilesService { ); } } + + /** + * テンプレートファイルのアップロード後にDBにテンプレートファイル情報を登録する + * @param context + * @param externalId + * @param url + * @param fileName + * @returns upload finished + */ + async templateUploadFinished( + context: Context, + externalId: string, + url: string, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.templateUploadFinished.name} | params: { externalId: ${externalId}, url: ${url}, fileName: ${fileName} };`, + ); + + try { + // ユーザー取得 + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(externalId); + + // URLにSASトークンがついている場合は取り除く; + const urlObj = new URL(url); + urlObj.search = ''; + const fileUrl = urlObj.toString(); + this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`); + + // テンプレートファイル情報をDBに登録 + await this.templateFilesRepository.upsertTemplateFile( + accountId, + fileName, + url, + ); + } catch (e) { + this.logger.error(`error=${e}`); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.templateUploadFinished.name}`, + ); + } + } } diff --git a/dictation_server/src/features/files/test/files.service.mock.ts b/dictation_server/src/features/files/test/files.service.mock.ts index b837d5c..362212c 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -5,6 +5,7 @@ import { UsersRepositoryService } from '../../../repositories/users/users.reposi import { FilesService } from '../files.service'; import { TasksRepositoryService } from '../../../repositories/tasks/tasks.repository.service'; import { Task } from '../../../repositories/tasks/entity/task.entity'; +import { TemplateFilesRepositoryService } from '../../../repositories/template_files/template_files.repository.service'; export type BlobstorageServiceMockValue = { createContainer: void | Error; @@ -39,6 +40,8 @@ export const makeFilesServiceMock = async ( return makeUsersRepositoryMock(usersRepositoryMockValue); case TasksRepositoryService: return makeTasksRepositoryMock(tasksRepositoryMockValue); + case TemplateFilesRepositoryService: + return {}; } }) .compile(); diff --git a/dictation_server/src/features/templates/test/utility.ts b/dictation_server/src/features/templates/test/utility.ts index 2643e88..ac78857 100644 --- a/dictation_server/src/features/templates/test/utility.ts +++ b/dictation_server/src/features/templates/test/utility.ts @@ -26,3 +26,15 @@ export const createTemplateFile = async ( return templateFile; }; + +export const getTemplateFiles = async ( + datasource: DataSource, + accountId: number, +): Promise => { + const templates = await datasource.getRepository(TemplateFile).find({ + where: { + account_id: accountId, + }, + }); + return templates; +}; 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 41d6738..f5f9064 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 @@ -18,12 +18,12 @@ export class TemplateFile { url: string; @Column() file_name: string; - @Column() - created_by: string; + @Column({ nullable: true }) + created_by?: string; @CreateDateColumn() created_at: Date; - @Column() - updated_by: string; + @Column({ nullable: true }) + updated_by?: string; @UpdateDateColumn() updated_at: Date; @OneToMany(() => Task, (task) => task.template_file) 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 ffe3b81..4db05b0 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 @@ -22,4 +22,40 @@ export class TemplateFilesRepositoryService { return templates; }); } + + /** + * アカウント内にテンプレートファイルを追加(すでに同名ファイルがあれば更新)する + * @param accountId + * @param fileName + * @param url + * @returns template file + */ + async upsertTemplateFile( + accountId: number, + fileName: string, + url: string, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const templateFilesRepo = entityManager.getRepository(TemplateFile); + + // アカウント内に同名ファイルがあるか確認 + const template = await templateFilesRepo.findOne({ + where: { account_id: accountId, file_name: fileName }, + }); + + // すでに同名ファイルがあれば更新、なければ追加 + if (template) { + await templateFilesRepo.update( + { id: template.id }, + { file_name: fileName, url: url }, + ); + } else { + const newTemplate = new TemplateFile(); + newTemplate.account_id = accountId; + newTemplate.file_name = fileName; + newTemplate.url = url; + await templateFilesRepo.save(newTemplate); + } + }); + } }