diff --git a/dictation_client/src/components/auth/constants.ts b/dictation_client/src/components/auth/constants.ts index ef98041..dd8ec52 100644 --- a/dictation_client/src/components/auth/constants.ts +++ b/dictation_client/src/components/auth/constants.ts @@ -43,7 +43,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [ * ローカルストレージに残すキー類 * @const {string[]} */ -export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"]; +export const KEYS_TO_PRESERVE = [ + "accessToken", + "refreshToken", + "displayInfo", + "sortCriteria", +]; /** * アクセストークンを更新する基準の秒数 diff --git a/dictation_client/src/features/dictation/constants.ts b/dictation_client/src/features/dictation/constants.ts index 69eae60..f7e22d1 100644 --- a/dictation_client/src/features/dictation/constants.ts +++ b/dictation_client/src/features/dictation/constants.ts @@ -28,6 +28,13 @@ export const SORTABLE_COLUMN = { export type SortableColumnType = typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; +export const isSortableColumnType = ( + value: string +): value is SortableColumnType => { + const arg = value as SortableColumnType; + return Object.values(SORTABLE_COLUMN).includes(arg); +}; + export type SortableColumnList = typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; @@ -38,6 +45,10 @@ export const DIRECTION = { export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; +// DirectionTypeの型チェック関数 +export const isDirectionType = (arg: string): arg is DirectionType => + arg in DIRECTION; + export interface DisplayInfoType { JobNumber: boolean; Status: boolean; diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 5bf50da..51579e7 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -280,7 +280,6 @@ export const playbackAsync = createAsyncThunk< direction: DirectionType; paramName: SortableColumnType; audioFileId: number; - isTypist: boolean; }, { // rejectした時の返却値の型 @@ -289,7 +288,7 @@ export const playbackAsync = createAsyncThunk< }; } >("dictations/playbackAsync", async (args, thunkApi) => { - const { audioFileId, direction, paramName, isTypist } = args; + const { audioFileId, direction, paramName } = args; // apiのConfigurationを取得する const { getState } = thunkApi; @@ -300,15 +299,12 @@ export const playbackAsync = createAsyncThunk< const tasksApi = new TasksApi(config); const usersApi = new UsersApi(config); try { - // ユーザーがタイピストである場合に、ソート条件を保存する - if (isTypist) { - await usersApi.updateSortCriteria( - { direction, paramName }, - { - headers: { authorization: `Bearer ${accessToken}` }, - } - ); - } + await usersApi.updateSortCriteria( + { direction, paramName }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); await tasksApi.checkout(audioFileId, { headers: { authorization: `Bearer ${accessToken}` }, }); diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index b024fd4..361be34 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -34,6 +34,8 @@ import { cancelAsync, PRIORITY, deleteTaskAsync, + isSortableColumnType, + isDirectionType, } from "features/dictation"; import { getTranslationID } from "translation"; import { Task } from "api/api"; @@ -245,6 +247,12 @@ const DictationPage: React.FC = (): JSX.Element => { dispatch(changeDirection({ direction: currentDirection })); dispatch(changeParamName({ paramName })); + // ローカルストレージにソート情報を保存する + localStorage.setItem( + "sortCriteria", + `direction:${currentDirection},paramName:${paramName}` + ); + const filter = getFilter( filterUploaded, filterInProgress, @@ -351,10 +359,11 @@ const DictationPage: React.FC = (): JSX.Element => { audioFileId, direction: sortDirection, paramName: sortableParamName, - isTypist, }) ); if (meta.requestStatus === "fulfilled") { + // ローカルストレージにソート情報を削除する + localStorage.removeItem("sortCriteria"); const filter = getFilter( filterUploaded, filterInProgress, @@ -391,7 +400,6 @@ const DictationPage: React.FC = (): JSX.Element => { filterInProgress, filterPending, filterUploaded, - isTypist, sortDirection, sortableParamName, t, @@ -572,13 +580,39 @@ const DictationPage: React.FC = (): JSX.Element => { dispatch(changeDisplayInfo({ column: displayInfo })); const filter = getFilter(true, true, true, true, false); + const { meta, payload } = await dispatch(getSortColumnAsync()); if ( meta.requestStatus === "fulfilled" && payload && !("error" in payload) ) { - const { direction, paramName } = payload; + // ソート情報をローカルストレージから取得する + const sortColumnValue = localStorage.getItem("sortCriteria") ?? ""; + let direction: DirectionType; + let paramName: SortableColumnType; + if (sortColumnValue === "") { + direction = payload.direction; + paramName = payload.paramName; + } else { + // ソート情報をDirectionとParamNameに分割する + const sortColumn = sortColumnValue?.split(","); + const localStorageDirection = sortColumn[0].split(":")[1] ?? ""; + + const localStorageParamName = sortColumn[1]?.split(":")[1] ?? ""; + + // 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する + direction = isDirectionType(localStorageDirection) + ? localStorageDirection + : payload.direction; + paramName = isSortableColumnType(localStorageParamName) + ? localStorageParamName + : payload.paramName; + + dispatch(changeDirection({ direction })); + dispatch(changeParamName({ paramName })); + } + dispatch( listTasksAsync({ limit: LIMIT_TASK_NUM, diff --git a/dictation_server/db/migrations/055-add_users_index.sql b/dictation_server/db/migrations/055-add_users_index.sql new file mode 100644 index 0000000..ea50c9d --- /dev/null +++ b/dictation_server/db/migrations/055-add_users_index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `users` ADD INDEX `idx_role` (role); + +-- +migrate Down +ALTER TABLE `users` DROP INDEX `idx_role`; \ No newline at end of file diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 2a0c328..6270b43 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -39,6 +39,8 @@ import { updateEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { User } from '../users/entity/user.entity'; +import { UserNotFoundError } from '../users/errors/types'; @Injectable() export class LicensesRepositoryService { @@ -559,6 +561,19 @@ export class LicensesRepositoryService { accountId: number, ): Promise { await this.dataSource.transaction(async (entityManager) => { + // 対象ユーザの存在チェック + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { + id: userId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!user) { + throw new UserNotFoundError(`User not exist. userId: ${userId}`); + } + const licenseRepo = entityManager.getRepository(License); const licenseAllocationHistoryRepo = entityManager.getRepository( LicenseAllocationHistory, diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index c721f18..50038f9 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -53,6 +53,7 @@ import { deleteEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { UserNotFoundError } from '../users/errors/types'; @Injectable() export class TasksRepositoryService { @@ -172,6 +173,20 @@ export class TasksRepositoryService { permittedSourceStatus: TaskStatus[], ): Promise { await this.dataSource.transaction(async (entityManager) => { + // 対象ユーザの存在確認 + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { + id: user_id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!user) { + throw new TypistUserNotFoundError( + `Typist user not exists. user_id:${user_id}`, + ); + } const taskRepo = entityManager.getRepository(Task); // 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得 const task = await taskRepo.findOne({ @@ -851,6 +866,22 @@ export class TasksRepositoryService { const createdEntity = await this.dataSource.transaction( async (entityManager) => { + // タスクの所有者の存在確認 + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { + id: owner_user_id, + account_id: account_id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!user) { + throw new UserNotFoundError( + `User not exists. owner_user_id:${owner_user_id}`, + ); + } + const audioFileRepo = entityManager.getRepository(AudioFile); const newAudioFile = audioFileRepo.create(audioFile); const savedAudioFile = await insertEntity( @@ -972,6 +1003,7 @@ export class TasksRepositoryService { deleted_at: IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // idはユニークであるため取得件数の一致でユーザーの存在を確認 if (typistUserIds.length !== userRecords.length) { 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 index eca0bc7..050d3e1 100644 --- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -122,6 +122,7 @@ export class UserGroupsRepositoryService { role: USER_ROLES.TYPIST, email_verified: true, }, + lock: { mode: 'pessimistic_write' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (userRecords.length !== typistIds.length) { @@ -188,6 +189,7 @@ export class UserGroupsRepositoryService { role: USER_ROLES.TYPIST, email_verified: true, }, + lock: { mode: 'pessimistic_write' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (userRecords.length !== typistIds.length) { @@ -204,6 +206,7 @@ export class UserGroupsRepositoryService { id: typistGroupId, account_id: accountId, }, + lock: { mode: 'pessimistic_write' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (!typistGroup) { diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts index cf28006..2ccaf5e 100644 --- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts +++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts @@ -87,6 +87,7 @@ export class WorkflowsRepositoryService { const author = await userRepo.findOne({ where: { account_id: accountId, id: authorId, email_verified: true }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!author) { throw new UserNotFoundError( @@ -100,6 +101,7 @@ export class WorkflowsRepositoryService { const worktypes = await worktypeRepo.find({ where: { account_id: accountId, id: worktypeId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (worktypes.length === 0) { throw new WorktypeIdNotFoundError( @@ -114,6 +116,7 @@ export class WorkflowsRepositoryService { const template = await templateRepo.findOne({ where: { account_id: accountId, id: templateId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!template) { throw new TemplateFileNotExistError('template not found.'); @@ -131,6 +134,7 @@ export class WorkflowsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistUsers.length !== typistIds.length) { throw new UserNotFoundError( @@ -146,6 +150,7 @@ export class WorkflowsRepositoryService { const typistGroups = await userGroupRepo.find({ where: { account_id: accountId, id: In(groupIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistGroups.length !== groupIds.length) { throw new TypistGroupNotExistError( @@ -163,6 +168,7 @@ export class WorkflowsRepositoryService { worktype_id: worktypeId !== undefined ? worktypeId : IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (workflow.length !== 0) { throw new AuthorIdAndWorktypeIdPairAlreadyExistsError( @@ -227,23 +233,12 @@ export class WorkflowsRepositoryService { ): Promise { return await this.dataSource.transaction(async (entityManager) => { const workflowRepo = entityManager.getRepository(Workflow); - - // ワークフローの存在確認 - const targetWorkflow = await workflowRepo.findOne({ - where: { account_id: accountId, id: workflowId }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - }); - if (!targetWorkflow) { - throw new WorkflowNotFoundError( - `workflow not found. id: ${workflowId}`, - ); - } - // authorの存在確認 const userRepo = entityManager.getRepository(User); const author = await userRepo.findOne({ where: { account_id: accountId, id: authorId, email_verified: true }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!author) { throw new UserNotFoundError( @@ -251,12 +246,44 @@ export class WorkflowsRepositoryService { ); } + // ルーティング候補ユーザーの存在確認 + const typistIds = typists.flatMap((typist) => + typist.typistId ? [typist.typistId] : [], + ); + const typistUsers = await userRepo.find({ + where: { + account_id: accountId, + id: In(typistIds), + email_verified: true, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (typistUsers.length !== typistIds.length) { + throw new UserNotFoundError( + `typist not found or email not verified. ids: ${typistIds}`, + ); + } + + // ワークフローの存在確認 + const targetWorkflow = await workflowRepo.findOne({ + where: { account_id: accountId, id: workflowId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!targetWorkflow) { + throw new WorkflowNotFoundError( + `workflow not found. id: ${workflowId}`, + ); + } + // worktypeの存在確認 if (worktypeId !== undefined) { const worktypeRepo = entityManager.getRepository(Worktype); const worktypes = await worktypeRepo.find({ where: { account_id: accountId, id: worktypeId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (worktypes.length === 0) { throw new WorktypeIdNotFoundError( @@ -271,6 +298,7 @@ export class WorkflowsRepositoryService { const template = await templateRepo.findOne({ where: { account_id: accountId, id: templateId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!template) { throw new TemplateFileNotExistError( @@ -279,24 +307,6 @@ export class WorkflowsRepositoryService { } } - // ルーティング候補ユーザーの存在確認 - const typistIds = typists.flatMap((typist) => - typist.typistId ? [typist.typistId] : [], - ); - const typistUsers = await userRepo.find({ - where: { - account_id: accountId, - id: In(typistIds), - email_verified: true, - }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - }); - if (typistUsers.length !== typistIds.length) { - throw new UserNotFoundError( - `typist not found or email not verified. ids: ${typistIds}`, - ); - } - // ルーティング候補ユーザーグループの存在確認 const groupIds = typists.flatMap((typist) => { return typist.typistGroupId ? [typist.typistGroupId] : []; @@ -305,6 +315,7 @@ export class WorkflowsRepositoryService { const typistGroups = await userGroupRepo.find({ where: { account_id: accountId, id: In(groupIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistGroups.length !== groupIds.length) { throw new TypistGroupNotExistError( @@ -399,6 +410,7 @@ export class WorkflowsRepositoryService { const workflow = await workflowRepo.findOne({ where: { account_id: accountId, id: workflowId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!workflow) { throw new WorkflowNotFoundError(