diff --git a/dictation_server/src/features/accounts/test/accounts.service.mock.ts b/dictation_server/src/features/accounts/test/accounts.service.mock.ts index 0330884..8c5c68d 100644 --- a/dictation_server/src/features/accounts/test/accounts.service.mock.ts +++ b/dictation_server/src/features/accounts/test/accounts.service.mock.ts @@ -14,7 +14,6 @@ import { UserGroup } from '../../../repositories/user_groups/entity/user_group.e import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; import { AdB2cUser } from '../../../gateways/adb2c/types/types'; import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service'; -import { Context } from '../../../common/log'; import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service'; import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity'; import { WorktypesRepositoryService } from '../../../repositories/worktypes/worktypes.repository.service'; diff --git a/dictation_server/src/features/tasks/tasks.controller.ts b/dictation_server/src/features/tasks/tasks.controller.ts index 72bd402..aa06e6d 100644 --- a/dictation_server/src/features/tasks/tasks.controller.ts +++ b/dictation_server/src/features/tasks/tasks.controller.ts @@ -153,11 +153,41 @@ export class TasksController { '指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します', }) @ApiBearerAuth() + @UseGuards( + RoleGuard.requireds({ + roles: [USER_ROLES.TYPIST], + }), + ) async getNextAudioFile( - @Headers() headers, - @Query() body: AudioNextRequest, + @Req() req: Request, + @Query() param: AudioNextRequest, ): Promise { - return { nextFileId: 1234 }; + const { endedFileId } = param; + + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + const context = makeContext(userId); + + const nextFileId = await this.taskService.getNextTask( + context, + userId, + endedFileId, + ); + + return { nextFileId }; } @Post(':audioFileId/checkout') diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index bc61100..a01cebc 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -20,8 +20,14 @@ import { } from './test/utility'; import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service'; import { makeContext } from '../../common/log'; -import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility'; -import { ADMIN_ROLES, USER_ROLES } from '../../constants'; +import { + makeTestAccount, + makeTestSimpleAccount, + makeTestUser, +} from '../../common/test/utility'; +import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants'; +import { makeTestingModule } from '../../common/test/modules'; +import { createSortCriteria } from '../users/test/utility'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { @@ -2460,3 +2466,480 @@ describe('cancel', () => { ); }); }); + +describe('getNextTask', () => { + let source: DataSource | null = 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 () => { + if (!source) return; + await source.destroy(); + source = null; + }); + + it('次タスクを取得できる(JobNumber順)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId, external_id: typistExternalId } = + await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId1, typistUserId); + + const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000003', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId3, typistUserId); + + const { taskId: taskId2, audioFileId: audioFileId2 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000002', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId2, typistUserId); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id); + + const nextAudioFileId = await service.getNextTask( + context, + typistExternalId, + audioFileId2, + ); + + // 実行結果が正しいか確認 + { + expect(nextAudioFileId).toEqual(audioFileId3); + } + }); + + it('次タスクを取得できる(JobNumber順+優先度)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId, external_id: typistExternalId } = + await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + + const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId1, typistUserId); + + const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '00', + '00000003', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId3, typistUserId); + + const { taskId: taskId2 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000002', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId2, typistUserId); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id); + + const nextAudioFileId = await service.getNextTask( + context, + typistExternalId, + audioFileId1, + ); + + // 実行結果が正しいか確認 + { + expect(nextAudioFileId).toEqual(audioFileId3); + } + }); + + it('次タスクを取得できる(JobNumber順、先頭)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId, external_id: typistExternalId } = + await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + + const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId1, typistUserId); + + const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000003', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId3, typistUserId); + + const { taskId: taskId2 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000002', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId2, typistUserId); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id); + + const nextAudioFileId = await service.getNextTask( + context, + typistExternalId, + audioFileId3, + ); + + // 実行結果が正しいか確認 + { + expect(nextAudioFileId).toEqual(audioFileId1); + } + }); + + it('次タスクを取得できる(Worktype順)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId, external_id: typistExternalId } = + await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + await createSortCriteria(source, typistUserId, 'WORK_TYPE', 'ASC'); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype1', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId1, typistUserId); + + const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype2', + '01', + '00000003', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId3, typistUserId); + + const { taskId: taskId2, audioFileId: audioFileId2 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype3', + '01', + '00000002', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId2, typistUserId); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id); + + const nextAudioFileId = await service.getNextTask( + context, + typistExternalId, + audioFileId3, + ); + + // 実行結果が正しいか確認 + { + expect(nextAudioFileId).toEqual(audioFileId2); + } + }); + + it('次タスクを取得できる(Status順)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId, external_id: typistExternalId } = + await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + await createSortCriteria(source, typistUserId, 'STATUS', 'ASC'); + + const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype1', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId1, typistUserId); + + const { taskId: taskId3 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype2', + '01', + '00000003', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId3, typistUserId); + + const { taskId: taskId2, audioFileId: audioFileId2 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype3', + '01', + '00000002', + TASK_STATUS.PENDING, + ); + await createCheckoutPermissions(source, taskId2, typistUserId); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id); + + const nextAudioFileId = await service.getNextTask( + context, + typistExternalId, + audioFileId2, + ); + + // 実行結果が正しいか確認 + { + expect(nextAudioFileId).toEqual(audioFileId1); + } + }); + + it('次タスクが存在しない場合undefinedを返す(JobNumber順)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId, external_id: typistExternalId } = + await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + + const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype1', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId1, typistUserId); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id); + + const nextAudioFileId = await service.getNextTask( + context, + typistExternalId, + audioFileId1, + ); + + // 実行結果が正しいか確認 + { + expect(nextAudioFileId).toEqual(undefined); + } + }); + it('指定タスクが存在しない場合エラーを返す(JobNumber順)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId, external_id: typistExternalId } = + await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + await createSortCriteria(source, typistUserId, 'WORK_TYPE', 'ASC'); + + const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask( + source, + account.id, + authorUserId, + 'MY_AUTHOR_ID', + 'worktype1', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId1, typistUserId); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id); + + // 実行結果が正しいか確認 + try { + await service.getNextTask(context, typistExternalId, audioFileId1 + 1); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010603')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index 902dfc1..d69a608 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -157,6 +157,81 @@ export class TasksService { this.logger.log(`[OUT] [${context.trackingId}] ${this.getTasks.name}`); } } + + /** + * 完了したタスクの次のタスクを取得します + * @param context + * @param externalId + * @param fileId + * @returns next task + */ + async getNextTask( + context: Context, + externalId: string, + fileId: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getNextTask.name} | params: { externalId: ${externalId}, fileId: ${fileId} };`, + ); + + try { + const { account_id: accountId, id } = + await this.usersRepository.findUserByExternalId(externalId); + + // タスク一覧を取得する + const tasks = await this.taskRepository.getSortedTasks( + accountId, + id, + fileId, + ); + + // 指定タスクのインデックスを取得する + const targetTaskIndex = tasks.findIndex((x) => x.audio_file_id == fileId); + + // 指定したタスクが見つからない場合はエラーとする(リポジトリからは必ず取得できる想定) + if (targetTaskIndex === -1) { + throw new TasksNotFoundError(`task not found: ${fileId}`); + } + + // ソート順に並んだタスクについて、指定した完了済みタスクの次のタスクを取得する + let nextTaskIndex = targetTaskIndex + 1; + + // 次のタスクがない場合は先頭のタスクを返す + if (tasks.length - 1 < nextTaskIndex) { + nextTaskIndex = 0; + } + + const nextTask = tasks[nextTaskIndex]; + + // 先頭のタスクが指定した完了済みタスクの場合は次のタスクがないためundefinedを返す + return nextTask.audio_file_id === fileId + ? undefined + : nextTask.audio_file_id; + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case TasksNotFoundError: + throw new HttpException( + makeErrorResponse('E010603'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log(`[OUT] [${context.trackingId}] ${this.getNextTask.name}`); + } + } + /** * 指定した音声ファイルに紐づくタスクをcheckoutする * @param audioFileId diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 8b8d1c4..65e629b 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -110,7 +110,7 @@ export const createTask = async ( jobNumber: string, status: string, typist_user_id?: number | undefined, -): Promise<{ taskId: number }> => { +): Promise<{ taskId: number; audioFileId: number }> => { const { identifiers: audioFileIdentifiers } = await datasource .getRepository(AudioFile) .insert({ @@ -144,7 +144,7 @@ export const createTask = async ( created_at: new Date(), }); const task = taskIdentifiers.pop() as Task; - return { taskId: task.id }; + return { taskId: task.id, audioFileId: audioFile.id }; }; /** * @@ -162,8 +162,8 @@ export const createCheckoutPermissions = async ( ): Promise => { await datasource.getRepository(CheckoutPermission).insert({ task_id: task_id, - user_id: user_id, - user_group_id: user_group_id, + user_id: user_id ?? null, + user_group_id: user_group_id ?? null, }); }; /** diff --git a/dictation_server/src/features/users/test/utility.ts b/dictation_server/src/features/users/test/utility.ts index ab4dfe2..58a7512 100644 --- a/dictation_server/src/features/users/test/utility.ts +++ b/dictation_server/src/features/users/test/utility.ts @@ -35,6 +35,7 @@ import { License } from '../../../repositories/licenses/entity/license.entity'; import { AdB2cMockValue, makeAdB2cServiceMock } from './users.service.mock'; import { AdB2cService } from '../../../gateways/adb2c/adb2c.service'; import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../../constants'; +import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity'; export const getLicenses = async ( datasource: DataSource, @@ -164,3 +165,16 @@ export const makeTestingModuleWithAdb2c = async ( console.log(e); } }; + +export const createSortCriteria = async ( + datasource: DataSource, + userId: number, + parameter: string, + direction: string, +): Promise => { + await datasource.getRepository(SortCriteria).insert({ + user_id: userId, + parameter: parameter, + direction: direction, + }); +}; 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 f0b6608..3653872 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 @@ -7,7 +7,6 @@ import { Column, PrimaryGeneratedColumn, JoinColumn, - OneToOne, ManyToOne, } from 'typeorm'; @@ -25,11 +24,11 @@ export class CheckoutPermission { @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) user_group_id: number | null; - @OneToOne(() => User, (user) => user.id) + @ManyToOne(() => User, (user) => user.id) @JoinColumn({ name: 'user_id' }) user: User | null; - @OneToOne(() => UserGroup, (group) => group.id) + @ManyToOne(() => UserGroup, (group) => group.id) @JoinColumn({ name: 'user_group_id' }) user_group: UserGroup | null; diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index b6edf11..63a813a 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -15,6 +15,8 @@ import { CheckoutPermission } from '../checkout_permissions/entity/checkout_perm import { SortDirection, TaskListSortableAttribute, + isSortDirection, + isTaskListSortableAttribute, } from '../../common/types/sort'; import { UserGroupMember } from '../user_groups/entity/user_group_member.entity'; import { Assignee } from '../../features/tasks/types/types'; @@ -32,6 +34,7 @@ import { } from './errors/types'; import { Roles } from '../../common/types/role'; import { TaskStatus, isTaskStatus } from '../../common/types/taskStatus'; +import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity'; @Injectable() export class TasksRepositoryService { @@ -853,6 +856,102 @@ export class TasksRepositoryService { return await checkoutPermissionRepo.save(checkoutPermissions); }); } + + /** + * 対象ユーザーのソート順でソートしたタスク一覧を取得します(指定タスクとユーザが着手可能なタスクの一覧を取得します) + * @param accountId + * @param userId + * @param audioFileId + * @returns sorted tasks + */ + async getSortedTasks( + accountId: number, + userId: number, + audioFileId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const taskRepo = entityManager.getRepository(Task); + const sortRepo = entityManager.getRepository(SortCriteria); + + const sort = await sortRepo.findOne({ where: { user_id: userId } }); + + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!sort) { + throw new Error(`sort criteria not found. userId: ${userId}`); + } + + const { direction, parameter } = sort; + //型チェック + if ( + !isTaskListSortableAttribute(parameter) || + !isSortDirection(direction) + ) { + throw new Error( + `The value stored in the DB is invalid. parameter: ${parameter}, direction: ${direction}`, + ); + } + + // 指定した音声ファイルIDのタスクを取得 + const targetTask = await taskRepo.findOne({ + where: { + account_id: accountId, + audio_file_id: audioFileId, + status: In([ + TASK_STATUS.PENDING, + TASK_STATUS.FINISHED, + TASK_STATUS.UPLOADED, + ]), + }, + }); + + if (!targetTask) { + throw new TasksNotFoundError( + `target task not found. audioFileId: ${audioFileId}`, + ); + } + + const groupMemberRepo = entityManager.getRepository(UserGroupMember); + // ユーザーの所属するすべてのグループを列挙 + const groups = await groupMemberRepo.find({ where: { user_id: userId } }); + // ユーザーの所属するすべてのグループIDを列挙 + const groupIds = groups.map((member) => member.user_group_id); + + const checkoutRepo = entityManager.getRepository(CheckoutPermission); + // ユーザーに対するチェックアウト権限、またはユーザーの所属するユーザーグループのチェックアウト権限を取得 + const related = await checkoutRepo.find({ + where: [ + // ユーザーがチェックアウト可能である + { user_id: userId }, + // ユーザーの所属するユーザーグループがチェックアウト可能である + { user_group_id: In(groupIds) }, + ], + }); + + // ユーザー本人、またはユーザーが所属するユーザーグループがチェックアウト可能なタスクIDの一覧を作成 + const relatedTaskIds = related.map((permission) => permission.task_id); + + const order = makeOrder(parameter, direction); + + // 引数の音声ファイルIDで指定したタスクとユーザが着手可能なタスクの一覧を取得 + const tasks = await taskRepo.find({ + where: [ + { + account_id: accountId, + id: targetTask.id, + }, + { + account_id: accountId, + status: In([TASK_STATUS.UPLOADED, TASK_STATUS.PENDING]), + // TypistまたはTypistが所属するユーザーグループが割り当て可能になっているTaskを取得 + id: In(relatedTaskIds), + }, + ], + order: order, + }); + + return tasks; + }); + } } // ソート用オブジェクトを生成する