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:
makabe.t 2023-05-30 03:40:36 +00:00
parent 81dca16d5f
commit 3191e22ab6
20 changed files with 739 additions and 13 deletions

View File

@ -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`;

View File

@ -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) => ({

View File

@ -22,6 +22,7 @@ export const ErrorCodes = [
'E000106', // トークンアルゴリズムエラー
'E000107', // トークン不足エラー
'E000108', // トークン権限エラー
'E010001', // パラメータ形式不正エラー
'E010201', // 未認証ユーザエラー
'E010202', // 認証済ユーザエラー
'E010203', // 管理ユーザ権限エラー

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
},
];

View File

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

View File

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

View File

@ -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 {}

View File

@ -0,0 +1,7 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class AudioFilesRepositoryService {
constructor(private dataSource: DataSource) {}
}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -0,0 +1,7 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class AudioOptionItemsRepositoryService {
constructor(private dataSource: DataSource) {}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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;
}
}