Merged PR 119: 音声ファイルアップロード完了API実装
## 概要 [Task1712: 音声ファイルアップロード完了API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1712) - 音声ファイルアップロード完了APIを実装しました。 - DBへの登録処理を追加しています - 音声ファイルテーブル - オプションアイテムテーブル - 文字起こしタスクテーブル - jwtトークンデコードがうまくいかないことがありましたので応急対応を入れています。 - 参考:https://github.com/auth0/node-jsonwebtoken/issues/875 ## レビューポイント - DBへの登録処理・内容は適切か - JOBナンバーの採番は適切か - jwtデコードの対応は適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
81dca16d5f
commit
3191e22ab6
@ -0,0 +1,5 @@
|
||||
-- +migrate Up
|
||||
ALTER TABLE `tasks` ADD COLUMN(`created_at` TIMESTAMP(6) DEFAULT now(6) COMMENT '作成時刻');
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `tasks` DROP COLUMN `created_at`;
|
||||
@ -19,6 +19,9 @@ import { AccountsRepositoryModule } from './repositories/accounts/accounts.repos
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SendGridModule } from './gateways/sendgrid/sendgrid.module';
|
||||
import { UsersRepositoryModule } from './repositories/users/users.repository.module';
|
||||
import { AudioFilesRepositoryModule } from './repositories/audio_files/audio_files.repository.module';
|
||||
import { AudioOptionItemsRepositoryModule } from './repositories/audio_option_items/audio_option_items.repository.module';
|
||||
import { TasksRepositoryModule } from './repositories/tasks/tasks.repository.module';
|
||||
import { NotificationhubModule } from './gateways/notificationhub/notificationhub.module';
|
||||
import { NotificationhubService } from './gateways/notificationhub/notificationhub.service';
|
||||
import { NotificationModule } from './features/notification/notification.module';
|
||||
@ -54,6 +57,9 @@ import { LicensesController } from './features/licenses/licenses.controller';
|
||||
SendGridModule,
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
AudioFilesRepositoryModule,
|
||||
AudioOptionItemsRepositoryModule,
|
||||
TasksRepositoryModule,
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
|
||||
@ -22,6 +22,7 @@ export const ErrorCodes = [
|
||||
'E000106', // トークンアルゴリズムエラー
|
||||
'E000107', // トークン不足エラー
|
||||
'E000108', // トークン権限エラー
|
||||
'E010001', // パラメータ形式不正エラー
|
||||
'E010201', // 未認証ユーザエラー
|
||||
'E010202', // 認証済ユーザエラー
|
||||
'E010203', // 管理ユーザ権限エラー
|
||||
|
||||
@ -11,6 +11,7 @@ export const errors: Errors = {
|
||||
E000106: 'Token invalid algorithm Error.',
|
||||
E000107: 'Token is not exist Error.',
|
||||
E000108: 'Token authority failed Error.',
|
||||
E010001: 'Param invalid format Error.',
|
||||
E010201: 'Email not verified user Error.',
|
||||
E010202: 'Email already verified user Error.',
|
||||
E010203: 'Administrator Permissions Error.',
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
// XXX: decodeがうまく使えないことがあるので応急対応 バージョン9以降だとなる?
|
||||
import { decode as jwtDecode } from 'jsonwebtoken';
|
||||
|
||||
export type VerifyError = {
|
||||
reason: 'ExpiredError' | 'InvalidToken' | 'InvalidTimeStamp' | 'Unknown';
|
||||
@ -96,7 +98,7 @@ export const verify = <T extends object>(
|
||||
*/
|
||||
export const decode = <T extends object>(token: string): T | VerifyError => {
|
||||
try {
|
||||
const payload = jwt.decode(token, {
|
||||
const payload = jwtDecode(token, {
|
||||
json: true,
|
||||
}) as T;
|
||||
return payload;
|
||||
|
||||
@ -87,3 +87,21 @@ export const BLOB_STORAGE_REGION_EU = [
|
||||
* @const {string}
|
||||
*/
|
||||
export const ROLE_NONE = 'None';
|
||||
|
||||
/**
|
||||
* 音声ファイルに紐づくオプションアイテムの数
|
||||
* @const {string}
|
||||
*/
|
||||
export const OPTION_ITEM_NUM = 10;
|
||||
|
||||
/**
|
||||
* 文字起こしタスクのステータス
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const TASK_STATUS = {
|
||||
UPLOADED: 'Uploaded',
|
||||
PENDING: 'Pending',
|
||||
IN_PROGRESS: 'InProgress',
|
||||
FINISHED: 'Finished',
|
||||
BACKUP: 'Backup',
|
||||
} as const;
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
TemplateDownloadLocationResponse,
|
||||
} from './types/types';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
|
||||
@ApiTags('files')
|
||||
@Controller('files')
|
||||
@ -61,13 +62,54 @@ export class FilesController {
|
||||
'アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: ['author'] }))
|
||||
@Post('audio/upload-finished')
|
||||
async uploadFinished(
|
||||
@Headers() headers,
|
||||
@Headers('authorization') authorization: string,
|
||||
@Body() body: AudioUploadFinishedRequest,
|
||||
): Promise<AudioUploadFinishedResponse> {
|
||||
console.log(body);
|
||||
return { jobNumber: '00000001' };
|
||||
const accessToken = jwt.decode(
|
||||
authorization.substring('Bearer '.length, authorization.length),
|
||||
{ json: true },
|
||||
) as AccessToken;
|
||||
|
||||
const {
|
||||
url,
|
||||
authorId,
|
||||
fileName,
|
||||
duration,
|
||||
createdDate,
|
||||
finishedDate,
|
||||
uploadedDate,
|
||||
fileSize,
|
||||
priority,
|
||||
audioFormat,
|
||||
comment,
|
||||
workType,
|
||||
optionItemList,
|
||||
isEncrypted,
|
||||
} = body;
|
||||
|
||||
const res = await this.filesService.uploadFinished(
|
||||
accessToken.userId,
|
||||
url,
|
||||
authorId,
|
||||
fileName,
|
||||
duration,
|
||||
createdDate,
|
||||
finishedDate,
|
||||
uploadedDate,
|
||||
fileSize,
|
||||
priority,
|
||||
audioFormat,
|
||||
comment,
|
||||
workType,
|
||||
optionItemList,
|
||||
isEncrypted,
|
||||
);
|
||||
|
||||
return { jobNumber: res.jobNumber };
|
||||
}
|
||||
|
||||
@Get('audio/upload-location')
|
||||
|
||||
@ -2,10 +2,19 @@ import { Module } from '@nestjs/common';
|
||||
import { FilesService } from './files.service';
|
||||
import { FilesController } from './files.controller';
|
||||
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
|
||||
import { AudioFilesRepositoryModule } from '../../repositories/audio_files/audio_files.repository.module';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [UsersRepositoryModule, BlobstorageModule],
|
||||
imports: [
|
||||
UsersRepositoryModule,
|
||||
AudioFilesRepositoryModule,
|
||||
AudioOptionItemsRepositoryModule,
|
||||
TasksRepositoryModule,
|
||||
BlobstorageModule,
|
||||
],
|
||||
providers: [FilesService],
|
||||
controllers: [FilesController],
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@ import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import {
|
||||
makeBlobstorageServiceMockValue,
|
||||
makeDefaultTasksRepositoryMockValue,
|
||||
makeDefaultUsersRepositoryMockValue,
|
||||
makeFilesServiceMock,
|
||||
} from './test/files.service.mock';
|
||||
@ -10,7 +11,12 @@ describe('FilesService', () => {
|
||||
it('アップロードSASトークンが乗っているURLを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, userRepoParam);
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
expect(
|
||||
await service.publishUploadSas({
|
||||
@ -23,9 +29,15 @@ describe('FilesService', () => {
|
||||
it('アカウント専用コンテナが無い場合でも、コンテナ作成しURLを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
|
||||
blobParam.containerExists = false;
|
||||
|
||||
const service = await makeFilesServiceMock(blobParam, userRepoParam);
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
expect(
|
||||
await service.publishUploadSas({
|
||||
@ -37,9 +49,15 @@ describe('FilesService', () => {
|
||||
|
||||
it('ユーザー情報の取得に失敗した場合、例外エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, {
|
||||
findUserByExternalId: new Error(''),
|
||||
});
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
{
|
||||
findUserByExternalId: new Error(''),
|
||||
},
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.publishUploadSas({
|
||||
@ -53,9 +71,15 @@ describe('FilesService', () => {
|
||||
|
||||
it('コンテナ作成に失敗した場合、例外エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, {
|
||||
findUserByExternalId: new Error(''),
|
||||
});
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
{
|
||||
findUserByExternalId: new Error(''),
|
||||
},
|
||||
taskRepoParam,
|
||||
);
|
||||
blobParam.publishUploadSas = new Error('Azure service down');
|
||||
|
||||
await expect(
|
||||
@ -67,4 +91,220 @@ describe('FilesService', () => {
|
||||
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
|
||||
);
|
||||
});
|
||||
|
||||
it('文字起こしタスクを作成できる', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
expect(
|
||||
await service.uploadFinished(
|
||||
'userId',
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'workTypeID',
|
||||
optionItemList,
|
||||
false,
|
||||
),
|
||||
).toEqual({ jobNumber: '00000001' });
|
||||
});
|
||||
|
||||
it('日付フォーマットが不正な場合、エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
'userId',
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'yyyy-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'workTypeID',
|
||||
optionItemList,
|
||||
false,
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
it('オプションアイテムが10個ない場合、エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
'userId',
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'workTypeID',
|
||||
[
|
||||
{
|
||||
optionItemLabel: 'label_01',
|
||||
optionItemValue: 'value_01',
|
||||
},
|
||||
],
|
||||
false,
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
it('タスク追加でユーザー情報の取得に失敗した場合、エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
{
|
||||
findUserByExternalId: new Error(''),
|
||||
},
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
'userId',
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'workTypeID',
|
||||
optionItemList,
|
||||
false,
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('タスクのDBへの追加に失敗した場合、エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, userRepoParam, {
|
||||
create: new Error(''),
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
'userId',
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'workTypeID',
|
||||
optionItemList,
|
||||
false,
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const optionItemList = [
|
||||
{
|
||||
optionItemLabel: 'label_01',
|
||||
optionItemValue: 'value_01',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_02',
|
||||
optionItemValue: 'value_02',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_03',
|
||||
optionItemValue: 'value_03',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_04',
|
||||
optionItemValue: 'value_04',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_05',
|
||||
optionItemValue: 'value_05',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_06',
|
||||
optionItemValue: 'value_06',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_07',
|
||||
optionItemValue: 'value_07',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_08',
|
||||
optionItemValue: 'value_08',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_09',
|
||||
optionItemValue: 'value_09',
|
||||
},
|
||||
{
|
||||
optionItemLabel: 'label_10',
|
||||
optionItemValue: 'value_10',
|
||||
},
|
||||
];
|
||||
|
||||
@ -2,15 +2,146 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
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 } from '../../constants/index';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FilesService {
|
||||
private readonly logger = new Logger(FilesService.name);
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly tasksRepositoryService: TasksRepositoryService,
|
||||
private readonly blobStorageService: BlobstorageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Uploads finished
|
||||
* @param url アップロード先Blob Storage(ファイル名含む)
|
||||
* @param authorId 自分自身(ログイン認証)したAuthorID
|
||||
* @param fileName 音声ファイル名
|
||||
* @param duration 音声ファイルの録音時間(yyyy-mm-ddThh:mm:ss.sss)
|
||||
* @param createdDate 音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss)'
|
||||
* @param finishedDate 音声ファイルの録音作成終了日時(yyyy-mm-ddThh:mm:ss.sss)
|
||||
* @param uploadedDate 音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss)
|
||||
* @param fileSize 音声ファイルのファイルサイズ(Byte)
|
||||
* @param priority 優先度 "00":Normal / "01":High
|
||||
* @param audioFormat 録音形式: DSS/DS2(SP)/DS2(QP)
|
||||
* @param comment コメント
|
||||
* @param workType WorkType
|
||||
* @param optionItemList オプションアイテム(音声メタデータ)10個固定
|
||||
* @param isEncrypted 暗号化されているか
|
||||
* @returns finished
|
||||
*/
|
||||
async uploadFinished(
|
||||
userId: string,
|
||||
url: string,
|
||||
authorId: string,
|
||||
fileName: string,
|
||||
duration: string,
|
||||
createdDate: string,
|
||||
finishedDate: string,
|
||||
uploadedDate: string,
|
||||
fileSize: number,
|
||||
priority: string,
|
||||
audioFormat: string,
|
||||
comment: string,
|
||||
workType: string,
|
||||
optionItemList: AudioOptionItem[],
|
||||
isEncrypted: boolean,
|
||||
): Promise<AudioUploadFinishedResponse> {
|
||||
const formattedCreatedDate = new Date(createdDate);
|
||||
const formattedFinishedDate = new Date(finishedDate);
|
||||
const formattedUploadedDate = new Date(uploadedDate);
|
||||
|
||||
const isInvalidCreatedDate = isNaN(formattedCreatedDate.getTime());
|
||||
const isInvalidFinishedDate = isNaN(formattedFinishedDate.getTime());
|
||||
const isInvalidUploadedDate = isNaN(formattedUploadedDate.getTime());
|
||||
|
||||
// 日付フォーマットが不正ならパラメータ不正
|
||||
if (
|
||||
isInvalidCreatedDate ||
|
||||
isInvalidFinishedDate ||
|
||||
isInvalidUploadedDate
|
||||
) {
|
||||
if (isInvalidCreatedDate) {
|
||||
this.logger.error(
|
||||
`param createdDate is invalid format:[createdDate=${createdDate}]`,
|
||||
);
|
||||
}
|
||||
if (isInvalidFinishedDate) {
|
||||
this.logger.error(
|
||||
`param finishedDate is invalid format:[finishedDate=${finishedDate}]`,
|
||||
);
|
||||
}
|
||||
if (isInvalidUploadedDate) {
|
||||
this.logger.error(
|
||||
`param uploadedDate is invalid format:[uploadedDate=${uploadedDate}]`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// オプションアイテムが10個ない場合はパラメータ不正
|
||||
if (optionItemList.length !== OPTION_ITEM_NUM) {
|
||||
this.logger.error(
|
||||
`param optionItemList expects ${OPTION_ITEM_NUM} items, but has ${optionItemList.length} items`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
let user: User;
|
||||
try {
|
||||
// ユーザー取得
|
||||
user = await this.usersRepository.findUserByExternalId(userId);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
|
||||
// 追加時に末尾のJOBナンバーにインクリメントする
|
||||
const task = await this.tasksRepositoryService.create(
|
||||
user.account_id,
|
||||
user.id,
|
||||
priority,
|
||||
url,
|
||||
fileName,
|
||||
authorId,
|
||||
workType,
|
||||
formattedCreatedDate,
|
||||
duration,
|
||||
formattedFinishedDate,
|
||||
formattedUploadedDate,
|
||||
fileSize,
|
||||
audioFormat,
|
||||
comment,
|
||||
isEncrypted,
|
||||
optionItemList,
|
||||
);
|
||||
return { jobNumber: task.job_number };
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishs upload sas
|
||||
* @param companyName
|
||||
|
||||
@ -3,6 +3,8 @@ import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.se
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
import { UsersRepositoryService } from '../../../repositories/users/users.repository.service';
|
||||
import { FilesService } from '../files.service';
|
||||
import { TasksRepositoryService } from '../../../repositories/tasks/tasks.repository.service';
|
||||
import { Task } from '../../../repositories/tasks/entity/task.entity';
|
||||
|
||||
export type BlobstorageServiceMockValue = {
|
||||
createContainer: void | Error;
|
||||
@ -14,9 +16,14 @@ export type UsersRepositoryMockValue = {
|
||||
findUserByExternalId: User | Error;
|
||||
};
|
||||
|
||||
export type TasksRepositoryMockValue = {
|
||||
create: Task | Error;
|
||||
};
|
||||
|
||||
export const makeFilesServiceMock = async (
|
||||
blobStorageService: BlobstorageServiceMockValue,
|
||||
usersRepositoryMockValue: UsersRepositoryMockValue,
|
||||
tasksRepositoryMockValue: TasksRepositoryMockValue,
|
||||
): Promise<FilesService> => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FilesService],
|
||||
@ -27,6 +34,8 @@ export const makeFilesServiceMock = async (
|
||||
return makeBlobstorageServiceMock(blobStorageService);
|
||||
case UsersRepositoryService:
|
||||
return makeUsersRepositoryMock(usersRepositoryMockValue);
|
||||
case TasksRepositoryService:
|
||||
return makeTasksRepositoryMock(tasksRepositoryMockValue);
|
||||
}
|
||||
})
|
||||
.compile();
|
||||
@ -75,6 +84,17 @@ export const makeBlobstorageServiceMockValue =
|
||||
};
|
||||
};
|
||||
|
||||
export const makeTasksRepositoryMock = (value: TasksRepositoryMockValue) => {
|
||||
const { create } = value;
|
||||
|
||||
return {
|
||||
create:
|
||||
create instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(create)
|
||||
: jest.fn<Promise<Task>, []>().mockResolvedValue(create),
|
||||
};
|
||||
};
|
||||
|
||||
// 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する
|
||||
export const makeDefaultUsersRepositoryMockValue =
|
||||
(): UsersRepositoryMockValue => {
|
||||
@ -114,3 +134,19 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const makeDefaultTasksRepositoryMockValue =
|
||||
(): TasksRepositoryMockValue => {
|
||||
return {
|
||||
create: {
|
||||
id: 1,
|
||||
job_number: '00000001',
|
||||
account_id: 1,
|
||||
is_job_number_enabled: true,
|
||||
audio_file_id: 1,
|
||||
status: 'Uploaded',
|
||||
priority: '01',
|
||||
created_at: new Date(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AudioFile } from './entity/audio_file.entity';
|
||||
import { AudioFilesRepositoryService } from './audio_files.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AudioFile])],
|
||||
providers: [AudioFilesRepositoryService],
|
||||
exports: [AudioFilesRepositoryService],
|
||||
})
|
||||
export class AudioFilesRepositoryModule {}
|
||||
@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class AudioFilesRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'audio_files' })
|
||||
export class AudioFile {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
@Column()
|
||||
owner_user_id: number;
|
||||
@Column()
|
||||
url: string;
|
||||
@Column()
|
||||
file_name: string;
|
||||
@Column()
|
||||
author_id: string;
|
||||
@Column()
|
||||
work_type_id: string;
|
||||
@Column()
|
||||
started_at: Date;
|
||||
@Column({ type: 'time' })
|
||||
duration: string;
|
||||
@Column()
|
||||
finished_at: Date;
|
||||
@Column()
|
||||
uploaded_at: Date;
|
||||
@Column()
|
||||
file_size: number;
|
||||
@Column()
|
||||
priority: string;
|
||||
@Column()
|
||||
audio_format: string;
|
||||
@Column({ nullable: true })
|
||||
comment?: string;
|
||||
@Column({ nullable: true })
|
||||
deleted_at?: Date;
|
||||
@Column()
|
||||
is_encrypted: boolean;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AudioOptionItem } from './entity/audio_option_item.entity';
|
||||
import { AudioOptionItemsRepositoryService } from './audio_option_items.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AudioOptionItem])],
|
||||
providers: [AudioOptionItemsRepositoryService],
|
||||
exports: [AudioOptionItemsRepositoryService],
|
||||
})
|
||||
export class AudioOptionItemsRepositoryModule {}
|
||||
@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class AudioOptionItemsRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'audio_option_items' })
|
||||
export class AudioOptionItem {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
audio_file_id: number;
|
||||
@Column()
|
||||
label: string;
|
||||
@Column()
|
||||
value: string;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tasks' })
|
||||
export class Task {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@Column()
|
||||
job_number: string;
|
||||
@Column()
|
||||
account_id: number;
|
||||
@Column({ nullable: true })
|
||||
is_job_number_enabled?: boolean;
|
||||
@Column()
|
||||
audio_file_id: number;
|
||||
@Column()
|
||||
status: string;
|
||||
@Column({ nullable: true })
|
||||
typist_user_id?: number;
|
||||
@Column()
|
||||
priority: string;
|
||||
@Column({ nullable: true })
|
||||
template_file_id?: number;
|
||||
@Column({ nullable: true })
|
||||
started_at?: number;
|
||||
@Column({ nullable: true })
|
||||
finished_at?: number;
|
||||
@Column({ type: 'timestamp' })
|
||||
created_at: Date;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Task } from './entity/task.entity';
|
||||
import { TasksRepositoryService } from './tasks.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Task])],
|
||||
providers: [TasksRepositoryService],
|
||||
exports: [TasksRepositoryService],
|
||||
})
|
||||
export class TasksRepositoryModule {}
|
||||
@ -0,0 +1,105 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Task } from './entity/task.entity';
|
||||
import { TASK_STATUS } from '../../constants/index';
|
||||
import { AudioFile } from '../audio_files/entity/audio_file.entity';
|
||||
import { AudioOptionItem as ParamOptionItem } from 'src/features/files/types/types';
|
||||
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TasksRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
* 文字起こしタスクと音声ファイル、オプションアイテムを追加
|
||||
*/
|
||||
async create(
|
||||
account_id: number,
|
||||
owner_user_id: number,
|
||||
priority: string,
|
||||
url: string,
|
||||
file_name: string,
|
||||
author_id: string,
|
||||
work_type_id: string,
|
||||
started_at: Date,
|
||||
duration: string,
|
||||
finished_at: Date,
|
||||
uploaded_at: Date,
|
||||
file_size: number,
|
||||
audio_format: string,
|
||||
comment: string,
|
||||
is_encrypted: boolean,
|
||||
paramOptionItems: ParamOptionItem[],
|
||||
): Promise<Task> {
|
||||
const audioFile = new AudioFile();
|
||||
audioFile.account_id = account_id;
|
||||
audioFile.owner_user_id = owner_user_id;
|
||||
audioFile.url = url;
|
||||
audioFile.file_name = file_name;
|
||||
audioFile.author_id = author_id;
|
||||
audioFile.work_type_id = work_type_id;
|
||||
audioFile.started_at = started_at;
|
||||
audioFile.duration = duration;
|
||||
audioFile.finished_at = finished_at;
|
||||
audioFile.uploaded_at = uploaded_at;
|
||||
audioFile.file_size = file_size;
|
||||
audioFile.priority = priority;
|
||||
audioFile.audio_format = audio_format;
|
||||
audioFile.comment = comment;
|
||||
audioFile.is_encrypted = is_encrypted;
|
||||
|
||||
const task = new Task();
|
||||
task.account_id = account_id;
|
||||
task.is_job_number_enabled = true;
|
||||
task.status = TASK_STATUS.UPLOADED;
|
||||
task.priority = priority;
|
||||
|
||||
const createdEntity = await this.dataSource.transaction(
|
||||
async (entityManager) => {
|
||||
const audioFileRepo = entityManager.getRepository(AudioFile);
|
||||
const newAudioFile = audioFileRepo.create(audioFile);
|
||||
const savedAudioFile = await audioFileRepo.save(newAudioFile);
|
||||
|
||||
task.audio_file_id = savedAudioFile.id;
|
||||
|
||||
const optionItems = paramOptionItems.map((x) => {
|
||||
return {
|
||||
audio_file_id: savedAudioFile.id,
|
||||
label: x.optionItemLabel,
|
||||
value: x.optionItemValue,
|
||||
};
|
||||
});
|
||||
|
||||
const optionItemRepo = entityManager.getRepository(AudioOptionItem);
|
||||
const newAudioOptionItems = optionItemRepo.create(optionItems);
|
||||
await optionItemRepo.save(newAudioOptionItems);
|
||||
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
|
||||
// アカウント内でJOBナンバーが有効なタスクのうち最新のものを取得
|
||||
const lastTask = await taskRepo.findOne({
|
||||
where: { account_id: account_id, is_job_number_enabled: true },
|
||||
order: { created_at: 'DESC', job_number: 'DESC' },
|
||||
});
|
||||
|
||||
let newJobNumber = '00000001';
|
||||
if (!lastTask) {
|
||||
// 初回は00000001
|
||||
newJobNumber = '00000001';
|
||||
} else if (lastTask.job_number === '99999999') {
|
||||
// 末尾なら00000001に戻る
|
||||
newJobNumber = '00000001';
|
||||
} else {
|
||||
// 最新のJOBナンバーをインクリメントして次の番号とする
|
||||
newJobNumber = `${Number(lastTask.job_number) + 1}`.padStart(8, '0');
|
||||
}
|
||||
task.job_number = newJobNumber;
|
||||
|
||||
const newTask = taskRepo.create(task);
|
||||
const persisted = await taskRepo.save(newTask);
|
||||
return persisted;
|
||||
},
|
||||
);
|
||||
return createdEntity;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user