Merged PR 130: API実装(タスク一覧取得 | admin)
## 概要 [Task1831: API実装(タスク一覧取得 | admin)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1831) - AdminがAPIを呼び出したときの処理である、アカウント内のTask情報すべてを取得するロジックを実装 ## レビュー対象外 - Transcription開始日時と終了日時が必須プロパティになっている - [Task一覧APIのResponseで省略可能でないといけないプロパティが必須になっている箇所を修正する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/1956)で修正予定のため - ユニットテストが未実装 - [テスト実装(タスク一覧取得 | admin)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/1955)で実施予定のため - API I/Fのstatusのバリデーションがされていない - [Task一覧APIのstatusの入力チェックを行うデコレータを実装する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/1957)で実施予定のため ## レビューポイント 1. SQLの発行方法は問題なさそうか - ~.entityに依存関係を記述( `@OneToOne(...)` や `@OneToMany(...)` )し、TypeORMでの取得時の挙動に任せる方法でよさそうか 2. Permissionテーブル以下も含めて一括でTypeORMによるクエリビルダーに任せたかったが、他の上手くいっている構造と同じ指定をしてもSQL発行時に指定したカラム名を取ってこなくなるという問題が解決できなかったため、2回に分けて取ってくるようにしたが許容可能そうか? 3. 各テーブルでRepositoryを作ってEntityを定義し、他RepositoryからはそのRepository配下のディレクトリを参照するという形を取ってみたが、方針として問題ないか - 各テーブルを個別に取得したい場合があるかも?という予想があったため 4. Serviceのつくりとして問題はなさそうか(roleによる呼び分けの実装方法など) 5. RepositoryDTO→ControllerDTOの型変換が複雑であったため、専用のconvert.tsというファイルに分離したが、方針として問題なさそうか ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
1082a48fe9
commit
26098cc400
@ -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",
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -241,6 +241,7 @@ describe('FilesService', () => {
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, userRepoParam, {
|
||||
create: new Error(''),
|
||||
getTasksFromAccountId: new Error(),
|
||||
});
|
||||
|
||||
await expect(
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -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<TasksResponse> {
|
||||
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')
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
dictation_server/src/features/tasks/types/convert.ts
Normal file
114
dictation_server/src/features/tasks/types/convert.ts
Normal file
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class CheckoutPermissionsRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<Task> => {
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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 {}
|
||||
@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class UserGroupsRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user