Merged PR 438: API実装(テンプレートファイルアップロード完了API)

## 概要
[Task2655: API実装(テンプレートファイルアップロード完了API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2655)

- テンプレートファイルのアップロード完了APIとテストを実装しました。

## レビューポイント
- テストケースは適切か
- 保存時のリポジトリ処理は適切か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-09-25 07:50:19 +00:00
parent f994c23b51
commit ecc44e58e0
8 changed files with 245 additions and 10 deletions

View File

@ -336,13 +336,10 @@ export class FilesController {
): Promise<TemplateUploadFinishedReqponse> {
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 {};
}
}

View File

@ -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],

View File

@ -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>(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>(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>(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>(
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',

View File

@ -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<void> {
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}`,
);
}
}
}

View File

@ -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();

View File

@ -26,3 +26,15 @@ export const createTemplateFile = async (
return templateFile;
};
export const getTemplateFiles = async (
datasource: DataSource,
accountId: number,
): Promise<TemplateFile[]> => {
const templates = await datasource.getRepository(TemplateFile).find({
where: {
account_id: accountId,
},
});
return templates;
};

View File

@ -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)

View File

@ -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<void> {
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);
}
});
}
}