diff --git a/dictation_server/db/migrations/007-add_task_column_created.sql b/dictation_server/db/migrations/007-add_task_column_created.sql new file mode 100644 index 0000000..3ca2b87 --- /dev/null +++ b/dictation_server/db/migrations/007-add_task_column_created.sql @@ -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`; \ No newline at end of file diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 1d76792..5c7821f 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -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) => ({ diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index e19c4ac..c8d799c 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -22,6 +22,7 @@ export const ErrorCodes = [ 'E000106', // トークンアルゴリズムエラー 'E000107', // トークン不足エラー 'E000108', // トークン権限エラー + 'E010001', // パラメータ形式不正エラー 'E010201', // 未認証ユーザエラー 'E010202', // 認証済ユーザエラー 'E010203', // 管理ユーザ権限エラー diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 31f5be9..0f8121f 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -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.', diff --git a/dictation_server/src/common/jwt/jwt.ts b/dictation_server/src/common/jwt/jwt.ts index 00a5714..72df919 100644 --- a/dictation_server/src/common/jwt/jwt.ts +++ b/dictation_server/src/common/jwt/jwt.ts @@ -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 = ( */ export const decode = (token: string): T | VerifyError => { try { - const payload = jwt.decode(token, { + const payload = jwtDecode(token, { json: true, }) as T; return payload; diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 3548667..d3fc87e 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -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; diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index 126d7c8..7d37307 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -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 { - 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') diff --git a/dictation_server/src/features/files/files.module.ts b/dictation_server/src/features/files/files.module.ts index 0b78d44..c69967f 100644 --- a/dictation_server/src/features/files/files.module.ts +++ b/dictation_server/src/features/files/files.module.ts @@ -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], }) diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 1986c7b..2b55cbf 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -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', + }, +]; diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 71154c3..f18c6fd 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -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 { + 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 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 a303346..ed95073 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -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 => { 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, []>().mockRejectedValue(create) + : jest.fn, []>().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(), + }, + }; + }; diff --git a/dictation_server/src/repositories/audio_files/audio_files.repository.module.ts b/dictation_server/src/repositories/audio_files/audio_files.repository.module.ts new file mode 100644 index 0000000..8a11afb --- /dev/null +++ b/dictation_server/src/repositories/audio_files/audio_files.repository.module.ts @@ -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 {} diff --git a/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts b/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts new file mode 100644 index 0000000..b9605e8 --- /dev/null +++ b/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class AudioFilesRepositoryService { + constructor(private dataSource: DataSource) {} +} diff --git a/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts b/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts new file mode 100644 index 0000000..2d4cd79 --- /dev/null +++ b/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts @@ -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; +} diff --git a/dictation_server/src/repositories/audio_option_items/audio_option_items.repository.module.ts b/dictation_server/src/repositories/audio_option_items/audio_option_items.repository.module.ts new file mode 100644 index 0000000..901ffea --- /dev/null +++ b/dictation_server/src/repositories/audio_option_items/audio_option_items.repository.module.ts @@ -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 {} diff --git a/dictation_server/src/repositories/audio_option_items/audio_option_items.repository.service.ts b/dictation_server/src/repositories/audio_option_items/audio_option_items.repository.service.ts new file mode 100644 index 0000000..ca31094 --- /dev/null +++ b/dictation_server/src/repositories/audio_option_items/audio_option_items.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class AudioOptionItemsRepositoryService { + constructor(private dataSource: DataSource) {} +} diff --git a/dictation_server/src/repositories/audio_option_items/entity/audio_option_item.entity.ts b/dictation_server/src/repositories/audio_option_items/entity/audio_option_item.entity.ts new file mode 100644 index 0000000..d85616d --- /dev/null +++ b/dictation_server/src/repositories/audio_option_items/entity/audio_option_item.entity.ts @@ -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; +} diff --git a/dictation_server/src/repositories/tasks/entity/task.entity.ts b/dictation_server/src/repositories/tasks/entity/task.entity.ts new file mode 100644 index 0000000..8a016fa --- /dev/null +++ b/dictation_server/src/repositories/tasks/entity/task.entity.ts @@ -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; +} diff --git a/dictation_server/src/repositories/tasks/tasks.repository.module.ts b/dictation_server/src/repositories/tasks/tasks.repository.module.ts new file mode 100644 index 0000000..0de0586 --- /dev/null +++ b/dictation_server/src/repositories/tasks/tasks.repository.module.ts @@ -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 {} diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts new file mode 100644 index 0000000..1f56d92 --- /dev/null +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -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 { + 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; + } +}