From e877942175d22a38ec09536b167ca3ac3e77a507 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Thu, 1 Feb 2024 06:06:10 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Merged=20PR=20711:=20Repository=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3523: Repositoryロック対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3523) LicensesRepository - allocateLicense 割当先ユーザ取得しロックする処理を追加 WorkflowRepository - createtWorkflows authorの存在確認時にロックを追加 - updatetWorkflows authorの存在確認時にロックを追加 UserGroup&UserGroupMemberのロックを追加 AccountsRepository - updateAccountInfo プライマリ/セカンダリ管理者ユーザーの存在チェックのロック (行ロック横展開1で修正されていた) TasksRepository - create タスクの所有者の存在確認とロックを追加 - checkout 対象ユーザの存在確認とロックを追加 - changeCheckoutPermission 対象ユーザの存在確認とロックを追加 UserGroupsRepository - createTypistGroup 対象ユーザ達のロックを追加 - updateTypistGroup 対象ユーザ達のロックを追加 ## レビューポイント ラフスケッチの、 ``` 競合ケース E-3. Typistが削除条件判定を行った直後に、チェックアウト候補に削除ユーザーが含まれるTypistGroupが割り当てられる TypistGroupに割り当たっている時点で削除条件を満たさないので、このケースはないはず ``` ここは未対応でよい認識か。 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../licenses/licenses.repository.service.ts | 15 +++++ .../tasks/tasks.repository.service.ts | 32 ++++++++++ .../user_groups.repository.service.ts | 2 + .../workflows/workflows.repository.service.ts | 63 ++++++++++--------- 4 files changed, 82 insertions(+), 30 deletions(-) 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 9ac0e48..824a85c 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -48,6 +48,7 @@ import { deleteEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { UserNotFoundError } from '../users/errors/types'; @Injectable() export class TasksRepositoryService { @@ -167,6 +168,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({ @@ -846,6 +861,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( @@ -967,6 +998,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..e7db5cf 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 @@ -123,6 +123,7 @@ export class UserGroupsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( @@ -189,6 +190,7 @@ export class UserGroupsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts index cf28006..de5e38e 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( @@ -227,6 +228,37 @@ export class WorkflowsRepositoryService { ): Promise { return await this.dataSource.transaction(async (entityManager) => { const workflowRepo = entityManager.getRepository(Workflow); + // 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( + `author not found or email not verified. id: ${authorId}`, + ); + } + + // ルーティング候補ユーザーの存在確認 + 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({ @@ -239,18 +271,6 @@ export class WorkflowsRepositoryService { ); } - // 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()}`, - }); - if (!author) { - throw new UserNotFoundError( - `author not found or email not verified. id: ${authorId}`, - ); - } - // worktypeの存在確認 if (worktypeId !== undefined) { const worktypeRepo = entityManager.getRepository(Worktype); @@ -279,24 +299,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 +307,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( From 06b5249e5ac48df6eeba024e38f5d295e5a68010 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Fri, 2 Feb 2024 02:00:47 +0000 Subject: [PATCH 2/3] =?UTF-8?q?Merged=20PR=20717:=20[FB=E5=AF=BE=E5=BF=9C]?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E6=9B=B4=E6=96=B0=E3=81=99=E3=82=8B=E3=81=A8?= =?UTF-8?q?=E3=83=98=E3=83=83=E3=83=80=E3=83=BC=E3=81=AE=E3=82=BD=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=8CJob=20number=E3=81=AB=E6=88=BB=E3=81=A3?= =?UTF-8?q?=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3611: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3611) - ソート条件を変更した際にローカルストレージに保持するように修正 - 例 `direction:ASC,paramName:RECORDING_STARTED_DATE` - ローカルストレージにソート条件が入っていれば、その条件でソートしたタスクを取得するように修正 - PlayBack時にローカルストレージにあるソート条件を削除するように修正 - 削除することで、次回の画面初期表示時はPlayBackを押したときのソート条件を使用することができる。 - PlayBack時にユーザーがタイピストの時のみソート条件を保存していたがその制限は不要そうだったのでAuthorでもソート条件を更新するように修正。 - AuthorがPlayBack押下時にソート条件を更新しても不都合はないため。 ## レビューポイント - ローカルストレージに保存する処理を入れる箇所に問題はないか ## UIの変更 -https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3611?csf=1&web=1&e=5uG6f4 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../src/components/auth/constants.ts | 7 +++- .../src/features/dictation/constants.ts | 11 +++++ .../src/features/dictation/operations.ts | 18 ++++----- .../src/pages/DictationPage/index.tsx | 40 +++++++++++++++++-- 4 files changed, 61 insertions(+), 15 deletions(-) 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 23b98f4..99b426e 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 bb70c47..cbeed07 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -33,6 +33,8 @@ import { playbackAsync, cancelAsync, PRIORITY, + isSortableColumnType, + isDirectionType, } from "features/dictation"; import { getTranslationID } from "translation"; import { Task } from "api/api"; @@ -242,6 +244,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, @@ -348,10 +356,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, @@ -388,7 +397,6 @@ const DictationPage: React.FC = (): JSX.Element => { filterInProgress, filterPending, filterUploaded, - isTypist, sortDirection, sortableParamName, t, @@ -522,13 +530,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, From 4548b5e5107bd941db0f177f2d5f636abd949b49 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Fri, 2 Feb 2024 02:37:39 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Merged=20PR=20715:=20=E8=A1=8C=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=A8=AA=E5=B1=95=E9=96=8B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3472: 行ロック横展開4](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3472) - 対象メソッド - user_groups - createTypistGroup - グループに含めるユーザー情報取得箇所でロック追加 - ユーザー削除と被ると、削除済みユーザーをユーザーグループに含めてしまう - updateTypistGroup - グループに含めるユーザー情報取得箇所でロック追加 - ユーザー削除と被ると、削除済みユーザーをユーザーグループに含めてしまう - グループの存在確認を行う箇所 - グループ削除と被ると、削除済みのグループにメンバーを割り当ててしまう - workflows - createtWorkflows - updatetWorkflow - インデックス追加 - user - role ## レビューポイント - インデックスの貼り忘れはないか - ロックの追加忘れはないか ## 共有資料 - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3472?csf=1&web=1&e=jjb0QV ## 動作確認状況 - ローカルでロックされている箇所で待ちが発生していることを確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/db/migrations/055-add_users_index.sql | 5 +++++ .../user_groups/user_groups.repository.service.ts | 5 +++-- .../workflows/workflows.repository.service.ts | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 dictation_server/db/migrations/055-add_users_index.sql 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/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts index e7db5cf..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,8 +122,8 @@ export class UserGroupsRepositoryService { role: USER_ROLES.TYPIST, email_verified: true, }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( @@ -189,8 +189,8 @@ export class UserGroupsRepositoryService { role: USER_ROLES.TYPIST, email_verified: true, }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( @@ -206,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 de5e38e..2ccaf5e 100644 --- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts +++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts @@ -101,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( @@ -115,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.'); @@ -132,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( @@ -147,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( @@ -164,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( @@ -264,6 +269,7 @@ export class WorkflowsRepositoryService { 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( @@ -277,6 +283,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( @@ -291,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( @@ -402,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(