From 1189e676b92ff14985f5853d6475d698e770b293 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 3 Jul 2023 01:09:06 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20178:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=82=BF=E3=82=B9=E3=82=AF=E3=83=81=E3=82=A7=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=A2=E3=82=A6=E3=83=88API=20(Typist)=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task1996: API実装(タスクチェックアウトAPI (Typist))](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1996) - タスクチェックアウトAPIのTypist用の処理を実装 - テスト実装 ## レビューポイント - DBアクセス処理に不足はないか - テスト内容に不足はないか - テストのチェック方法に問題はないか - 特に今回の「started_at」はcheckoutした日時を入れるが、それがいつなのかを完全一致でチェックするのは大変なため、checkout前とcheckout後で値が異なっていることを確認するまでのチェックとした - changeCheckoutPermissionsのPathパラメータにつけたコメントについて ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 - テストが通ることを確認 ## 補足 - 「タスク 1476: [Sp12-1]アクセストークンの寿命を2時間にする」も実施しています --- dictation_server/.env | 2 +- dictation_server/src/api/odms/openapi.json | 1 + dictation_server/src/common/error/code.ts | 3 +- dictation_server/src/common/error/message.ts | 1 + .../src/features/tasks/errors/types.ts | 2 + .../src/features/tasks/tasks.controller.ts | 27 +- .../src/features/tasks/tasks.service.spec.ts | 287 +++++++++++++++++- .../src/features/tasks/tasks.service.ts | 55 ++++ .../src/features/tasks/test/utility.ts | 136 +++++++-- .../src/features/tasks/types/types.ts | 3 + .../entity/checkout_permission.entity.ts | 6 +- .../src/repositories/tasks/errors/types.ts | 2 + .../tasks/tasks.repository.service.ts | 103 +++++++ .../entity/user_group_member.entity.ts | 2 +- 14 files changed, 592 insertions(+), 38 deletions(-) create mode 100644 dictation_server/src/features/tasks/errors/types.ts diff --git a/dictation_server/.env b/dictation_server/.env index a362c4e..cfcf9f8 100644 --- a/dictation_server/.env +++ b/dictation_server/.env @@ -6,7 +6,7 @@ DB_ROOT_PASS=omdsdbpass DB_USERNAME=omdsdbuser DB_PASSWORD=omdsdbpass NO_COLOR=TRUE -ACCESS_TOKEN_LIFETIME_WEB=1600000 +ACCESS_TOKEN_LIFETIME_WEB=7200000 REFRESH_TOKEN_LIFETIME_WEB=86400000 REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 TENANT_NAME=adb2codmsdev diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index d20932f..085d944 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1317,6 +1317,7 @@ "name": "audioFileId", "required": true, "in": "path", + "description": "ODMS Cloud上の音声ファイルID", "schema": { "type": "number" } } ], diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index d885497..08823d3 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -34,5 +34,6 @@ export const ErrorCodes = [ 'E010302', // authorId重複エラー 'E010401', // PONumber重複エラー 'E010501', // アカウント不在エラー - 'E010601', // タスク変更不可エラー + 'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) + 'E010602', // タスク変更権限不足エラー ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 720ea35..4e74380 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -24,4 +24,5 @@ export const errors: Errors = { E010401: 'This PoNumber already used Error', E010501: 'Account not Found Error.', E010601: 'Task is not Editable Error', + E010602: 'No task edit permissions Error', }; diff --git a/dictation_server/src/features/tasks/errors/types.ts b/dictation_server/src/features/tasks/errors/types.ts new file mode 100644 index 0000000..a9faba0 --- /dev/null +++ b/dictation_server/src/features/tasks/errors/types.ts @@ -0,0 +1,2 @@ +// ロール不正エラー +export class InvalidRoleError extends Error {} diff --git a/dictation_server/src/features/tasks/tasks.controller.ts b/dictation_server/src/features/tasks/tasks.controller.ts index 6920119..38fcf6a 100644 --- a/dictation_server/src/features/tasks/tasks.controller.ts +++ b/dictation_server/src/features/tasks/tasks.controller.ts @@ -16,6 +16,7 @@ import { ApiOperation, ApiTags, ApiBearerAuth, + ApiParam, } from '@nestjs/swagger'; import { ErrorResponse } from '../../common/error/types/types'; import { Request } from 'express'; @@ -170,12 +171,19 @@ export class TasksController { }) @ApiBearerAuth() async checkout( - @Headers() headers, - @Param() params: ChangeStatusRequest, + @Req() req: Request, + @Param() param: ChangeStatusRequest, ): Promise { - const { audioFileId } = params; - console.log(audioFileId); + // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない + const accessToken = retrieveAuthorizationToken(req); + const { role, userId } = jwt.decode(accessToken, { + json: true, + }) as AccessToken; + // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う + const roles = role.split(' ') as Roles[]; + + await this.taskService.checkout(param.audioFileId, roles, userId); return {}; } @@ -420,6 +428,11 @@ export class TasksController { operationId: 'changeCheckoutPermission', description: '指定した文字起こしタスクのチェックアウト候補を変更します。', }) + @ApiParam({ + name: 'audioFileId', + required: true, + description: 'ODMS Cloud上の音声ファイルID', + }) @ApiBearerAuth() @UseGuards(AuthGuard) @UseGuards( @@ -427,7 +440,9 @@ export class TasksController { ) async changeCheckoutPermission( @Req() req: Request, - @Param(`audioFileId`, ParseIntPipe) audioFileId: number, + //TODOcheckoutやcheckinと同じパスパラメータなので記述方法を統一したい + @Param(`audioFileId`, ParseIntPipe) + audioFileId: number, @Body() body: PostCheckoutPermissionRequest, ): Promise { const { assignees } = body; @@ -436,7 +451,7 @@ export class TasksController { const { role, userId } = jwt.decode(accessToken, { json: true, }) as AccessToken; - // RoleGuardでroleの要素が正しい値であることは担保されているためここでは型変換のみ行う + // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; await this.taskService.changeCheckoutPermission( diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 59890f9..a619585 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -8,7 +8,15 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TasksService } from './tasks.service'; import { DataSource } from 'typeorm'; -import { createAccount, createTask, createUser } from './test/utility'; +import { + createAccount, + createCheckoutPermissions, + createTask, + createUser, + createUserGroup, + getCheckoutPermissions, + getTask, +} from './test/utility'; import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service'; import { makeTestingModule } from '../../common/test/modules'; import { TasksNotFoundError } from '../../repositories/tasks/errors/types'; @@ -723,3 +731,280 @@ describe('changeCheckoutPermission', () => { ); }); }); + +describe('checkout', () => { + 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('ユーザーのRoleがTypistで、タスクのチェックアウト権限が個人指定である時、タスクをチェックアウトできる', async () => { + const module = await makeTestingModule(source); + const { accountId } = await createAccount(source); + const { userId: typistUserId } = await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + ); + const { userId: authorUserId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'MY_AUTHOR_ID', + ); + const { taskId } = await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Uploaded', + ); + const { userGroupId } = await createUserGroup( + source, + accountId, + 'USER_GROUP_A', + typistUserId, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + await createCheckoutPermissions(source, taskId, undefined, userGroupId); + + const service = module.get(TasksService); + + const initTask = await getTask(source, taskId); + + await service.checkout(1, ['typist'], 'typist-user-external-id'); + const { status, typist_user_id, started_at } = await getTask( + source, + taskId, + ); + const permisions = await getCheckoutPermissions(source, taskId); + + expect(status).toEqual('InProgress'); + expect(typist_user_id).toEqual(typistUserId); + expect(started_at).not.toEqual(initTask.started_at); + expect(permisions.length).toEqual(1); + expect(permisions[0]).toEqual({ + id: 3, + task_id: 1, + user_id: 1, + user_group_id: null, + }); + }, 600000); + + it('ユーザーのRoleがTypistで、タスクのチェックアウト権限がグループ指定である時、タスクをチェックアウトできる', async () => { + const module = await makeTestingModule(source); + const { accountId } = await createAccount(source); + const { userId: typistUserId } = await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + ); + const { userId: authorUserId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'MY_AUTHOR_ID', + ); + const { taskId } = await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Uploaded', + ); + const { userGroupId } = await createUserGroup( + source, + accountId, + 'USER_GROUP_A', + typistUserId, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + await createCheckoutPermissions(source, taskId, undefined, userGroupId); + + const service = module.get(TasksService); + + const initTask = await getTask(source, taskId); + + await service.checkout(1, ['typist'], 'typist-user-external-id'); + const { status, typist_user_id, started_at } = await getTask( + source, + taskId, + ); + const permisions = await getCheckoutPermissions(source, taskId); + + expect(status).toEqual('InProgress'); + expect(typist_user_id).toEqual(typistUserId); + expect(started_at).not.toEqual(initTask.started_at); + expect(permisions.length).toEqual(1); + expect(permisions[0]).toEqual({ + id: 3, + task_id: 1, + user_id: 1, + user_group_id: null, + }); + }); + + it('ユーザーのRoleがTypistで、タスクのステータスがPendingである時、タスクをチェックアウトできる', async () => { + const module = await makeTestingModule(source); + const { accountId } = await createAccount(source); + const { userId: typistUserId } = await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + ); + const { userId: authorUserId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'MY_AUTHOR_ID', + ); + const { taskId } = await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Pending', + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + const service = module.get(TasksService); + + const initTask = await getTask(source, taskId); + + await service.checkout(1, ['typist'], 'typist-user-external-id'); + const { status, typist_user_id, started_at } = await getTask( + source, + taskId, + ); + const permisions = await getCheckoutPermissions(source, taskId); + + expect(status).toEqual('InProgress'); + expect(typist_user_id).toEqual(typistUserId); + //タスクの元々のステータスがPending,Inprogressの場合、文字起こし開始時刻は更新されない + expect(started_at).toEqual(initTask.started_at); + expect(permisions.length).toEqual(1); + expect(permisions[0]).toEqual({ + id: 2, + task_id: 1, + user_id: 1, + user_group_id: null, + }); + }); + + it('ユーザーのRoleがTypistで、対象のタスクのStatus[Uploaded,Inprogress,Pending]以外の時、タスクをチェックアウトできない', async () => { + const module = await makeTestingModule(source); + const { accountId } = await createAccount(source); + await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + 'MY_AUTHOR_ID', + ); + const { userId: authorUserId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'MY_AUTHOR_ID', + ); + await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Backup', + ); + + const service = module.get(TasksService); + await expect( + service.checkout(1, ['typist'], 'typist-user-external-id'), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST), + ); + }); + + it('ユーザーのRoleがTypistで、チェックアウト権限が存在しない時、タスクをチェックアウトできない', async () => { + const module = await makeTestingModule(source); + const { accountId } = await createAccount(source); + await createUser( + source, + accountId, + 'typist-user-external-id', + 'typist', + 'MY_AUTHOR_ID', + ); + const { userId: authorUserId } = await createUser( + source, + accountId, + 'author-user-external-id', + 'author', + 'MY_AUTHOR_ID', + ); + await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Uploaded', + ); + + const service = module.get(TasksService); + await expect( + service.checkout(1, ['typist'], 'typist-user-external-id'), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010602'), HttpStatus.BAD_REQUEST), + ); + }); + + it('ユーザーのRoleに[Typist,author]が設定されていない時、タスクをチェックアウトできない', async () => { + const module = await makeTestingModule(source); + const { accountId } = await createAccount(source); + await createUser( + source, + accountId, + 'none-user-external-id', + 'none', + 'MY_AUTHOR_ID', + ); + + const service = module.get(TasksService); + await expect( + service.checkout(1, ['none'], 'none-user-external-id'), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010602'), HttpStatus.BAD_REQUEST), + ); + }); +}); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index f46be41..d4b9d01 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -18,11 +18,13 @@ import { import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { CheckoutPermission } from '../../repositories/checkout_permissions/entity/checkout_permission.entity'; import { + CheckoutPermissionNotFoundError, TasksNotFoundError, TypistUserGroupNotFoundError, TypistUserNotFoundError, } from '../../repositories/tasks/errors/types'; import { Roles } from '../../common/types/role'; +import { InvalidRoleError } from './errors/types'; @Injectable() export class TasksService { @@ -132,6 +134,59 @@ export class TasksService { ); } } + /** + * 指定した音声ファイルに紐づくタスクをcheckoutする + * @param audioFileId + * @param roles + * @param externalId + * @returns checkout + */ + async checkout( + audioFileId: number, + roles: Roles[], + externalId: string, + ): Promise { + try { + const { id, account_id } = + await this.usersRepository.findUserByExternalId(externalId); + // TODO authorの処理は別タスクで対応 + if (roles.includes(USER_ROLES.AUTHOR)) { + } + + if (roles.includes(USER_ROLES.TYPIST)) { + return await this.taskRepository.checkout(audioFileId, account_id, id); + } + + throw new InvalidRoleError(`invalid roles: ${roles.join(',')}`); + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case CheckoutPermissionNotFoundError: + case InvalidRoleError: + throw new HttpException( + makeErrorResponse('E010602'), + HttpStatus.BAD_REQUEST, + ); + case TasksNotFoundError: + throw new HttpException( + makeErrorResponse('E010601'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + private async getB2cUsers( tasks: TaskEntity[], permissions: CheckoutPermission[], diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index b872c69..e527fe8 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -3,6 +3,9 @@ import { User } from '../../../repositories/users/entity/user.entity'; import { Account } from '../../../repositories/accounts/entity/account.entity'; import { Task } from '../../../repositories/tasks/entity/task.entity'; import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity'; +import { CheckoutPermission } from '../../../repositories/checkout_permissions/entity/checkout_permission.entity'; +import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity'; +import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity'; export const createAccount = async ( datasource: DataSource, @@ -59,31 +62,114 @@ export const createTask = async ( priority: string, jobNumber: string, status: string, +): Promise<{ taskId: number }> => { + const { identifiers: audioFileIdentifiers } = await datasource + .getRepository(AudioFile) + .insert({ + account_id: account_id, + owner_user_id: owner_user_id, + url: '', + file_name: 'x.zip', + author_id: author_id, + work_type_id: work_type_id, + started_at: new Date(), + duration: '100000', + finished_at: new Date(), + uploaded_at: new Date(), + file_size: 10000, + priority: priority, + audio_format: 'audio_format', + is_encrypted: true, + }); + const audioFile = audioFileIdentifiers.pop() as AudioFile; + const { identifiers: taskIdentifiers } = await datasource + .getRepository(Task) + .insert({ + job_number: jobNumber, + account_id: account_id, + is_job_number_enabled: true, + audio_file_id: audioFile.id, + status: status, + priority: priority, + started_at: new Date().toISOString(), + created_at: new Date(), + }); + const task = taskIdentifiers.pop() as Task; + return { taskId: task.id }; +}; +/** + * + * @param datasource + * @param task_id + * @param user_id + * @param user_group_id + * + */ +export const createCheckoutPermissions = async ( + datasource: DataSource, + task_id: number, + user_id?: number, + user_group_id?: number, ): Promise => { - const { identifiers } = await datasource.getRepository(AudioFile).insert({ - account_id: account_id, - owner_user_id: owner_user_id, - url: '', - file_name: 'x.zip', - author_id: author_id, - work_type_id: work_type_id, - started_at: new Date(), - duration: '100000', - finished_at: new Date(), - uploaded_at: new Date(), - file_size: 10000, - priority: priority, - audio_format: 'audio_format', - is_encrypted: true, - }); - const audioFile = identifiers.pop() as AudioFile; - await datasource.getRepository(Task).insert({ - job_number: jobNumber, - account_id: account_id, - is_job_number_enabled: true, - audio_file_id: audioFile.id, - status: status, - priority: priority, - created_at: new Date(), + await datasource.getRepository(CheckoutPermission).insert({ + task_id: task_id, + user_id: user_id, + user_group_id: user_group_id, }); }; +/** + * + * @param datasource + * @param account_id + * @param user_group_name + * @param user_id + * @returns + */ +export const createUserGroup = async ( + datasource: DataSource, + account_id: number, + user_group_name: string, + user_id: number, +): Promise<{ userGroupId: number }> => { + const { identifiers: userGroupIdentifiers } = await datasource + .getRepository(UserGroup) + .insert({ + account_id: account_id, + name: user_group_name, + created_by: 'test', + updated_by: 'test', + }); + const userGroup = userGroupIdentifiers.pop() as UserGroup; + await datasource.getRepository(UserGroupMember).insert({ + user_group_id: userGroup.id, + user_id: user_id, + created_by: 'test', + updated_by: 'test', + }); + + return { userGroupId: userGroup.id }; +}; + +export const getTask = async ( + datasource: DataSource, + task_id: number, +): Promise => { + const task = await datasource.getRepository(Task).findOne({ + where: { + id: task_id, + }, + }); + return task; +}; + +export const getCheckoutPermissions = async ( + datasource: DataSource, + task_id: number, +): Promise => { + const permissions = await datasource.getRepository(CheckoutPermission).find({ + where: { + task_id: task_id, + }, + }); + return permissions; +}; diff --git a/dictation_server/src/features/tasks/types/types.ts b/dictation_server/src/features/tasks/types/types.ts index c5aba6d..ac11e1b 100644 --- a/dictation_server/src/features/tasks/types/types.ts +++ b/dictation_server/src/features/tasks/types/types.ts @@ -203,6 +203,9 @@ export class AudioNextResponse { export class ChangeStatusRequest { @ApiProperty({ description: 'ODMS Cloud上の音声ファイルID' }) + @Type(() => Number) + @IsInt() + @Min(1) audioFileId: number; } diff --git a/dictation_server/src/repositories/checkout_permissions/entity/checkout_permission.entity.ts b/dictation_server/src/repositories/checkout_permissions/entity/checkout_permission.entity.ts index fb03060..cd6b116 100644 --- a/dictation_server/src/repositories/checkout_permissions/entity/checkout_permission.entity.ts +++ b/dictation_server/src/repositories/checkout_permissions/entity/checkout_permission.entity.ts @@ -15,13 +15,13 @@ export class CheckoutPermission { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({}) task_id: number; - @Column() + @Column({ nullable: true }) user_id?: number; - @Column() + @Column({ nullable: true }) user_group_id?: number; @OneToOne(() => User, (user) => user.id) diff --git a/dictation_server/src/repositories/tasks/errors/types.ts b/dictation_server/src/repositories/tasks/errors/types.ts index 8d828d4..1799418 100644 --- a/dictation_server/src/repositories/tasks/errors/types.ts +++ b/dictation_server/src/repositories/tasks/errors/types.ts @@ -4,3 +4,5 @@ export class TypistUserGroupNotFoundError extends Error {} export class TypistUserNotFoundError extends Error {} // タスク未発見エラー export class TasksNotFoundError extends Error {} +// チェックアウト権限未発見エラー +export class CheckoutPermissionNotFoundError extends Error {} diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index e338883..9ecf93d 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -21,6 +21,7 @@ import { Assignee } from '../../features/tasks/types/types'; import { UserGroup } from '../user_groups/entity/user_group.entity'; import { User } from '../users/entity/user.entity'; import { + CheckoutPermissionNotFoundError, TasksNotFoundError, TypistUserGroupNotFoundError, TypistUserNotFoundError, @@ -30,6 +31,108 @@ import { Roles } from '../../common/types/role'; @Injectable() export class TasksRepositoryService { constructor(private dataSource: DataSource) {} + + async checkout( + audioFileId: number, + account_id: number, + user_id: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const taskRepo = entityManager.getRepository(Task); + // 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得 + const task = await taskRepo.findOne({ + relations: { + file: true, + option_items: true, + typist_user: true, + }, + where: { + audio_file_id: audioFileId, + account_id: account_id, + status: In([ + TASK_STATUS.UPLOADED, + TASK_STATUS.IN_PROGRESS, + TASK_STATUS.PENDING, + ]), + }, + }); + if (!task) { + throw new TasksNotFoundError( + `task not found. audio_file_id:${audioFileId}`, + ); + } + + const groupMemberRepo = entityManager.getRepository(UserGroupMember); + // ユーザーの所属するすべてのグループを列挙 + const groups = await groupMemberRepo.find({ + relations: { + user: true, + }, + where: { + user: { + id: user_id, + }, + }, + }); + // ユーザーの所属するすべてのグループIDを列挙 + const groupIds = groups.map((member) => member.user_group_id); + + const checkoutRepo = entityManager.getRepository(CheckoutPermission); + // 対象タスクに紐づくユーザーが含まれるチェックアウト権限を取得する + const related = await checkoutRepo.find({ + relations: { + task: true, + }, + where: [ + { + task_id: task.id, + // ユーザーがチェックアウト可能である + user: { + id: user_id, + }, + }, + { + task_id: task.id, + // ユーザーの所属するユーザーグループがチェックアウト可能である + user_group_id: In(groupIds), + }, + ], + }); + + //チェックアウト権限がなければエラー + if (related.length === 0) { + throw new CheckoutPermissionNotFoundError( + `Checkout Permission not found. task_id:${task.id}, user_id:${user_id}, user_group_id:${groupIds}`, + ); + } + + // 対象タスクの文字起こし開始日時を現在時刻に更新。割り当てユーザーを自身のユーザーIDに更新 + // タスクのステータスがUploaded以外の場合、文字起こし開始時刻は更新しない + await taskRepo.update( + { audio_file_id: audioFileId }, + { + started_at: + task.status === TASK_STATUS.UPLOADED + ? new Date().toISOString() + : undefined, + typist_user_id: user_id, + status: TASK_STATUS.IN_PROGRESS, + }, + ); + + //対象のタスクに紐づくチェックアウト権限レコードを削除 + await checkoutRepo.delete({ + task_id: task.id, + }); + + //対象のタスクチェックアウト権限を自身のユーザーIDで作成 + await checkoutRepo.save({ + task_id: task.id, + user_id: user_id, + }); + }); + } + /** * 指定したアカウントIDに紐づくTask関連情報の一覧を取得します * @param account_id diff --git a/dictation_server/src/repositories/user_groups/entity/user_group_member.entity.ts b/dictation_server/src/repositories/user_groups/entity/user_group_member.entity.ts index 15aba70..d010434 100644 --- a/dictation_server/src/repositories/user_groups/entity/user_group_member.entity.ts +++ b/dictation_server/src/repositories/user_groups/entity/user_group_member.entity.ts @@ -16,7 +16,7 @@ export class UserGroupMember { user_group_id: number; @Column() - user_id: string; + user_id: number; @Column({ nullable: true }) deleted_at?: Date;