diff --git a/dictation_server/package.json b/dictation_server/package.json index ad284a5..2d79ef0 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -23,7 +23,9 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "og": "openapi-generator-cli", - "openapi-format": "cat \"src/api/odms/openapi.json\" | jq -c . > \"src/api/odms/openapi.json\" && prettier --write \"src/api/odms/*.json\"" + "openapi-format": "cat \"src/api/odms/openapi.json\" | jq -c . > \"src/api/odms/openapi.json\" && prettier --write \"src/api/odms/*.json\"", + "migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=local", + "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=local" }, "dependencies": { "@azure/identity": "^3.1.3", diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 1e1169d..017192b 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -36,6 +36,8 @@ import { BlobstorageModule } from './gateways/blobstorage/blobstorage.module'; import { LicensesModule } from './features/licenses/licenses.module'; import { LicensesService } from './features/licenses/licenses.service'; import { LicensesController } from './features/licenses/licenses.controller'; +import { CheckoutPermissionsRepositoryModule } from './repositories/checkout_permissions/checkout_permissions.repository.module'; +import { UserGroupsRepositoryModule } from './repositories/user_groups/user_groups.repository.module'; import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module'; @Module({ @@ -62,6 +64,8 @@ import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_ AudioFilesRepositoryModule, AudioOptionItemsRepositoryModule, TasksRepositoryModule, + CheckoutPermissionsRepositoryModule, + UserGroupsRepositoryModule, TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 928b56e..12110eb 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -101,6 +101,13 @@ export const USER_ROLES = { TYPIST: 'typist', } as const; +/** + * Token.roleに配置されうる文字列リテラル型 + */ +export type Roles = + | (typeof ADMIN_ROLES)[keyof typeof ADMIN_ROLES] + | (typeof USER_ROLES)[keyof typeof USER_ROLES]; + /** * ライセンス注文ステータス(発行待ち) * @const {string} diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index b536394..645be8c 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -241,6 +241,7 @@ describe('FilesService', () => { const userRepoParam = makeDefaultUsersRepositoryMockValue(); const service = await makeFilesServiceMock(blobParam, userRepoParam, { create: new Error(''), + getTasksFromAccountId: new Error(), }); await expect( 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 ed95073..c6b87cb 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -18,6 +18,7 @@ export type UsersRepositoryMockValue = { export type TasksRepositoryMockValue = { create: Task | Error; + getTasksFromAccountId: { tasks: Task[]; count: number } | Error; }; export const makeFilesServiceMock = async ( @@ -148,5 +149,9 @@ export const makeDefaultTasksRepositoryMockValue = priority: '01', created_at: new Date(), }, + getTasksFromAccountId: { + tasks: [], + count: 0, + }, }; }; diff --git a/dictation_server/src/features/tasks/tasks.controller.ts b/dictation_server/src/features/tasks/tasks.controller.ts index 1e24675..304db38 100644 --- a/dictation_server/src/features/tasks/tasks.controller.ts +++ b/dictation_server/src/features/tasks/tasks.controller.ts @@ -6,6 +6,8 @@ import { Param, Post, Query, + Req, + UseGuards, } from '@nestjs/common'; import { ApiResponse, @@ -23,6 +25,14 @@ import { TasksRequest, TasksResponse, } from './types/types'; +import { + isSortDirection, + isTaskListSortableAttribute, +} from '../../common/types/sort'; +import jwt from 'jsonwebtoken'; +import { retrieveAuthorizationToken } from '../../common/http/helper'; +import { AccessToken } from '../../common/token'; +import { AuthGuard } from 'src/common/guards/auth/authguards'; @ApiTags('tasks') @Controller('tasks') @@ -54,19 +64,32 @@ export class TasksController { description: '音声ファイル・文字起こしタスク情報をページ指定して取得します', }) @ApiBearerAuth() + @UseGuards(AuthGuard) @Get() async getTasks( - @Headers() headers, + @Req() req, @Query() body: TasksRequest, ): Promise { - console.log(headers); - console.log(body); - return { - limit: 200, - offset: 0, - total: 0, - tasks: [], - }; + const accessToken = retrieveAuthorizationToken(req); + const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken; + + const { limit, offset, status } = body; + const paramName = isTaskListSortableAttribute(body.paramName) + ? body.paramName + : undefined; + const direction = isSortDirection(body.direction) + ? body.direction + : undefined; + + const { tasks, total } = await this.taskService.getTasksFromAccountId( + decodedToken, + offset, + limit, + status?.split(',') ?? [], + paramName, + direction, + ); + return { tasks, total, limit, offset }; } @Get('next') diff --git a/dictation_server/src/features/tasks/tasks.module.ts b/dictation_server/src/features/tasks/tasks.module.ts index 29b72f3..e1d2803 100644 --- a/dictation_server/src/features/tasks/tasks.module.ts +++ b/dictation_server/src/features/tasks/tasks.module.ts @@ -1,8 +1,11 @@ import { Module } from '@nestjs/common'; import { TasksService } from './tasks.service'; import { TasksController } from './tasks.controller'; +import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; +import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository.module'; @Module({ + imports: [UsersRepositoryModule, TasksRepositoryModule], providers: [TasksService], controllers: [TasksController], }) diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index cb48230..4ce75f6 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -1,11 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TasksService } from './tasks.service'; +import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository.module'; +import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; describe('TasksService', () => { let service: TasksService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [TasksRepositoryModule, UsersRepositoryModule], providers: [TasksService], }).compile(); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index 8d051eb..b9e21fa 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -1,4 +1,75 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; +import { AccessToken } from '../../common/token'; +import { Task } from './types/types'; +import { createTasks } from './types/convert'; +import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; +import { + SortDirection, + TaskListSortableAttribute, +} from '../../common/types/sort'; +import { ADMIN_ROLES, USER_ROLES } from 'src/constants'; @Injectable() -export class TasksService {} +export class TasksService { + private readonly logger = new Logger(TasksService.name); + constructor( + private readonly taskRepository: TasksRepositoryService, + private readonly usersRepository: UsersRepositoryService, + ) {} + + // TODO: 引数にAccessTokenがあるのは不適切なのでController側で分解したい + async getTasksFromAccountId( + accessToken: AccessToken, + offset: number, + limit: number, + status: string[], + paramName?: TaskListSortableAttribute, + direction?: SortDirection, + ): Promise<{ tasks: Task[]; total: number }> { + const { role, userId } = accessToken; + const roles = role.split(' '); // TODO: Roleを型で定義されているものに修正する + + // パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える + const defaultParamName: TaskListSortableAttribute = 'JOB_NUMBER'; + const defaultDirection: SortDirection = 'ASC'; + + try { + const { account_id } = await this.usersRepository.findUserByExternalId( + userId, + ); + + if (roles.includes(ADMIN_ROLES.ADMIN)) { + const result = await this.taskRepository.getTasksFromAccountId( + account_id, + offset, + limit, + paramName ?? defaultParamName, + direction ?? defaultDirection, + status, + ); + + const tasks = createTasks(result.tasks, result.permissions); + + return { tasks: tasks, total: result.count }; + } + + if (roles.includes(USER_ROLES.AUTHOR)) { + throw new Error(`NOT IMPLEMENTED`); + } + + if (roles.includes(USER_ROLES.TYPIST)) { + throw new Error(`NOT IMPLEMENTED`); + } + + throw new Error(`invalid roles: ${roles.join(',')}`); + } catch (e) { + this.logger.error(`error=${e}`); + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/dictation_server/src/features/tasks/types/convert.ts b/dictation_server/src/features/tasks/types/convert.ts new file mode 100644 index 0000000..0356d75 --- /dev/null +++ b/dictation_server/src/features/tasks/types/convert.ts @@ -0,0 +1,114 @@ +import { Task as TaskEntity } from '../../../repositories/tasks/entity/task.entity'; +import { User as UserEntity } from '../../../repositories/users/entity/user.entity'; +import { UserGroup as UserGroupEntity } from '../../../repositories/user_groups/entity/user_group.entity'; +import { CheckoutPermission as CheckoutPermissionEntity } from '../../../repositories/checkout_permissions/entity/checkout_permission.entity'; +import { AudioOptionItem as AudioOptionItemEntity } from '../../../repositories/audio_option_items/entity/audio_option_item.entity'; +import { Task, Typist } from './types'; +import { AudioOptionItem } from '../../files/types/types'; + +// Repository側のDTOからTaskオブジェクトの一覧を構築する +export const createTasks = ( + tasks: TaskEntity[], + permissions: CheckoutPermissionEntity[], +): Task[] => { + // Taskオブジェクトを構築 + const convertedTasks = tasks.map((task) => { + const targets = permissions.filter( + (permission) => permission.task_id === task.id, + ); + return createTask(task, targets); + }); + return convertedTasks; +}; + +// Repository側のDTOからTaskオブジェクトを構築する +const createTask = ( + task: TaskEntity, + permissions: CheckoutPermissionEntity[], +): Task => { + const { file, option_items, typist_user } = task; + if (!file) { + throw new Error('file not found.'); + } + if (!option_items) { + throw new Error('option_items not found.'); + } + + // RepositoryDTO => ControllerDTOに変換 + const optionItems = createAudioOptionItems(option_items); + + // RepositoryDTO => ControllerDTOに変換 + const assignees = createAssignees(permissions); + + // RepositoryDTO => ControllerDTOに変換 + const typist: Typist = + typist_user != null ? convertUserToTypist(typist_user) : undefined; + + return { + audioFileId: task.audio_file_id, + priority: task.priority, + status: task.status, + jobNumber: task.job_number, + transcriptionFinishedDate: task.finished_at?.toISOString() ?? '', // XXX Responseの型がnullableでないとおかしいのでdevelopマージ前に修正を行う[2023/06/07 17:43] + transcriptionStartedDate: task.started_at?.toISOString() ?? '', // XXX Responseの型がnullableでないとおかしいのでdevelopマージ前に修正を行う[2023/06/07 17:43] + authorId: file.author_id, + workType: file.work_type_id, + audioCreatedDate: file.started_at.toISOString(), + audioDuration: file.duration, + audioFinishedDate: file.finished_at.toISOString(), + audioUploadedDate: file.uploaded_at.toISOString(), + audioFormat: file.audio_format, + comment: file.comment ?? '', + fileName: file.file_name, + fileSize: file.file_size, + isEncrypted: file.is_encrypted, + url: file.url, + optionItemList: optionItems, + assignees: assignees, + typist: typist, + }; +}; + +// Repository側のDTOからAudioOptionItemオブジェクトを構築する +const createAudioOptionItems = ( + optionItems: AudioOptionItemEntity[], +): AudioOptionItem[] => { + return optionItems.map((x) => { + return { + optionItemLabel: x.label, + optionItemValue: x.value, + }; + }); +}; + +// Repository側のDTOからAudioOptionItemオブジェクトを構築する +const createAssignees = (permissions: CheckoutPermissionEntity[]): Typist[] => { + return permissions.flatMap((x): Typist[] => { + if (x.user != null) { + return [convertUserToTypist(x.user)]; + } + + if (x.user_group != null) { + return [convertUserGroupToTypist(x.user_group)]; + } + + // JOINしようとしたがUserが存在しなかったというケースはSkipする + return []; + }); +}; + +// RepositoryDTOのUserからTypistオブジェクトを生成します +const convertUserToTypist = (user: UserEntity): Typist => { + return { + typistUserId: user.id, + typistName: `USER_${user?.external_id}`, // XXX Azure AD B2Cから取得した名前を入れる + }; +}; + +// RepositoryDTOのUserGroupからTypistオブジェクトを生成します +const convertUserGroupToTypist = (userGroup: UserGroupEntity): Typist => { + return { + typistGroupId: userGroup.id, + typistName: userGroup.name, + }; +}; diff --git a/dictation_server/src/features/tasks/types/types.ts b/dictation_server/src/features/tasks/types/types.ts index dc7094d..7fe5ab6 100644 --- a/dictation_server/src/features/tasks/types/types.ts +++ b/dictation_server/src/features/tasks/types/types.ts @@ -1,8 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { AudioOptionItem } from '../../../features/files/types/types'; -import { IsIn, IsInt, IsOptional, Min } from 'class-validator'; - import { Type } from 'class-transformer'; +import { IsIn, IsInt, IsOptional, Min } from 'class-validator'; import { TASK_LIST_SORTABLE_ATTRIBUTES } from '../../../constants'; export class TasksRequest { @@ -13,6 +12,7 @@ export class TasksRequest { }) @IsInt() @Min(0) + @Type(() => Number) @IsOptional() @Type(() => Number) limit: number; @@ -25,6 +25,7 @@ export class TasksRequest { }) @IsInt() @Min(0) + @Type(() => Number) @IsOptional() @Type(() => Number) offset: number; 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 index 2d4cd79..244f955 100644 --- a/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts +++ b/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts @@ -1,4 +1,5 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { Task } from '../../../repositories/tasks/entity/task.entity'; +import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm'; @Entity({ name: 'audio_files' }) export class AudioFile { @@ -37,4 +38,6 @@ export class AudioFile { deleted_at?: Date; @Column() is_encrypted: boolean; + @OneToOne(() => Task, (task) => task.file) + task?: Task; } 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 index d85616d..ca39f80 100644 --- 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 @@ -1,14 +1,23 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { Task } from '../../../repositories/tasks/entity/task.entity'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; @Entity({ name: 'audio_option_items' }) export class AudioOptionItem { @PrimaryGeneratedColumn() id: number; - @Column() audio_file_id: number; @Column() label: string; @Column() value: string; + @ManyToOne(() => Task, (task) => task.audio_file_id) + @JoinColumn({ name: 'audio_file_id' }) + task?: Task; } diff --git a/dictation_server/src/repositories/checkout_permissions/checkout_permissions.repository.module.ts b/dictation_server/src/repositories/checkout_permissions/checkout_permissions.repository.module.ts new file mode 100644 index 0000000..d9c13e1 --- /dev/null +++ b/dictation_server/src/repositories/checkout_permissions/checkout_permissions.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CheckoutPermission } from './entity/checkout_permission.entity'; +import { CheckoutPermissionsRepositoryService } from './checkout_permissions.repository.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([CheckoutPermission])], + providers: [CheckoutPermissionsRepositoryService], + exports: [CheckoutPermissionsRepositoryService], +}) +export class CheckoutPermissionsRepositoryModule {} diff --git a/dictation_server/src/repositories/checkout_permissions/checkout_permissions.repository.service.ts b/dictation_server/src/repositories/checkout_permissions/checkout_permissions.repository.service.ts new file mode 100644 index 0000000..e3f6944 --- /dev/null +++ b/dictation_server/src/repositories/checkout_permissions/checkout_permissions.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class CheckoutPermissionsRepositoryService { + constructor(private dataSource: DataSource) {} +} 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 new file mode 100644 index 0000000..c4587e4 --- /dev/null +++ b/dictation_server/src/repositories/checkout_permissions/entity/checkout_permission.entity.ts @@ -0,0 +1,32 @@ +import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity'; +import { User } from '../../../repositories/users/entity/user.entity'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + JoinColumn, + OneToOne, +} from 'typeorm'; + +@Entity({ name: 'checkout_permission' }) +export class CheckoutPermission { + @PrimaryGeneratedColumn() + id: number; + + @Column() + task_id: number; + + @Column() + user_id?: number; + + @Column() + user_group_id?: number; + + @OneToOne(() => User, (user) => user.id) + @JoinColumn({ name: 'user_id' }) + user?: User; + + @OneToOne(() => UserGroup, (group) => group.id) + @JoinColumn({ name: 'user_group_id' }) + user_group?: UserGroup; +} diff --git a/dictation_server/src/repositories/tasks/entity/task.entity.ts b/dictation_server/src/repositories/tasks/entity/task.entity.ts index 8a016fa..c61ad1a 100644 --- a/dictation_server/src/repositories/tasks/entity/task.entity.ts +++ b/dictation_server/src/repositories/tasks/entity/task.entity.ts @@ -1,4 +1,14 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { AudioOptionItem } from '../../../repositories/audio_option_items/entity/audio_option_item.entity'; +import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity'; +import { User } from '../../../repositories/users/entity/user.entity'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + OneToOne, + JoinColumn, + OneToMany, +} from 'typeorm'; @Entity({ name: 'tasks' }) export class Task { @@ -21,9 +31,17 @@ export class Task { @Column({ nullable: true }) template_file_id?: number; @Column({ nullable: true }) - started_at?: number; + started_at?: Date; @Column({ nullable: true }) - finished_at?: number; + finished_at?: Date; @Column({ type: 'timestamp' }) created_at: Date; + @OneToOne(() => AudioFile, (audiofile) => audiofile.task) + @JoinColumn({ name: 'audio_file_id' }) + file?: AudioFile; + @OneToMany(() => AudioOptionItem, (option) => option.task) + option_items?: AudioOptionItem[]; + @OneToOne(() => User, (user) => user.id) + @JoinColumn({ name: 'typist_user_id' }) + typist_user?: User; } diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 2ef4d20..e0e2fca 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -1,14 +1,93 @@ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { + DataSource, + FindOptionsOrder, + FindOptionsOrderValue, + In, +} from 'typeorm'; import { Task } from './entity/task.entity'; -import { TASK_STATUS } from '../../constants/index'; -import { AudioFile } from '../audio_files/entity/audio_file.entity'; +import { TASK_STATUS } from '../../constants'; import { AudioOptionItem as ParamOptionItem } from '../../features/files/types/types'; +import { AudioFile } from '../audio_files/entity/audio_file.entity'; import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; +import { + SortDirection, + TaskListSortableAttribute, +} from '../../common/types/sort'; @Injectable() export class TasksRepositoryService { constructor(private dataSource: DataSource) {} + /** + * 指定したアカウントIDに紐づくTask関連情報の一覧を取得します + * @param account_id + * @param offset + * @param limit + * @param sort_criteria + * @param direction + * @param status + * @returns tasks: タスク情報 / permissions:タスクに紐づくチェックアウト権限情報 / count: offset|limitを行わなかった場合の該当タスクの合計 + */ + async getTasksFromAccountId( + account_id: number, + offset: number, + limit: number, + sort_criteria: TaskListSortableAttribute, + direction: SortDirection, + status: string[], + ): Promise<{ + tasks: Task[]; + permissions: CheckoutPermission[]; + count: number; + }> { + const order = makeOrder(sort_criteria, direction); + + const value = await this.dataSource.transaction(async (entityManager) => { + const taskRepo = entityManager.getRepository(Task); + + // limit/offsetによらず条件に一致するすべてのレコード数を取得 + const count = await taskRepo.count({ + where: { + account_id: account_id, + status: In(status), + }, + }); + + // 条件に該当するTask一覧を取得 + const tasks = await taskRepo.find({ + relations: { + file: true, + option_items: true, + typist_user: true, + }, + where: { + account_id: account_id, + status: In(status), + }, + order: order, // 引数によってOrderに使用するパラメータを変更 + take: limit, + skip: offset, + }); + + // TODO: Task内にCheckoutPermissionを含める方法が上手くいかなかった(複雑になりすぎた? 原因未調査)ため、 + // 確実に上手くいく方法としてQueryの分割を行ったが、本来はオブジェクトの構築はTypeORMに一任したい + const checkoutRepo = entityManager.getRepository(CheckoutPermission); + + const taskIds = tasks.map((x) => x.id); + const permissions = await checkoutRepo.find({ + relations: { + user: true, + user_group: true, + }, + where: { + task_id: In(taskIds), + }, + }); + return { tasks, permissions, count }; + }); + return value; + } /** * 文字起こしタスクと音声ファイル、オプションアイテムを追加 @@ -103,3 +182,89 @@ export class TasksRepositoryService { return createdEntity; } } + +// ソート用オブジェクトを生成する +const makeOrder = ( + sort_criteria: TaskListSortableAttribute, + direction: FindOptionsOrderValue, +): FindOptionsOrder => { + // Priorityで最優先で昇順ソートし、 + // その後指定パラメータで任意順ソートし、 + // 最後に順序の固定のためPrimaryKeyで昇順ソートを行う + switch (sort_criteria) { + case 'JOB_NUMBER': + return { + priority: 'ASC', + job_number: direction, + id: 'ASC', + }; + case 'STATUS': + return { + priority: 'ASC', + status: direction, + id: 'ASC', + }; + case 'TRANSCRIPTION_FINISHED_DATE': + return { + priority: 'ASC', + finished_at: direction, + id: 'ASC', + }; + case 'TRANSCRIPTION_STARTED_DATE': + return { + priority: 'ASC', + started_at: direction, + id: 'ASC', + }; + case 'AUTHOR_ID': + return { + priority: 'ASC', + file: { author_id: direction }, + id: 'ASC', + }; + case 'ENCRYPTION': + return { + priority: 'ASC', + file: { is_encrypted: direction }, + id: 'ASC', + }; + case 'FILE_LENGTH': + return { + priority: 'ASC', + file: { duration: direction }, + id: 'ASC', + }; + case 'FILE_NAME': + return { + priority: 'ASC', + file: { file_name: direction }, + id: 'ASC', + }; + case 'FILE_SIZE': + return { + priority: 'ASC', + file: { file_size: direction }, + id: 'ASC', + }; + case 'RECORDING_FINISHED_DATE': + return { + priority: 'ASC', + file: { finished_at: direction }, + id: 'ASC', + }; + case 'RECORDING_STARTED_DATE': + return { + priority: 'ASC', + file: { started_at: direction }, + id: 'ASC', + }; + case 'UPLOAD_DATE': + return { + priority: 'ASC', + file: { uploaded_at: direction }, + id: 'ASC', + }; + default: + throw new Error(); + } +}; diff --git a/dictation_server/src/repositories/user_groups/entity/user_group.entity.ts b/dictation_server/src/repositories/user_groups/entity/user_group.entity.ts new file mode 100644 index 0000000..d79aaef --- /dev/null +++ b/dictation_server/src/repositories/user_groups/entity/user_group.entity.ts @@ -0,0 +1,28 @@ +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity({ name: 'user_group' }) +export class UserGroup { + @PrimaryGeneratedColumn() + id: number; + + @Column() + account_id: number; + + @Column() + name: string; + + @Column({ nullable: true }) + deleted_at?: Date; + + @Column() + created_by: string; + + @Column({ nullable: true }) + created_at?: Date; + + @Column() + updated_by: string; + + @Column({ nullable: true }) + updated_at?: Date; +} diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.module.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.module.ts new file mode 100644 index 0000000..8f194c4 --- /dev/null +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserGroupsRepositoryService } from './user_groups.repository.service'; +import { UserGroup } from './entity/user_group.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([UserGroup])], + providers: [UserGroupsRepositoryService], + exports: [UserGroupsRepositoryService], +}) +export class UserGroupsRepositoryModule {} diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts new file mode 100644 index 0000000..5248f1c --- /dev/null +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class UserGroupsRepositoryService { + constructor(private dataSource: DataSource) {} +}