From 81c299dd99a3d509036f0891e3f7ebc54b98e75c Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 11 Jan 2024 08:47:28 +0000 Subject: [PATCH 001/109] =?UTF-8?q?Merged=20PR=20680:=20=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E5=89=8A=E9=99=A4API=20IF=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3456: タスク削除API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3456) - タスク削除APIのIFを実装しopenapi.jsonを更新しました。 ## レビューポイント - パス、バリデータは想定通りでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_client/codegen.sh | 2 +- dictation_client/src/api/api.ts | 18 ----- .../src/features/user/operations.ts | 1 - .../src/features/user/selectors.ts | 2 - dictation_client/src/features/user/types.ts | 3 - .../src/features/user/userSlice.ts | 21 ----- dictation_client/src/pages/AuthPage/index.tsx | 41 ++++++++++ .../pages/DictationPage/filePropertyPopup.tsx | 2 +- .../src/pages/LoginPage/index.tsx | 81 ++++++++++++------- .../src/pages/TermsPage/index.tsx | 37 +++++++++ .../src/pages/UserListPage/index.tsx | 4 - .../src/pages/UserListPage/popup.tsx | 17 ---- .../src/pages/UserListPage/updatePopup.tsx | 19 ----- dictation_server/src/api/odms/openapi.json | 70 +++++++++++++--- dictation_server/src/common/test/utility.ts | 2 - .../accounts/test/accounts.service.mock.ts | 3 - .../src/features/accounts/types/types.ts | 8 +- .../features/files/test/files.service.mock.ts | 1 - .../licenses/test/liscense.service.mock.ts | 1 - .../test/notification.service.mock.ts | 1 - .../src/features/tasks/tasks.controller.ts | 80 ++++++++++++++++++ .../features/tasks/test/tasks.service.mock.ts | 2 - .../src/features/tasks/types/types.ts | 10 +++ .../features/users/test/users.service.mock.ts | 2 - .../src/features/users/types/types.ts | 22 ++--- .../src/features/users/users.controller.ts | 4 - .../src/features/users/users.service.spec.ts | 44 ---------- .../src/features/users/users.service.ts | 20 +---- .../accounts/accounts.repository.service.ts | 6 +- .../repositories/users/entity/user.entity.ts | 6 -- .../users/users.repository.service.ts | 4 - .../src/templates/template_U_113.html | 6 +- .../src/templates/template_U_113.txt | 6 +- 33 files changed, 301 insertions(+), 245 deletions(-) diff --git a/dictation_client/codegen.sh b/dictation_client/codegen.sh index 5d09ae3..d165c0c 100644 --- a/dictation_client/codegen.sh +++ b/dictation_client/codegen.sh @@ -1,2 +1,2 @@ -npx openapi-generator-cli version-manager set latest +npx openapi-generator-cli version-manager set 7.1.0 npx openapi-generator-cli generate -g typescript-axios -i /app/dictation_server/src/api/odms/openapi.json -o /app/dictation_client/src/api/ diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 1c1ac87..645f058 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -1549,12 +1549,6 @@ export interface PostUpdateUserRequest { * @memberof PostUpdateUserRequest */ 'autoRenew': boolean; - /** - * - * @type {boolean} - * @memberof PostUpdateUserRequest - */ - 'licenseAlart': boolean; /** * * @type {boolean} @@ -1660,12 +1654,6 @@ export interface SignupRequest { * @memberof SignupRequest */ 'autoRenew': boolean; - /** - * - * @type {boolean} - * @memberof SignupRequest - */ - 'licenseAlert': boolean; /** * * @type {boolean} @@ -2212,12 +2200,6 @@ export interface User { * @memberof User */ 'autoRenew': boolean; - /** - * - * @type {boolean} - * @memberof User - */ - 'licenseAlert': boolean; /** * * @type {boolean} diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index f02e38c..b7ca2b0 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -164,7 +164,6 @@ export const updateUserAsync = createAsyncThunk< encryptionPassword, prompt, autoRenew: updateUser.autoRenew, - licenseAlart: updateUser.licenseAlert, notification: updateUser.notification, }, { diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index e1e3cf1..ee1f3fa 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -131,8 +131,6 @@ export const selectAuthorId = (state: RootState) => state.user.apps.addUser.authorId; export const selectAutoRenew = (state: RootState) => state.user.apps.addUser.autoRenew; -export const selectLicenseAlert = (state: RootState) => - state.user.apps.addUser.licenseAlert; export const selectNotification = (state: RootState) => state.user.apps.addUser.notification; // AddUserを返却する diff --git a/dictation_client/src/features/user/types.ts b/dictation_client/src/features/user/types.ts index 7f41187..649b63f 100644 --- a/dictation_client/src/features/user/types.ts +++ b/dictation_client/src/features/user/types.ts @@ -16,7 +16,6 @@ export interface UserView encryption: boolean | string; emailVerified: boolean; autoRenew: boolean; - licenseAlert: boolean; notification: boolean; name: string; email: string; @@ -28,7 +27,6 @@ export interface AddUser { role: RoleType; email: string; autoRenew: boolean; - licenseAlert: boolean; notification: boolean; authorId?: string; encryption?: boolean; @@ -46,7 +44,6 @@ export interface UpdateUser { encryptionPassword?: string | undefined; prompt?: boolean | undefined; autoRenew: boolean; - licenseAlert: boolean; notification: boolean; } diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 52ffbc4..ef0589a 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -23,7 +23,6 @@ const initialState: UsersState = { encryptionPassword: undefined, prompt: undefined, autoRenew: true, - licenseAlert: true, notification: true, }, selectedUser: { @@ -36,7 +35,6 @@ const initialState: UsersState = { encryptionPassword: undefined, prompt: undefined, autoRenew: true, - licenseAlert: true, notification: true, }, addUser: { @@ -44,7 +42,6 @@ const initialState: UsersState = { role: USER_ROLES.NONE, email: "", autoRenew: true, - licenseAlert: true, notification: true, authorId: undefined, encryption: false, @@ -93,13 +90,6 @@ export const userSlice = createSlice({ const { autoRenew } = action.payload; state.apps.addUser.autoRenew = autoRenew; }, - changeLicenseAlert: ( - state, - action: PayloadAction<{ licenseAlert: boolean }> - ) => { - const { licenseAlert } = action.payload; - state.apps.addUser.licenseAlert = licenseAlert; - }, changeEncryption: ( state, action: PayloadAction<{ encryption: boolean }> @@ -149,7 +139,6 @@ export const userSlice = createSlice({ state.apps.updateUser.encryptionPassword = undefined; state.apps.updateUser.prompt = user.prompt; state.apps.updateUser.autoRenew = user.autoRenew; - state.apps.updateUser.licenseAlert = user.licenseAlert; state.apps.updateUser.notification = user.notification; state.apps.selectedUser.id = user.id; @@ -161,7 +150,6 @@ export const userSlice = createSlice({ state.apps.selectedUser.encryptionPassword = undefined; state.apps.selectedUser.prompt = user.prompt; state.apps.selectedUser.autoRenew = user.autoRenew; - state.apps.selectedUser.licenseAlert = user.licenseAlert; state.apps.selectedUser.notification = user.notification; state.apps.hasPasswordMask = user.encryption; @@ -211,13 +199,6 @@ export const userSlice = createSlice({ const { autoRenew } = action.payload; state.apps.updateUser.autoRenew = autoRenew; }, - changeUpdateLicenseAlert: ( - state, - action: PayloadAction<{ licenseAlert: boolean }> - ) => { - const { licenseAlert } = action.payload; - state.apps.updateUser.licenseAlert = licenseAlert; - }, changeUpdateNotification: ( state, action: PayloadAction<{ notification: boolean }> @@ -318,7 +299,6 @@ export const { changeRole, changeAuthorId, changeAutoRenew, - changeLicenseAlert, changeNotification, cleanupAddUser, changeUpdateUser, @@ -328,7 +308,6 @@ export const { changeUpdateEncryptionPassword, changeUpdatePrompt, changeUpdateAutoRenew, - changeUpdateLicenseAlert, changeUpdateNotification, cleanupUpdateUser, changeEncryption, diff --git a/dictation_client/src/pages/AuthPage/index.tsx b/dictation_client/src/pages/AuthPage/index.tsx index a1559bd..a17880d 100644 --- a/dictation_client/src/pages/AuthPage/index.tsx +++ b/dictation_client/src/pages/AuthPage/index.tsx @@ -10,6 +10,14 @@ import { import React, { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; +import { + clearToken, + isAdminUser, + isApproveTier, + isStandardUser, + loadAccessToken, +} from "features/auth"; +import { TIERS } from "components/auth/constants"; const AuthPage: React.FC = (): JSX.Element => { const { instance } = useMsal(); @@ -26,6 +34,39 @@ const AuthPage: React.FC = (): JSX.Element => { (async () => { try { + // ログイン済みの場合、ログイン後の遷移先を決定する + if (loadAccessToken()) { + // 第一~第四階層の管理者はライセンス画面へ遷移 + if ( + isApproveTier([ + TIERS.TIER1, + TIERS.TIER2, + TIERS.TIER3, + TIERS.TIER4, + ]) && + isAdminUser() + ) { + navigate("/license"); + return; + } + // 第五階層の管理者はユーザー画面へ遷移 + if (isApproveTier([TIERS.TIER5]) && isAdminUser()) { + navigate("/user"); + return; + } + // 一般ユーザーはdictationPageへ遷移 + if (isStandardUser()) { + navigate("/dictations"); + return; + } + // それ以外は認証エラー画面へ遷移 + instance.logoutRedirect({ + postLogoutRedirectUri: "/AuthError", + }); + clearToken(); + return; + } + const loginResult = await instance.handleRedirectPromise(); // eslint-disable-next-line diff --git a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx index 0bcd5ba..1a4104c 100644 --- a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx +++ b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx @@ -122,7 +122,7 @@ export const FilePropertyPopup: React.FC = (props) => {
{t(getTranslationID("dictationPage.label.transcriptionist"))}
{selectedFileTask?.typist?.name ?? ""}
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} close {t(getTranslationID("filePropertyPopup.label.close"))} diff --git a/dictation_client/src/pages/LoginPage/index.tsx b/dictation_client/src/pages/LoginPage/index.tsx index e298c40..c335bd3 100644 --- a/dictation_client/src/pages/LoginPage/index.tsx +++ b/dictation_client/src/pages/LoginPage/index.tsx @@ -28,6 +28,33 @@ const LoginPage: React.FC = (): JSX.Element => { selectLocalStorageKeyforIdToken ); + // ログイン後の遷移先を決定する + const navigateToLoginedPage = useCallback(() => { + // 第一~第四階層の管理者はライセンス画面へ遷移 + if ( + isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]) && + isAdminUser() + ) { + navigate("/license"); + return; + } + // 第五階層の管理者はユーザー画面へ遷移 + if (isApproveTier([TIERS.TIER5]) && isAdminUser()) { + navigate("/user"); + return; + } + // 一般ユーザーはdictationPageへ遷移 + if (isStandardUser()) { + navigate("/dictations"); + return; + } + // それ以外は認証エラー画面へ遷移 + instance.logoutRedirect({ + postLogoutRedirectUri: "/AuthError", + }); + clearToken(); + }, [instance, navigate]); + const tokenSet = useCallback( async (idToken: string) => { // ログイン処理呼び出し @@ -59,44 +86,36 @@ const LoginPage: React.FC = (): JSX.Element => { document.body.appendChild(a); a.click(); document.body.removeChild(a); - // 第一~第四階層の管理者はライセンス画面へ遷移 - if ( - isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]) && - isAdminUser() - ) { - navigate("/license"); - return; - } - // 第五階層の管理者はユーザー画面へ遷移 - if (isApproveTier([TIERS.TIER5]) && isAdminUser()) { - navigate("/user"); - return; - } - // 一般ユーザーはdictationPageへ遷移 - if (isStandardUser()) { - navigate("/dictations"); - return; - } - // それ以外は認証エラー画面へ遷移 - instance.logoutRedirect({ - postLogoutRedirectUri: "/AuthError", - }); - clearToken(); + + // ログイン成功した場合、適切なページに遷移する + navigateToLoginedPage(); } }, - [dispatch, i18n.language, instance, navigate] + [dispatch, i18n.language, instance, navigate, navigateToLoginedPage] ); useEffect(() => { - // AADB2Cのログイン画面とLoginPageを経由していない場合はトップページに遷移する - if (!localStorageKeyforIdToken) { - navigate("/"); - return; - } - + // idTokenStringがあるか⇒認証中 + // accessTokenがある場合⇒ログイン済み + // どちらもなければ直打ち (async () => { - // IDトークンの取得 + if (loadAccessToken()) { + navigateToLoginedPage(); + return; + } + + // AADB2Cのログイン画面とLoginPageを経由していない場合はトップページに遷移する + if (!localStorageKeyforIdToken) { + navigate("/"); + return; + } const idTokenString = localStorage.getItem(localStorageKeyforIdToken); + + if (idTokenString === null) { + navigate("/"); + return; + } + if (idTokenString) { const idTokenObject = JSON.parse(idTokenString); if (isIdToken(idTokenObject)) { diff --git a/dictation_client/src/pages/TermsPage/index.tsx b/dictation_client/src/pages/TermsPage/index.tsx index 974487e..52f2a0b 100644 --- a/dictation_client/src/pages/TermsPage/index.tsx +++ b/dictation_client/src/pages/TermsPage/index.tsx @@ -18,10 +18,19 @@ import { } from "features//terms"; import { selectLocalStorageKeyforIdToken } from "features/login"; import { useNavigate } from "react-router-dom"; +import { + clearToken, + isAdminUser, + isApproveTier, + isStandardUser, + loadAccessToken, +} from "features/auth"; +import { useMsal } from "@azure/msal-react"; const TermsPage: React.FC = (): JSX.Element => { const [t] = useTranslation(); const dispatch: AppDispatch = useDispatch(); + const { instance } = useMsal(); const navigate = useNavigate(); const updateAccceptVersions = useSelector(selectTermVersions); const localStorageKeyforIdToken = useSelector( @@ -40,6 +49,34 @@ const TermsPage: React.FC = (): JSX.Element => { // 画面起動時 useEffect(() => { + // ログイン済みの場合、ログイン後の遷移先を決定する + if (loadAccessToken()) { + // 第一~第四階層の管理者はライセンス画面へ遷移 + if ( + isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]) && + isAdminUser() + ) { + navigate("/license"); + return; + } + // 第五階層の管理者はユーザー画面へ遷移 + if (isApproveTier([TIERS.TIER5]) && isAdminUser()) { + navigate("/user"); + return; + } + // 一般ユーザーはdictationPageへ遷移 + if (isStandardUser()) { + navigate("/dictations"); + return; + } + // それ以外は認証エラー画面へ遷移 + instance.logoutRedirect({ + postLogoutRedirectUri: "/AuthError", + }); + clearToken(); + return; + } + dispatch(getTermsInfoAsync()); if (localStorageKeyforIdToken) { dispatch(getAccountInfoMinimalAccessAsync({ localStorageKeyforIdToken })); diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index 46f1223..a1de616 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -174,9 +174,6 @@ const UserListPage: React.FC = (): JSX.Element => { {t(getTranslationID("userListPage.label.autoRenew"))} - - {t(getTranslationID("userListPage.label.licenseAlert"))} - {t(getTranslationID("userListPage.label.notification"))} @@ -292,7 +289,6 @@ const UserListPage: React.FC = (): JSX.Element => { {boolToElement(user.autoRenew)} - {boolToElement(user.licenseAlert)} {boolToElement(user.notification)} {boolToElement(user.emailVerified)} diff --git a/dictation_client/src/pages/UserListPage/popup.tsx b/dictation_client/src/pages/UserListPage/popup.tsx index 50ed98f..97b3301 100644 --- a/dictation_client/src/pages/UserListPage/popup.tsx +++ b/dictation_client/src/pages/UserListPage/popup.tsx @@ -10,7 +10,6 @@ import { changeRole, changeAuthorId, changeAutoRenew, - changeLicenseAlert, changeNotification, cleanupAddUser, addUserAsync, @@ -324,22 +323,6 @@ export const UserAddPopup: React.FC = (props) => { {t(getTranslationID("userListPage.label.autoRenew"))}

-

- -

-

- -

-

Temporary password:$TEMPORARY_PASSWORD$

+

Temporary password: $TEMPORARY_PASSWORD$

If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_NAME$. @@ -34,7 +34,7 @@ Passwort zu ändern, klicken Sie auf dem ODMS Cloud-Anmeldebildschirm auf den Link [Kennwort vergessen?].

-

Temporäres Passwort:$TEMPORARY_PASSWORD$

+

Temporäres Passwort: $TEMPORARY_PASSWORD$

Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_NAME$. @@ -57,7 +57,7 @@ lien [Vous avez oublié votre mot de passe ?] sur l'écran de connexion ODMS Cloud.

-

Temporary password:$TEMPORARY_PASSWORD$

+

Temporary password: $TEMPORARY_PASSWORD$

Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_NAME$. diff --git a/dictation_server/src/templates/template_U_113.txt b/dictation_server/src/templates/template_U_113.txt index 5c444c6..f84bd50 100644 --- a/dictation_server/src/templates/template_U_113.txt +++ b/dictation_server/src/templates/template_U_113.txt @@ -2,7 +2,7 @@ Your user registration has been completed. Please login to ODMS Cloud with the following temporary password. You may continue using your temporary password; however, we strongly recommend that you change your password for security reasons. To change your password, click on [Forgot your password?] link on the ODMS Cloud Sign in screen. -Temporary password:$TEMPORARY_PASSWORD$ +Temporary password: $TEMPORARY_PASSWORD$ If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_NAME$. @@ -13,7 +13,7 @@ This is an automatically generated e-mail and this mailbox is not monitored. P Ihre Benutzerregistrierung ist abgeschlossen. Bitte melden Sie sich mit dem folgenden temporären Passwort bei ODMS Cloud an. Sie können Ihr temporäres Passwort weiterhin verwenden; Aus Sicherheitsgründen empfehlen wir Ihnen jedoch dringend, Ihr Passwort zu ändern. Um Ihr Passwort zu ändern, klicken Sie auf dem ODMS Cloud-Anmeldebildschirm auf den Link [Kennwort vergessen?]. -Temporäres Passwort:$TEMPORARY_PASSWORD$ +Temporäres Passwort: $TEMPORARY_PASSWORD$ Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_NAME$. @@ -24,7 +24,7 @@ Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht über Votre inscription d'utilisateur est terminée. Veuillez vous connecter à ODMS Cloud avec le mot de passe temporaire suivant. Vous pouvez continuer à utiliser votre mot de passe temporaire ; cependant, nous vous recommandons fortement de changer votre mot de passe pour des raisons de sécurité. Pour modifier votre mot de passe, cliquez sur le lien [Vous avez oublié votre mot de passe ?] sur l'écran de connexion ODMS Cloud. -Temporary password:$TEMPORARY_PASSWORD$ +Temporary password: $TEMPORARY_PASSWORD$ Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_NAME$. From 8793606070aab4b2eb213a507fde98112a4f1dd1 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 16 Jan 2024 00:17:45 +0000 Subject: [PATCH 002/109] =?UTF-8?q?Merged=20PR=20682:=20=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E4=B8=80=E8=A6=A7=E7=94=BB=E9=9D=A2=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3458: タスク一覧画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3458) - タスク一覧画面のタスク削除ボタンからタスクを削除する処理を実装しました。 - タスクがInProgress、ユーザーがTypistの場合にはボタンを非活性となるようにしています。 ## レビューポイント - エラーごとの処理内容は適切でしょうか? - ボタンの活性制御は適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_client/src/api/api.ts | 73 +++++++++++++++++++ .../src/features/dictation/constants.ts | 2 +- .../src/features/dictation/dictationSlice.ts | 10 +++ .../src/features/dictation/operations.ts | 72 ++++++++++++++++++ .../src/pages/DictationPage/index.tsx | 61 +++++++++++++++- dictation_client/src/styles/app.module.scss | 6 +- dictation_client/src/translation/de.json | 5 +- dictation_client/src/translation/en.json | 5 +- dictation_client/src/translation/es.json | 5 +- dictation_client/src/translation/fr.json | 5 +- .../migrations/051-delete-license-alert.sql | 6 ++ 11 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 dictation_server/db/migrations/051-delete-license-alert.sql diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 645f058..c50df11 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -5984,6 +5984,44 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('deleteTask', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/delete` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -6207,6 +6245,19 @@ export const TasksApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['TasksApi.checkout']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTask(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTask(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.deleteTask']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します * @summary @@ -6311,6 +6362,16 @@ export const TasksApiFactory = function (configuration?: Configuration, basePath checkout(audioFileId: number, options?: any): AxiosPromise { return localVarFp.checkout(audioFileId, options).then((request) => request(axios, basePath)); }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTask(audioFileId, options).then((request) => request(axios, basePath)); + }, /** * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します * @summary @@ -6416,6 +6477,18 @@ export class TasksApi extends BaseAPI { return TasksApiFp(this.configuration).checkout(audioFileId, options).then((request) => request(this.axios, this.basePath)); } + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public deleteTask(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).deleteTask(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + /** * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します * @summary diff --git a/dictation_client/src/features/dictation/constants.ts b/dictation_client/src/features/dictation/constants.ts index de262e0..69eae60 100644 --- a/dictation_client/src/features/dictation/constants.ts +++ b/dictation_client/src/features/dictation/constants.ts @@ -8,7 +8,7 @@ export const STATUS = { export type StatusType = typeof STATUS[keyof typeof STATUS]; -export const LIMIT_TASK_NUM = 20; +export const LIMIT_TASK_NUM = 100; export const SORTABLE_COLUMN = { JobNumber: "JOB_NUMBER", diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index c434639..b187c88 100644 --- a/dictation_client/src/features/dictation/dictationSlice.ts +++ b/dictation_client/src/features/dictation/dictationSlice.ts @@ -11,6 +11,7 @@ import { playbackAsync, updateAssigneeAsync, cancelAsync, + deleteTaskAsync, } from "./operations"; import { SORTABLE_COLUMN, @@ -218,6 +219,15 @@ export const dictationSlice = createSlice({ builder.addCase(backupTasksAsync.rejected, (state) => { state.apps.isDownloading = false; }); + builder.addCase(deleteTaskAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteTaskAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteTaskAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 23b98f4..5bf50da 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -572,3 +572,75 @@ export const backupTasksAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteTaskAsync = createAsyncThunk< + { + // empty + }, + { + // パラメータ + audioFileId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("dictations/deleteTaskAsync", async (args, thunkApi) => { + const { audioFileId } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const tasksApi = new TasksApi(config); + + try { + await tasksApi.deleteTask(audioFileId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let message = getTranslationID("dictationPage.message.backupFailedError"); + + if (error.statusCode === 400) { + if (error.code === "E010603") { + // タスクが削除済みの場合は成功扱いとする + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + + if (error.code === "E010601") { + // タスクがInprogressの場合はエラー + message = getTranslationID("dictationPage.message.deleteFailedError"); + } + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index b2591d3..b024fd4 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -33,6 +33,7 @@ import { playbackAsync, cancelAsync, PRIORITY, + deleteTaskAsync, } from "features/dictation"; import { getTranslationID } from "translation"; import { Task } from "api/api"; @@ -61,6 +62,8 @@ const DictationPage: React.FC = (): JSX.Element => { const isTypist = isTypistUser(); const isNone = !isAuthor && !isTypist; + const isDeletableRole = isAdmin || isAuthor; + // popup制御関係 const [ isChangeTranscriptionistPopupOpen, @@ -506,6 +509,53 @@ const DictationPage: React.FC = (): JSX.Element => { return styles.isActiveAz; }; + const onDeleteTask = useCallback( + async (audioFileId: number) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + const { meta } = await dispatch( + deleteTaskAsync({ + audioFileId, + }) + ); + if (meta.requestStatus === "fulfilled") { + const filter = getFilter( + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup + ); + dispatch( + listTasksAsync({ + limit: LIMIT_TASK_NUM, + offset: 0, + filter, + direction: sortDirection, + paramName: sortableParamName, + }) + ); + dispatch(listTypistsAsync()); + dispatch(listTypistGroupsAsync()); + } + }, + [ + dispatch, + filterBackup, + filterFinished, + filterInProgress, + filterPending, + filterUploaded, + sortDirection, + sortableParamName, + t, + ] + ); + // 初回読み込み処理 useEffect(() => { (async () => { @@ -1150,7 +1200,16 @@ const DictationPage: React.FC = (): JSX.Element => {
  • - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + onDeleteTask(x.audioFileId)} + > {t( getTranslationID( "dictationPage.label.deleteDictation" diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index f5399f2..2435346 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -2047,7 +2047,7 @@ tr.isSelected .menuInTable li a.isDisable { position: sticky; top: 0; background: #282828; - z-index: 1; + z-index: 3; } .dictation .table.dictation tr.tableHeader th.clm0 { width: 0px; @@ -2482,8 +2482,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input:checked + label:hover, .formChange ul.holdMember li input:checked + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat - right center; + background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right + center; background-size: 1.3rem; } .formChange > p { diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 011bfb2..6b1395d 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -206,7 +206,8 @@ "taskToPlaybackNoExists": "Die Datei kann nicht abgespielt werden, da sie bereits transkribiert wurde oder nicht existiert.", "taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.", "backupFailedError": "(de)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "(de)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "cancelFailedError": "(de)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Diktate", @@ -555,4 +556,4 @@ "close": "(de)Close" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index c541797..e18c689 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -206,7 +206,8 @@ "taskToPlaybackNoExists": "The file cannot be played because it has already been transcribed or does not exist.", "taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.", "backupFailedError": "ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "cancelFailedError": "タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Dictations", @@ -555,4 +556,4 @@ "close": "Close" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 6726027..36d8ff6 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -206,7 +206,8 @@ "taskToPlaybackNoExists": "El archivo no se puede reproducir porque ya ha sido transcrito o no existe.", "taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.", "backupFailedError": "(es)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "(es)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "cancelFailedError": "(es)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Dictado", @@ -555,4 +556,4 @@ "close": "(es)Close" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 4e3fbbc..b13a338 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -206,7 +206,8 @@ "taskToPlaybackNoExists": "Le fichier ne peut pas être lu car il a déjà été transcrit ou n'existe pas.", "taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.", "backupFailedError": "(fr)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "(fr)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "cancelFailedError": "(fr)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Dictées", @@ -555,4 +556,4 @@ "close": "(fr)Close" } } -} \ No newline at end of file +} diff --git a/dictation_server/db/migrations/051-delete-license-alert.sql b/dictation_server/db/migrations/051-delete-license-alert.sql new file mode 100644 index 0000000..e191a81 --- /dev/null +++ b/dictation_server/db/migrations/051-delete-license-alert.sql @@ -0,0 +1,6 @@ +-- +migrate Up +ALTER TABLE `users` DROP COLUMN `license_alert`; + + +-- +migrate Down +ALTER TABLE `users` ADD COLUMN `license_alert` BOOLEAN DEFAULT TRUE NOT NULL COMMENT 'ライセンスの期限切れ通知をするかどうか'; \ No newline at end of file From d08c6c99af0ba0d2374874d61f5d464caf844a8f Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 16 Jan 2024 07:55:38 +0000 Subject: [PATCH 003/109] =?UTF-8?q?Merged=20PR=20681:=20=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E5=89=8A=E9=99=A4API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3457: タスク削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3457) - タスク削除APIとUTを実装しました。 ## レビューポイント - テストケースは適切でしょうか? - リポジトリでの削除処理は適切でしょうか? - エラー時のコード使い分けは適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_client/src/pages/AuthPage/index.tsx | 8 +- .../db/migrations/052-add-task-index.sql | 5 + dictation_server/src/common/test/overrides.ts | 12 + .../src/features/tasks/tasks.controller.ts | 3 +- .../src/features/tasks/tasks.module.ts | 2 + .../src/features/tasks/tasks.service.spec.ts | 532 ++++++++++++++++++ .../src/features/tasks/tasks.service.ts | 112 +++- .../features/tasks/test/tasks.service.mock.ts | 3 + .../src/features/tasks/test/utility.ts | 75 +++ .../blobstorage/blobstorage.service.ts | 54 ++ .../tasks/tasks.repository.service.ts | 122 +++- 11 files changed, 918 insertions(+), 10 deletions(-) create mode 100644 dictation_server/db/migrations/052-add-task-index.sql diff --git a/dictation_client/src/pages/AuthPage/index.tsx b/dictation_client/src/pages/AuthPage/index.tsx index a17880d..5a66e4c 100644 --- a/dictation_client/src/pages/AuthPage/index.tsx +++ b/dictation_client/src/pages/AuthPage/index.tsx @@ -66,12 +66,10 @@ const AuthPage: React.FC = (): JSX.Element => { clearToken(); return; } - const loginResult = await instance.handleRedirectPromise(); // eslint-disable-next-line console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除(eslint-disable含めて)する。 - if (loginResult && loginResult.account) { const { homeAccountId, idTokenClaims } = loginResult.account; if (idTokenClaims && idTokenClaims.aud) { @@ -85,11 +83,11 @@ const AuthPage: React.FC = (): JSX.Element => { localStorageKeyforIdToken, }) ); - - // トークン取得と設定を行う - navigate("/login"); } } + // ログインページに遷移し、トークン取得と設定を行う + // 何らかの原因で、loginResultがnullの場合でも、ログイン画面に遷移する(ログイン画面でトップページに戻る) + navigate("/login"); } catch (e) { // eslint-disable-next-line console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除(eslint-disable含めて)する。 diff --git a/dictation_server/db/migrations/052-add-task-index.sql b/dictation_server/db/migrations/052-add-task-index.sql new file mode 100644 index 0000000..459a54f --- /dev/null +++ b/dictation_server/db/migrations/052-add-task-index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `tasks` ADD INDEX `idx_account_id_and_audio_file_id` (account_id,audio_file_id); + +-- +migrate Down +ALTER TABLE `tasks` DROP INDEX `idx_account_id_and_audio_file_id`; \ No newline at end of file diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index 146d00a..e9081f6 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -158,6 +158,12 @@ export const overrideBlobstorageService = ( accountId: number, country: string, ) => Promise; + deleteFile?: ( + context: Context, + accountId: number, + country: string, + fileName: string, + ) => Promise; containerExists?: ( context: Context, accountId: number, @@ -189,6 +195,12 @@ export const overrideBlobstorageService = ( writable: true, }); } + if (overrides.deleteFile) { + Object.defineProperty(obj, obj.deleteFile.name, { + value: overrides.deleteFile, + writable: true, + }); + } if (overrides.containerExists) { Object.defineProperty(obj, obj.containerExists.name, { value: overrides.containerExists, diff --git a/dictation_server/src/features/tasks/tasks.controller.ts b/dictation_server/src/features/tasks/tasks.controller.ts index df8afcb..7855c34 100644 --- a/dictation_server/src/features/tasks/tasks.controller.ts +++ b/dictation_server/src/features/tasks/tasks.controller.ts @@ -828,8 +828,7 @@ export class TasksController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: Task削除処理を実装する - console.log(audioFileId); + await this.taskService.deleteTask(context, userId, audioFileId); return {}; } } diff --git a/dictation_server/src/features/tasks/tasks.module.ts b/dictation_server/src/features/tasks/tasks.module.ts index c3e10e0..5a44eb9 100644 --- a/dictation_server/src/features/tasks/tasks.module.ts +++ b/dictation_server/src/features/tasks/tasks.module.ts @@ -8,6 +8,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_ import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module'; import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; +import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; @Module({ imports: [ @@ -18,6 +19,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r AdB2cModule, NotificationhubModule, SendGridModule, + BlobstorageModule, ], 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 eaf6e56..ad85be7 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -14,6 +14,8 @@ import { createCheckoutPermissions, createTask, createUserGroup, + getAudioFile, + getAudioOptionItems, getCheckoutPermissions, getTask, makeTaskTestingModuleWithNotificaiton, @@ -37,6 +39,8 @@ import { createTemplateFile } from '../templates/test/utility'; import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service'; import { Roles } from '../../common/types/role'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; +import { overrideBlobstorageService } from '../../common/test/overrides'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { @@ -3775,3 +3779,531 @@ describe('getNextTask', () => { } }); }); + +describe('deleteTask', () => { + let source: DataSource | null = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + if (!source) return; + await source.destroy(); + source = null; + }); + + it('管理者として、アカウント内のタスクを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const blobStorageService = + module.get(BlobstorageService); + const context = makeContext(admin.external_id, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + await service.deleteTask(context, admin.external_id, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'x.zip', + ); + } + }); + it('Authorとして、自身が追加したタスクを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const blobStorageService = + module.get(BlobstorageService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + await service.deleteTask(context, authorExternalId, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'x.zip', + ); + } + }); + it('ステータスがInProgressのタスクを削除しようとした場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.IN_PROGRESS, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.IN_PROGRESS); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010601')); + } else { + fail(); + } + } + }); + it('Authorが自身が作成したタスク以外を削除しようとした場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId1 = 'AUTHOR_ID1'; + const authorId2 = 'AUTHOR_ID2'; + + const { id: authorUserId1 } = await makeTestUser(source, { + account_id: account.id, + author_id: authorId1, + external_id: 'author-user-external-id1', + role: USER_ROLES.AUTHOR, + }); + const { external_id: authorExternalId2 } = await makeTestUser(source, { + account_id: account.id, + author_id: authorId2, + external_id: 'author-user-external-id2', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId1, + authorId1, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId1); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId2, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId2, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010602')); + } else { + fail(); + } + } + }); + it('削除対象タスクが存在しない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, admin.external_id, 1); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010603')); + } else { + fail(); + } + } + }); + it('タスクのDB削除に失敗した場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId, 'requestId'); + + // DBアクセスに失敗するようにする + const tasksRepositoryService = module.get( + TasksRepositoryService, + ); + tasksRepositoryService.deleteTask = jest + .fn() + .mockRejectedValue('DB failed'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + it('blobストレージからの音声ファイル削除に失敗した場合でも、エラーとならないこと', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + + const context = makeContext(admin.external_id, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: async () => { + throw new Error('blob failed'); + }, + }); + + await service.deleteTask(context, admin.external_id, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + } + }); +}); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index d56055b..4b1b5d5 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -1,6 +1,6 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; -import { Assignee, Task } from './types/types'; +import { Assignee, PostDeleteTaskRequest, Task } from './types/types'; import { Task as TaskEntity } from '../../repositories/tasks/entity/task.entity'; import { createTasks } from './types/convert'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; @@ -9,7 +9,12 @@ import { SortDirection, TaskListSortableAttribute, } from '../../common/types/sort'; -import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants'; +import { + ADMIN_ROLES, + MANUAL_RECOVERY_REQUIRED, + TASK_STATUS, + USER_ROLES, +} from '../../constants'; import { AdB2cService, Adb2cTooManyRequestsError, @@ -36,6 +41,7 @@ import { User } from '../../repositories/users/entity/user.entity'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; @Injectable() export class TasksService { @@ -48,6 +54,7 @@ export class TasksService { private readonly adB2cService: AdB2cService, private readonly sendgridService: SendGridService, private readonly notificationhubService: NotificationhubService, + private readonly blobStorageService: BlobstorageService, ) {} async getTasks( @@ -848,6 +855,107 @@ export class TasksService { } } + /** + * 指定した音声ファイルに紐づくタスクを削除します + * @param context + * @param externalId 実行ユーザーの外部ID + * @param audioFileId 削除対象のタスクのaudio_file_id + * @returns task + */ + async deleteTask( + context: Context, + externalId: string, + audioFileId: number, + ): Promise { + try { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTask.name + } | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`, + ); + + // 実行ユーザーの情報を取得する + const user = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + const account = user.account; + if (!account) { + throw new Error(`account not found. externalId: ${externalId}`); + } + + // 削除対象の音声ファイル情報を取得する + const task = await this.taskRepository.getTaskAndAudioFile( + context, + audioFileId, + user.account_id, + Object.values(TASK_STATUS), + ); + + const targetFileName = task.file?.file_name; + if (!targetFileName) { + throw new Error(`target file not found. audioFileId: ${audioFileId}`); + } + + // DBからタスクと紐づくデータを削除する + await this.taskRepository.deleteTask(context, user.id, audioFileId); + + // Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行するため、try-catchで囲む + try { + // BlobStorageから音声ファイルを削除する + await this.blobStorageService.deleteFile( + context, + account.id, + account.country, + targetFileName, + ); + } catch (e) { + // Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 + this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete Blob: accountId: ${ + account.id + }, fileName: ${targetFileName}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case StatusNotMatchError: + throw new HttpException( + makeErrorResponse('E010601'), + HttpStatus.BAD_REQUEST, + ); + case TaskAuthorIdNotMatchError: + throw new HttpException( + makeErrorResponse('E010602'), + HttpStatus.BAD_REQUEST, + ); + case TasksNotFoundError: + throw new HttpException( + makeErrorResponse('E010603'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteTask.name}`, + ); + } + } + // 通知を送信するプライベートメソッド private async sendNotify( context: Context, diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index 70dc66a..282691b 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -17,6 +17,7 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; +import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service'; export type TasksRepositoryMockValue = { getTasksFromAccountId: @@ -92,6 +93,8 @@ export const makeTasksServiceMock = async ( // メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。 case SendGridService: return {}; + case BlobstorageService: + return {}; } }) .compile(); diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index b4de930..c691934 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -38,6 +38,7 @@ import { NotificationhubServiceMockValue, makeNotificationhubServiceMock, } from './tasks.service.mock'; +import { AudioOptionItem } from '../../../repositories/audio_option_items/entity/audio_option_item.entity'; export const makeTaskTestingModuleWithNotificaiton = async ( datasource: DataSource, @@ -145,6 +146,60 @@ export const createTask = async ( created_at: new Date(), }); const task = taskIdentifiers.pop() as Task; + + await datasource.getRepository(AudioOptionItem).insert([ + { + audio_file_id: audioFile.id, + label: 'label01', + value: 'value01', + }, + { + audio_file_id: audioFile.id, + label: 'label02', + value: 'value02', + }, + { + audio_file_id: audioFile.id, + label: 'label03', + value: 'value03', + }, + { + audio_file_id: audioFile.id, + label: 'label04', + value: 'value04', + }, + { + audio_file_id: audioFile.id, + label: 'label05', + value: 'value05', + }, + { + audio_file_id: audioFile.id, + label: 'label06', + value: 'value06', + }, + { + audio_file_id: audioFile.id, + label: 'label07', + value: 'value07', + }, + { + audio_file_id: audioFile.id, + label: 'label08', + value: 'value08', + }, + { + audio_file_id: audioFile.id, + label: 'label09', + value: 'value09', + }, + { + audio_file_id: audioFile.id, + label: 'label10', + value: 'value10', + }, + ]); + return { taskId: task.id, audioFileId: audioFile.id }; }; /** @@ -229,3 +284,23 @@ export const getCheckoutPermissions = async ( }); return permissions; }; + +export const getAudioFile = async ( + datasource: DataSource, + audio_file_id: number, +): Promise => { + const audioFile = await datasource.getRepository(AudioFile).findOne({ + where: { id: audio_file_id }, + }); + return audioFile; +}; + +export const getAudioOptionItems = async ( + datasource: DataSource, + audio_file_id: number, +): Promise => { + const audioOptionItems = await datasource + .getRepository(AudioOptionItem) + .find({ where: { audio_file_id: audio_file_id } }); + return audioOptionItems; +}; diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index 2381148..9199ad9 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -142,6 +142,60 @@ export class BlobstorageService { } } + /** + * 指定されたファイルを削除します。 + * @param context + * @param accountId + * @param country + * @param fileName + * @returns file + */ + async deleteFile( + context: Context, + accountId: number, + country: string, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.deleteFile.name} | params: { ` + + `accountId: ${accountId} ` + + `country: ${country} ` + + `fileName: ${fileName} };`, + ); + + try { + // 国に応じたリージョンでコンテナ名を指定してClientを取得 + const containerClient = this.getContainerClient( + context, + accountId, + country, + ); + // コンテナ内のBlobパス名を指定してClientを取得 + const blobClient = containerClient.getBlobClient(fileName); + + const { succeeded, errorCode, date } = await blobClient.deleteIfExists(); + this.logger.log( + `[${context.getTrackingId()}] succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`, + ); + + // 失敗時、Blobが存在しない場合以外はエラーとして例外をスローする + // Blob不在の場合のエラーコードは「BlobNotFound」以下を参照 + // https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes + if (!succeeded && errorCode !== 'BlobNotFound') { + throw new Error( + `delete blob failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteFile.name}`, + ); + } + } + /** * Containers exists * @param country diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 9ac0e48..c721f18 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -10,7 +10,12 @@ import { Repository, } from 'typeorm'; import { Task } from './entity/task.entity'; -import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants'; +import { + ADMIN_ROLES, + NODE_ENV_TEST, + TASK_STATUS, + USER_ROLES, +} 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'; @@ -1280,6 +1285,121 @@ export class TasksRepositoryService { ); }); } + /** + * Deletes task + * @param context + * @param userId 削除を行うユーザーID + * @param audioFileId 削除を行うタスクの音声ファイルID + * @returns task + */ + async deleteTask( + context: Context, + userId: number, + audioFileId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + + // 削除を行うユーザーとアカウントを取得 + const user = await userRepo.findOne({ + where: { id: userId }, + relations: { account: true }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!user) { + throw new Error(`user not found. userId:${userId}`); + } + const account = user.account; + if (!account) { + throw new Error(`account not found. userId:${userId}`); + } + + // ユーザーがアカウントの管理者であるかを確認 + const isAdmin = + account.primary_admin_user_id === userId || + account.secondary_admin_user_id === userId; + + // 削除を行うタスクを取得 + const taskRepo = entityManager.getRepository(Task); + const task = await taskRepo.findOne({ + where: { + account_id: account.id, + audio_file_id: audioFileId, + }, + relations: { file: true }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + // テスト環境の場合はロックを行わない(sqliteがlockに対応していないため) + lock: + process.env.NODE_ENV !== NODE_ENV_TEST + ? { mode: 'pessimistic_write' } + : undefined, + }); + if (!task) { + throw new TasksNotFoundError( + `task not found. audio_file_id:${audioFileId}`, + ); + } + if (!task.file) { + throw new Error(`audio file not found. audio_file_id:${audioFileId}`); + } + + // ユーザーが管理者でない場合は、タスクのAuthorIdとユーザーのAuthorIdが一致するかを確認 + if (!isAdmin) { + // ユーザーがAuthorである場合 + if (user.role === USER_ROLES.AUTHOR) { + if (task.file.author_id !== user.author_id) { + throw new TaskAuthorIdNotMatchError( + `Task authorId not match. userId:${userId}, authorId:${user.author_id}`, + ); + } + } else { + // ユーザーが管理者でもAuthorでもない場合はエラー + throw new Error(`The user is not admin or author. userId:${userId}`); + } + } + + // タスクのステータスがInProgressの場合はエラー + if (task.status === TASK_STATUS.IN_PROGRESS) { + throw new StatusNotMatchError( + `task status is InProgress. audio_file_id:${audioFileId}`, + ); + } + + // タスクに紐づくオプションアイテムを削除 + const optionItemRepo = entityManager.getRepository(AudioOptionItem); + await deleteEntity( + optionItemRepo, + { audio_file_id: task.audio_file_id }, + this.isCommentOut, + context, + ); + // タスクに紐づくチェックアウト候補を削除 + const checkoutRepo = entityManager.getRepository(CheckoutPermission); + await deleteEntity( + checkoutRepo, + { task_id: task.id }, + this.isCommentOut, + context, + ); + // タスクを削除 + await deleteEntity( + taskRepo, + { audio_file_id: audioFileId }, + this.isCommentOut, + context, + ); + // タスクに紐づく音声ファイル情報を削除 + const audioFileRepo = entityManager.getRepository(AudioFile); + await deleteEntity( + audioFileRepo, + { id: audioFileId }, + this.isCommentOut, + context, + ); + }); + } /** * workflowに紐づけられているタイピスト・タイピストグループで、タスクのチェックアウト権限を設定 From a7bb32ec4ae12e8eda939811c9cdf1ad8977db64 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 23 Jan 2024 10:30:14 +0000 Subject: [PATCH 004/109] =?UTF-8?q?Merged=20PR=20699:=20DB=E3=83=9E?= =?UTF-8?q?=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=ABCCB=E7=94=A8?= =?UTF-8?q?=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3510: DBマイグレーションファイルにCCB用の設定を追加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3510) - DBマイグレーションファイルにCCB用の設定を追加し、コマンドを修正しました。 ## レビューポイント - 設定内容は認識通りでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 - CCB用のスキーマに対してmigrate:up/downできるところまで確認 --- dictation_server/db/dbconfig.yml | 4 ++++ dictation_server/package.json | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dictation_server/db/dbconfig.yml b/dictation_server/db/dbconfig.yml index 25fdfa9..c7ef32d 100644 --- a/dictation_server/db/dbconfig.yml +++ b/dictation_server/db/dbconfig.yml @@ -2,6 +2,10 @@ local: dialect: mysql dir: /app/dictation_server/db/migrations datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true +ccb: + dialect: mysql + dir: /app/dictation_server/db/migrations + datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME_CCB}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true ci: dialect: mysql dir: ./dictation_server/db/migrations diff --git a/dictation_server/package.json b/dictation_server/package.json index 1afac08..d92d3d4 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -24,9 +24,9 @@ "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\"", - "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", - "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=local" + "migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=ccb" }, "dependencies": { "@azure/identity": "^3.1.3", From 1524ec247329de0930d33c5a39f27332cf9f92bf Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 24 Jan 2024 00:48:21 +0000 Subject: [PATCH 005/109] =?UTF-8?q?Merged=20PR=20701:=20=E3=83=91=E3=82=A4?= =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=A4=E3=83=B3=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3531: パイプラインエラー対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3531) - パイプラインエラー解消 ## レビューポイント - 共有 --- azure-pipelines-staging.yml | 68 +- .../src/pages/AccountPage/index.tsx | 2 + dictation_client/src/pages/AuthPage/index.tsx | 3 - .../src/pages/LicensePage/licenseSummary.tsx | 8 +- .../src/pages/PartnerPage/index.tsx | 2 + .../src/pages/SignupPage/signupInput.tsx | 54 +- .../src/pages/TemplateFilePage/index.tsx | 2 + .../src/pages/TermsPage/index.tsx | 9 +- .../src/pages/UserListPage/index.tsx | 2 + dictation_client/src/translation/de.json | 6 +- dictation_client/src/translation/en.json | 5 +- dictation_client/src/translation/es.json | 5 +- dictation_client/src/translation/fr.json | 5 +- dictation_function/package-lock.json | 20 + dictation_function/package.json | 2 + .../src/functions/licenseAlert.ts | 2 +- .../src/test/licenseAlert.spec.ts | 36 +- .../src/test/licenseAutoAllocation.spec.ts | 26 +- .../.devcontainer/docker-compose.yml | 10 + .../.devcontainer/pipeline-docker-compose.yml | 35 + dictation_server/.env.test | 37 + dictation_server/db/dbconfig.yml | 4 + .../migrations/053-delete-license-alert.sql | 6 + dictation_server/package.json | 5 +- dictation_server/src/common/test/init.ts | 21 + dictation_server/src/common/test/modules.ts | 2 +- dictation_server/src/constants/index.ts | 10 + .../accounts/accounts.controller.spec.ts | 2 +- .../accounts/accounts.service.spec.ts | 951 ++++++++++++------ .../src/features/auth/auth.controller.spec.ts | 2 +- .../src/features/auth/auth.service.spec.ts | 147 ++- .../features/files/files.controller.spec.ts | 2 +- .../src/features/files/files.service.spec.ts | 217 ++-- .../src/features/files/test/utility.ts | 2 +- .../licenses/licenses.controller.spec.ts | 2 +- .../licenses/licenses.service.spec.ts | 227 +++-- .../src/features/licenses/test/utility.ts | 1 + .../notification.controller.spec.ts | 2 +- .../test/notification.service.mock.ts | 2 +- .../features/tasks/tasks.controller.spec.ts | 2 +- .../src/features/tasks/tasks.service.spec.ts | 291 ++++-- .../src/features/tasks/test/utility.ts | 2 +- .../templates/templates.controller.spec.ts | 2 +- .../templates/templates.service.spec.ts | 41 +- .../src/features/terms/terms.service.spec.ts | 37 +- .../features/users/test/users.service.mock.ts | 2 +- .../src/features/users/test/utility.ts | 2 +- .../features/users/users.controller.spec.ts | 2 +- .../src/features/users/users.service.spec.ts | 277 +++-- .../workflows/workflows.controller.spec.ts | 2 +- .../workflows/workflows.service.spec.ts | 233 +++-- .../users/users.repository.service.ts | 16 +- .../workflows/workflows.repository.service.ts | 7 +- 53 files changed, 1933 insertions(+), 927 deletions(-) create mode 100644 dictation_server/.devcontainer/pipeline-docker-compose.yml create mode 100644 dictation_server/.env.test create mode 100644 dictation_server/db/migrations/053-delete-license-alert.sql create mode 100644 dictation_server/src/common/test/init.ts diff --git a/azure-pipelines-staging.yml b/azure-pipelines-staging.yml index 86f970d..65c19c4 100644 --- a/azure-pipelines-staging.yml +++ b/azure-pipelines-staging.yml @@ -27,9 +27,30 @@ jobs: exit 1 fi displayName: 'タグが付けられたCommitがmainブランチに存在するか確認' -- job: backend_build +- job: backend_test dependsOn: initialize condition: succeeded('initialize') + displayName: UnitTest + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Bash@3 + displayName: Bash Script (Test) + inputs: + targetType: inline + workingDirectory: dictation_server/.devcontainer + script: | + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_server sudo npm ci + docker-compose exec -T dictation_server sudo npm run migrate:up:test + docker-compose exec -T dictation_server sudo npm run test +- job: backend_build + dependsOn: backend_test + condition: succeeded('backend_test') displayName: Build And Push Backend Image pool: name: odms-deploy-pipeline @@ -43,51 +64,6 @@ jobs: command: ci workingDir: dictation_server verbose: false - - task: AzureKeyVault@2 - displayName: 'Azure Key Vault: kv-odms-secret-stg' - inputs: - ConnectedServiceName: 'omds-service-connection-stg' - KeyVaultName: kv-odms-secret-stg - SecretsFilter: '*' - - task: Bash@3 - displayName: Bash Script (Test) - inputs: - targetType: inline - script: | - cd dictation_server - npm run test - env: - JWT_PUBLIC_KEY: $(token-public-key) - JWT_PRIVATE_KEY: $(token-private-key) - SENDGRID_API_KEY: $(sendgrid-api-key) - NOTIFICATION_HUB_NAME: $(notification-hub-name) - NOTIFICATION_HUB_CONNECT_STRING: $(notification-hub-connect-string) - STORAGE_ACCOUNT_NAME_US: $(storage-account-name-us) - STORAGE_ACCOUNT_NAME_AU: $(storage-account-name-au) - STORAGE_ACCOUNT_NAME_EU: $(storage-account-name-eu) - STORAGE_ACCOUNT_KEY_US: $(storage-account-key-us) - STORAGE_ACCOUNT_KEY_AU: $(storage-account-key-au) - STORAGE_ACCOUNT_KEY_EU: $(storage-account-key-eu) - STORAGE_ACCOUNT_ENDPOINT_US: $(storage-account-endpoint-us) - STORAGE_ACCOUNT_ENDPOINT_AU: $(storage-account-endpoint-au) - STORAGE_ACCOUNT_ENDPOINT_EU: $(storage-account-endpoint-eu) - ADB2C_TENANT_ID: $(adb2c-tenant-id) - ADB2C_CLIENT_ID: $(adb2c-client-id) - ADB2C_CLIENT_SECRET: $(adb2c-client-secret) - MAIL_FROM: xxxxxx - APP_DOMAIN: xxxxxxxxx - EMAIL_CONFIRM_LIFETIME: 0 - TENANT_NAME: xxxxxxxxxxxx - SIGNIN_FLOW_NAME: xxxxxxxxxxxx - STORAGE_TOKEN_EXPIRE_TIME: 0 - REFRESH_TOKEN_LIFETIME_WEB: 86400000 - REFRESH_TOKEN_LIFETIME_DEFAULT: 2592000000 - ACCESS_TOKEN_LIFETIME_WEB: 7200000 - REDIS_HOST: xxxxxxxxxxxx - REDIS_PORT: 0 - REDIS_PASSWORD: xxxxxxxxxxxx - ADB2C_CACHE_TTL: 0 - STAGE: local - task: Docker@0 displayName: build inputs: diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx index 79963ae..6fd0877 100644 --- a/dictation_client/src/pages/AccountPage/index.tsx +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -102,6 +102,7 @@ const AccountPage: React.FC = (): JSX.Element => {
    + {/* File Delete Setting は現状不要のため非表示 + */}
    diff --git a/dictation_client/src/pages/AuthPage/index.tsx b/dictation_client/src/pages/AuthPage/index.tsx index 5a66e4c..e8d81f9 100644 --- a/dictation_client/src/pages/AuthPage/index.tsx +++ b/dictation_client/src/pages/AuthPage/index.tsx @@ -67,9 +67,6 @@ const AuthPage: React.FC = (): JSX.Element => { return; } const loginResult = await instance.handleRedirectPromise(); - - // eslint-disable-next-line - console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除(eslint-disable含めて)する。 if (loginResult && loginResult.account) { const { homeAccountId, idTokenClaims } = loginResult.account; if (idTokenClaims && idTokenClaims.aud) { diff --git a/dictation_client/src/pages/LicensePage/licenseSummary.tsx b/dictation_client/src/pages/LicensePage/licenseSummary.tsx index 058350d..a343fa5 100644 --- a/dictation_client/src/pages/LicensePage/licenseSummary.tsx +++ b/dictation_client/src/pages/LicensePage/licenseSummary.tsx @@ -289,13 +289,17 @@ export const LicenseSummary: React.FC = ( ) )} -
    {licenseSummaryInfo.storageSize}GB
    + {/* Storage Usedの値表示をハイフンに置き換え */} + {/*
    {licenseSummaryInfo.storageSize}GB
    */} +
    -
    {t( getTranslationID("LicenseSummaryPage.label.usedSize") )}
    -
    {licenseSummaryInfo.usedSize}GB
    + {/* Storage Usedの値表示をハイフンに置き換え */} + {/*
    {licenseSummaryInfo.usedSize}GB
    */} +
    -
    {t( getTranslationID( diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx index 7f73f66..835cfa3 100644 --- a/dictation_client/src/pages/PartnerPage/index.tsx +++ b/dictation_client/src/pages/PartnerPage/index.tsx @@ -185,6 +185,7 @@ const PartnerPage: React.FC = (): JSX.Element => {
      + {/* パートナーアカウント削除はCCB後回し分なので非表示 {isVisibleButton && (
    • @@ -196,6 +197,7 @@ const PartnerPage: React.FC = (): JSX.Element => {
    • )} + */} {isVisibleDealerManagement && (
    • {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} diff --git a/dictation_client/src/pages/SignupPage/signupInput.tsx b/dictation_client/src/pages/SignupPage/signupInput.tsx index 3f388ce..be83cf7 100644 --- a/dictation_client/src/pages/SignupPage/signupInput.tsx +++ b/dictation_client/src/pages/SignupPage/signupInput.tsx @@ -39,7 +39,11 @@ const SignupInput: React.FC = (): JSX.Element => { const navigate = useNavigate(); const [isPasswordHide, setIsPasswordHide] = useState(true); const [isOpenPolicy, setIsOpenPolicy] = useState(false); - const [isAgreePolicy, setIsAgreePolicy] = useState(false); + const [isOpenPrivacyNotice, setIsOpenPrivacyNoyice] = + useState(false); + const [isCheckedEula, setIsCheckedEula] = useState(false); + const [isCheckedPrivacyNotice, setIsCheckedPrivacyNotice] = + useState(false); const [isPushCreateButton, setIsPushCreateButton] = useState(false); const { hasErrorEmptyAdminName, @@ -90,6 +94,9 @@ const SignupInput: React.FC = (): JSX.Element => { dispatch(getLatestEulaVersionAsync()); }, [dispatch]); + // ボタン押下可否判定ロジック + const canClickButton = () => isCheckedEula && isCheckedPrivacyNotice; + useEffect(() => { // 外部のWebサイトからの遷移時にURLのパラメータを取得 // 以下のようなURLで遷移してきた場合に、Dealerと言語を変更する @@ -371,18 +378,48 @@ const SignupInput: React.FC = (): JSX.Element => { setIsOpenPolicy(true); }} > - {t(getTranslationID("signupPage.label.termsLink"))} + {t(getTranslationID("signupPage.label.linkOfEula"))} - {` ${t(getTranslationID("signupPage.label.termsLinkFor"))} `} + {` ${t(getTranslationID("signupPage.label.forOdms"))} `}
      -
    • )} + {/* ユーザー削除 CCB後回し分なので今は非表示
    • {t( @@ -252,6 +253,7 @@ const UserListPage: React.FC = (): JSX.Element => { )}
    • + */}
    {user.name} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 6b1395d..01a37b9 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -73,9 +73,9 @@ "adminName": "Name des Administrators", "email": "E-Mail-Addresse", "password": "Passwort", - "termsLink": "Klicken Sie hier, um die Nutzungsbedingungen zu lesen.", - "termsLinkFor": "für ODMS Cloud.", - "termsCheckBox": "Ja, ich stimme den Nutzungsbedingungen zu.", + "linkOfEula": "Klicken Sie hier, um die Endbenutzer-Lizenzvereinbarung zu lesen.", + "linkOfPrivacyNotice": "Klicken Sie hier, um die Datenschutzerklärung zu lesen.", + "forOdms": "für ODMS Cloud.", "createAccountButton": "Einreichen" } }, diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index e18c689..c37d4ad 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -73,8 +73,9 @@ "adminName": "Administrator‘s Name", "email": "Email Address", "password": "Password", - "termsLink": "Click here to read the terms of use", - "termsLinkFor": "for OMDS Cloud.", + "linkOfEula": "Click here to read the End User License Agreement.", + "linkOfPrivacyNotice": "Click here to read the Privacy Notice.", + "forOdms": "for ODMS Cloud.", "termsCheckBox": "Yes, I agree to the terms of use.", "createAccountButton": "Submit" } diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 36d8ff6..bf3d0f7 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -73,8 +73,9 @@ "adminName": "Nombre del administrador", "email": "Dirección de correo electrónico", "password": "Contraseña", - "termsLink": "Haga clic aquí para leer el término de uso.", - "termsLinkFor": "para la nube ODMS.", + "linkOfEula": "Haga clic aquí para leer el Acuerdo de licencia de usuario final.", + "linkOfPrivacyNotice": "Haga clic aquí para leer el Aviso de Privacidad.", + "forOdms": "para la nube ODMS.", "termsCheckBox": "Sí, estoy de acuerdo con los términos de uso.", "createAccountButton": "Entregar" } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index b13a338..4c79233 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -73,8 +73,9 @@ "adminName": "Nom de l'administrateur", "email": "Adresse e-mail", "password": "Mot de passe", - "termsLink": "Cliquez ici pour lire les conditions d'utilisation.", - "termsLinkFor": "pour ODMS Cloud.", + "linkOfEula": "Cliquez ici pour lire le contrat de licence utilisateur final.", + "linkOfPrivacyNotice": "Cliquez ici pour lire l'avis de confidentialité.", + "forOdms": "pour ODMS Cloud.", "termsCheckBox": "Oui, j'accepte les conditions d'utilisation.", "createAccountButton": "Soumettre" } diff --git a/dictation_function/package-lock.json b/dictation_function/package-lock.json index 4da5447..8ebaf00 100644 --- a/dictation_function/package-lock.json +++ b/dictation_function/package-lock.json @@ -20,8 +20,10 @@ "@types/jest": "^27.5.0", "@types/node": "18.x", "@types/redis": "^2.8.13", + "@types/redis-mock": "^0.17.3", "azure-functions-core-tools": "^4.x", "jest": "^28.0.3", + "redis-mock": "^0.56.3", "rimraf": "^5.0.0", "sqlite3": "^5.1.6", "supertest": "^6.1.3", @@ -2000,6 +2002,15 @@ "@types/node": "*" } }, + "node_modules/@types/redis-mock": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/redis-mock/-/redis-mock-0.17.3.tgz", + "integrity": "sha512-1baXyGxRKEDog8p1ReiypODwiST2n3/0pBbgUKEuv9pBXnY6ttRzKATcW5Xz20ZOl9qkKtPIeq20tHgHSdQBAQ==", + "dev": true, + "dependencies": { + "@types/redis": "^2.8.0" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", @@ -6409,6 +6420,15 @@ "node": ">=4" } }, + "node_modules/redis-mock": { + "version": "0.56.3", + "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.56.3.tgz", + "integrity": "sha512-ynaJhqk0Qf3Qajnwvy4aOjS4Mdf9IBkELWtjd+NYhpiqu4QCNq6Vf3Q7c++XRPGiKiwRj9HWr0crcwy7EiPjYQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", diff --git a/dictation_function/package.json b/dictation_function/package.json index 8a6260d..33f2287 100644 --- a/dictation_function/package.json +++ b/dictation_function/package.json @@ -25,8 +25,10 @@ "@types/jest": "^27.5.0", "@types/node": "18.x", "@types/redis": "^2.8.13", + "@types/redis-mock": "^0.17.3", "azure-functions-core-tools": "^4.x", "jest": "^28.0.3", + "redis-mock": "^0.56.3", "rimraf": "^5.0.0", "sqlite3": "^5.1.6", "supertest": "^6.1.3", diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts index bbab8b0..ad4119f 100644 --- a/dictation_function/src/functions/licenseAlert.ts +++ b/dictation_function/src/functions/licenseAlert.ts @@ -77,7 +77,7 @@ export async function licenseAlertProcessing( const keys = await keysAsync(`${SEND_COMPLETE_PREFIX}${formattedDate}*`); console.log(`delete terget:${keys}`); if (keys.length > 0) { - const delResult = await delAsync(...keys); + const delResult = await delAsync(keys); console.log(`delete number:${delResult}`); } } catch (e) { diff --git a/dictation_function/src/test/licenseAlert.spec.ts b/dictation_function/src/test/licenseAlert.spec.ts index 390dc18..c3891a0 100644 --- a/dictation_function/src/test/licenseAlert.spec.ts +++ b/dictation_function/src/test/licenseAlert.spec.ts @@ -13,13 +13,15 @@ import { ADB2C_SIGN_IN_TYPE } from "../constants"; import { SendGridService } from "../sendgrid/sendgrid"; import { AdB2cService } from "../adb2c/adb2c"; import { InvocationContext } from "@azure/functions"; -import { RedisClient } from "redis"; -import { createRedisClient } from "../redis/redis"; +import { RedisClient, createClient } from "redis-mock"; +import { promisify } from "util"; describe("licenseAlert", () => { dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); let source: DataSource | null = null; + const redisClient = createClient(); + beforeEach(async () => { source = new DataSource({ type: "sqlite", @@ -35,17 +37,15 @@ describe("licenseAlert", () => { if (!source) return; await source.destroy(); source = null; + //licenseAlertProcessingの処理の最後にキャッシュ削除処理があるため、ここでクリーンアップは行わない }); - it("テストを通すための仮", async () => {}); - /* - it("ライセンス在庫不足メールが送信され、ライセンス失効警告メールが送信されないこと", async () => { if (!source) fail(); const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; - const redisClient = createRedisClient(); + // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -77,7 +77,10 @@ describe("licenseAlert", () => { adb2cMock ); expect(spySend.mock.calls).toHaveLength(1); - redisClient.quit; + // redisからキャッシュが削除されていることを確認 + const getAsync = promisify(redisClient.keys).bind(redisClient); + const keys = await getAsync(`*`); + expect(keys).toHaveLength(0); }); it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => { @@ -85,7 +88,6 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; - const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -118,7 +120,10 @@ describe("licenseAlert", () => { adb2cMock ); expect(spySend.mock.calls).toHaveLength(2); - redisClient.quit; + // redisからキャッシュが削除されていることを確認 + const getAsync = promisify(redisClient.keys).bind(redisClient); + const keys = await getAsync(`*`); + expect(keys).toHaveLength(0); }); it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => { @@ -126,7 +131,6 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; - const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -172,7 +176,10 @@ describe("licenseAlert", () => { adb2cMock ); expect(spySend.mock.calls).toHaveLength(0); - redisClient.quit; + // redisからキャッシュが削除されていることを確認 + const getAsync = promisify(redisClient.keys).bind(redisClient); + const keys = await getAsync(`*`); + expect(keys).toHaveLength(0); }); it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => { @@ -180,7 +187,6 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; - const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -213,9 +219,11 @@ describe("licenseAlert", () => { adb2cMock ); expect(spySend.mock.calls).toHaveLength(1); - redisClient.quit; + // redisからキャッシュが削除されていることを確認 + const getAsync = promisify(redisClient.keys).bind(redisClient); + const keys = await getAsync(`*`); + expect(keys).toHaveLength(0); }); - */ }); // テスト用sendgrid diff --git a/dictation_function/src/test/licenseAutoAllocation.spec.ts b/dictation_function/src/test/licenseAutoAllocation.spec.ts index 170862a..d0bf9ef 100644 --- a/dictation_function/src/test/licenseAutoAllocation.spec.ts +++ b/dictation_function/src/test/licenseAutoAllocation.spec.ts @@ -282,13 +282,9 @@ describe("licenseAutoAllocation", () => { it("有効期限が指定日のライセンスが自動更新されること(リトライ用)", async () => { if (!source) fail(); const context = new InvocationContext(); - // 11/22の日付を作成 - const dateSeptember22 = new Date(); - dateSeptember22.setMonth(11); - dateSeptember22.setDate(22); - dateSeptember22.setHours(23, 59, 59); - const currentDateEndTime = new DateWithDayEndTime(dateSeptember22); - + // 2023/11/22の日付を作成 + const date1122 = new Date(2023, 10, 22, 23, 59, 59); + const currentDateEndTime = new DateWithDayEndTime(date1122); // アカウント const account1 = await makeTestAccount( source, @@ -328,7 +324,7 @@ describe("licenseAutoAllocation", () => { auto_renew: false, }); - // 割り当て済みで有効期限が12/31のライセンス + // 割り当て済みで有効期限が11/22のライセンス await createLicense( source, 1, @@ -389,10 +385,9 @@ describe("licenseAutoAllocation", () => { null, null ); - // 割り当て済みの更新対象ではないライセンス const nextDate = new Date(); - nextDate.setDate(dateSeptember22.getDate() + 1); + nextDate.setDate(date1122.getDate() + 1); nextDate.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定 nextDate.setMilliseconds(0); await createLicense( @@ -407,15 +402,13 @@ describe("licenseAutoAllocation", () => { null, null ); - // 有効期限が先の未割当ライセンスを作成 // idが100のものは有効期限が当日なので自動割り当て対象外 // idが101のものから割り当てられる for (let i = 0; i < 10; i++) { - const date = new Date(); - date.setDate(dateSeptember22.getDate() + i); - date.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定 - date.setMilliseconds(0); + // 2023/11/22の日付を作成 + const date = new Date(2023, 10, 22, 23, 59, 59); + date.setDate(date.getDate() + i); await createLicense( source, i + 100, @@ -446,8 +439,7 @@ describe("licenseAutoAllocation", () => { null, null ); - - await licenseAutoAllocationProcessing(context, source, dateSeptember22); + await licenseAutoAllocationProcessing(context, source, date1122); const user1Allocated = await selectLicenseByAllocatedUser(source, user1.id); const user2Allocated = await selectLicenseByAllocatedUser(source, user2.id); const user3Allocated = await selectLicenseByAllocatedUser(source, user3.id); diff --git a/dictation_server/.devcontainer/docker-compose.yml b/dictation_server/.devcontainer/docker-compose.yml index 47cf396..ca3a9a8 100644 --- a/dictation_server/.devcontainer/docker-compose.yml +++ b/dictation_server/.devcontainer/docker-compose.yml @@ -2,6 +2,7 @@ version: '3' services: dictation_server: + container_name: dictation_server_dev_container env_file: ../.env build: . working_dir: /app/dictation_server @@ -16,6 +17,15 @@ services: - CHOKIDAR_USEPOLLING=true networks: - external + test_mysql_db: + image: mysql:8.0-bullseye + environment: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: odms + MYSQL_USER: user + MYSQL_PASSWORD: password + networks: + - external networks: external: name: omds_network diff --git a/dictation_server/.devcontainer/pipeline-docker-compose.yml b/dictation_server/.devcontainer/pipeline-docker-compose.yml new file mode 100644 index 0000000..19708a2 --- /dev/null +++ b/dictation_server/.devcontainer/pipeline-docker-compose.yml @@ -0,0 +1,35 @@ +version: '3' + +services: + dictation_server: + container_name: dictation_server_dev_container + env_file: ../.env + build: . + working_dir: /app/dictation_server + ports: + - '8081:8081' + volumes: + - ../../:/app + - node_modules:/app/dictation_server/node_modules + expose: + - '8081' + environment: + - CHOKIDAR_USEPOLLING=true + depends_on: + - test_mysql_db + networks: + - network + test_mysql_db: + image: mysql:8.0-bullseye + environment: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: odms + MYSQL_USER: user + MYSQL_PASSWORD: password + networks: + - network +networks: + network: + name: test_network +volumes: + node_modules: \ No newline at end of file diff --git a/dictation_server/.env.test b/dictation_server/.env.test new file mode 100644 index 0000000..a2748df --- /dev/null +++ b/dictation_server/.env.test @@ -0,0 +1,37 @@ +STAGE=local +NO_COLOR=TRUE +CORS=TRUE +PORT=8081 +TENANT_NAME=xxxxxxxxxx +SIGNIN_FLOW_NAME=b2c_1_signin_xxx +ADB2C_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +ADB2C_ORIGIN=https://example.com/xxxxxxxxx.onmicrosoft.com/b2c_1_signin_xxx/ +KEY_VAULT_NAME=xxxxxxxxxxxx +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n" +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n" +SENDGRID_API_KEY=SG.P_xxxx_xxxxxxxxxxxxxxx.xxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx-pc +MAIL_FROM=noreply@example.com +NOTIFICATION_HUB_NAME=ntf-odms-dev +NOTIFICATION_HUB_CONNECT_STRING=Endpoint=sb://example.com/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= +APP_DOMAIN=http://localhost:8081/ +STORAGE_TOKEN_EXPIRE_TIME=30 +STORAGE_ACCOUNT_NAME_US=saxxxxusxxx +STORAGE_ACCOUNT_NAME_AU=saxxxxauxxx +STORAGE_ACCOUNT_NAME_EU=saxxxxeuxxx +STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== +STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== +STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== +STORAGE_ACCOUNT_ENDPOINT_US=https://xxxxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_AU=https://xxxxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_EU=https://xxxxxxxxxxxx.blob.core.windows.net/ +ACCESS_TOKEN_LIFETIME_WEB=7200000 +REFRESH_TOKEN_LIFETIME_WEB=86400000 +REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 +EMAIL_CONFIRM_LIFETIME=86400000 +REDIS_HOST=redis-cache +REDIS_PORT=6379 +REDIS_PASSWORD=omdsredispass +ADB2C_CACHE_TTL=86400 +TEMPLATE_ROOT=dist \ No newline at end of file diff --git a/dictation_server/db/dbconfig.yml b/dictation_server/db/dbconfig.yml index c7ef32d..38bd96b 100644 --- a/dictation_server/db/dbconfig.yml +++ b/dictation_server/db/dbconfig.yml @@ -10,3 +10,7 @@ ci: dialect: mysql dir: ./dictation_server/db/migrations datasource: DB_USERNAME:DB_PASS@tcp(DB_HOST:DB_PORT)/DB_NAME?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true +test: + dialect: mysql + dir: /app/dictation_server/db/migrations + datasource: user:password@tcp(test_mysql_db:3306)/odms?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true \ No newline at end of file diff --git a/dictation_server/db/migrations/053-delete-license-alert.sql b/dictation_server/db/migrations/053-delete-license-alert.sql new file mode 100644 index 0000000..e7e6871 --- /dev/null +++ b/dictation_server/db/migrations/053-delete-license-alert.sql @@ -0,0 +1,6 @@ +-- +migrate Up +ALTER TABLE `users_archive` DROP COLUMN `license_alert`; + + +-- +migrate Down +ALTER TABLE `users_archive` ADD COLUMN `license_alert` BOOLEAN NOT NULL COMMENT 'ライセンスの期限切れ通知をするかどうか'; \ No newline at end of file diff --git a/dictation_server/package.json b/dictation_server/package.json index d92d3d4..a8d50bd 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -17,7 +17,7 @@ "tc": "tsc --noEmit", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", + "test": "jest -w 1", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", @@ -26,7 +26,8 @@ "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=ccb", "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=ccb", - "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=ccb" + "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test" }, "dependencies": { "@azure/identity": "^3.1.3", diff --git a/dictation_server/src/common/test/init.ts b/dictation_server/src/common/test/init.ts new file mode 100644 index 0000000..e919d16 --- /dev/null +++ b/dictation_server/src/common/test/init.ts @@ -0,0 +1,21 @@ +import { DataSource } from "typeorm"; + +export const truncateAllTable = async (source: DataSource) => { + const entities = source.entityMetadatas; + const queryRunner = source.createQueryRunner(); + + try { + await queryRunner.startTransaction(); + await queryRunner.query('SET FOREIGN_KEY_CHECKS=0'); + for (const entity of entities) { + await queryRunner.query(`TRUNCATE TABLE \`${entity.tableName}\``); + } + await queryRunner.query('SET FOREIGN_KEY_CHECKS=1'); + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); + } +}; \ No newline at end of file diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 61c0396..7c51a50 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -48,7 +48,7 @@ export const makeTestingModule = async ( const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), AuthModule, diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index da502d7..511e4b8 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -88,6 +88,16 @@ export const USER_ROLES = { TYPIST: 'typist', } as const; +/** + * ロールのソート順 + * @const {string[]} + */ +export const USER_ROLE_ORDERS = [ + USER_ROLES.AUTHOR, + USER_ROLES.TYPIST, + USER_ROLES.NONE, +] as string[]; + /** * ライセンス注文状態 * @const {string[]} diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index 2761b0e..f69ece1 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -13,7 +13,7 @@ describe('AccountsController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 9b192c9..4bfd687 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -78,23 +78,36 @@ import { AccountsRepositoryService } from '../../repositories/accounts/accounts. import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { createWorkflow, getWorkflows } from '../workflows/test/utility'; import { UsersService } from '../users/users.service'; +import { truncateAllTable } from '../../common/test/init'; describe('createAccount', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('アカウントを作成できる', async () => { @@ -754,20 +767,32 @@ describe('createAccount', () => { describe('createPartnerAccount', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1740,20 +1765,32 @@ describe('AccountsService', () => { describe('getLicenseSummary', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('第五階層のライセンス情報を取得する', async () => { @@ -1787,7 +1824,7 @@ describe('getLicenseSummary', () => { // 有効期限が14日後のライセンスを追加(5ライセンス) const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 14); - expiryDate.setHours(23, 59, 59, 999); + expiryDate.setHours(23, 59, 59, 0); for (let i = 0; i < 5; i++) { await createLicenseSetExpiryDateAndStatus( source, @@ -1812,7 +1849,7 @@ describe('getLicenseSummary', () => { await createLicenseSetExpiryDateAndStatus( source, childAccountId1, - new Date(2500, 1, 1, 23, 59, 59), + new Date(2037, 1, 1, 23, 59, 59), element, 1, ); @@ -1823,7 +1860,7 @@ describe('getLicenseSummary', () => { await createLicenseSetExpiryDateAndStatus( source, childAccountId2, - new Date(2500, 1, 1, 23, 59, 59), + new Date(2037, 1, 1, 23, 59, 59), 'Unallocated', ); } @@ -1887,20 +1924,32 @@ describe('getLicenseSummary', () => { describe('getPartnerAccount', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1942,7 +1991,7 @@ describe('getPartnerAccount', () => { // 有効期限が14日後のライセンスを追加(5ライセンス) const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 14); - expiryDate.setHours(23, 59, 59, 999); + expiryDate.setHours(23, 59, 59, 0); for (let i = 0; i < 5; i++) { await createLicenseSetExpiryDateAndStatus( source, @@ -1960,7 +2009,7 @@ describe('getPartnerAccount', () => { // 有効期限が15日後のライセンスを追加 expiryDate.setDate(expiryDate.getDate() + 15); - expiryDate.setHours(23, 59, 59, 999); + expiryDate.setHours(23, 59, 59, 0); await createLicenseSetExpiryDateAndStatus( source, childAccountId3, @@ -1975,7 +2024,7 @@ describe('getPartnerAccount', () => { await createLicenseSetExpiryDateAndStatus( source, childAccountId1, - new Date(2500, 1, 1, 23, 59, 59), + new Date(2037, 1, 1, 23, 59, 59), element, ); }); @@ -1985,7 +2034,7 @@ describe('getPartnerAccount', () => { await createLicenseSetExpiryDateAndStatus( source, childAccountId2, - new Date(2500, 1, 1, 23, 59, 59), + new Date(2037, 1, 1, 23, 59, 59), 'Unallocated', ); } @@ -2029,20 +2078,32 @@ describe('getPartnerAccount', () => { describe('getOrderHistories', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2161,20 +2222,32 @@ describe('getOrderHistories', () => { describe('issueLicense', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2468,20 +2541,32 @@ describe('issueLicense', () => { describe('getDealers', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('Dealerを取得できる', async () => { @@ -2551,20 +2636,32 @@ describe('getDealers', () => { describe('createTypistGroup', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('TypistGroupを作成できる', async () => { @@ -2851,20 +2948,32 @@ describe('createTypistGroup', () => { describe('getTypistGroup', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('指定したIDのTypistGroupを取得できる', async () => { @@ -3051,20 +3160,32 @@ describe('getTypistGroup', () => { describe('updateTypistGroup', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('TypistGroupを更新できる', async () => { @@ -3467,20 +3588,32 @@ describe('updateTypistGroup', () => { describe('getWorktypes', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -3587,20 +3720,32 @@ describe('getWorktypes', () => { describe('createWorktype', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -3740,20 +3885,32 @@ describe('createWorktype', () => { describe('updateWorktype', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -4019,20 +4176,32 @@ describe('updateWorktype', () => { describe('deleteWorktype', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -4267,20 +4436,32 @@ describe('deleteWorktype', () => { describe('getOptionItems', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -4406,20 +4587,32 @@ describe('getOptionItems', () => { describe('updateOptionItems', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -4732,20 +4925,32 @@ describe('updateOptionItems', () => { describe('updateActiveWorktype', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -4960,20 +5165,32 @@ describe('updateActiveWorktype', () => { describe('ライセンス発行キャンセル', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('ライセンス発行のキャンセルが完了する(第一階層で実行)', async () => { @@ -5274,20 +5491,32 @@ describe('ライセンス発行キャンセル', () => { describe('パートナー一覧取得', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('パートナー一覧を取得する', async () => { @@ -5442,20 +5671,32 @@ describe('パートナー一覧取得', () => { describe('アカウント情報更新', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('アカウント情報を更新する(第五階層が実行/セカンダリ管理者ユーザがnull)', async () => { @@ -5660,20 +5901,32 @@ describe('アカウント情報更新', () => { describe('getAccountInfo', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('パラメータのユーザに対応するアカウント情報を取得できる', async () => { @@ -5720,20 +5973,32 @@ describe('getAccountInfo', () => { }); describe('getAuthors', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('アカウント内のAuthorユーザーの一覧を取得できる', async () => { @@ -5872,20 +6137,32 @@ describe('getAuthors', () => { describe('getTypists', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('アカウント内のTypistユーザーの一覧を取得できる', async () => { @@ -6054,20 +6331,32 @@ describe('getTypists', () => { describe('deleteAccountAndData', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('アカウント情報が削除されること', async () => { @@ -6450,20 +6739,32 @@ describe('deleteAccountAndData', () => { }); describe('getAccountInfoMinimalAccess', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('IDトークンのsub情報からアカウントの階層情報を取得できること(第五階層)', async () => { @@ -6583,20 +6884,32 @@ describe('getAccountInfoMinimalAccess', () => { }); describe('getCompanyName', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); diff --git a/dictation_server/src/features/auth/auth.controller.spec.ts b/dictation_server/src/features/auth/auth.controller.spec.ts index 93a7999..a762b74 100644 --- a/dictation_server/src/features/auth/auth.controller.spec.ts +++ b/dictation_server/src/features/auth/auth.controller.spec.ts @@ -14,7 +14,7 @@ describe('AuthController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts index 856f719..c0b47d4 100644 --- a/dictation_server/src/features/auth/auth.service.spec.ts +++ b/dictation_server/src/features/auth/auth.service.spec.ts @@ -20,6 +20,7 @@ import { v4 as uuidv4 } from 'uuid'; import { TIERS, USER_ROLES } from '../../constants'; import { decode, isVerifyError } from '../../common/jwt'; import { RefreshToken, AccessToken } from '../../common/token'; +import { truncateAllTable } from '../../common/test/init'; describe('AuthService', () => { it('IDトークンの検証とペイロードの取得に成功する', async () => { @@ -162,20 +163,32 @@ describe('AuthService', () => { describe('checkIsAcceptedLatestVersion', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('同意済み利用規約バージョンが最新のときにチェックが通ること(第五)', async () => { @@ -325,20 +338,32 @@ describe('checkIsAcceptedLatestVersion', () => { describe('generateDelegationRefreshToken', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('代行操作が許可されたパートナーの代行操作用リフレッシュトークンを取得できること', async () => { @@ -459,20 +484,32 @@ describe('generateDelegationRefreshToken', () => { describe('generateDelegationAccessToken', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('代行操作用リフレッシュトークンから代行操作用アクセストークンを取得できること', async () => { @@ -558,20 +595,32 @@ describe('generateDelegationAccessToken', () => { describe('updateDelegationAccessToken', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -743,7 +792,7 @@ describe('updateDelegationAccessToken', () => { } // 代行操作対象アカウントを削除 - deleteAccount(source, partnerAccount.id); + await deleteAccount(source, partnerAccount.id); try { await service.updateDelegationAccessToken( diff --git a/dictation_server/src/features/files/files.controller.spec.ts b/dictation_server/src/features/files/files.controller.spec.ts index 9b7eb76..3986b38 100644 --- a/dictation_server/src/features/files/files.controller.spec.ts +++ b/dictation_server/src/features/files/files.controller.spec.ts @@ -10,7 +10,7 @@ describe('FilesController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 89c9f68..62e4286 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -40,23 +40,36 @@ import { TASK_STATUS, USER_ROLES, } from '../../constants'; +import { truncateAllTable } from '../../common/test/init'; describe('publishUploadSas', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -287,20 +300,32 @@ describe('publishUploadSas', () => { describe('タスク作成から自動ルーティング(DB使用)', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('タスク作成時に、自動ルーティングを行うことができる(APIの引数として渡されたAuthorIDとworkType)', async () => { @@ -997,20 +1022,32 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { describe('音声ファイルダウンロードURL取得', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1490,20 +1527,32 @@ describe('音声ファイルダウンロードURL取得', () => { describe('テンプレートファイルダウンロードURL取得', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1933,20 +1982,32 @@ describe('テンプレートファイルダウンロードURL取得', () => { describe('publishTemplateFileUploadSas', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2036,20 +2097,32 @@ describe('publishTemplateFileUploadSas', () => { describe('templateUploadFinished', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index 12bb21c..1fe086b 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -152,7 +152,7 @@ export const makeTestingModuleWithBlobAndNotification = async ( const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), AuthModule, diff --git a/dictation_server/src/features/licenses/licenses.controller.spec.ts b/dictation_server/src/features/licenses/licenses.controller.spec.ts index 70daf6c..1b3fdc4 100644 --- a/dictation_server/src/features/licenses/licenses.controller.spec.ts +++ b/dictation_server/src/features/licenses/licenses.controller.spec.ts @@ -11,7 +11,7 @@ describe('LicensesController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 1a5b023..63db663 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -26,23 +26,36 @@ import { } from '../../common/test/utility'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { overrideSendgridService } from '../../common/test/overrides'; +import { truncateAllTable } from '../../common/test/init'; describe('ライセンス注文', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -190,20 +203,32 @@ describe('ライセンス注文', () => { describe('カードライセンス発行', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -268,20 +293,32 @@ describe('カードライセンス発行', () => { describe('カードライセンスを取り込む', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); it('カードライセンス取り込みが完了する', async () => { @@ -601,20 +638,32 @@ describe('カードライセンスを取り込む', () => { describe('ライセンス割り当て', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -702,6 +751,7 @@ describe('ライセンス割り当て', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -768,6 +818,8 @@ describe('ライセンス割り当て', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); + await createLicense( source, 1, @@ -874,6 +926,7 @@ describe('ライセンス割り当て', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -939,6 +992,7 @@ describe('ライセンス割り当て', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -1004,6 +1058,7 @@ describe('ライセンス割り当て', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -1069,6 +1124,7 @@ describe('ライセンス割り当て', () => { }); const date = new Date(); date.setDate(date.getDate() - 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -1110,6 +1166,7 @@ describe('ライセンス割り当て', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -1161,20 +1218,32 @@ describe('ライセンス割り当て', () => { describe('ライセンス割り当て解除', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1192,6 +1261,7 @@ describe('ライセンス割り当て解除', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -1271,6 +1341,7 @@ describe('ライセンス割り当て解除', () => { }); const date = new Date(); date.setDate(date.getDate() + 30); + date.setMilliseconds(0); await createLicense( source, 1, @@ -1316,20 +1387,32 @@ describe('ライセンス割り当て解除', () => { describe('ライセンス注文キャンセル', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); diff --git a/dictation_server/src/features/licenses/test/utility.ts b/dictation_server/src/features/licenses/test/utility.ts index 321f9c0..3dfdda3 100644 --- a/dictation_server/src/features/licenses/test/utility.ts +++ b/dictation_server/src/features/licenses/test/utility.ts @@ -172,6 +172,7 @@ export const selectLicenseAllocationHistory = async ( license_id: licence_id, }, order: { + id: 'DESC', executed_at: 'DESC', }, }); diff --git a/dictation_server/src/features/notification/notification.controller.spec.ts b/dictation_server/src/features/notification/notification.controller.spec.ts index fc14685..bbf71a7 100644 --- a/dictation_server/src/features/notification/notification.controller.spec.ts +++ b/dictation_server/src/features/notification/notification.controller.spec.ts @@ -10,7 +10,7 @@ describe('NotificationController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/notification/test/notification.service.mock.ts b/dictation_server/src/features/notification/test/notification.service.mock.ts index b11c985..1984c27 100644 --- a/dictation_server/src/features/notification/test/notification.service.mock.ts +++ b/dictation_server/src/features/notification/test/notification.service.mock.ts @@ -21,7 +21,7 @@ export const makeNotificationServiceMock = async ( providers: [NotificationService], imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], }), ], }) diff --git a/dictation_server/src/features/tasks/tasks.controller.spec.ts b/dictation_server/src/features/tasks/tasks.controller.spec.ts index 01e7e75..72b8ac5 100644 --- a/dictation_server/src/features/tasks/tasks.controller.spec.ts +++ b/dictation_server/src/features/tasks/tasks.controller.spec.ts @@ -10,7 +10,7 @@ describe('TasksController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index ad85be7..f0de160 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -41,6 +41,7 @@ import { Roles } from '../../common/types/role'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { overrideBlobstorageService } from '../../common/test/overrides'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; +import { truncateAllTable } from '../../common/test/init'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { @@ -588,20 +589,32 @@ describe('TasksService', () => { describe('DBテスト', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -779,20 +792,32 @@ describe('TasksService', () => { describe('changeCheckoutPermission', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1409,20 +1434,32 @@ describe('changeCheckoutPermission', () => { describe('checkout', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1609,7 +1646,6 @@ describe('checkout', () => { account_id: accountId, external_id: 'typist-user-external-id', role: 'typist', - author_id: 'MY_AUTHOR_ID', }); const { id: authorUserId } = await makeTestUser(source, { account_id: accountId, @@ -1656,7 +1692,6 @@ describe('checkout', () => { account_id: accountId, external_id: 'typist-user-external-id', role: 'typist', - author_id: 'MY_AUTHOR_ID', }); const { id: authorUserId } = await makeTestUser(source, { account_id: accountId, @@ -1993,20 +2028,32 @@ describe('checkout', () => { describe('checkin', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2196,20 +2243,32 @@ describe('checkin', () => { describe('suspend', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2395,20 +2454,32 @@ describe('suspend', () => { describe('cancel', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -3011,20 +3082,32 @@ describe('cancel', () => { describe('backup', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -3305,20 +3388,32 @@ describe('backup', () => { describe('getNextTask', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index c691934..1d8efc9 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -48,7 +48,7 @@ export const makeTaskTestingModuleWithNotificaiton = async ( const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), AuthModule, diff --git a/dictation_server/src/features/templates/templates.controller.spec.ts b/dictation_server/src/features/templates/templates.controller.spec.ts index 00bd2fc..20ffb00 100644 --- a/dictation_server/src/features/templates/templates.controller.spec.ts +++ b/dictation_server/src/features/templates/templates.controller.spec.ts @@ -10,7 +10,7 @@ describe('TemplatesController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index a366b30..8ef101f 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -7,24 +7,37 @@ import { makeContext } from '../../common/log'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; +import { truncateAllTable } from '../../common/test/init'; describe('getTemplates', () => { - let source: DataSource | undefined = undefined; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); - source = undefined; + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; }); it('テンプレートファイル一覧を取得できる', async () => { diff --git a/dictation_server/src/features/terms/terms.service.spec.ts b/dictation_server/src/features/terms/terms.service.spec.ts index 5950404..4a6ac7e 100644 --- a/dictation_server/src/features/terms/terms.service.spec.ts +++ b/dictation_server/src/features/terms/terms.service.spec.ts @@ -6,23 +6,36 @@ import { makeContext } from '../../common/log'; import { v4 as uuidv4 } from 'uuid'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; +import { truncateAllTable } from '../../common/test/init'; describe('利用規約取得', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 0984d0f..c2da3db 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -67,7 +67,7 @@ export const makeUsersServiceMock = async ( providers: [UsersService], imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], }), ], }) diff --git a/dictation_server/src/features/users/test/utility.ts b/dictation_server/src/features/users/test/utility.ts index 58a7512..849aa34 100644 --- a/dictation_server/src/features/users/test/utility.ts +++ b/dictation_server/src/features/users/test/utility.ts @@ -113,7 +113,7 @@ export const makeTestingModuleWithAdb2c = async ( const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), AuthModule, diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index b64a9fc..d060ac2 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -13,7 +13,7 @@ describe('UsersController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 82a8199..dc3b656 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -1,5 +1,4 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { AccessToken } from '../../common/token'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { makeDefaultAdB2cMockValue, @@ -47,21 +46,36 @@ import { import { v4 as uuidv4 } from 'uuid'; import { createOptionItems, createWorktype } from '../accounts/test/utility'; import { createWorkflow, getWorkflows } from '../workflows/test/utility'; +import { truncateAllTable } from '../../common/test/init'; describe('UsersService.confirmUser', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -420,20 +434,32 @@ describe('UsersService.confirmUserAndInitPassword', () => { describe('UsersService.createUser', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1365,20 +1391,32 @@ describe('UsersService.createUser', () => { describe('UsersService.getUsers', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1389,6 +1427,16 @@ describe('UsersService.getUsers', () => { if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'external_id2', + role: 'typist', + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + }); const { external_id: externalId_author, id: authorUserId } = await makeTestUser(source, { account_id: accountId, @@ -1401,17 +1449,6 @@ describe('UsersService.getUsers', () => { prompt: false, }); - const { id: typistUserId } = await makeTestUser(source, { - account_id: accountId, - external_id: 'external_id2', - role: 'typist', - author_id: undefined, - auto_renew: true, - encryption: false, - encryption_password: undefined, - prompt: false, - }); - await createUserGroup(source, accountId, 'group1', [typistUserId]); const { id: noneUserId } = await makeTestUser(source, { @@ -1843,20 +1880,32 @@ describe('UsersService.getSortCriteria', () => { describe('UsersService.updateUser', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2427,20 +2476,32 @@ describe('UsersService.updateUser', () => { describe('UsersService.updateAcceptedVersion', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2515,20 +2576,32 @@ describe('UsersService.updateAcceptedVersion', () => { describe('UsersService.getUserName', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2557,20 +2630,32 @@ describe('UsersService.getUserName', () => { describe('UsersService.getRelations', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -2622,6 +2707,8 @@ describe('UsersService.getRelations', () => { // 作成したデータを確認 { const workflows = await getWorkflows(source, account.id); + workflows.sort((a, b) => a.id - b.id); + expect(workflows.length).toBe(4); expect(workflows[0].worktype_id).toBe(worktype1.id); expect(workflows[0].author_id).toBe(user1); diff --git a/dictation_server/src/features/workflows/workflows.controller.spec.ts b/dictation_server/src/features/workflows/workflows.controller.spec.ts index afab754..3b12b75 100644 --- a/dictation_server/src/features/workflows/workflows.controller.spec.ts +++ b/dictation_server/src/features/workflows/workflows.controller.spec.ts @@ -10,7 +10,7 @@ describe('WorkflowsController', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ - envFilePath: ['.env.local', '.env'], + envFilePath: ['.env.test', '.env'], isGlobal: true, }), ], diff --git a/dictation_server/src/features/workflows/workflows.service.spec.ts b/dictation_server/src/features/workflows/workflows.service.spec.ts index 177a048..d04e4c5 100644 --- a/dictation_server/src/features/workflows/workflows.service.spec.ts +++ b/dictation_server/src/features/workflows/workflows.service.spec.ts @@ -19,23 +19,36 @@ import { overrideAdB2cService } from '../../common/test/overrides'; import { WorkflowsRepositoryService } from '../../repositories/workflows/workflows.repository.service'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; +import { truncateAllTable } from '../../common/test/init'; describe('getWorkflows', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await(async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -47,22 +60,23 @@ describe('getWorkflows', () => { const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { external_id: 'author1', - author_id: 'AUTHOR1', + author_id: 'BBBBB', account_id: account.id, role: USER_ROLES.AUTHOR, }); const { id: authorId2 } = await makeTestUser(source, { external_id: 'author2', - author_id: 'AUTHOR2', + author_id: 'AAAAA', account_id: account.id, role: USER_ROLES.AUTHOR, }); const { id: authorId3 } = await makeTestUser(source, { external_id: 'author3', - author_id: 'AUTHOR3', + author_id: 'CCCCC', account_id: account.id, role: USER_ROLES.AUTHOR, }); + const { id: typistId, external_id: typistExternalId } = await makeTestUser( source, { @@ -79,6 +93,12 @@ describe('getWorkflows', () => { ); const { id: worktypeId1 } = await createWorktype( + source, + account.id, + 'worktype2', + ); + + const { id: worktypeId2 } = await createWorktype( source, account.id, 'worktype1', @@ -95,7 +115,7 @@ describe('getWorkflows', () => { source, account.id, authorId1, - worktypeId1, + worktypeId2, templateId1, ); const workflow2 = await createWorkflow( @@ -112,10 +132,18 @@ describe('getWorkflows', () => { worktypeId1, undefined, ); + const workflow4 = await createWorkflow( + source, + account.id, + authorId3, + worktypeId2, + undefined, + ); await createWorkflowTypist(source, workflow1.id, typistId, undefined); await createWorkflowTypist(source, workflow2.id, undefined, userGroupId); await createWorkflowTypist(source, workflow3.id, undefined, userGroupId); + await createWorkflowTypist(source, workflow4.id, undefined, userGroupId); const service = module.get(WorkflowsService); const context = makeContext(admin.external_id, 'requestId'); @@ -123,10 +151,10 @@ describe('getWorkflows', () => { //作成したデータを確認 { const workflows = await getWorkflows(source, account.id); - expect(workflows.length).toBe(3); + expect(workflows.length).toBe(4); expect(workflows[0].id).toBe(workflow1.id); expect(workflows[0].author_id).toBe(authorId1); - expect(workflows[0].worktype_id).toBe(worktypeId1); + expect(workflows[0].worktype_id).toBe(worktypeId2); expect(workflows[0].template_id).toBe(templateId1); expect(workflows[1].id).toBe(workflow2.id); @@ -138,6 +166,11 @@ describe('getWorkflows', () => { expect(workflows[2].author_id).toBe(authorId3); expect(workflows[2].worktype_id).toBe(worktypeId1); expect(workflows[2].template_id).toBe(null); + + expect(workflows[3].id).toBe(workflow4.id); + expect(workflows[3].author_id).toBe(authorId3); + expect(workflows[3].worktype_id).toBe(worktypeId2); + expect(workflows[3].template_id).toBe(null); } overrideAdB2cService(service, { @@ -148,37 +181,48 @@ describe('getWorkflows', () => { //実行結果を確認 { - expect(resWorkflows.length).toBe(3); - expect(resWorkflows[0].id).toBe(workflow1.id); - expect(resWorkflows[0].author.id).toBe(authorId1); - expect(resWorkflows[0].author.authorId).toBe('AUTHOR1'); - expect(resWorkflows[0].worktype?.id).toBe(worktypeId1); - expect(resWorkflows[0].worktype?.worktypeId).toBe('worktype1'); + expect(resWorkflows.length).toBe(4); + + expect(resWorkflows[0].id).toBe(workflow2.id); + expect(resWorkflows[0].author.id).toBe(authorId2); + expect(resWorkflows[0].author.authorId).toBe('AAAAA'); + expect(resWorkflows[0].worktype).toBe(undefined); expect(resWorkflows[0].template?.id).toBe(templateId1); expect(resWorkflows[0].template?.fileName).toBe('fileName1'); expect(resWorkflows[0].typists.length).toBe(1); - expect(resWorkflows[0].typists[0].typistUserId).toBe(typistId); - expect(resWorkflows[0].typists[0].typistName).toBe('typist1'); + expect(resWorkflows[0].typists[0].typistGroupId).toBe(userGroupId); + expect(resWorkflows[0].typists[0].typistName).toBe('group1'); - expect(resWorkflows[1].id).toBe(workflow2.id); - expect(resWorkflows[1].author.id).toBe(authorId2); - expect(resWorkflows[1].author.authorId).toBe('AUTHOR2'); - expect(resWorkflows[1].worktype).toBe(undefined); + expect(resWorkflows[1].id).toBe(workflow1.id); + expect(resWorkflows[1].author.id).toBe(authorId1); + expect(resWorkflows[1].author.authorId).toBe('BBBBB'); + expect(resWorkflows[1].worktype?.id).toBe(worktypeId2); + expect(resWorkflows[1].worktype?.worktypeId).toBe('worktype1'); expect(resWorkflows[1].template?.id).toBe(templateId1); expect(resWorkflows[1].template?.fileName).toBe('fileName1'); expect(resWorkflows[1].typists.length).toBe(1); - expect(resWorkflows[1].typists[0].typistGroupId).toBe(userGroupId); - expect(resWorkflows[1].typists[0].typistName).toBe('group1'); + expect(resWorkflows[1].typists[0].typistUserId).toBe(typistId); + expect(resWorkflows[1].typists[0].typistName).toBe('typist1'); - expect(resWorkflows[2].id).toBe(workflow3.id); + expect(resWorkflows[2].id).toBe(workflow4.id); expect(resWorkflows[2].author.id).toBe(authorId3); - expect(resWorkflows[2].author.authorId).toBe('AUTHOR3'); - expect(resWorkflows[2].worktype?.id).toBe(worktypeId1); + expect(resWorkflows[2].author.authorId).toBe('CCCCC'); + expect(resWorkflows[2].worktype?.id).toBe(worktypeId2); expect(resWorkflows[2].worktype?.worktypeId).toBe('worktype1'); expect(resWorkflows[2].template).toBe(undefined); expect(resWorkflows[2].typists.length).toBe(1); expect(resWorkflows[2].typists[0].typistGroupId).toBe(userGroupId); expect(resWorkflows[2].typists[0].typistName).toBe('group1'); + + expect(resWorkflows[3].id).toBe(workflow3.id); + expect(resWorkflows[3].author.id).toBe(authorId3); + expect(resWorkflows[3].author.authorId).toBe('CCCCC'); + expect(resWorkflows[3].worktype?.id).toBe(worktypeId1); + expect(resWorkflows[3].worktype?.worktypeId).toBe('worktype2'); + expect(resWorkflows[3].template).toBe(undefined); + expect(resWorkflows[3].typists.length).toBe(1); + expect(resWorkflows[3].typists[0].typistGroupId).toBe(userGroupId); + expect(resWorkflows[3].typists[0].typistName).toBe('group1'); } }); @@ -209,7 +253,7 @@ describe('getWorkflows', () => { const module = await makeTestingModule(source); if (!module) fail(); // 第五階層のアカウント作成 - const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { admin } = await makeTestAccount(source, { tier: 5 }); const service = module.get(WorkflowsService); const context = makeContext(admin.external_id, 'requestId'); @@ -236,22 +280,34 @@ describe('getWorkflows', () => { describe('createWorkflows', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); - }); - afterEach(async () => { - if (!source) return; - await source.destroy(); - source = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); it('アカウント内にWorkflowを作成できる(WorktypeIDあり、テンプレートファイルあり)', async () => { if (!source) fail(); const module = await makeTestingModule(source); @@ -575,6 +631,7 @@ describe('createWorkflows', () => { //実行結果を確認 { const workflows = await getWorkflows(source, account.id); + workflows.sort((a, b) => a.id - b.id); expect(workflows.length).toBe(2); expect(workflows[1].account_id).toBe(account.id); expect(workflows[1].author_id).toBe(authorId); @@ -1162,19 +1219,32 @@ describe('createWorkflows', () => { describe('updateWorkflow', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); @@ -1592,6 +1662,7 @@ describe('updateWorkflow', () => { //作成したデータを確認 { const workflows = await getWorkflows(source, account.id); + workflows.sort((a, b) => a.id - b.id); const workflowTypists = await getAllWorkflowTypists(source); expect(workflows.length).toBe(2); expect(workflows[0].id).toBe(preWorkflow1.id); @@ -1627,6 +1698,7 @@ describe('updateWorkflow', () => { //実行結果を確認 { const workflows = await getWorkflows(source, account.id); + workflows.sort((a, b) => a.id - b.id); expect(workflows.length).toBe(2); expect(workflows[1].account_id).toBe(account.id); expect(workflows[1].author_id).toBe(authorId2); @@ -2349,19 +2421,32 @@ describe('updateWorkflow', () => { describe('deleteWorkflows', () => { let source: DataSource | null = null; - beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); source = null; }); diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 9237bdd..ae3fccc 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -29,6 +29,7 @@ import { TIERS, TRIAL_LICENSE_ISSUE_NUM, USER_ROLES, + USER_ROLE_ORDERS, } from '../../constants'; import { License } from '../licenses/entity/license.entity'; import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types'; @@ -496,10 +497,23 @@ export class UsersRepositoryService { license: true, }, where: { account_id: accountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); - return dbUsers; + // RoleのAuthor、Typist、Noneの順に並び替える + const roleSortedUsers = dbUsers.sort((a, b) => { + // Roleが同じ場合はIDの昇順で並び替える + if (a.role === b.role) { + return a.id - b.id; + } + + return ( + USER_ROLE_ORDERS.indexOf(a.role) - USER_ROLE_ORDERS.indexOf(b.role) + ); + }); + + return roleSortedUsers; }); } diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts index b945380..cf28006 100644 --- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts +++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts @@ -50,7 +50,12 @@ export class WorkflowsRepositoryService { }, }, order: { - id: 'ASC', + author: { + author_id: 'ASC', + }, + worktype: { + custom_worktype_id: 'ASC', + }, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); From f2f8728319532f26a7b5f16d2917d10cbcf70af9 Mon Sep 17 00:00:00 2001 From: "SAITO-PC-3\\saito.k" Date: Wed, 24 Jan 2024 11:15:27 +0900 Subject: [PATCH 006/109] =?UTF-8?q?ccb=E3=81=AE=E7=AB=B6=E5=90=88=E8=A7=A3?= =?UTF-8?q?=E6=B6=88=E6=BC=8F=E3=82=8C=E3=82=92=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictation_client/src/pages/DictationPage/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index df16114..b024fd4 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -1199,7 +1199,6 @@ const DictationPage: React.FC = (): JSX.Element => { )}
  • - {/* タスク削除はCCB後回し分なので今は非表示
  • {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} { ) )} -
  • - */} + {displayColumn.JobNumber && ( From 8aa45baee847555be433b0e554187a79786924d1 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 24 Jan 2024 02:52:13 +0000 Subject: [PATCH 007/109] =?UTF-8?q?Merged=20PR=20697:=20DB=E9=96=A2?= =?UTF-8?q?=E9=80=A3=E3=82=B3=E3=83=BC=E3=83=89=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3509: DB関連コード修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3509) - 参照するDBがCCB用のスキーマとなるようにserverの参照DB名を変更しました。 対象: - server - .env - app.module.ts - function - .env - functions/* ※マージの取り込み分も入ってしまったので他は無視してください。 ## レビューポイント - 対応箇所は適切でしょうか。 ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 - ローカルにスキーマを追加・マイグレーションしてserverを起動できることを確認 --- dictation_function/.env | 1 + dictation_function/src/functions/licenseAlert.ts | 2 +- dictation_function/src/functions/licenseAutoAllocation.ts | 2 +- .../src/functions/licenseAutoAllocationManualRetry.ts | 2 +- dictation_server/.env | 1 + dictation_server/src/app.module.ts | 2 +- dictation_server/src/common/validators/env.validator.ts | 4 ++++ 7 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dictation_function/.env b/dictation_function/.env index f8bd781..9f8024b 100644 --- a/dictation_function/.env +++ b/dictation_function/.env @@ -1,5 +1,6 @@ DB_HOST=omds-mysql DB_PORT=3306 DB_NAME=omds +DB_NAME_CCB=omds_ccb DB_USERNAME=omdsdbuser DB_PASSWORD=omdsdbpass \ No newline at end of file diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts index ad4119f..a11af0d 100644 --- a/dictation_function/src/functions/licenseAlert.ts +++ b/dictation_function/src/functions/licenseAlert.ts @@ -107,7 +107,7 @@ export async function licenseAlert( port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, + database: process.env.DB_NAME_CCB, entities: [User, Account, License], }); await datasource.initialize(); diff --git a/dictation_function/src/functions/licenseAutoAllocation.ts b/dictation_function/src/functions/licenseAutoAllocation.ts index b4b623f..a1ece5d 100644 --- a/dictation_function/src/functions/licenseAutoAllocation.ts +++ b/dictation_function/src/functions/licenseAutoAllocation.ts @@ -85,7 +85,7 @@ export async function licenseAutoAllocation( port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, + database: process.env.DB_NAME_CCB, entities: [User, Account, License, LicenseAllocationHistory], }); await datasource.initialize(); diff --git a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts index 18fa0f2..c34de46 100644 --- a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts +++ b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts @@ -48,7 +48,7 @@ export async function licenseAutoAllocationManualRetry( port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, + database: process.env.DB_NAME_CCB, entities: [User, Account, License, LicenseAllocationHistory], }); await datasource.initialize(); diff --git a/dictation_server/.env b/dictation_server/.env index c104e29..e519213 100644 --- a/dictation_server/.env +++ b/dictation_server/.env @@ -1,5 +1,6 @@ DB_HOST=omds-mysql DB_PORT=3306 DB_NAME=omds +DB_NAME_CCB=omds_ccb DB_USERNAME=omdsdbuser DB_PASSWORD=omdsdbpass diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 667cde8..1157fa4 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -98,7 +98,7 @@ import * as redisStore from 'cache-manager-redis-store'; port: configService.get('DB_PORT'), username: configService.get('DB_USERNAME'), password: configService.get('DB_PASSWORD'), - database: configService.get('DB_NAME'), + database: configService.get('DB_NAME_CCB'), autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード synchronize: false, // trueにすると自動的にmigrationが行われるため注意 }), diff --git a/dictation_server/src/common/validators/env.validator.ts b/dictation_server/src/common/validators/env.validator.ts index 8ce706f..bd931d3 100644 --- a/dictation_server/src/common/validators/env.validator.ts +++ b/dictation_server/src/common/validators/env.validator.ts @@ -24,6 +24,10 @@ export class EnvValidator { @IsString() DB_NAME: string; + @IsNotEmpty() + @IsString() + DB_NAME_CCB: string; + @IsNotEmpty() @IsString() DB_USERNAME: string; From 8dfbcea0dad7f6db9d061f520a95100783f546a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Thu, 25 Jan 2024 04:00:54 +0000 Subject: [PATCH 008/109] =?UTF-8?q?Merged=20PR=20702:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3520: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3520) - ユーザー削除APIのI/Fを実装 ## レビューポイント - バリデーターは適切に設定されているか - 不要な処理が混入していないか - 代行操作による実行を許可しているが、認識は間違っていないか - マージ先ブランチは間違っていないか ## 動作確認状況 - openapi.jsonの生成成功を確認 --- dictation_server/src/api/odms/openapi.json | 62 +++++++++++++++ .../src/features/users/types/types.ts | 9 +++ .../src/features/users/users.controller.ts | 75 +++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 9b20a43..4c8b812 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -2050,6 +2050,60 @@ "security": [{ "bearer": [] }] } }, + "/users/delete": { + "post": { + "operationId": "updeateUser", + "summary": "", + "description": "ユーザーを削除します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PostDeleteUserRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostDeleteUserResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, "/files/audio/upload-finished": { "post": { "operationId": "uploadFinished", @@ -4363,6 +4417,14 @@ }, "required": ["userName"] }, + "PostDeleteUserRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "削除対象のユーザーID" } + }, + "required": ["userId"] + }, + "PostDeleteUserResponse": { "type": "object", "properties": {} }, "AudioOptionItem": { "type": "object", "properties": { diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 80b1a13..2e957a6 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -252,6 +252,15 @@ export class PostUpdateUserRequest { export class PostUpdateUserResponse {} +export class PostDeleteUserRequest { + @ApiProperty({ description: '削除対象のユーザーID' }) + @Type(() => Number) + @IsInt() + userId: number; +} + +export class PostDeleteUserResponse {} + export class AllocateLicenseRequest { @ApiProperty({ description: 'ユーザーID' }) @Type(() => Number) diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index d607e50..6a1e74e 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -42,6 +42,8 @@ import { UpdateAcceptedVersionRequest, UpdateAcceptedVersionResponse, GetMyUserResponse, + PostDeleteUserRequest, + PostDeleteUserResponse, } from './types/types'; import { UsersService } from './users.service'; import { AuthService } from '../auth/auth.service'; @@ -916,4 +918,77 @@ export class UsersController { const userName = await this.usersService.getUserName(context, userId); return { userName }; } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostDeleteUserResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'updeateUser', + description: 'ユーザーを削除します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('delete') + async deleteUser( + @Body() body: PostDeleteUserRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + return {}; + } } From c32b38b78319dfe1ba769619884edcc9cf511745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Tue, 30 Jan 2024 07:10:11 +0000 Subject: [PATCH 009/109] =?UTF-8?q?Merged=20PR=20703:=20API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E5=89=8A?= =?UTF-8?q?=E9=99=A4=EF=BC=89/Repository=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3521: API実装(ユーザー削除)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3521) - ユーザー削除を行うRepository(DB操作部分)を実装 - 削除不可条件をチェックして削除できなければエラー - 削除可能だった場合、以下の処理を実行 - ユーザーをアーカイブ - ユーザーを削除 ## レビューポイント - 「ライセンス割り当て解除」をせずにユーザーを削除するため、ライセンスがUserテーブルに存在しないIDを指したままになってしまうが問題ないか - ラフスケッチ時には、UserArchiveのidには紐づく & UserArchiveに紐づくことによって期限切れのライセンスが誰に割り当たっていたかを把握できるという話だったと思うが、これは"そういう必要がある"という仕様という認識でよいか - ロック対象の指定は妥当であるか - デッドロックは発生しなさそうか - User -> UserGroup -> Workflow -> Task -> CheckoutPermission -> Licenseの順番 ## 動作確認状況 - 動作確認なし --- .../src/repositories/users/errors/types.ts | 56 ++++ .../users/users.repository.service.ts | 260 +++++++++++++++++- 2 files changed, 314 insertions(+), 2 deletions(-) diff --git a/dictation_server/src/repositories/users/errors/types.ts b/dictation_server/src/repositories/users/errors/types.ts index 6f90ede..217d54d 100644 --- a/dictation_server/src/repositories/users/errors/types.ts +++ b/dictation_server/src/repositories/users/errors/types.ts @@ -54,3 +54,59 @@ export class DelegationNotAllowedError extends Error { this.name = 'DelegationNotAllowedError'; } } + +// 削除対象ユーザーが管理者である事が原因の削除失敗エラー +export class AdminDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AdminDeleteFailedError'; + } +} + +// 削除対象ユーザー(Author)がWorkflowにアサインされている事が原因の削除失敗エラー +export class AssignedWorkflowWithAuthorDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssignedWorkflowWithAuthorDeleteFailedError'; + } +} + +// 削除対象ユーザー(Typist)がWorkflowにアサインされている事が原因の削除失敗エラー +export class AssignedWorkflowWithTypistDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssignedWorkflowWithTypistDeleteFailedError'; + } +} + +// 削除対象ユーザーがGroupに所属している事が原因の削除失敗エラー +export class ExistsGroupMemberDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsGroupMemberDeleteFailedError'; + } +} + +// 削除対象ユーザーが有効なタスクをまだ持っている事が原因の削除失敗エラー +export class ExistsValidTaskDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsValidTaskDeleteFailedError'; + } +} + +// 削除対象ユーザーがチェックアウト権限を持っている事が原因の削除失敗エラー +export class ExistsCheckoutPermissionDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsCheckoutPermissionDeleteFailedError'; + } +} + +// 削除対象ユーザーが有効なライセンスをまだ持っている事が原因の削除失敗エラー +export class ExistsValidLicenseDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsValidLicenseDeleteFailedError'; + } +} \ No newline at end of file diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 8c39441..0355ec5 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { User, newUser } from './entity/user.entity'; +import { User, UserArchive, newUser } from './entity/user.entity'; import { DataSource, FindOptionsWhere, + In, IsNull, Not, UpdateResult, @@ -21,17 +22,29 @@ import { TermInfoNotFoundError, UpdateTermsVersionNotSetError, DelegationNotAllowedError, + ExistsGroupMemberDeleteFailedError, + AssignedWorkflowWithTypistDeleteFailedError, + AssignedWorkflowWithAuthorDeleteFailedError, + AdminDeleteFailedError, + ExistsValidTaskDeleteFailedError, + ExistsCheckoutPermissionDeleteFailedError, + ExistsValidLicenseDeleteFailedError, } from './errors/types'; import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE, + SWITCH_FROM_TYPE, + TASK_STATUS, TERM_TYPE, TIERS, TRIAL_LICENSE_ISSUE_NUM, USER_ROLES, USER_ROLE_ORDERS, } from '../../constants'; -import { License } from '../licenses/entity/license.entity'; +import { + License, + LicenseAllocationHistory, +} from '../licenses/entity/license.entity'; import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types'; import { Term } from '../terms/entity/term.entity'; import { TermsCheckInfo } from '../../features/auth/types/types'; @@ -49,6 +62,9 @@ import { updateEntity, deleteEntity, } from '../../common/repository'; +import { UserGroup } from '../user_groups/entity/user_group.entity'; +import { Task } from '../tasks/entity/task.entity'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; @Injectable() export class UsersRepositoryService { @@ -574,6 +590,246 @@ export class UsersRepositoryService { }); } + /** + * Deletes user + * @param context Context + * @param userId 削除対象ユーザーのID + * @param currentTime ライセンス有効期限のチェックに使用する現在時刻 + * @returns user + */ + async deleteUser( + context: Context, + userId: number, + currentTime: Date, + ): Promise<{ isSuccess: boolean }> { + return await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + + // 削除対象ユーザーをロックを取った上で取得 + const target = await userRepo.findOne({ + relations: { + account: true, + }, + where: { + id: userId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 削除済みであれば失敗する + if (target == null) { + return { isSuccess: false }; + } + + const { account } = target; + if (account == null) { + // 通常ありえないが、アカウントが存在しない場合はエラー + throw new AccountNotFoundError('Account is Not Found.'); + } + + // 管理者IDの一覧を作成 + const adminIds = [account] + .flatMap((x) => [x?.primary_admin_user_id, x?.secondary_admin_user_id]) + .flatMap((x) => (x != null ? [x] : [])); + // 削除対象が管理者であるかを確認 + if (adminIds.some((adminId) => adminId === target.id)) { + throw new AdminDeleteFailedError('User is an admin.'); + } + + const userGroupRepo = entityManager.getRepository(UserGroup); + const groups = await userGroupRepo.find({ + relations: { + userGroupMembers: true, + }, + where: { + account_id: target.account_id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + const workflowRepo = entityManager.getRepository(Workflow); + const workflows = await workflowRepo.find({ + where: { + account_id: target.account_id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // WorkflowのAuthor枠に設定されているユーザーは削除できない + if (workflows.some((x) => x.author_id === target.id)) { + throw new AssignedWorkflowWithAuthorDeleteFailedError( + 'Author is assigned to a workflow.', + ); + } + + // Workflowに直接個人で指定されているTypistのID一覧を作成する + const typistIds = workflows + .flatMap((x) => x.workflowTypists) + .flatMap((x) => (x?.typist_id != null ? [x.typist_id] : [])); + // Workflowに直接個人で指定されているTypistは削除できない + if (typistIds.some((typistId) => typistId === target.id)) { + throw new AssignedWorkflowWithTypistDeleteFailedError( + 'Typist is assigned to a workflow.', + ); + } + + // いずれかのGroupに属しているユーザーIDの一覧を作成 + const groupMemberIds = groups + .flatMap((group) => group.userGroupMembers) + .flatMap((member) => (member != null ? [member.user_id] : [])); + + // 削除対象ユーザーがGroupに属しているユーザーに含まれていたら削除できない + if (groupMemberIds.some((id) => id === target.id)) { + throw new ExistsGroupMemberDeleteFailedError( + 'User is a member of a group', + ); + } + + // 削除対象ユーザーがAuthorであった時、 + if (target.role === USER_ROLES.AUTHOR) { + const taskRepo = entityManager.getRepository(Task); + // 自分が所有者のタスクの一覧を取得する + const tasks = await taskRepo.find({ + relations: { + file: true, + }, + where: { + account_id: target.account_id, + status: Not(TASK_STATUS.BACKUP), // 数が膨大になりうる&有効なタスクへの状態遷移ができないBACKUPは除いて取得 + file: { + owner_user_id: target.id, + }, + }, + lock: { mode: 'pessimistic_write' }, // lockする事で状態遷移の競合をブロックし、新規追加以外で所有タスク群の状態変更を防ぐ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 未完了タスクが残っていたら削除できない + const enableStatus: string[] = [ + TASK_STATUS.UPLOADED, + TASK_STATUS.IN_PROGRESS, + TASK_STATUS.PENDING, + ]; + // 未完了タスクを列挙 + const enableTasks = tasks.filter((task) => + enableStatus.includes(task.status), + ); + if (enableTasks.length > 0) { + throw new ExistsValidTaskDeleteFailedError('User has valid tasks.'); + } + } + + // 削除対象ユーザーがTypistであった時、 + if (target.role === USER_ROLES.TYPIST) { + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + const permissions = await checkoutPermissionRepo.find({ + where: { + user_id: target.id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // タスクのチェックアウト権限が残っていたら削除できない + if (permissions.length !== 0) { + throw new ExistsCheckoutPermissionDeleteFailedError( + 'User has checkout permissions.', + ); + } + } + + // 対象ユーザーのライセンス割り当て状態を取得 + const licenseRepo = entityManager.getRepository(License); + const allocatedLicense = await licenseRepo.findOne({ + where: { + allocated_user_id: userId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ライセンスが割り当て済みかつ、 + if (allocatedLicense !== null) { + const { status, expiry_date } = allocatedLicense; + // 有効な状態かつ、 + if (status === LICENSE_ALLOCATED_STATUS.ALLOCATED) { + // 有効期限を迎えていない場合は削除できない + if (expiry_date !== null && expiry_date > currentTime) { + throw new ExistsValidLicenseDeleteFailedError( + 'User has valid licenses.', + ); + } + } + } + + // ユーザーを削除する前に、削除対象ユーザーをアーカイブする + const userArchiveRepo = entityManager.getRepository(UserArchive); + await insertEntity( + UserArchive, + userArchiveRepo, + target, + this.isCommentOut, + context, + ); + + // 期限切れライセンスが割り当てられていた場合、ユーザーを削除する前にライセンスを割り当て解除する + // ※この処理時点で有効期限外ライセンスであることは確定であるため、期限切れ判定をここでは行わない + if (allocatedLicense != null) { + allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE; + allocatedLicense.allocated_user_id = null; + + await updateEntity( + licenseRepo, + { id: allocatedLicense.id }, + allocatedLicense, + this.isCommentOut, + context, + ); + + // ライセンス割り当て履歴テーブルへ登録 + const licenseAllocationHistoryRepo = entityManager.getRepository( + LicenseAllocationHistory, + ); + const deallocationHistory = new LicenseAllocationHistory(); + deallocationHistory.user_id = userId; + deallocationHistory.license_id = allocatedLicense.id; + deallocationHistory.account_id = account.id; + deallocationHistory.is_allocated = false; + deallocationHistory.executed_at = new Date(); + deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE; + await insertEntity( + LicenseAllocationHistory, + licenseAllocationHistoryRepo, + deallocationHistory, + this.isCommentOut, + context, + ); + } + + // ユーザテーブルのレコードを削除する + await deleteEntity( + userRepo, + { user_id: target.id }, + this.isCommentOut, + context, + ); + + // ソート条件のテーブルのレコードを削除する + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + await deleteEntity( + sortCriteriaRepo, + { user_id: target.id }, + this.isCommentOut, + context, + ); + return { isSuccess: true }; + }); + } + /** * UserID指定のユーザーとソート条件を同時に削除する * @param userId From 9f7c8c99c0c7c5e2333f030cfce7a5257db549c6 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 2 Feb 2024 00:27:08 +0000 Subject: [PATCH 010/109] =?UTF-8?q?Merged=20PR=20720:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3534: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3534) - タイピストグループ削除APIのIFを実装し、OpenAPIを更新しました。 ## レビューポイント - パラメータとバリデータは想定通りでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/api/odms/openapi.json | 54 ++++++++++++ .../features/accounts/accounts.controller.ts | 82 +++++++++++++++++++ .../src/features/accounts/types/types.ts | 10 +++ 3 files changed, 146 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 4c8b812..df4a888 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -651,6 +651,59 @@ "security": [{ "bearer": [] }] } }, + "/accounts/typist-groups/{typistGroupId}/delete": { + "post": { + "operationId": "deleteTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTypistGroupResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / タスクの割り当て候補に設定されている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/accounts/partner": { "post": { "operationId": "createPartnerAccount", @@ -3823,6 +3876,7 @@ }, "required": ["typistGroupName", "typistIds"] }, + "DeleteTypistGroupResponse": { "type": "object", "properties": {} }, "CreatePartnerAccountRequest": { "type": "object", "properties": { diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index e8a8ecd..7ab72b5 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -71,6 +71,8 @@ import { DeleteWorktypeResponse, GetCompanyNameRequest, GetCompanyNameResponse, + DeleteTypistGroupRequestParam, + DeleteTypistGroupResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -755,6 +757,86 @@ export class AccountsController { return {}; } + @ApiResponse({ + status: HttpStatus.OK, + type: DeleteTypistGroupResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + 'ルーティングルールに設定されている / タスクの割り当て候補に設定されている / 削除済み', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'deleteTypistGroup', + description: + 'ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('typist-groups/:typistGroupId/delete') + async deleteTypistGroup( + @Req() req: Request, + @Param() param: DeleteTypistGroupRequestParam, + ): Promise { + const { typistGroupId } = param; + + // アクセストークン取得 + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: 削除処理 + + return {}; + } + @Post('partner') @ApiResponse({ status: HttpStatus.OK, diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 2648785..d604419 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -120,6 +120,14 @@ export class UpdateTypistGroupRequestParam { @Min(1) typistGroupId: number; } +export class DeleteTypistGroupRequestParam { + @ApiProperty() + @Type(() => Number) + @IsInt() + @Min(1) + typistGroupId: number; +} + export class CreatePartnerAccountRequest { @ApiProperty() @MaxLength(255) @@ -481,6 +489,8 @@ export class CreateTypistGroupResponse {} export class UpdateTypistGroupResponse {} +export class DeleteTypistGroupResponse {} + export class CreatePartnerAccountResponse {} export class PartnerLicenseInfo { From 48a2bddfd9a7231f371f68664de590d8db38fafe Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Fri, 2 Feb 2024 04:57:25 +0000 Subject: [PATCH 011/109] =?UTF-8?q?Merged=20PR=20721:=20DB=E3=83=9E?= =?UTF-8?q?=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3556: DBマイグレーション](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3556) - アカウントテーブルに対して、以下の2カラムを追加しました。 - 自動ファイル削除要否 - 文字起こし完了してからファイル削除するまでのファイル保持日数 ## レビューポイント - カラム名としてほかに適切なものはないか? - 初期値合ってますよね? ## 動作確認状況 - ローカルでmigrate up と downが出来ることを確認 ## 補足 - もしこの値の扱いが、「Finishedになってから○○日後にファイルを削除する」のものであればそのとき修正します。 --- .../db/migrations/057-add_accounts_file-delete.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 dictation_server/db/migrations/057-add_accounts_file-delete.sql diff --git a/dictation_server/db/migrations/057-add_accounts_file-delete.sql b/dictation_server/db/migrations/057-add_accounts_file-delete.sql new file mode 100644 index 0000000..df96546 --- /dev/null +++ b/dictation_server/db/migrations/057-add_accounts_file-delete.sql @@ -0,0 +1,9 @@ +-- +migrate Up +ALTER TABLE `accounts` +ADD COLUMN `auto_file_delete` BOOLEAN NOT NULL DEFAULT 0 COMMENT '自動ファイル削除をするかどうか' AFTER `active_worktype_id`, +ADD COLUMN `file_retention_days` INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '文字起こし完了してから自動ファイル削除するまでのファイル保持日数' AFTER `auto_file_delete`; + +-- +migrate Down +ALTER TABLE `accounts` +DROP COLUMN `auto_file_delete`, +DROP COLUMN `file_retention_days`; \ No newline at end of file From f595cae1b88834be520c8ff20590349757428cf7 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Sat, 3 Feb 2024 06:22:53 +0000 Subject: [PATCH 012/109] =?UTF-8?q?Merged=20PR=20728:=20=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E5=89=8A=E9=99=A4=E8=A8=AD=E5=AE=9A=E6=9B=B4?= =?UTF-8?q?=E6=96=B0API=20IF=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3557: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3557) - ファイル削除設定更新APIのIFを実装してOpenAPI生成しました。 ## 補足 - 別PRでOKもらってるのでそのままマージします --- dictation_server/src/api/odms/openapi.json | 70 +++++++++++++++++ .../features/accounts/accounts.controller.ts | 77 +++++++++++++++++++ .../src/features/accounts/types/types.ts | 19 +++++ 3 files changed, 166 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index df4a888..653ce1d 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1417,6 +1417,61 @@ "security": [{ "bearer": [] }] } }, + "/accounts/me/file-delete-setting": { + "post": { + "operationId": "updateFileDeleteSetting", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/アカウント・ユーザー不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/accounts/delete": { "post": { "operationId": "deleteAccountAndData", @@ -4203,6 +4258,21 @@ "required": ["delegationPermission", "primaryAdminUserId"] }, "UpdateAccountInfoResponse": { "type": "object", "properties": {} }, + "UpdateFileDeleteSettingRequest": { + "type": "object", + "properties": { + "autoFileDelete": { + "type": "boolean", + "description": "自動ファイル削除をするかどうか" + }, + "retentionDays": { + "type": "number", + "description": "文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間" + } + }, + "required": ["autoFileDelete", "retentionDays"] + }, + "UpdateFileDeleteSettingResponse": { "type": "object", "properties": {} }, "DeleteAccountRequest": { "type": "object", "properties": { diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 7ab72b5..c75d786 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -73,6 +73,8 @@ import { GetCompanyNameResponse, DeleteTypistGroupRequestParam, DeleteTypistGroupResponse, + UpdateFileDeleteSettingRequest, + UpdateFileDeleteSettingResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -1961,6 +1963,81 @@ export class AccountsController { return {}; } + @Post('me/file-delete-setting') + @ApiResponse({ + status: HttpStatus.OK, + type: UpdateFileDeleteSettingResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正/アカウント・ユーザー不在', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'updateFileDeleteSetting' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + }), + ) + async updateFileDeleteSetting( + @Req() req: Request, + @Body() body: UpdateFileDeleteSettingRequest, + ): Promise { + const { autoFileDelete, retentionDays } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO:Service層呼び出し + + return {}; + } + @Post('/delete') @ApiResponse({ status: HttpStatus.OK, diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index d604419..2600295 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -12,6 +12,7 @@ import { IsIn, ArrayMaxSize, ValidateNested, + Max, } from 'class-validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator'; @@ -647,6 +648,24 @@ export class GetAccountInfoMinimalAccessResponse { tier: number; } +export class UpdateFileDeleteSettingRequest { + @ApiProperty({ description: '自動ファイル削除をするかどうか' }) + @Type(() => Boolean) + autoFileDelete: boolean; + + @ApiProperty({ + description: + '文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間', + }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(999) + retentionDays: number; +} + +export class UpdateFileDeleteSettingResponse {} + // ============================== // Request/Response外の型 // TODO: Request/Response/その他の型を別ファイルに分ける From 44759b1aacaa6dc041ca8b5fbcc031f5fbeb66b4 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Mon, 5 Feb 2024 00:31:47 +0000 Subject: [PATCH 013/109] =?UTF-8?q?Merged=20PR=20725:=20=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=94=E3=82=B9=E3=83=88=E3=82=B0=E3=83=AB=E3=83=BC=E3=83=97?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=80=80=E7=94=BB=E9=9D=A2=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3536: 画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3536) - タイピストグループ画面に削除ボタン配置 - 削除ボタンクリック時に確認ダイアログ表示→OKで削除API実行&ぐるぐる表示 - API実行結果によってメッセージの出し分け&画面リスト更新 - 翻訳情報追加 ## レビューポイント - エラーコードに認識違いないか? - 画面実装のお作法にそぐわないところがないか? - なんか抜けてる実装などあれば ## UIの変更 大した変更ではないので、スクショ置き場に置く手間を省いてここに貼り付けます ![image (2).png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/725/attachments/image%20%282%29.png) ## 動作確認状況 - 期待される動作を一通りローカルで確認しました。 - API実行中はぐるぐる表示されること - API成功または削除済みの場合は成功メッセージ表示され、画面更新されること - API失敗時、理由によってエラーメッセージが表示分けされること --- dictation_client/src/api/api.ts | 161 ++++++++++++++++++ dictation_client/src/common/errors/code.ts | 3 + .../workflow/typistGroup/operations.ts | 67 ++++++++ .../workflow/typistGroup/typistGroupSlice.ts | 10 ++ .../pages/TypistGroupSettingPage/index.tsx | 31 ++++ dictation_client/src/translation/de.json | 19 ++- dictation_client/src/translation/en.json | 19 ++- dictation_client/src/translation/es.json | 33 ++-- dictation_client/src/translation/fr.json | 33 ++-- 9 files changed, 350 insertions(+), 26 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index c50df11..70d8d8f 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -1500,6 +1500,19 @@ export interface PostCheckoutPermissionRequest { */ 'assignees': Array; } +/** + * + * @export + * @interface PostDeleteUserRequest + */ +export interface PostDeleteUserRequest { + /** + * 削除対象のユーザーID + * @type {number} + * @memberof PostDeleteUserRequest + */ + 'userId': number; +} /** * * @export @@ -2639,6 +2652,44 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup: async (typistGroupId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('deleteTypistGroup', 'typistGroupId', typistGroupId) + const localVarPath = `/accounts/typist-groups/{typistGroupId}/delete` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3510,6 +3561,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.deleteAccountAndData']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTypistGroup(typistGroupId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -3848,6 +3912,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise { return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath)); }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup(typistGroupId: number, options?: any): AxiosPromise { + return localVarFp.deleteTypistGroup(typistGroupId, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4140,6 +4214,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteTypistGroup(typistGroupId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -7183,6 +7269,46 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(postUpdateUserRequest, localVarRequestOptions, configuration) + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updeateUser: async (postDeleteUserRequest: PostDeleteUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postDeleteUserRequest' is not null or undefined + assertParamExists('updeateUser', 'postDeleteUserRequest', postDeleteUserRequest) + const localVarPath = `/users/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postDeleteUserRequest, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -7350,6 +7476,19 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.updateUser']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updeateUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updeateUser(postDeleteUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.updeateUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, } }; @@ -7476,6 +7615,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: any): AxiosPromise { return localVarFp.updateUser(postUpdateUserRequest, options).then((request) => request(axios, basePath)); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updeateUser(postDeleteUserRequest: PostDeleteUserRequest, options?: any): AxiosPromise { + return localVarFp.updeateUser(postDeleteUserRequest, options).then((request) => request(axios, basePath)); + }, }; }; @@ -7625,6 +7774,18 @@ export class UsersApi extends BaseAPI { public updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig) { return UsersApiFp(this.configuration).updateUser(postUpdateUserRequest, options).then((request) => request(this.axios, this.basePath)); } + + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public updeateUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updeateUser(postDeleteUserRequest, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index da0d9b9..5d0ffd8 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -60,4 +60,7 @@ export const errorCodes = [ "E011004", // ワークタイプ使用中エラー "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー "E013002", // ワークフロー不在エラー + "E015001", // タイピストグループ削除済みエラー + "E015002", // タイピストグループがワークフローに紐づいているエラー + "E015003", // タイピストグループがルーティングされているエラー ] as const; diff --git a/dictation_client/src/features/workflow/typistGroup/operations.ts b/dictation_client/src/features/workflow/typistGroup/operations.ts index a98ff1b..8ee85f1 100644 --- a/dictation_client/src/features/workflow/typistGroup/operations.ts +++ b/dictation_client/src/features/workflow/typistGroup/operations.ts @@ -256,3 +256,70 @@ export const updateTypistGroupAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteTypistGroupAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + typistGroupId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/deleteTypistGroupAsync", async (args, thunkApi) => { + const { typistGroupId } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + + try { + await accountsApi.deleteTypistGroup(typistGroupId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + // すでに削除されていた場合は成功扱いする + if (error.code === "E015001") { + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + + // 以下は実際の削除失敗 + let message = getTranslationID("common.message.internalServerError"); + if (error.code === "E015002") + message = getTranslationID( + "typistGroupSetting.message.deleteFailedWorkflowAssigned" + ); + if (error.code === "E015003") + message = getTranslationID( + "typistGroupSetting.message.deleteFailedCheckoutPermissionExisted" + ); + + thunkApi.dispatch(openSnackbar({ level: "error", message })); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts b/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts index 1574a3f..623cfcf 100644 --- a/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts +++ b/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts @@ -6,6 +6,7 @@ import { listTypistGroupsAsync, listTypistsAsync, updateTypistGroupAsync, + deleteTypistGroupAsync, } from "./operations"; const initialState: TypistGroupState = { @@ -106,6 +107,15 @@ export const typistGroupSlice = createSlice({ builder.addCase(updateTypistGroupAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(deleteTypistGroupAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteTypistGroupAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteTypistGroupAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/pages/TypistGroupSettingPage/index.tsx b/dictation_client/src/pages/TypistGroupSettingPage/index.tsx index 05cf232..c4f9e1d 100644 --- a/dictation_client/src/pages/TypistGroupSettingPage/index.tsx +++ b/dictation_client/src/pages/TypistGroupSettingPage/index.tsx @@ -11,6 +11,7 @@ import { selectTypistGroups, selectIsLoading, listTypistGroupsAsync, + deleteTypistGroupAsync, } from "features/workflow/typistGroup"; import { AppDispatch } from "app/store"; import { useTranslation } from "react-i18next"; @@ -47,6 +48,25 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => { [setIsEditPopupOpen] ); + const onDeleteTypistGroup = useCallback( + async (typistGroupId: number) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch( + deleteTypistGroupAsync({ typistGroupId }) + ); + if (meta.requestStatus === "fulfilled") { + dispatch(listTypistGroupsAsync()); + } + }, + [dispatch, t] + ); + useEffect(() => { dispatch(listTypistGroupsAsync()); }, [dispatch]); @@ -142,6 +162,17 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => { {t(getTranslationID("common.label.edit"))} +
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onDeleteTypistGroup(group.id); + }} + > + {t(getTranslationID("common.label.delete"))} + +
  • diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 4283e39..f756410 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -128,7 +128,14 @@ "authorIdIncorrectError": "Das Format der Autoren-ID ist ungültig. Als Autoren-ID können nur alphanumerische Zeichen und „_“ eingegeben werden.", "roleChangeError": "Die Benutzerrolle kann nicht geändert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", "encryptionPasswordCorrectError": "Das Verschlüsselungskennwort entspricht nicht den Regeln.", - "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen." + "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", + "UserDeletionLicenseActiveError": "(de)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "(de)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Benutzer", @@ -417,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.", - "groupSaveFailedError": "Die Schreibkraftgruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen." + "groupSaveFailedError": "Die Transkriptionistengruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", + "GroupNameAlreadyExistError": "(de)TypistGroupの保存に失敗しました。同名のTranscriptionistGroupは登録できません。", + "deleteFailedWorkflowAssigned": "(de)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", + "deleteFailedCheckoutPermissionExisted": "(de)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } }, "worktypeIdSetting": { @@ -509,6 +519,9 @@ }, "message": { "updateAccountFailedError": "Kontoinformationen konnten nicht gespeichert werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut." + }, + "text": { + "dealerManagementAnnotation": "Durch die Aktivierung der Option „Erlauben Sie dem Händler, Änderungen vorzunehmen“ erklären Sie sich damit einverstanden, dass Ihr Händler die Rechte erhält, auf Ihr ODMS Cloud-Konto zuzugreifen, um in Ihrem Namen Lizenzen zu bestellen und Benutzer zu registrieren. Ihr Händler hat keinen Zugriff auf Sprachdateien oder Dokumente, die in Ihrem ODMS Cloud-Konto gespeichert sind." } }, "deleteAccountPopup": { @@ -558,4 +571,4 @@ "close": "Schließen" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 2a4baf1..1ac4bf5 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -128,7 +128,14 @@ "authorIdIncorrectError": "Author ID format is invalid. Only alphanumeric characters and \"_\" can be entered for Author ID.", "roleChangeError": "Unable to change the User Role. The displayed information may be outdated, so please refresh the screen to see the latest status.", "encryptionPasswordCorrectError": "Encryption password does not meet the rules.", - "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status." + "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status.", + "UserDeletionLicenseActiveError": "ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "User", @@ -417,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.", - "groupSaveFailedError": "Typist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status." + "groupSaveFailedError": "Transcriptionist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status.", + "GroupNameAlreadyExistError": "TypistGroupの保存に失敗しました。同名のTranscriptionistGroupは登録できません。", + "deleteFailedWorkflowAssigned": "TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", + "deleteFailedCheckoutPermissionExisted": "TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } }, "worktypeIdSetting": { @@ -509,6 +519,9 @@ }, "message": { "updateAccountFailedError": "Failed to save account information. Please refresh the screen and try again." + }, + "text": { + "dealerManagementAnnotation": "By enabling the \"Dealer Management\" option, you are agreeing to allow your dealer to have the rights to access your ODMS Cloud account to order licenses and register users on your behalf. Your dealer will not have access to any voice file(s) or document(s) stored in your ODMS Cloud account." } }, "deleteAccountPopup": { @@ -558,4 +571,4 @@ "close": "Close" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 43d4d44..70f8bf8 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -28,7 +28,7 @@ "tier1": "Admin", "tier2": "BC", "tier3": "Distribuidor", - "tier4": "Concesionario", + "tier4": "Distribuidor", "tier5": "Cliente", "notSelected": "Ninguno", "signOutButton": "cerrar sesión" @@ -62,14 +62,14 @@ "title": "Crea tu cuenta", "accountInfoTitle": "Información de Registro", "countryExplanation": "Seleccione el país donde se encuentra. Si su país no aparece en la lista, seleccione el país más cercano.", - "dealerExplanation": "Seleccione el concesionario al que le gustaría comprar la licencia.", + "dealerExplanation": "Seleccione el distribuidor al que le gustaría comprar la licencia.", "adminInfoTitle": "Registre la información del administrador principal", "passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 25 caracteres y debe contener letras, números y símbolos. (Debe enumerar el símbolo compatible e indicar si se necesita una letra mayúscula)." }, "label": { "company": "Nombre de empresa", "country": "País", - "dealer": "Concesionario (Opcional)", + "dealer": "Distribuidor (Opcional)", "adminName": "Nombre del administrador", "email": "Dirección de correo electrónico", "password": "Contraseña", @@ -93,7 +93,7 @@ "label": { "company": "Nombre de empresa", "country": "País", - "dealer": "Concesionario (Opcional)", + "dealer": "Distribuidor (Opcional)", "adminName": "Nombre del administrador", "email": "Dirección de correo electrónico", "password": "Contraseña", @@ -128,7 +128,14 @@ "authorIdIncorrectError": "El formato de ID del autor no es válido. Sólo se pueden ingresar caracteres alfanuméricos y \"_\" para la ID del autor.", "roleChangeError": "No se puede cambiar la función de usuario. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", "encryptionPasswordCorrectError": "La contraseña de cifrado no cumple con las reglas.", - "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente." + "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", + "UserDeletionLicenseActiveError": "(es)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "(es)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Usuario", @@ -417,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.", - "groupSaveFailedError": "No se pudo guardar el grupo mecanógrafo. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente." + "groupSaveFailedError": "El grupo transcriptor no se pudo salvar. La información mostrada puede estar desactualizada. Así que actualice la pantalla para ver el estado más reciente.", + "GroupNameAlreadyExistError": "(es)TypistGroupの保存に失敗しました。同名のTranscriptionistGroupは登録できません。", + "deleteFailedWorkflowAssigned": "(es)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", + "deleteFailedCheckoutPermissionExisted": "(es)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } }, "worktypeIdSetting": { @@ -496,9 +506,9 @@ "accountID": "ID de la cuenta", "yourCategory": "Tipo de cuenta", "yourCountry": "País", - "yourDealer": "Concesionario", - "selectDealer": "Seleccionar Concesionario", - "dealerManagement": "Permitir que el concesionario realice los cambios", + "yourDealer": "Distribuidor", + "selectDealer": "Seleccionar distribuidor", + "dealerManagement": "Permitir que el distribuidor realice los cambios", "administratorInformation": "Información del administrador", "primaryAdministrator": "Administrador primario", "secondaryAdministrator": "Administrador secundario", @@ -509,6 +519,9 @@ }, "message": { "updateAccountFailedError": "No se pudo guardar la información de la cuenta. Actualice la pantalla e inténtelo de nuevo." + }, + "text": { + "dealerManagementAnnotation": "Al habilitar la opción \"Permitir que el distribuidor realice los cambios\", usted acepta permitir que su distribuidor tenga derechos para acceder a su cuenta de ODMS Cloud para solicitar licencias y registrar usuarios en su nombre. Su distribuidor no tendrá acceso a ningún archivo de voz o documento almacenado en su cuenta de ODMS Cloud." } }, "deleteAccountPopup": { @@ -558,4 +571,4 @@ "close": "Cerrar" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 57c5da9..3717a7e 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -28,7 +28,7 @@ "tier1": "Admin", "tier2": "BC", "tier3": "Distributeur", - "tier4": "Concessionnaire", + "tier4": "Revendeur", "tier5": "Client", "notSelected": "Aucune", "signOutButton": "se déconnecter" @@ -62,14 +62,14 @@ "title": "Créez votre compte", "accountInfoTitle": "Information d'inscription", "countryExplanation": "Sélectionnez le pays où vous vous trouvez. Si votre pays ne figure pas dans la liste, veuillez sélectionner le pays le plus proche.", - "dealerExplanation": "Veuillez sélectionner le concessionnaire auprès duquel vous souhaitez acheter la licence.", + "dealerExplanation": "Veuillez sélectionner le revendeur auprès duquel vous souhaitez acheter la licence.", "adminInfoTitle": "Enregistrer les informations de l'administrateur principal", "passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 25 caractères et doit contenir des lettres, des chiffres et des symboles. (Devrait lister les symboles compatibles et indiquer si une majuscule est nécessaire)." }, "label": { "company": "Nom de l'entreprise", "country": "Pays", - "dealer": "Concessionnaire (Facultatif)", + "dealer": "Revendeur (Facultatif)", "adminName": "Nom de l'administrateur", "email": "Adresse e-mail", "password": "Mot de passe", @@ -93,7 +93,7 @@ "label": { "company": "Nom de l'entreprise", "country": "Pays", - "dealer": "Concessionnaire (Facultatif)", + "dealer": "Revendeur (Facultatif)", "adminName": "Nom de l'administrateur", "email": "Adresse e-mail", "password": "Mot de passe", @@ -128,7 +128,14 @@ "authorIdIncorrectError": "Le format de l'identifiant de l'auteur n'est pas valide. Seuls les caractères alphanumériques et \"_\" peuvent être saisis pour l'ID d'auteur.", "roleChangeError": "Impossible de modifier le rôle de l'utilisateur. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", "encryptionPasswordCorrectError": "Le mot de passe de cryptage n'est pas conforme aux règles.", - "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut." + "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", + "UserDeletionLicenseActiveError": "(fr)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "(fr)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Utilisateur", @@ -417,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.", - "groupSaveFailedError": "Le groupe de dactylographes n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut." + "groupSaveFailedError": "Le groupe de transcriptionniste n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", + "GroupNameAlreadyExistError": "(fr)TypistGroupの保存に失敗しました。同名のTranscriptionistGroupは登録できません。", + "deleteFailedWorkflowAssigned": "(fr)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", + "deleteFailedCheckoutPermissionExisted": "(fr)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } }, "worktypeIdSetting": { @@ -496,9 +506,9 @@ "accountID": "identifiant de compte", "yourCategory": "Type de compte", "yourCountry": "Pays", - "yourDealer": "Concessionnaire", - "selectDealer": "Sélectionner le Concessionnaire", - "dealerManagement": "Autoriser le concessionnaire à modifier les paramètres", + "yourDealer": "Revendeur", + "selectDealer": "Sélectionner le revendeur", + "dealerManagement": "Autoriser le revendeur à modifier les paramètres", "administratorInformation": "Informations sur l'administrateur", "primaryAdministrator": "Administrateur principal", "secondaryAdministrator": "Administrateur secondaire", @@ -509,6 +519,9 @@ }, "message": { "updateAccountFailedError": "Échec de l'enregistrement des informations du compte. Veuillez actualiser l'écran et réessayer." + }, + "text": { + "dealerManagementAnnotation": "En activant l'option « Autoriser le revendeur à modifier les paramètres », vous acceptez que votre concessionnaire ait les droits d'accès à votre compte ODMS Cloud pour commander des licences et enregistrer des utilisateurs en votre nom. Votre revendeur n'aura accès à aucun fichier(s) vocal(s) ou document(s) stocké(s) dans votre compte ODMS Cloud." } }, "deleteAccountPopup": { @@ -558,4 +571,4 @@ "close": "Fermer" } } -} +} \ No newline at end of file From 4be13e002da4b204dbdc528139e5538c6c5100fb Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 5 Feb 2024 00:39:53 +0000 Subject: [PATCH 014/109] =?UTF-8?q?Merged=20PR=20710:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E5=89=8A=E9=99=A4=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3488: 画面実装(削除操作)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3488) - ユーザー削除の画面実装 - 確認ダイアログ - 削除API呼び出し - エラーハンドリング - 成功時のメッセージ - 成功時のユーザー一覧更新 ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ## 補足 - API呼び出しのエラーハンドリング部分はエラーコードが採番されたら追従します --- dictation_client/src/api/api.ts | 75 ++++++++++++ dictation_client/src/common/errors/code.ts | 8 ++ .../src/features/dictation/operations.ts | 2 +- .../src/features/user/operations.ts | 115 ++++++++++++++++++ .../src/features/user/userSlice.ts | 10 ++ .../src/pages/UserListPage/index.tsx | 44 +++++-- dictation_server/src/api/odms/openapi.json | 2 +- .../src/features/users/users.controller.ts | 2 +- 8 files changed, 244 insertions(+), 14 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 70d8d8f..6ed3907 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -6982,6 +6982,46 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser: async (postDeleteUserRequest: PostDeleteUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postDeleteUserRequest' is not null or undefined + assertParamExists('deleteUser', 'postDeleteUserRequest', postDeleteUserRequest) + const localVarPath = `/users/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postDeleteUserRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7376,6 +7416,19 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.deallocateLicense']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(postDeleteUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.deleteUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7539,6 +7592,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: any): AxiosPromise { return localVarFp.deallocateLicense(deallocateLicenseRequest, options).then((request) => request(axios, basePath)); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: any): AxiosPromise { + return localVarFp.deleteUser(postDeleteUserRequest, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7683,6 +7746,18 @@ export class UsersApi extends BaseAPI { return UsersApiFp(this.configuration).deallocateLicense(deallocateLicenseRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).deleteUser(postDeleteUserRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーの情報を取得します * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 5d0ffd8..336b41d 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -60,6 +60,14 @@ export const errorCodes = [ "E011004", // ワークタイプ使用中エラー "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー "E013002", // ワークフロー不在エラー + "E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった) + "E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった) + "E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた) + "E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた) + "E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた) + "E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている) + "E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) + "E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている) "E015001", // タイピストグループ削除済みエラー "E015002", // タイピストグループがワークフローに紐づいているエラー "E015003", // タイピストグループがルーティングされているエラー diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 51579e7..4a80927 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -610,7 +610,7 @@ export const deleteTaskAsync = createAsyncThunk< // e ⇒ errorObjectに変換" const error = createErrorObject(e); - let message = getTranslationID("dictationPage.message.backupFailedError"); + let message = getTranslationID("common.message.internalServerError"); if (error.statusCode === 400) { if (error.code === "E010603") { diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index b7ca2b0..f9d2578 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -383,3 +383,118 @@ export const deallocateLicenseAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteUserAsync = createAsyncThunk< + // 正常時の戻り値の型 + { + /* Empty Object */ + }, + // 引数 + { + userId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/deleteUserAsync", async (args, thunkApi) => { + const { userId } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + await usersApi.deleteUser( + { + userId, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + if (error.statusCode === 400) { + if (error.code === "E014001") { + // ユーザーが削除済みのため成功 + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + } + + // ユーザーに有効なライセンスが割り当たっているため削除不可 + if (error.code === "E014007") { + errorMessage = getTranslationID( + "userListPage.message.UserDeletionLicenseActiveError" + ); + } + // 管理者ユーザーため削除不可 + if (error.code === "E014002") { + errorMessage = getTranslationID( + "userListPage.message.AdminUserDeletionError" + ); + } + // タイピストユーザーで担当タスクがあるため削除不可 + if (error.code === "E014009") { + errorMessage = getTranslationID( + "userListPage.message.TypistUserDeletionTranscriptionTaskError" + ); + } + // タイピストユーザーでルーティングルールに設定されているため削除不可 + if (error.code === "E014004") { + errorMessage = getTranslationID( + "userListPage.message.TypistDeletionRoutingRuleError" + ); + } + // タイピストユーザーでTranscriptionistGroupに所属しているため削除不可 + if (error.code === "E014005") { + errorMessage = getTranslationID( + "userListPage.message.TypistUserDeletionTranscriptionistGroupError" + ); + } + // Authorユーザーで同一AuthorIDのタスクがあるため削除不可 + if (error.code === "E014006") { + errorMessage = getTranslationID( + "userListPage.message.AuthorUserDeletionTranscriptionTaskError" + ); + } + // Authorユーザーで同一AuthorIDがルーティングルールに設定されているため削除不可 + if (error.code === "E014003") { + errorMessage = getTranslationID( + "userListPage.message.AuthorDeletionRoutingRuleError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 881dba9..42baced 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -7,6 +7,7 @@ import { updateUserAsync, getAllocatableLicensesAsync, deallocateLicenseAsync, + deleteUserAsync, } from "./operations"; import { RoleType, UserView } from "./types"; @@ -290,6 +291,15 @@ export const userSlice = createSlice({ builder.addCase(deallocateLicenseAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(deleteUserAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteUserAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteUserAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index b6b0072..4dc2cee 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -10,6 +10,7 @@ import { selectUserViews, selectIsLoading, deallocateLicenseAsync, + deleteUserAsync, } from "features/user"; import { useTranslation } from "react-i18next"; import { getTranslationID } from "translation"; @@ -84,6 +85,24 @@ const UserListPage: React.FC = (): JSX.Element => { [dispatch, t] ); + const onDeleteUser = useCallback( + async (userId: number) => { + // ダイアログ確認 + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch(deleteUserAsync({ userId })); + if (meta.requestStatus === "fulfilled") { + dispatch(listUsersAsync()); + } + }, + [dispatch, t] + ); + useEffect(() => { // ユーザ一覧取得処理を呼び出す dispatch(listUsersAsync()); @@ -244,17 +263,20 @@ const UserListPage: React.FC = (): JSX.Element => { )} - {/* ユーザー削除 CCB後回し分なので今は非表示 -
  • - - {t( - getTranslationID( - "userListPage.label.deleteUser" - ) - )} - -
  • - */} +
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onDeleteUser(user.id); + }} + > + {t( + getTranslationID( + "userListPage.label.deleteUser" + ) + )} + +
  • {user.name} diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 653ce1d..0c49633 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -2160,7 +2160,7 @@ }, "/users/delete": { "post": { - "operationId": "updeateUser", + "operationId": "deleteUser", "summary": "", "description": "ユーザーを削除します", "parameters": [], diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 6a1e74e..6b19b15 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -940,7 +940,7 @@ export class UsersController { type: ErrorResponse, }) @ApiOperation({ - operationId: 'updeateUser', + operationId: 'deleteUser', description: 'ユーザーを削除します', }) @ApiBearerAuth() From df74dc358c725c1166f2fd06b8499983a815b5f6 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Mon, 5 Feb 2024 02:27:43 +0000 Subject: [PATCH 015/109] =?UTF-8?q?Merged=20PR=20731:=20=E6=97=A2=E5=AD=98?= =?UTF-8?q?API=E4=BF=AE=E6=AD=A3=EF=BC=88=E3=82=A2=E3=82=AB=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=88=E6=83=85=E5=A0=B1=E5=8F=96=E5=BE=97API?= =?UTF-8?q?=E3=80=81=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E4=BD=9C?= =?UTF-8?q?=E6=88=90API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3555: 既存API修正(アカウント情報取得API、アカウント作成API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3555) Accountにauto_file_delete、file_retention_daysを追加 既存のテストでアカウントを作成している部分に項目の値を追加。 ## レビューポイント -entityの ``` @Column({ default: 30 }) ``` が必要かどうか。 ## UIの変更 なし ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/api/odms/openapi.json | 6 +++++- dictation_server/src/common/test/utility.ts | 11 ++++++++++- dictation_server/src/constants/index.ts | 6 ++++++ .../src/features/accounts/accounts.service.spec.ts | 2 ++ .../src/features/accounts/accounts.service.ts | 2 ++ dictation_server/src/features/accounts/types/types.ts | 6 ++++++ .../src/features/files/test/files.service.mock.ts | 3 +++ .../accounts/accounts.repository.service.ts | 1 - .../repositories/accounts/entity/account.entity.ts | 7 +++++++ 9 files changed, 41 insertions(+), 3 deletions(-) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 0c49633..ce07f3b 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -3819,6 +3819,8 @@ "country": { "type": "string" }, "parentAccountId": { "type": "number" }, "delegationPermission": { "type": "boolean" }, + "autoFileDelete": { "type": "boolean" }, + "fileRetentionDays": { "type": "number" }, "primaryAdminUserId": { "type": "number" }, "secondryAdminUserId": { "type": "number" }, "parentAccountName": { "type": "string" } @@ -3828,7 +3830,9 @@ "companyName", "tier", "country", - "delegationPermission" + "delegationPermission", + "autoFileDelete", + "fileRetentionDays" ] }, "GetMyAccountResponse": { diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index 60a6a41..6fb99ab 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -2,7 +2,11 @@ import { v4 as uuidv4 } from 'uuid'; import { DataSource } from 'typeorm'; import { User, UserArchive } from '../../repositories/users/entity/user.entity'; import { Account } from '../../repositories/accounts/entity/account.entity'; -import { ADMIN_ROLES, USER_ROLES } from '../../constants'; +import { + ADMIN_ROLES, + FILE_RETENTION_DAYS_DEFAULT, + USER_ROLES, +} from '../../constants'; import { License } from '../../repositories/licenses/entity/license.entity'; type InitialTestDBState = { @@ -162,6 +166,9 @@ export const makeTestAccount = async ( parent_account_id: d?.parent_account_id ?? undefined, country: d?.country ?? 'US', delegation_permission: d?.delegation_permission ?? false, + auto_file_delete: d?.auto_file_delete ?? false, + file_retention_days: + d?.file_retention_days ?? FILE_RETENTION_DAYS_DEFAULT, locked: d?.locked ?? false, company_name: d?.company_name ?? 'test inc.', verified: d?.verified ?? true, @@ -252,6 +259,8 @@ export const makeTestSimpleAccount = async ( parent_account_id: d?.parent_account_id ?? undefined, country: d?.country ?? 'US', delegation_permission: d?.delegation_permission ?? false, + auto_file_delete: d?.auto_file_delete ?? false, + file_retention_days: d?.file_retention_days ?? FILE_RETENTION_DAYS_DEFAULT, locked: d?.locked ?? false, company_name: d?.company_name ?? 'test inc.', verified: d?.verified ?? true, diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 6657210..979f864 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -321,3 +321,9 @@ export const USER_LICENSE_STATUS = { ALLOCATED: 'allocated', EXPIRED: 'expired', } as const; + +/** + * ファイル保持日数の初期値 + * @const {number} + */ +export const FILE_RETENTION_DAYS_DEFAULT = 30; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 4bfd687..75555a9 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -166,6 +166,8 @@ describe('createAccount', () => { expect(account?.country).toBe(country); expect(account?.parent_account_id).toBe(dealerAccountId); expect(account?.tier).toBe(TIERS.TIER5); + expect(account?.auto_file_delete).toBe(false); + expect(account?.file_retention_days).toBe(30); expect(account?.primary_admin_user_id).toBe(user?.id); expect(account?.secondary_admin_user_id).toBe(null); expect(user?.accepted_eula_version).toBe(acceptedEulaVersion); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 50273e6..92b6c77 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -452,6 +452,8 @@ export class AccountsService { country: accountInfo.country, parentAccountId: accountInfo.parent_account_id ?? undefined, delegationPermission: accountInfo.delegation_permission, + autoFileDelete: accountInfo.auto_file_delete, + fileRetentionDays: accountInfo.file_retention_days, primaryAdminUserId: accountInfo.primary_admin_user_id ?? undefined, secondryAdminUserId: accountInfo.secondary_admin_user_id ?? undefined, parentAccountName: parentInfo ? parentInfo.company_name : undefined, diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 2600295..08217f8 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -422,6 +422,12 @@ export class Account { @ApiProperty() delegationPermission: boolean; + @ApiProperty() + autoFileDelete: boolean; + + @ApiProperty() + fileRetentionDays: number; + @ApiProperty({ required: false }) primaryAdminUserId?: number; 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 8e9ca51..1505888 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -8,6 +8,7 @@ import { Task } from '../../../repositories/tasks/entity/task.entity'; import { TemplateFilesRepositoryService } from '../../../repositories/template_files/template_files.repository.service'; import { NotificationhubService } from '../../../gateways/notificationhub/notificationhub.service'; import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; +import { FILE_RETENTION_DAYS_DEFAULT } from '../../../constants'; export type BlobstorageServiceMockValue = { createContainer: void | Error; @@ -160,6 +161,8 @@ export const makeDefaultUsersRepositoryMockValue = tier: 5, country: '', delegation_permission: true, + auto_file_delete: false, + file_retention_days: FILE_RETENTION_DAYS_DEFAULT, locked: false, company_name: '', verified: true, diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 2832672..8252ec1 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -169,7 +169,6 @@ export class AccountsRepositoryService { this.isCommentOut, context, ); - // 作成されたAccountのIDを使用してユーザーを作成 const user = new User(); { diff --git a/dictation_server/src/repositories/accounts/entity/account.entity.ts b/dictation_server/src/repositories/accounts/entity/account.entity.ts index 3c40a03..255c9c1 100644 --- a/dictation_server/src/repositories/accounts/entity/account.entity.ts +++ b/dictation_server/src/repositories/accounts/entity/account.entity.ts @@ -1,4 +1,5 @@ import { bigintTransformer } from '../../../common/entity'; +import { FILE_RETENTION_DAYS_DEFAULT } from '../../../constants'; import { User } from '../../../repositories/users/entity/user.entity'; import { Entity, @@ -44,6 +45,12 @@ export class Account { @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) active_worktype_id: number | null; + @Column({ default: false }) + auto_file_delete: boolean; + + @Column({ default: FILE_RETENTION_DAYS_DEFAULT }) + file_retention_days: number; + @Column({ nullable: true, type: 'datetime' }) deleted_at: Date | null; From feeec9d1f539771610b23032a19d6d5444c2592a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Tue, 6 Feb 2024 07:12:11 +0000 Subject: [PATCH 016/109] =?UTF-8?q?Merged=20PR=20714:=20API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E5=89=8A?= =?UTF-8?q?=E9=99=A4|Repository=E4=BB=A5=E5=A4=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3594: API実装(ユーザー削除|Repository以外)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3594) ユーザー削除API実装 ユニットテスト実装 ## レビューポイント - `'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった)`が用意されているが、 `'E014002', // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)`とわけて実装する必要あるか。 管理者でしか削除処理は行えない&管理者ユーザは削除できない。 - `ExistsCheckoutPermissionDeleteFailedError` 削除対象ユーザーがチェックアウト権限を持っている事が原因の削除失敗エラーは、ユーザ削除エラーの一つとして、`code.ts`にコードを用意してあげる必要があるか? (引継ぎ時あえて用意していないように見えなくもなかったので) ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/error/code.ts | 9 + dictation_server/src/common/error/message.ts | 9 + .../src/features/files/test/utility.ts | 3 +- .../src/features/users/users.controller.ts | 3 +- .../src/features/users/users.service.spec.ts | 777 +++++++++++++++++- .../src/features/users/users.service.ts | 223 ++++- .../src/gateways/sendgrid/sendgrid.service.ts | 57 ++ .../src/repositories/users/errors/types.ts | 4 +- .../users/users.repository.service.ts | 30 +- .../src/templates/template_U_116.html | 49 ++ .../src/templates/template_U_116.txt | 32 + 11 files changed, 1163 insertions(+), 33 deletions(-) create mode 100644 dictation_server/src/templates/template_U_116.html create mode 100644 dictation_server/src/templates/template_U_116.txt diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 3c488da..661c648 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -67,4 +67,13 @@ export const ErrorCodes = [ 'E012001', // テンプレートファイル不在エラー 'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー 'E013002', // ワークフロー不在エラー + 'E014001', // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった) + 'E014002', // ユーザー削除エラー(削除しようとしたユーザーが管理者だった) + 'E014003', // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた) + 'E014004', // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた) + 'E014005', // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた) + 'E014006', // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている) + 'E014007', // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) + 'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった) + 'E014009', // ユーザー削除エラー(削除しようとしたユーザーがタスクのルーティング(文字起こし候補)になっている場合) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 9383694..f885d26 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -56,4 +56,13 @@ export const errors: Errors = { E012001: 'Template file not found Error', E013001: 'AuthorId and WorktypeId pair already exists Error', E013002: 'Workflow not found Error', + E014001: 'User delete failed Error: already deleted', + E014002: 'User delete failed Error: target is admin', + E014003: 'User delete failed Error: workflow assigned(AUTHOR_ID)', + E014004: 'User delete failed Error: workflow assigned(TYPIST)', + E014005: 'User delete failed Error: typist group assigned', + E014006: 'User delete failed Error: checkout permission existed', + E014007: 'User delete failed Error: enabled license assigned', + E014008: 'User delete failed Error: delete myself', + E014009: 'User delete failed Error: user has checkout permissions.', }; diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index 1fe086b..79766a7 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -53,12 +53,13 @@ export const createTask = async ( status: string, typist_user_id?: number | undefined, author_id?: string | undefined, + owner_user_id?: number | undefined, ): Promise<{ audioFileId: number }> => { const { identifiers: audioFileIdentifiers } = await datasource .getRepository(AudioFile) .insert({ account_id: account_id, - owner_user_id: 1, + owner_user_id: owner_user_id ?? 1, url: url, file_name: fileName, author_id: author_id ?? 'DEFAULT_ID', diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 6b19b15..02f1c68 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -988,7 +988,8 @@ export class UsersController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - + const now = new Date(); + await this.usersService.deleteUser(context, body.userId, now); return {}; } } diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index c6a39dc..c738fb6 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -18,9 +18,11 @@ import { import { DataSource } from 'typeorm'; import { UsersService } from './users.service'; import { + ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_EXPIRATION_THRESHOLD_DAYS, LICENSE_TYPE, + TASK_STATUS, USER_AUDIO_FORMAT, USER_LICENSE_EXPIRY_STATUS, USER_ROLES, @@ -37,6 +39,7 @@ import { License } from '../../repositories/licenses/entity/license.entity'; import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; import { getUser, + getUserArchive, getUserFromExternalId, getUsers, makeTestAccount, @@ -45,8 +48,14 @@ import { } from '../../common/test/utility'; import { v4 as uuidv4 } from 'uuid'; import { createOptionItems, createWorktype } from '../accounts/test/utility'; -import { createWorkflow, getWorkflows } from '../workflows/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflows, +} from '../workflows/test/utility'; import { truncateAllTable } from '../../common/test/init'; +import { createTask } from '../files/test/utility'; +import { createCheckoutPermissions } from '../tasks/test/utility'; describe('UsersService.confirmUser', () => { let source: DataSource | null = null; @@ -2868,3 +2877,769 @@ describe('UsersService.getRelations', () => { } }); }); +describe('UsersService.deleteUser', () => { + let source: DataSource | null = null; + + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ユーザーを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + + // ユーザーが削除されたことを確認 + { + const user = await getUser(source, user1); + expect(user).toBeNull(); + // ユーザアーカイブが作成されたことを確認 + const userArchive = await getUserArchive(source); + expect(userArchive[0].external_id).toBe(external_id); + } + }); + it('存在しないユーザは削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + + try { + await service.deleteUser(context, 100, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014001')); + } else { + fail(); + } + } + }); + it('管理者ユーザは削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + + try { + await service.deleteUser(context, admin.id, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014002')); + } else { + fail(); + } + } + }); + it('WorkFlowに割り当てられているユーザ(Auhtor)は削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + const worktype1 = await createWorktype( + source, + account.id, + 'worktype1', + undefined, + true, + ); + await createOptionItems(source, worktype1.id); + await createWorkflow(source, account.id, user1, worktype1.id); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014003')); + } else { + fail(); + } + } + }); + it('WorkFlowに割り当てられているユーザ(Typist)は削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + const worktype1 = await createWorktype( + source, + account.id, + 'worktype1', + undefined, + true, + ); + await createOptionItems(source, worktype1.id); + const workflow1 = await createWorkflow( + source, + account.id, + user1, + worktype1.id, + ); + await createWorkflowTypist(source, workflow1.id, user2); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014004')); + } else { + fail(); + } + } + }); + it('ユーザグループに所属しているユーザは削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // ユーザグループを作成 + const userGroup = await createUserGroup(source, account.id, 'userGroup', [ + user1, + user2, + ]); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014005')); + } else { + fail(); + } + } + }); + it('削除対象ユーザーが有効なタスクをまだ持っている場合、削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.IN_PROGRESS, + user2, + 'AUTHOR', + user1, + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザータスクのチェックアウト権限まだ持っている場合、削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // CheckoutPermissionを作成 + await createCheckoutPermissions(source, 100, user2); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014009')); + } else { + fail(); + } + } + }); + it('削除対象ユーザーが有効なライセンスをまだ持っている場合、削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // 明日まで有効なライセンスを作成して紐づける + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + await createLicense(source, account.id, user2, tomorrow); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014007')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 359f163..bdde739 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -25,9 +25,16 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { GetRelationsResponse, User } from './types/types'; import { + AdminDeleteFailedError, + AssignedWorkflowWithAuthorDeleteFailedError, + AssignedWorkflowWithTypistDeleteFailedError, AuthorIdAlreadyExistsError, EmailAlreadyVerifiedError, EncryptionPasswordNeedError, + ExistsCheckoutPermissionDeleteFailedError, + ExistsGroupMemberDeleteFailedError, + ExistsValidLicenseDeleteFailedError, + ExistsValidTaskDeleteFailedError, InvalidRoleChangeError, UpdateTermsVersionNotSetError, UserNotFoundError, @@ -286,7 +293,7 @@ export class UsersService { this.logger.error(`[${context.getTrackingId()}]create user failed`); //リカバリー処理 //Azure AD B2Cに登録したユーザー情報を削除する - await this.deleteB2cUser(externalUser.sub, context); + await this.internalDeleteB2cUser(externalUser.sub, context); switch (e.code) { case 'ER_DUP_ENTRY': @@ -337,9 +344,9 @@ export class UsersService { this.logger.error(`[${context.getTrackingId()}] create user failed`); //リカバリー処理 //Azure AD B2Cに登録したユーザー情報を削除する - await this.deleteB2cUser(externalUser.sub, context); + await this.internalDeleteB2cUser(externalUser.sub, context); // DBからユーザーを削除する - await this.deleteUser(newUser.id, context); + await this.internalDeleteUser(newUser.id, context); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, @@ -353,10 +360,13 @@ export class UsersService { // Azure AD B2Cに登録したユーザー情報を削除する // TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補 - private async deleteB2cUser(externalUserId: string, context: Context) { + private async internalDeleteB2cUser( + externalUserId: string, + context: Context, + ) { this.logger.log( `[IN] [${context.getTrackingId()}] ${ - this.deleteB2cUser.name + this.internalDeleteB2cUser.name } | params: { externalUserId: ${externalUserId} }`, ); try { @@ -371,16 +381,16 @@ export class UsersService { ); } finally { this.logger.log( - `[OUT] [${context.getTrackingId()}] ${this.deleteB2cUser.name}`, + `[OUT] [${context.getTrackingId()}] ${this.internalDeleteB2cUser.name}`, ); } } // DBに登録したユーザー情報を削除する - private async deleteUser(userId: number, context: Context) { + private async internalDeleteUser(userId: number, context: Context) { this.logger.log( `[IN] [${context.getTrackingId()}] ${ - this.deleteUser.name + this.internalDeleteUser.name } | params: { userId: ${userId} }`, ); try { @@ -393,7 +403,7 @@ export class UsersService { ); } finally { this.logger.log( - `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`, + `[OUT] [${context.getTrackingId()}] ${this.internalDeleteUser.name}`, ); } } @@ -1287,6 +1297,201 @@ export class UsersService { ); } } + /** + * ユーザーを削除する + * @param context + * @param extarnalId + * @param currentTime ライセンス有効期限のチェックに使用する現在時刻 + * @returns user + */ + async deleteUser( + context: Context, + userId: number, + currentTime: Date, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteUser.name + } | params: { userId: ${userId} };`, + ); + try { + // 削除対象のユーザーが存在するかを確認する + let user: EntityUser; + try { + user = await this.usersRepository.findUserById(context, userId); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case UserNotFoundError: + throw new HttpException( + makeErrorResponse('E014001'), + HttpStatus.BAD_REQUEST, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + // 削除対象のユーザーが管理者かどうかを確認する + const admins = await this.usersRepository.findAdminUsers( + context, + user.account_id, + ); + const adminIds = admins.map((admin) => admin.id); + if (adminIds.includes(user.id)) { + throw new HttpException( + makeErrorResponse('E014002'), + HttpStatus.BAD_REQUEST, + ); + } + // Azure AD B2Cからユーザー情報(e-mail, name)を取得する + const adminExternalIds = admins.map((admin) => admin.external_id); + const externalIds = [user.external_id, ...adminExternalIds]; + // Azure AD B2CのRateLimit対策のため、ユーザー情報を一括取得する + const details = await this.adB2cService.getUsers(context, externalIds); + // 削除対象のユーザーがAzure AD B2Cに存在するかを確認する + const deleteTargetDetail = details.find( + (details) => details.id === user.external_id, + ); + if (deleteTargetDetail == null) { + throw new HttpException( + makeErrorResponse('E014001'), + HttpStatus.BAD_REQUEST, + ); + } + // 管理者の情報が0件(=競合でアカウントが削除された場合等)の場合はエラーを返す + const adminDetails = details.filter((details) => + adminExternalIds.includes(details.id), + ); + if (adminDetails.length === 0) { + // 通常ユーザーが取得できていて管理者が取得できない事は通常ありえないため、汎用エラー + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const { emailAddress } = getUserNameAndMailAddress(deleteTargetDetail); + // メールアドレスが設定されていない場合はエラーを返す + if (emailAddress == null) { + throw new Error(`emailAddress is null. externalId=${user.external_id}`); + } + // 管理者のメールアドレスを取得 + const { adminEmails } = await this.getAccountInformation( + context, + user.account_id, + ); + // プライマリ管理者を取得 + const { external_id: adminExternalId } = await this.getPrimaryAdminUser( + context, + user.account_id, + ); + const adb2cAdminUser = await this.adB2cService.getUser( + context, + adminExternalId, + ); + const { displayName: primaryAdminName } = + getUserNameAndMailAddress(adb2cAdminUser); + + let isSuccess = false; + try { + const result = await this.usersRepository.deleteUser( + context, + userId, + currentTime, + ); + isSuccess = result.isSuccess; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + switch (e.constructor) { + case AdminDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014002'), + HttpStatus.BAD_REQUEST, + ); + case AssignedWorkflowWithAuthorDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014003'), + HttpStatus.BAD_REQUEST, + ); + case AssignedWorkflowWithTypistDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014004'), + HttpStatus.BAD_REQUEST, + ); + case ExistsGroupMemberDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014005'), + HttpStatus.BAD_REQUEST, + ); + case ExistsValidTaskDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014006'), + HttpStatus.BAD_REQUEST, + ); + case ExistsValidLicenseDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014007'), + HttpStatus.BAD_REQUEST, + ); + case ExistsCheckoutPermissionDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014009'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + // トランザクションレベルで厳密に削除が成功したかを判定する + if (!isSuccess) { + // 既に削除されている場合はエラーを返す + throw new HttpException( + makeErrorResponse('E014001'), + HttpStatus.BAD_REQUEST, + ); + } + try { + // 削除を実施したことが確定したので、Azure AD B2Cからユーザーを削除する + await this.adB2cService.deleteUser(user.external_id, context); + this.logger.log( + `[${context.getTrackingId()}] delete externalUser: ${ + user.external_id + } | params: { ` + `externalUserId: ${user.external_id}, };`, + ); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${ + user.external_id + }`, + ); + } + // 削除を実施したことが確定したので、メール送信処理を実施する + try { + await this.sendgridService.sendMailWithU116( + context, + deleteTargetDetail.displayName, + emailAddress, + primaryAdminName, + adminEmails, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`, + ); + } + } /** * アカウントIDを指定して、アカウント情報と管理者情報を取得する diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index 985c7b1..a467b3b 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -56,6 +56,8 @@ export class SendGridService { private readonly templateU114Text: string; private readonly templateU115Html: string; private readonly templateU115Text: string; + private readonly templateU116Html: string; + private readonly templateU116Text: string; private readonly templateU117Html: string; private readonly templateU117Text: string; @@ -177,6 +179,14 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_115.txt`), 'utf-8', ); + this.templateU116Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_116.html`), + 'utf-8', + ); + this.templateU116Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_116.txt`), + 'utf-8', + ); this.templateU117Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_117.html`), 'utf-8', @@ -833,6 +843,53 @@ export class SendGridService { } } + /** + * U-116のテンプレートを使用したメールを送信する + * @param context + * @param userName 削除されたユーザーの名前 + * @param userMail 削除されたユーザーのメールアドレス + * @param primaryAdminName 削除されたユーザーの所属するアカウントの管理者(primary)の名前 + * @param adminMails 削除されたユーザーの所属するアカウントの管理者(primary/secondary)のメールアドレス + * @returns mail with u116 + */ + async sendMailWithU116( + context: Context, + userName: string, + userMail: string, + primaryAdminName: string, + adminMails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU116.name}`, + ); + try { + const subject = 'Edit User Notification [U-116]'; + + // メールの本文を作成する + const html = this.templateU116Html + .replaceAll(USER_NAME, userName) + .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); + const text = this.templateU116Text + .replaceAll(USER_NAME, userName) + .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); + + // メールを送信する + await this.sendMail( + context, + [userMail], + adminMails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU116.name}`, + ); + } + } + /** * U-117のテンプレートを使用したメールを送信する * @param context diff --git a/dictation_server/src/repositories/users/errors/types.ts b/dictation_server/src/repositories/users/errors/types.ts index 217d54d..5468900 100644 --- a/dictation_server/src/repositories/users/errors/types.ts +++ b/dictation_server/src/repositories/users/errors/types.ts @@ -87,7 +87,7 @@ export class ExistsGroupMemberDeleteFailedError extends Error { } } -// 削除対象ユーザーが有効なタスクをまだ持っている事が原因の削除失敗エラー +// 削除対象ユーザー(Author)に未完了のタスクがまだ残っている事が原因の削除失敗エラー export class ExistsValidTaskDeleteFailedError extends Error { constructor(message: string) { super(message); @@ -109,4 +109,4 @@ export class ExistsValidLicenseDeleteFailedError extends Error { super(message); this.name = 'ExistsValidLicenseDeleteFailedError'; } -} \ No newline at end of file +} diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 0355ec5..e132ad2 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -65,6 +65,7 @@ import { import { UserGroup } from '../user_groups/entity/user_group.entity'; import { Task } from '../tasks/entity/task.entity'; import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; +import { WorkflowTypist } from '../workflows/entity/workflow_typists.entity'; @Injectable() export class UsersRepositoryService { @@ -604,7 +605,6 @@ export class UsersRepositoryService { ): Promise<{ isSuccess: boolean }> { return await this.dataSource.transaction(async (entityManager) => { const userRepo = entityManager.getRepository(User); - // 削除対象ユーザーをロックを取った上で取得 const target = await userRepo.findOne({ relations: { @@ -616,7 +616,6 @@ export class UsersRepositoryService { lock: { mode: 'pessimistic_write' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); - // 削除済みであれば失敗する if (target == null) { return { isSuccess: false }; @@ -636,7 +635,6 @@ export class UsersRepositoryService { if (adminIds.some((adminId) => adminId === target.id)) { throw new AdminDeleteFailedError('User is an admin.'); } - const userGroupRepo = entityManager.getRepository(UserGroup); const groups = await userGroupRepo.find({ relations: { @@ -648,7 +646,6 @@ export class UsersRepositoryService { lock: { mode: 'pessimistic_write' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); - const workflowRepo = entityManager.getRepository(Workflow); const workflows = await workflowRepo.find({ where: { @@ -664,18 +661,20 @@ export class UsersRepositoryService { 'Author is assigned to a workflow.', ); } - - // Workflowに直接個人で指定されているTypistのID一覧を作成する - const typistIds = workflows - .flatMap((x) => x.workflowTypists) - .flatMap((x) => (x?.typist_id != null ? [x.typist_id] : [])); + const workflowTypistsRepo = entityManager.getRepository(WorkflowTypist); + const workflowTypists = await workflowTypistsRepo.find({ + where: { + typist_id: target.id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); // Workflowに直接個人で指定されているTypistは削除できない - if (typistIds.some((typistId) => typistId === target.id)) { + if (workflowTypists.some((x) => x.typist_id === target.id)) { throw new AssignedWorkflowWithTypistDeleteFailedError( 'Typist is assigned to a workflow.', ); } - // いずれかのGroupに属しているユーザーIDの一覧を作成 const groupMemberIds = groups .flatMap((group) => group.userGroupMembers) @@ -687,7 +686,6 @@ export class UsersRepositoryService { 'User is a member of a group', ); } - // 削除対象ユーザーがAuthorであった時、 if (target.role === USER_ROLES.AUTHOR) { const taskRepo = entityManager.getRepository(Task); @@ -706,7 +704,6 @@ export class UsersRepositoryService { lock: { mode: 'pessimistic_write' }, // lockする事で状態遷移の競合をブロックし、新規追加以外で所有タスク群の状態変更を防ぐ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); - // 未完了タスクが残っていたら削除できない const enableStatus: string[] = [ TASK_STATUS.UPLOADED, @@ -721,7 +718,6 @@ export class UsersRepositoryService { throw new ExistsValidTaskDeleteFailedError('User has valid tasks.'); } } - // 削除対象ユーザーがTypistであった時、 if (target.role === USER_ROLES.TYPIST) { const checkoutPermissionRepo = @@ -741,7 +737,6 @@ export class UsersRepositoryService { ); } } - // 対象ユーザーのライセンス割り当て状態を取得 const licenseRepo = entityManager.getRepository(License); const allocatedLicense = await licenseRepo.findOne({ @@ -775,7 +770,6 @@ export class UsersRepositoryService { this.isCommentOut, context, ); - // 期限切れライセンスが割り当てられていた場合、ユーザーを削除する前にライセンスを割り当て解除する // ※この処理時点で有効期限外ライセンスであることは確定であるため、期限切れ判定をここでは行わない if (allocatedLicense != null) { @@ -809,15 +803,13 @@ export class UsersRepositoryService { context, ); } - // ユーザテーブルのレコードを削除する await deleteEntity( userRepo, - { user_id: target.id }, + { id: target.id }, this.isCommentOut, context, ); - // ソート条件のテーブルのレコードを削除する const sortCriteriaRepo = entityManager.getRepository(SortCriteria); await deleteEntity( diff --git a/dictation_server/src/templates/template_U_116.html b/dictation_server/src/templates/template_U_116.html new file mode 100644 index 0000000..7bb71c1 --- /dev/null +++ b/dictation_server/src/templates/template_U_116.html @@ -0,0 +1,49 @@ + + + User Deleted Notification [U-116] + + + +
    +

    <English>

    +

    Dear $USER_NAME$,

    +

    + Thank you for using ODMS Cloud. Your user information has been deleted from ODMS Cloud. +

    +

    + If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_NAME$. +

    +

    + If you have received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $USER_NAME$,

    +

    + Vielen Dank, dass Sie ODMS Cloud verwenden. Ihre Benutzerinformationen wurden aus ODMS Cloud gelöscht. +

    +

    + Wenn Sie Unterstützung in Bezug auf ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_NAME$. +

    +

    + Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten. +

    +
    +
    +

    <Français>

    +

    Chère/Cher $USER_NAME$,

    +

    + Merci d'utiliser ODMS Cloud. Vos informations utilisateur ont été supprimées d'ODMS Cloud. +

    +

    + Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_NAME$. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_116.txt b/dictation_server/src/templates/template_U_116.txt new file mode 100644 index 0000000..0b00153 --- /dev/null +++ b/dictation_server/src/templates/template_U_116.txt @@ -0,0 +1,32 @@ + + +Dear $USER_NAME$, + +Thank you for using ODMS Cloud. Your user information has been deleted from ODMS Cloud. + +If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_NAME$. + +If you have received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $USER_NAME$, + +Vielen Dank, dass Sie ODMS Cloud nutzen. Ihre Benutzerinformationen wurden aus der ODMS Cloud gelöscht. + +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_NAME$. + +Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten. + + + +Chère/Cher $USER_NAME$, + +Merci d'utiliser ODMS Cloud. Vos informations utilisateur ont été supprimées d'ODMS Cloud. + +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_NAME$. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file From 19b544540ebc45010de74bceebfb51d46664b3ad Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 6 Feb 2024 07:46:57 +0000 Subject: [PATCH 017/109] =?UTF-8?q?Merged=20PR=20724:=20API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3535: API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3535) - タイピストグループ削除APIとテストを実装しました。 accountのテストがうまくいっていないようなので別途見直します。 ※タイピストグループ削除のテストはうまくいっています ## レビューポイント - エラーケースと出力されるコードは適切でしょうか? - テストケースは適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/common/error/code.ts | 3 + dictation_server/src/common/error/message.ts | 3 + dictation_server/src/common/test/modules.ts | 2 +- .../features/accounts/accounts.controller.ts | 2 +- .../accounts/accounts.service.spec.ts | 444 +++++++++++++++++- .../src/features/accounts/accounts.service.ts | 74 +++ .../accounts/test/accounts.service.mock.ts | 11 +- .../src/features/accounts/test/utility.ts | 7 + .../repositories/user_groups/errors/types.ts | 18 +- .../user_groups.repository.service.ts | 81 ++++ 10 files changed, 620 insertions(+), 25 deletions(-) diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 661c648..b6ad2af 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -76,4 +76,7 @@ export const ErrorCodes = [ 'E014007', // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) 'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった) 'E014009', // ユーザー削除エラー(削除しようとしたユーザーがタスクのルーティング(文字起こし候補)になっている場合) + 'E015001', // タイピストグループ削除エラー(削除しようとしたタイピストグループがすでに削除済みだった) + 'E015002', // タイピストグループ削除エラー(削除しようとしたタイピストグループがWorkflowのTypist候補として指定されていた) + 'E015003', // タイピストグループ削除エラー(削除しようとしたタイピストグループがチェックアウト可能なタスクが存在した) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index f885d26..20a176f 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -65,4 +65,7 @@ export const errors: Errors = { E014007: 'User delete failed Error: enabled license assigned', E014008: 'User delete failed Error: delete myself', E014009: 'User delete failed Error: user has checkout permissions.', + E015001: 'Typist Group delete failed Error: already deleted', + E015002: 'Typist Group delete failed Error: workflow assigned', + E015003: 'Typist Group delete failed Error: checkout permission existed', }; diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 7c51a50..8d50139 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -80,7 +80,7 @@ export const makeTestingModule = async ( WorktypesRepositoryModule, TermsRepositoryModule, RedisModule, - CacheModule.register({ isGlobal: true }), + CacheModule.register({ isGlobal: true, ttl: 86400 }), ], providers: [ AuthService, diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index c75d786..8e4c87f 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -834,7 +834,7 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: 削除処理 + await this.accountService.deleteTypistGroup(context, userId, typistGroupId); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index fd8975f..f6f3096 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -22,6 +22,7 @@ import { getSortCriteria, getTypistGroup, getTypistGroupMember, + getTypistGroupMembers, getWorktypes, } from './test/utility'; import { DataSource } from 'typeorm'; @@ -46,6 +47,7 @@ import { LICENSE_ISSUE_STATUS, LICENSE_TYPE, OPTION_ITEM_VALUE_TYPE, + TASK_STATUS, TIERS, USER_ROLES, WORKTYPE_MAX_COUNT, @@ -76,9 +78,16 @@ import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { Worktype } from '../../repositories/worktypes/entity/worktype.entity'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; -import { createWorkflow, getWorkflows } from '../workflows/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflowTypists, + getWorkflows, +} from '../workflows/test/utility'; import { UsersService } from '../users/users.service'; import { truncateAllTable } from '../../common/test/init'; +import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; +import { createCheckoutPermissions } from '../tasks/test/utility'; describe('createAccount', () => { let source: DataSource | null = null; @@ -134,8 +143,8 @@ describe('createAccount', () => { }, }); - let _subject: string = ""; - let _url: string | undefined = ""; + let _subject: string = ''; + let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( context: Context, @@ -199,7 +208,9 @@ describe('createAccount', () => { // 想定通りのメールが送られているか確認 expect(_subject).toBe('User Registration Notification [U-102]'); - expect(_url?.startsWith('http://localhost:8081/mail-confirm?verify=')).toBeTruthy(); + expect( + _url?.startsWith('http://localhost:8081/mail-confirm?verify='), + ).toBeTruthy(); }); it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => { @@ -2281,6 +2292,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); const now = new Date(); @@ -2378,6 +2392,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); const now = new Date(); // 親と子アカウントを作成する @@ -2477,6 +2494,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); const now = new Date(); // 親と子アカウントを作成する const { id: parentAccountId } = ( @@ -3611,6 +3631,319 @@ describe('updateTypistGroup', () => { }); }); +describe('deleteTypistGroup', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('TypistGroupを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + }); + it('TypistGroupを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + } + + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + //実行結果を確認 + { + const typistGroups = await getTypistGroup(source, account.id); + expect(typistGroups.length).toBe(0); + + const typistGroupUsers = await getTypistGroupMembers(source); + expect(typistGroupUsers.length).toBe(0); + } + }); + it('タイピストグループが存在しない場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + // 作成したアカウントにユーザーを追加する + const user = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [user.id], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, 999); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015001')); + } else { + fail(); + } + } + }); + it('タイピストグループがルーティングルールに紐づいていた場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + const group = await getTypistGroup(source, account.id); + const workflow = await createWorkflow(source, account.id, authorUserId); + await createWorkflowTypist(source, workflow.id, undefined, group[0].id); + //作成したデータを確認 + { + const workflowTypists = await getWorkflowTypists(source, workflow.id); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + expect(workflowTypists.length).toBe(1); + expect(workflowTypists[0].typist_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015002')); + } else { + fail(); + } + } + }); + it('タイピストグループがタスクのチェックアウト候補だった場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + author_id: authorId, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + const group = await getTypistGroup(source, account.id); + const { taskId } = await createTask( + source, + account.id, + authorUserId, + authorId, + 'worktypeId', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + + await createCheckoutPermissions(source, taskId, undefined, group[0].id); + + //作成したデータを確認 + { + const checkoutPermission = await getCheckoutPermissions(source, taskId); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + expect(checkoutPermission.length).toBe(1); + expect(checkoutPermission[0].user_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const typiptUserExternalId = 'typist-user-external-id'; + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: USER_ROLES.TYPIST, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(typistUserId); + } + + //DBアクセスに失敗するようにする + const typistGroupService = module.get( + UserGroupsRepositoryService, + ); + typistGroupService.deleteTypistGroup = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + describe('getWorktypes', () => { let source: DataSource | null = null; beforeAll(async () => { @@ -5256,6 +5589,9 @@ describe('ライセンス発行キャンセル', () => { ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await service.cancelIssue( makeContext('trackingId', 'requestId'), @@ -5320,6 +5656,9 @@ describe('ライセンス発行キャンセル', () => { ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await service.cancelIssue( makeContext('trackingId', 'requestId'), @@ -5358,6 +5697,9 @@ describe('ライセンス発行キャンセル', () => { }); const poNumber = 'CANCEL_TEST'; const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5405,6 +5747,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5452,6 +5797,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5500,6 +5848,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5729,8 +6080,13 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - let _subject: string = ""; - let _url: string | undefined = ""; + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); + let _subject: string = ''; + let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( context: Context, @@ -5797,6 +6153,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5831,6 +6192,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5866,6 +6232,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5898,6 +6269,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5929,6 +6305,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -6000,6 +6381,9 @@ describe('getAccountInfo', () => { }); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); const context = makeContext(admin.external_id, 'requestId'); const accountResponse = await service.getAccountInfo( @@ -6422,6 +6806,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); let _subject: string = ''; let _url: string | undefined = ''; overrideSendgridService(service, { @@ -6672,6 +7064,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); // 第五階層のアカウント作成 @@ -6698,11 +7098,6 @@ describe('deleteAccountAndData', () => { deleteAccountAndInsertArchives: jest.fn().mockRejectedValue(new Error()), }); - // ADB2Cユーザーの削除成功 - overrideAdB2cService(service, { - deleteUsers: jest.fn(), - }); - // blobstorageコンテナの削除成功 overrideBlobstorageService(service, { deleteContainer: jest.fn(), @@ -6737,6 +7132,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除失敗 + overrideAdB2cService(service, { + deleteUsers: jest.fn().mockRejectedValue(new Error()), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); // 第五階層のアカウント作成 @@ -6758,11 +7161,6 @@ describe('deleteAccountAndData', () => { account_id: tier5Accounts.account.id, }); - // ADB2Cユーザーの削除失敗 - overrideAdB2cService(service, { - deleteUsers: jest.fn().mockRejectedValue(new Error()), - }); - // blobstorageコンテナの削除成功 overrideBlobstorageService(service, { deleteContainer: jest.fn(), @@ -6792,6 +7190,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); @@ -6814,11 +7220,6 @@ describe('deleteAccountAndData', () => { account_id: tier5Accounts.account.id, }); - // ADB2Cユーザーの削除成功 - overrideAdB2cService(service, { - deleteUsers: jest.fn(), - }); - // blobstorageコンテナの削除失敗 overrideBlobstorageService(service, { deleteContainer: jest.fn().mockRejectedValue(new Error()), @@ -6989,6 +7390,7 @@ describe('getAccountInfoMinimalAccess', () => { } }); }); + describe('getCompanyName', () => { let source: DataSource | null = null; beforeAll(async () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 460d843..d200861 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -60,6 +60,8 @@ import { } from '../../repositories/licenses/errors/types'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { + AssignedWorkflowDeleteFailedError, + ExistsCheckoutPermissionDeleteFailedError, TypistGroupNameAlreadyExistError, TypistGroupNotExistError, TypistIdInvalidError, @@ -1348,6 +1350,78 @@ export class AccountsService { } } + /** + * タイピストグループを削除する + * @param context + * @param externalId + * @param typistGroupId + * @returns typist group + */ + async deleteTypistGroup( + context: Context, + externalId: string, + typistGroupId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTypistGroup.name + } | params: { ` + + `externalId: ${externalId}, ` + + `typistGroupId: ${typistGroupId}, `, + ); + try { + // 外部IDをもとにユーザー情報を取得する + const { account_id } = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + // タイピストグループを削除する + await this.userGroupsRepository.deleteTypistGroup( + context, + account_id, + typistGroupId, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + // タイピストグループ削除済み + case TypistGroupNotExistError: + throw new HttpException( + makeErrorResponse('E015001'), + HttpStatus.BAD_REQUEST, + ); + // タイピストグループがルーティングルールに使用されている + case AssignedWorkflowDeleteFailedError: + throw new HttpException( + makeErrorResponse('E015002'), + HttpStatus.BAD_REQUEST, + ); + // タイピストグループがタスクの文字起こし候補に使用されている + case ExistsCheckoutPermissionDeleteFailedError: + throw new HttpException( + makeErrorResponse('E015003'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteTypistGroup.name}`, + ); + } + } + /** * ライセンス発行をキャンセルする * @param context diff --git a/dictation_server/src/features/accounts/test/accounts.service.mock.ts b/dictation_server/src/features/accounts/test/accounts.service.mock.ts index a031dae..58b1dd1 100644 --- a/dictation_server/src/features/accounts/test/accounts.service.mock.ts +++ b/dictation_server/src/features/accounts/test/accounts.service.mock.ts @@ -42,6 +42,7 @@ export type UserGroupsRepositoryMockValue = { export type AdB2cMockValue = { createUser: string | ConflictError | Error; getUsers: AdB2cUser[] | Error; + getUser: AdB2cUser | Error; }; export type SendGridMockValue = { sendMail: undefined | Error; @@ -206,7 +207,7 @@ export const makeUserGroupsRepositoryMock = ( }; }; export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { - const { createUser, getUsers } = value; + const { createUser, getUsers, getUser } = value; return { createUser: @@ -219,6 +220,10 @@ export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { getUsers instanceof Error ? jest.fn, []>().mockRejectedValue(getUsers) : jest.fn, []>().mockResolvedValue(getUsers), + getUser: + getUser instanceof Error + ? jest.fn, []>().mockRejectedValue(getUser) + : jest.fn, []>().mockResolvedValue(getUser), }; }; export const makeConfigMock = (value: ConfigMockValue) => { @@ -437,6 +442,10 @@ export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { displayName: 'Typist3', }, ], + getUser: { + id: 'typist1', + displayName: 'Typist1', + }, }; }; export const makeDefaultSendGridlValue = (): SendGridMockValue => { diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index e658d9b..b68b5fd 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -123,6 +123,13 @@ export const getTypistGroupMember = async ( }); }; +// タイピストグループメンバー一覧を取得する +export const getTypistGroupMembers = async ( + datasource: DataSource, +): Promise => { + return await datasource.getRepository(UserGroupMember).find(); +}; + // Worktypeを作成する export const createWorktype = async ( datasource: DataSource, diff --git a/dictation_server/src/repositories/user_groups/errors/types.ts b/dictation_server/src/repositories/user_groups/errors/types.ts index 4a215a7..a1ebabe 100644 --- a/dictation_server/src/repositories/user_groups/errors/types.ts +++ b/dictation_server/src/repositories/user_groups/errors/types.ts @@ -12,10 +12,26 @@ export class TypistIdInvalidError extends Error { this.name = 'TypistIdInvalidError'; } } + +// 削除対象グループがWorkflowにアサインされている事が原因の削除失敗エラー +export class AssignedWorkflowDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssignedWorkflowDeleteFailedError'; + } +} + +// 削除対象グループがチェックアウト権限を持っている事が原因の削除失敗エラー +export class ExistsCheckoutPermissionDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsCheckoutPermissionDeleteFailedError'; + } +} // 同名のタイピストグループが存在する場合のエラー export class TypistGroupNameAlreadyExistError extends Error { constructor(message: string) { super(message); this.name = 'TypistGroupNameAlreadyExistError'; } -} \ 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 a900569..8fbbc25 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 @@ -3,7 +3,10 @@ import { DataSource, In, IsNull, Not } from 'typeorm'; import { UserGroup } from './entity/user_group.entity'; import { UserGroupMember } from './entity/user_group_member.entity'; import { User } from '../users/entity/user.entity'; +import { WorkflowTypist } from '../workflows/entity/workflow_typists.entity'; import { + AssignedWorkflowDeleteFailedError, + ExistsCheckoutPermissionDeleteFailedError, TypistGroupNameAlreadyExistError, TypistGroupNotExistError, TypistIdInvalidError, @@ -16,6 +19,7 @@ import { deleteEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; @Injectable() export class UserGroupsRepositoryService { @@ -285,4 +289,81 @@ export class UserGroupsRepositoryService { return typistGroup; }); } + + /** + * 指定したIDのタイピストグループを削除します + * @param context + * @param accountId + * @param typistGroupId + * @returns typist group + */ + async deleteTypistGroup( + context: Context, + accountId: number, + typistGroupId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userGroupRepo = entityManager.getRepository(UserGroup); + // GroupIdが自アカウント内に存在するか確認する + const typistGroup = await userGroupRepo.findOne({ + relations: { userGroupMembers: true }, + where: { + id: typistGroupId, + account_id: accountId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (!typistGroup) { + throw new TypistGroupNotExistError( + `TypistGroup not exists Error. accountId: ${accountId}; typistGroupId: ${typistGroupId}`, + ); + } + + // ルーティングルールに紐づくタイピストグループは削除できない + const workflowTypistRepo = entityManager.getRepository(WorkflowTypist); + const workflowTypist = await workflowTypistRepo.findOne({ + where: { typist_group_id: typistGroupId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (workflowTypist) { + throw new AssignedWorkflowDeleteFailedError( + `Typist Group is used in routing rule. typistGroupId: ${typistGroupId}`, + ); + } + + // タスクのチェックアウト候補のタイピストグループは削除できない + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + + const checkoutPermission = await checkoutPermissionRepo.findOne({ + where: { user_group_id: typistGroupId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (checkoutPermission) { + throw new ExistsCheckoutPermissionDeleteFailedError( + `Typist Group is used in task checkout permission. typistGroupId: ${typistGroupId}`, + ); + } + + const userGroupMemberRepo = entityManager.getRepository(UserGroupMember); + // 対象のタイピストグループのユーザーを削除する + await deleteEntity( + userGroupMemberRepo, + { user_group_id: typistGroupId }, + this.isCommentOut, + context, + ); + + // 対象のタイピストグループを削除する + await deleteEntity( + userGroupRepo, + { id: typistGroupId }, + this.isCommentOut, + context, + ); + }); + } } From eaf1b3c8b8c9d002beab5c0aa3fbac7c2b080bf5 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 6 Feb 2024 10:26:42 +0000 Subject: [PATCH 018/109] =?UTF-8?q?Merged=20PR=20738:=20develop=E5=8B=95?= =?UTF-8?q?=E4=BD=9C=E7=A2=BA=E8=AA=8D=E4=B8=8D=E5=85=B7=E5=90=88=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3645: develop動作確認不具合対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3645) - 削除完了メールのタイトルを修正しました。 ## レビューポイント - 共有 ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/gateways/sendgrid/sendgrid.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index a467b3b..068ccd4 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -863,7 +863,7 @@ export class SendGridService { `[IN] [${context.getTrackingId()}] ${this.sendMailWithU116.name}`, ); try { - const subject = 'Edit User Notification [U-116]'; + const subject = 'User Deleted Notification [U-116]'; // メールの本文を作成する const html = this.templateU116Html From 548c7a05e9d6f643b514bc071b10dbc1c8ae4708 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Tue, 6 Feb 2024 12:58:55 +0000 Subject: [PATCH 019/109] =?UTF-8?q?Merged=20PR=20729:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E8=A8=AD=E5=AE=9A=E3=83=9D=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E7=94=BB=E9=9D=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3547: 画面実装(ファイル削除設定ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3547) - ファイル削除設定ポップアップの新規実装をしました。 ## レビューポイント - reduxとの連携部分の設計でNGな部分ないか? - 処理の抜け漏れないか? ## 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/Task3547?csf=1&web=1&e=0pu92Q ## 動作確認状況 - ローカルで一通りの動作確認しました。 ## 補足 - とくになし --- dictation_client/src/api/api.ts | 181 ++++++++++-------- .../src/features/account/accountSlice.ts | 34 ++++ .../src/features/account/operations.ts | 53 +++++ .../src/features/account/selectors.ts | 15 ++ .../src/features/account/state.ts | 2 + .../partnerLicense/partnerLicenseSlice.ts | 2 + .../src/features/user/operations.ts | 14 +- .../AccountPage/fileDeleteSettingPopup.tsx | 169 ++++++++++++++++ .../src/pages/AccountPage/index.tsx | 21 +- dictation_client/src/translation/de.json | 24 ++- dictation_client/src/translation/en.json | 24 ++- dictation_client/src/translation/es.json | 24 ++- dictation_client/src/translation/fr.json | 24 ++- 13 files changed, 474 insertions(+), 113 deletions(-) create mode 100644 dictation_client/src/pages/AccountPage/fileDeleteSettingPopup.tsx diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 6ed3907..d510afb 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -78,6 +78,18 @@ export interface Account { * @memberof Account */ 'delegationPermission': boolean; + /** + * + * @type {boolean} + * @memberof Account + */ + 'autoFileDelete': boolean; + /** + * + * @type {number} + * @memberof Account + */ + 'fileRetentionDays': number; /** * * @type {number} @@ -2077,6 +2089,25 @@ export interface UpdateAccountInfoRequest { */ 'secondryAdminUserId'?: number; } +/** + * + * @export + * @interface UpdateFileDeleteSettingRequest + */ +export interface UpdateFileDeleteSettingRequest { + /** + * 自動ファイル削除をするかどうか + * @type {boolean} + * @memberof UpdateFileDeleteSettingRequest + */ + 'autoFileDelete': boolean; + /** + * 文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間 + * @type {number} + * @memberof UpdateFileDeleteSettingRequest + */ + 'retentionDays': number; +} /** * * @export @@ -3328,6 +3359,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting: async (updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateFileDeleteSettingRequest' is not null or undefined + assertParamExists('updateFileDeleteSetting', 'updateFileDeleteSettingRequest', updateFileDeleteSettingRequest) + const localVarPath = `/accounts/me/file-delete-setting`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateFileDeleteSettingRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3790,6 +3861,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.updateAccountInfo']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateFileDeleteSetting(updateFileDeleteSettingRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateFileDeleteSetting']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -4087,6 +4171,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: any): AxiosPromise { return localVarFp.updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: any): AxiosPromise { + return localVarFp.updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4425,6 +4519,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -7309,46 +7415,6 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(postUpdateUserRequest, localVarRequestOptions, configuration) - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * ユーザーを削除します - * @summary - * @param {PostDeleteUserRequest} postDeleteUserRequest - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - updeateUser: async (postDeleteUserRequest: PostDeleteUserRequest, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'postDeleteUserRequest' is not null or undefined - assertParamExists('updeateUser', 'postDeleteUserRequest', postDeleteUserRequest) - const localVarPath = `/users/delete`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(postDeleteUserRequest, localVarRequestOptions, configuration) - return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -7529,19 +7595,6 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.updateUser']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, - /** - * ユーザーを削除します - * @summary - * @param {PostDeleteUserRequest} postDeleteUserRequest - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async updeateUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.updeateUser(postDeleteUserRequest, options); - const index = configuration?.serverIndex ?? 0; - const operationBasePath = operationServerMap['UsersApi.updeateUser']?.[index]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); - }, } }; @@ -7678,16 +7731,6 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: any): AxiosPromise { return localVarFp.updateUser(postUpdateUserRequest, options).then((request) => request(axios, basePath)); }, - /** - * ユーザーを削除します - * @summary - * @param {PostDeleteUserRequest} postDeleteUserRequest - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - updeateUser(postDeleteUserRequest: PostDeleteUserRequest, options?: any): AxiosPromise { - return localVarFp.updeateUser(postDeleteUserRequest, options).then((request) => request(axios, basePath)); - }, }; }; @@ -7849,18 +7892,6 @@ export class UsersApi extends BaseAPI { public updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig) { return UsersApiFp(this.configuration).updateUser(postUpdateUserRequest, options).then((request) => request(this.axios, this.basePath)); } - - /** - * ユーザーを削除します - * @summary - * @param {PostDeleteUserRequest} postDeleteUserRequest - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof UsersApi - */ - public updeateUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig) { - return UsersApiFp(this.configuration).updeateUser(postDeleteUserRequest, options).then((request) => request(this.axios, this.basePath)); - } } diff --git a/dictation_client/src/features/account/accountSlice.ts b/dictation_client/src/features/account/accountSlice.ts index 48be409..ff4cd33 100644 --- a/dictation_client/src/features/account/accountSlice.ts +++ b/dictation_client/src/features/account/accountSlice.ts @@ -4,6 +4,7 @@ import { updateAccountInfoAsync, getAccountRelationsAsync, deleteAccountAsync, + updateFileDeleteSettingAsync, } from "./operations"; const initialState: AccountState = { @@ -15,6 +16,8 @@ const initialState: AccountState = { tier: 0, country: "", delegationPermission: false, + autoFileDelete: false, + fileRetentionDays: 0, }, }, dealers: [], @@ -29,6 +32,8 @@ const initialState: AccountState = { secondryAdminUserId: undefined, }, isLoading: false, + autoFileDelete: false, + fileRetentionDays: 0, }, }; @@ -64,6 +69,20 @@ export const accountSlice = createSlice({ const { secondryAdminUserId } = action.payload; state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId; }, + changeAutoFileDelete: ( + state, + action: PayloadAction<{ autoFileDelete: boolean }> + ) => { + const { autoFileDelete } = action.payload; + state.apps.autoFileDelete = autoFileDelete; + }, + changeFileRetentionDays: ( + state, + action: PayloadAction<{ fileRetentionDays: number }> + ) => { + const { fileRetentionDays } = action.payload; + state.apps.fileRetentionDays = fileRetentionDays; + }, cleanupApps: (state) => { state.domain = initialState.domain; }, @@ -85,6 +104,10 @@ export const accountSlice = createSlice({ action.payload.accountInfo.account.primaryAdminUserId; state.apps.updateAccountInfo.secondryAdminUserId = action.payload.accountInfo.account.secondryAdminUserId; + state.apps.autoFileDelete = + action.payload.accountInfo.account.autoFileDelete; + state.apps.fileRetentionDays = + action.payload.accountInfo.account.fileRetentionDays; state.apps.isLoading = false; }); builder.addCase(getAccountRelationsAsync.rejected, (state) => { @@ -99,6 +122,15 @@ export const accountSlice = createSlice({ builder.addCase(updateAccountInfoAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(updateFileDeleteSettingAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateFileDeleteSettingAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateFileDeleteSettingAsync.rejected, (state) => { + state.apps.isLoading = false; + }); builder.addCase(deleteAccountAsync.pending, (state) => { state.apps.isLoading = true; }); @@ -115,6 +147,8 @@ export const { changeDealerPermission, changePrimaryAdministrator, changeSecondryAdministrator, + changeAutoFileDelete, + changeFileRetentionDays, cleanupApps, } = accountSlice.actions; export default accountSlice.reducer; diff --git a/dictation_client/src/features/account/operations.ts b/dictation_client/src/features/account/operations.ts index bbe1f09..c7ccaad 100644 --- a/dictation_client/src/features/account/operations.ts +++ b/dictation_client/src/features/account/operations.ts @@ -9,6 +9,7 @@ import { UpdateAccountInfoRequest, UsersApi, DeleteAccountRequest, + UpdateFileDeleteSettingRequest, } from "../../api/api"; import { Configuration } from "../../api/configuration"; import { ViewAccountRelationsInfo } from "./types"; @@ -112,6 +113,58 @@ export const updateAccountInfoAsync = createAsyncThunk< } }); +export const updateFileDeleteSettingAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { autoFileDelete: boolean; fileRetentionDays: number }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/updateFileDeleteSettingAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + const requestParam: UpdateFileDeleteSettingRequest = { + autoFileDelete: args.autoFileDelete, + retentionDays: args.fileRetentionDays, + }; + + try { + await accountApi.updateFileDeleteSetting(requestParam, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + + const errorMessage = getTranslationID("common.message.internalServerError"); + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + export const deleteAccountAsync = createAsyncThunk< { /* Empty Object */ diff --git a/dictation_client/src/features/account/selectors.ts b/dictation_client/src/features/account/selectors.ts index e99fdc1..c48a569 100644 --- a/dictation_client/src/features/account/selectors.ts +++ b/dictation_client/src/features/account/selectors.ts @@ -16,3 +16,18 @@ export const selectIsLoading = (state: RootState) => state.account.apps.isLoading; export const selectUpdateAccountInfo = (state: RootState) => state.account.apps.updateAccountInfo; +export const selectFileDeleteSetting = (state: RootState) => { + const { autoFileDelete, fileRetentionDays } = state.account.apps; + return { + autoFileDelete, + fileRetentionDays, + }; +}; +export const selectInputValidationErrors = (state: RootState) => { + const { fileRetentionDays } = state.account.apps; + const hasFileRetentionDaysError = + fileRetentionDays <= 0 || fileRetentionDays >= 1000; + return { + hasFileRetentionDaysError, + }; +}; diff --git a/dictation_client/src/features/account/state.ts b/dictation_client/src/features/account/state.ts index 6d3cbe7..0876512 100644 --- a/dictation_client/src/features/account/state.ts +++ b/dictation_client/src/features/account/state.ts @@ -19,4 +19,6 @@ export interface Domain { export interface Apps { updateAccountInfo: UpdateAccountInfoRequest; isLoading: boolean; + autoFileDelete: boolean; + fileRetentionDays: number; } diff --git a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts index 8fd549a..1f79b09 100644 --- a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts +++ b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts @@ -12,6 +12,8 @@ const initialState: PartnerLicensesState = { tier: 0, country: "", delegationPermission: false, + autoFileDelete: false, + fileRetentionDays: 0, }, total: 0, ownPartnerLicense: { diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index f9d2578..f9e2663 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -448,43 +448,43 @@ export const deleteUserAsync = createAsyncThunk< // ユーザーに有効なライセンスが割り当たっているため削除不可 if (error.code === "E014007") { errorMessage = getTranslationID( - "userListPage.message.UserDeletionLicenseActiveError" + "userListPage.message.userDeletionLicenseActiveError" ); } // 管理者ユーザーため削除不可 if (error.code === "E014002") { errorMessage = getTranslationID( - "userListPage.message.AdminUserDeletionError" + "userListPage.message.adminUserDeletionError" ); } // タイピストユーザーで担当タスクがあるため削除不可 if (error.code === "E014009") { errorMessage = getTranslationID( - "userListPage.message.TypistUserDeletionTranscriptionTaskError" + "userListPage.message.typistUserDeletionTranscriptionTaskError" ); } // タイピストユーザーでルーティングルールに設定されているため削除不可 if (error.code === "E014004") { errorMessage = getTranslationID( - "userListPage.message.TypistDeletionRoutingRuleError" + "userListPage.message.typistDeletionRoutingRuleError" ); } // タイピストユーザーでTranscriptionistGroupに所属しているため削除不可 if (error.code === "E014005") { errorMessage = getTranslationID( - "userListPage.message.TypistUserDeletionTranscriptionistGroupError" + "userListPage.message.typistUserDeletionTranscriptionistGroupError" ); } // Authorユーザーで同一AuthorIDのタスクがあるため削除不可 if (error.code === "E014006") { errorMessage = getTranslationID( - "userListPage.message.AuthorUserDeletionTranscriptionTaskError" + "userListPage.message.authorUserDeletionTranscriptionTaskError" ); } // Authorユーザーで同一AuthorIDがルーティングルールに設定されているため削除不可 if (error.code === "E014003") { errorMessage = getTranslationID( - "userListPage.message.AuthorDeletionRoutingRuleError" + "userListPage.message.authorDeletionRoutingRuleError" ); } diff --git a/dictation_client/src/pages/AccountPage/fileDeleteSettingPopup.tsx b/dictation_client/src/pages/AccountPage/fileDeleteSettingPopup.tsx new file mode 100644 index 0000000..1153daf --- /dev/null +++ b/dictation_client/src/pages/AccountPage/fileDeleteSettingPopup.tsx @@ -0,0 +1,169 @@ +import React, { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectInputValidationErrors, + selectFileDeleteSetting, + updateFileDeleteSettingAsync, + selectIsLoading, + getAccountRelationsAsync, +} from "features/account"; +import { AppDispatch } from "app/store"; +import { useTranslation } from "react-i18next"; +import styles from "../../styles/app.module.scss"; +import { getTranslationID } from "../../translation"; +import close from "../../assets/images/close.svg"; +import { + changeAutoFileDelete, + changeFileRetentionDays, +} from "../../features/account/accountSlice"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +interface FileDeleteSettingPopupProps { + // eslint-disable-next-line react/no-unused-prop-types + onClose: () => void; +} + +export const FileDeleteSettingPopup: React.FC = ( + props +) => { + const { onClose } = props; + + const dispatch: AppDispatch = useDispatch(); + const [t] = useTranslation(); + + const isLoading = useSelector(selectIsLoading); + const fileDeleteSetting = useSelector(selectFileDeleteSetting); + const { hasFileRetentionDaysError } = useSelector( + selectInputValidationErrors + ); + + const closePopup = useCallback(() => { + if (isLoading) return; + onClose(); + }, [isLoading, onClose]); + + const [isPushSubmitButton, setIsPushSubmitButton] = useState(false); + + const onUpdateFileDeleteSetting = useCallback(async () => { + if (isLoading) return; + setIsPushSubmitButton(true); + if (hasFileRetentionDaysError) { + return; + } + + const { meta } = await dispatch( + updateFileDeleteSettingAsync({ + autoFileDelete: fileDeleteSetting.autoFileDelete, + fileRetentionDays: fileDeleteSetting.fileRetentionDays, + }) + ); + setIsPushSubmitButton(false); + if (meta.requestStatus === "fulfilled") { + closePopup(); + dispatch(getAccountRelationsAsync()); + } + }, [ + closePopup, + dispatch, + fileDeleteSetting.autoFileDelete, + fileDeleteSetting.fileRetentionDays, + hasFileRetentionDaysError, + isLoading, + ]); + + return ( +
    +
    +

    + {t(getTranslationID("fileDeleteSettingPopup.label.title"))} + +

    +
    +
    +
    +
    + {t( + getTranslationID( + "fileDeleteSettingPopup.label.autoFileDeleteCheck" + ) + )} +
    +
    +

    + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +

    +

    + {t( + getTranslationID( + "fileDeleteSettingPopup.label.daysAnnotation" + ) + )} +

    + { + dispatch( + changeFileRetentionDays({ + fileRetentionDays: Number(e.target.value), + }) + ); + }} + />{" "} + {t(getTranslationID("fileDeleteSettingPopup.label.days"))} + {isPushSubmitButton && hasFileRetentionDaysError && ( + + {t( + getTranslationID( + "fileDeleteSettingPopup.label.daysValidationError" + ) + )} + + )} +
    +
    + +
    + Loading +
    +
    +
    +
    + ); +}; diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx index 8e3a149..1714b6c 100644 --- a/dictation_client/src/pages/AccountPage/index.tsx +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -23,6 +23,7 @@ import { getTranslationID } from "translation"; import { TIERS } from "components/auth/constants"; import { isApproveTier } from "features/auth"; import { DeleteAccountPopup } from "./deleteAccountPopup"; +import { FileDeleteSettingPopup } from "./fileDeleteSettingPopup"; import progress_activit from "../../assets/images/progress_activit.svg"; const AccountPage: React.FC = (): JSX.Element => { @@ -40,10 +41,17 @@ const AccountPage: React.FC = (): JSX.Element => { const [isDeleteAccountPopupOpen, setIsDeleteAccountPopupOpen] = useState(false); + const [isFileDeleteSettingPopupOpen, setIsFileDeleteSettingPopupOpen] = + useState(false); + const onDeleteAccountOpen = useCallback(() => { setIsDeleteAccountPopupOpen(true); }, [setIsDeleteAccountPopupOpen]); + const onDeleteFileDeleteSettingOpen = useCallback(() => { + setIsFileDeleteSettingPopupOpen(true); + }, [setIsFileDeleteSettingPopupOpen]); + // 階層表示用 const tierNames: { [key: number]: string } = { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -89,6 +97,13 @@ const AccountPage: React.FC = (): JSX.Element => { }} /> )} + {isFileDeleteSettingPopupOpen && ( + { + setIsFileDeleteSettingPopupOpen(false); + }} + /> + )}
    @@ -102,12 +117,13 @@ const AccountPage: React.FC = (): JSX.Element => {
    - {/* File Delete Setting は現状不要のため非表示
    • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} {
    - */}
    diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index bff0bba..ad4d910 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -129,13 +129,13 @@ "roleChangeError": "Die Benutzerrolle kann nicht geändert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", "encryptionPasswordCorrectError": "Das Verschlüsselungskennwort entspricht nicht den Regeln.", "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", - "UserDeletionLicenseActiveError": "(de)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "TypistDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "AdminUserDeletionError": "(de)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "TypistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "AuthorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "TypistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "AuthorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "userDeletionLicenseActiveError": "(de)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "typistDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "adminUserDeletionError": "(de)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "typistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "authorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "typistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Benutzer", @@ -570,5 +570,15 @@ "job": "Aufgabe", "close": "Schließen" } + }, + "fileDeleteSettingPopup": { + "label": { + "title": "(de)Auto File Delete Setting", + "autoFileDeleteCheck": "(de)Auto file delete", + "daysAnnotation": "(de)Number of days from transcription finished to delete the files.", + "days": "(de)Days", + "saveButton": "(de)Save Settings", + "daysValidationError": "(de)Daysには1~999の数字を入力してください。" + } } } diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index e0f1800..2fe0ccf 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -129,13 +129,13 @@ "roleChangeError": "Unable to change the User Role. The displayed information may be outdated, so please refresh the screen to see the latest status.", "encryptionPasswordCorrectError": "Encryption password does not meet the rules.", "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status.", - "UserDeletionLicenseActiveError": "ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "TypistDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "AdminUserDeletionError": "ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "TypistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "AuthorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "TypistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "AuthorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "userDeletionLicenseActiveError": "ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "typistDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "adminUserDeletionError": "ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "typistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "authorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "typistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "User", @@ -570,5 +570,15 @@ "job": "Job", "close": "Close" } + }, + "fileDeleteSettingPopup": { + "label": { + "title": "Auto File Delete Setting", + "autoFileDeleteCheck": "Auto file delete", + "daysAnnotation": "Number of days from transcription finished to delete the files.", + "days": "Days", + "saveButton": "Save Settings", + "daysValidationError": "Daysには1~999の数字を入力してください。" + } } } diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 564958a..e317bb7 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -129,13 +129,13 @@ "roleChangeError": "No se puede cambiar la función de usuario. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", "encryptionPasswordCorrectError": "La contraseña de cifrado no cumple con las reglas.", "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", - "UserDeletionLicenseActiveError": "(es)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "TypistDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "AdminUserDeletionError": "(es)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "TypistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "AuthorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "TypistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "AuthorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "userDeletionLicenseActiveError": "(es)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "typistDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "adminUserDeletionError": "(es)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "typistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "authorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "typistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Usuario", @@ -570,5 +570,15 @@ "job": "Trabajo", "close": "Cerrar" } + }, + "fileDeleteSettingPopup": { + "label": { + "title": "(es)Auto File Delete Setting", + "autoFileDeleteCheck": "(es)Auto file delete", + "daysAnnotation": "(es)Number of days from transcription finished to delete the files.", + "days": "(es)Days", + "saveButton": "(es)Save Settings", + "daysValidationError": "(es)Daysには1~999の数字を入力してください。" + } } } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 9cbdcd2..7371245 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -129,13 +129,13 @@ "roleChangeError": "Impossible de modifier le rôle de l'utilisateur. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", "encryptionPasswordCorrectError": "Le mot de passe de cryptage n'est pas conforme aux règles.", "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", - "UserDeletionLicenseActiveError": "(fr)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "TypistDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "AdminUserDeletionError": "(fr)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "TypistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "AuthorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "TypistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "AuthorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "userDeletionLicenseActiveError": "(fr)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "typistDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "adminUserDeletionError": "(fr)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "typistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "authorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "typistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Utilisateur", @@ -570,5 +570,15 @@ "job": "Tâches", "close": "Fermer" } + }, + "fileDeleteSettingPopup": { + "label": { + "title": "(fr)Auto File Delete Setting", + "autoFileDeleteCheck": "(fr)Auto file delete", + "daysAnnotation": "(fr)Number of days from transcription finished to delete the files.", + "days": "(fr)Days", + "saveButton": "(fr)Save Settings", + "daysValidationError": "(fr)Daysには1~999の数字を入力してください。" + } } } From 91c27b768453041c03e506baa6f0ea2a3c372848 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Tue, 6 Feb 2024 23:53:00 +0000 Subject: [PATCH 020/109] =?UTF-8?q?Merged=20PR=20736:=20=E3=83=86=E3=83=B3?= =?UTF-8?q?=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E5=89=8A=E9=99=A4API=20IF=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3598: テンプレートファイル削除API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3598) - 掲題のとおり、IF実装しました。 ## レビューポイント - 認可処理に認識違いないか? - パラメータに不足ないか? ## 動作確認状況 - ローカルでAPI実行できることを確認 ## 補足 - とくになし --- dictation_server/src/api/odms/openapi.json | 54 ++++++++++++ .../templates/templates.controller.ts | 83 ++++++++++++++++++- .../src/features/templates/types/types.ts | 13 +++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index ce07f3b..eb999e6 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -3368,6 +3368,59 @@ "security": [{ "bearer": [] }] } }, + "/templates/{templateFileId}/delete": { + "post": { + "operationId": "deleteTemplateFile", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します", + "parameters": [ + { + "name": "templateFileId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTemplateResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / 未完了タスクに紐づいている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["templates"], + "security": [{ "bearer": [] }] + } + }, "/workflows": { "get": { "operationId": "getWorkflows", @@ -4918,6 +4971,7 @@ }, "required": ["templates"] }, + "DeleteTemplateResponse": { "type": "object", "properties": {} }, "WorkflowWorktype": { "type": "object", "properties": { diff --git a/dictation_server/src/features/templates/templates.controller.ts b/dictation_server/src/features/templates/templates.controller.ts index d07db64..f4dce06 100644 --- a/dictation_server/src/features/templates/templates.controller.ts +++ b/dictation_server/src/features/templates/templates.controller.ts @@ -4,6 +4,8 @@ import { HttpException, HttpStatus, Logger, + Param, + Post, Req, UseGuards, } from '@nestjs/common'; @@ -16,7 +18,7 @@ import { import jwt from 'jsonwebtoken'; import { AccessToken } from '../../common/token'; import { ErrorResponse } from '../../common/error/types/types'; -import { GetTemplatesResponse } from './types/types'; +import { DeleteTemplateRequestParam, DeleteTemplateResponse, GetTemplatesResponse } from './types/types'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES } from '../../constants'; @@ -97,4 +99,83 @@ export class TemplatesController { return { templates }; } + + @ApiResponse({ + status: HttpStatus.OK, + type: DeleteTemplateResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + 'ルーティングルールに設定されている / 未完了タスクに紐づいている / 削除済み', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'deleteTemplateFile', + description: + 'ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post(':templateFileId/delete') + async deleteTypistGroup( + @Req() req: Request, + @Param() param: DeleteTemplateRequestParam, + ): Promise { + const { templateFileId } = param; + + // アクセストークン取得 + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: service層呼び出し + + return {}; + } } diff --git a/dictation_server/src/features/templates/types/types.ts b/dictation_server/src/features/templates/types/types.ts index 243298d..a3ca106 100644 --- a/dictation_server/src/features/templates/types/types.ts +++ b/dictation_server/src/features/templates/types/types.ts @@ -1,4 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt, Min } from 'class-validator'; export class TemplateFile { @ApiProperty({ description: 'テンプレートファイルのID' }) @@ -14,3 +16,14 @@ export class GetTemplatesResponse { }) templates: TemplateFile[]; } + + +export class DeleteTemplateRequestParam { + @ApiProperty() + @Type(() => Number) + @IsInt() + @Min(1) + templateFileId: number; +} + +export class DeleteTemplateResponse {} From 32d8c6b8962bbe3255f0ca9a031b650ef447483b Mon Sep 17 00:00:00 2001 From: masaaki Date: Wed, 7 Feb 2024 02:47:41 +0000 Subject: [PATCH 021/109] =?UTF-8?q?Merged=20PR=20737:=20=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E5=89=8A=E9=99=A4=E8=A8=AD=E5=AE=9AAPI?= =?UTF-8?q?=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3546: ファイル削除設定API作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3546) - プロダクト バックログ項目 1199: アカウント設定を変更したい(ファイル削除設定) - ファイル削除設定API作成を実装しました - 変数定義時に型指定が不要で、自動的に明示的な型宣言が削除されています。非テストコードについてはeslint-disable-lineによって無視設定を行っています。テストコードについては削除したままとしています。 ## レビューポイント - 特筆するものはありません ## UIの変更 - 無し ## 動作確認状況 - ローカル及びユニットテストで確認 ## 補足 - 無し --- .../features/accounts/accounts.controller.ts | 7 +- .../accounts/accounts.service.spec.ts | 158 ++++++++++++++++-- .../src/features/accounts/accounts.service.ts | 47 ++++++ .../accounts/accounts.repository.service.ts | 47 +++++- 4 files changed, 240 insertions(+), 19 deletions(-) diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 8e4c87f..1dacf58 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2033,7 +2033,12 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO:Service層呼び出し + await this.accountService.updateFileDeleteSetting( + context, + userId, + autoFileDelete, + retentionDays, + ); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index f6f3096..5334092 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -143,7 +143,7 @@ describe('createAccount', () => { }, }); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -6080,12 +6080,7 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - overrideAdB2cService(service, { - getUser: async () => { - return { id: 'admin.external_id', displayName: 'admin' }; - }, - }); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -6806,15 +6801,7 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - // ADB2Cユーザーの削除成功 - overrideAdB2cService(service, { - deleteUsers: jest.fn(), - getUsers: jest.fn(), - getUser: async () => { - return { id: 'admin.external_id', displayName: 'admin' }; - }, - }); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -7460,3 +7447,142 @@ describe('getCompanyName', () => { } }); }); + +describe('updateFileDeleteSetting', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('自動削除の設定を更新できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account?.tier).toBe(5); + } + + // 更新するデータを設定 + const autoFileDelete = true; + const retentionDays = 100; + + await service.updateFileDeleteSetting( + context, + admin.external_id, + autoFileDelete, + retentionDays, + ); + + // 更新後データの確認 + { + const updatedAccount = await getAccount(source, account.id); + expect(updatedAccount?.auto_file_delete).toBe(autoFileDelete); + expect(updatedAccount?.file_retention_days).toBe(retentionDays); + } + }); + + it('対象アカウント非存在時に500エラーを返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 更新するデータを設定 + const nonExistentId = `nonExistentId`; // 存在しないIDを指定 + const autoFileDelete = true; + const retentionDays = 100; + + const context = makeContext(nonExistentId, 'requestId'); + try { + await service.updateFileDeleteSetting( + context, + nonExistentId, + autoFileDelete, + retentionDays, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account?.tier).toBe(5); + } + + //DBアクセスに失敗するようにする + const usersRepositoryService = module.get( + UsersRepositoryService, + ); + usersRepositoryService.findUserByExternalId = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.updateFileDeleteSetting( + context, + admin.external_id, + true, + 100, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index d200861..bad5d49 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -2534,4 +2534,51 @@ export class AccountsService { adminEmails: adminEmails, }; } + + /** + * 自動ファイル削除に関する設定を更新する + * @param context + * @param externalId + * @param isAutoFileDelete // ファイルの自動削除可否 + * @param retentionDays // ファイルの保持期間 + */ + async updateFileDeleteSetting( + context: Context, + externalId: string, + isAutoFileDelete: boolean, + retentionDays: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updateFileDeleteSetting.name + } | params: { ` + + `externalId: ${externalId}, ` + + `autoFileDelete: ${isAutoFileDelete}, ` + + `retentionDays: ${retentionDays}, };`, + ); + + // アカウントテーブルの更新を行う + try { + // externalIdを基に自アカウントの情報を取得する + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(context, externalId); + + await this.accountRepository.updateFileDeleteSetting( + context, + accountId, + isAutoFileDelete, + retentionDays, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // アカウントが存在しない場合のエラーもINTERNAL_SERVER_ERROR扱いのため個別の判定は行わない + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log(`[OUT] [${context.getTrackingId()}]`); + } + } } diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 8252ec1..ee4fc2d 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -662,8 +662,8 @@ export class AccountsRepositoryService { ); // 第五の不足数を算出するためのライセンス数情報を取得する - let expiringSoonLicense: number = 0; - let allocatableLicenseWithMargin: number = 0; + let expiringSoonLicense: number = 0; // eslint-disable-line + let allocatableLicenseWithMargin: number = 0; // eslint-disable-line if (childAccount.tier === TIERS.TIER5) { expiringSoonLicense = await this.getExpiringSoonLicense( context, @@ -1334,4 +1334,47 @@ export class AccountsRepositoryService { return users; }); } + + /* + * 自動ファイル削除に関する項目を更新する + * @param accountId + * @param isAutoFileDelete + * @param retentionDays + */ + async updateFileDeleteSetting( + context: Context, + accountId: number, + isAutoFileDelete: boolean, + retentionDays: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + + // アカウントが存在するかチェック + const account = await accountRepo.findOne({ + where: { id: accountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // アカウントが存在しない場合はエラー + if (!account) { + throw new AccountNotFoundError( + `Account is not found. id: ${accountId}`, + ); + } + + // 自動ファイル削除設定の更新を行う + await updateEntity( + accountRepo, + { id: accountId }, + { + auto_file_delete: isAutoFileDelete, + file_retention_days: retentionDays, + }, + this.isCommentOut, + context, + ); + }); + } } From 270122b135515d8a7d82103461fb6d9238ce18d2 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 9 Feb 2024 01:05:15 +0000 Subject: [PATCH 022/109] =?UTF-8?q?Merged=20PR=20741:=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E6=83=85=E5=A0=B1=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=EF=BC=88=E4=BF=9D=E5=AD=98=E6=97=A5=E6=95=B0?= =?UTF-8?q?=E3=81=AE=E8=A1=A8=E7=A4=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3691: アカウント情報画面修正(保存日数の表示)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3691) - アカウント情報画面にファイルの保存日数を表示するよう修正しました。 - ファイル削除が設定されている場合は日数、されていない場合はハイフン表示となるようにしています。 ## レビューポイント - 画面イメージは想定通りでしょうか? - 表示する保存日数は想定通りの値でしょうか? ## UIの変更 - [Task3691](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/Task3691?csf=1&web=1&e=Gthaa8) ## 動作確認状況 - ローカルで確認 --- dictation_client/src/pages/AccountPage/index.tsx | 14 ++++++++++++++ dictation_client/src/translation/de.json | 5 +++-- dictation_client/src/translation/en.json | 5 +++-- dictation_client/src/translation/es.json | 5 +++-- dictation_client/src/translation/fr.json | 5 +++-- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx index 1714b6c..bb3cf00 100644 --- a/dictation_client/src/pages/AccountPage/index.tsx +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -231,9 +231,23 @@ const AccountPage: React.FC = (): JSX.Element => { ) )} +
    )} {!isTier5 &&
    -
    } +
    + {t( + getTranslationID("accountPage.label.fileRetentionDays") + )} +
    +
    + {viewInfo.account.autoFileDelete + ? viewInfo.account.fileRetentionDays + : "-"} +
    diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index ad4d910..17289e4 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -515,7 +515,8 @@ "emailAddress": "E-Mail-Addresse", "selectSecondaryAdministrator": "Sekundäradministrator auswählen", "saveChanges": "Änderungen speichern", - "deleteAccount": "Konto löschen" + "deleteAccount": "Konto löschen", + "fileRetentionDays": "(de)自動ファイル削除までの保持日数" }, "message": { "updateAccountFailedError": "Kontoinformationen konnten nicht gespeichert werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut." @@ -581,4 +582,4 @@ "daysValidationError": "(de)Daysには1~999の数字を入力してください。" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 2fe0ccf..cdd73e7 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -515,7 +515,8 @@ "emailAddress": "Email Address", "selectSecondaryAdministrator": "Select Secondary Administrator", "saveChanges": "Save Changes", - "deleteAccount": "Delete Account" + "deleteAccount": "Delete Account", + "fileRetentionDays": "自動ファイル削除までの保持日数" }, "message": { "updateAccountFailedError": "Failed to save account information. Please refresh the screen and try again." @@ -581,4 +582,4 @@ "daysValidationError": "Daysには1~999の数字を入力してください。" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index e317bb7..2256420 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -515,7 +515,8 @@ "emailAddress": "Dirección de correo electrónico", "selectSecondaryAdministrator": "Seleccionar administrador secundario", "saveChanges": "Guardar cambios", - "deleteAccount": "Borrar cuenta" + "deleteAccount": "Borrar cuenta", + "fileRetentionDays": "(es)自動ファイル削除までの保持日数" }, "message": { "updateAccountFailedError": "No se pudo guardar la información de la cuenta. Actualice la pantalla e inténtelo de nuevo." @@ -581,4 +582,4 @@ "daysValidationError": "(es)Daysには1~999の数字を入力してください。" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 7371245..403e693 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -515,7 +515,8 @@ "emailAddress": "Adresse e-mail", "selectSecondaryAdministrator": "Sélectionner le administrateur secondaire", "saveChanges": "Sauvegarder les modifications", - "deleteAccount": "Supprimer le compte" + "deleteAccount": "Supprimer le compte", + "fileRetentionDays": "(fr)自動ファイル削除までの保持日数" }, "message": { "updateAccountFailedError": "Échec de l'enregistrement des informations du compte. Veuillez actualiser l'écran et réessayer." @@ -581,4 +582,4 @@ "daysValidationError": "(fr)Daysには1~999の数字を入力してください。" } } -} +} \ No newline at end of file From 83efd97bdf100af5c6ab63bcc5bdc93918d773f8 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 13 Feb 2024 00:22:38 +0000 Subject: [PATCH 023/109] =?UTF-8?q?Merged=20PR=20739:=20=E3=83=86=E3=83=B3?= =?UTF-8?q?=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E5=89=8A=E9=99=A4API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3599: テンプレートファイル削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3599) - テンプレートファイル削除APIとテストを実装しました。 ## レビューポイント - テンプレートファイル削除できないエラーの条件は適切でしょうか? - テストケースは適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../059-add_tasks_template_index.sql | 5 + dictation_server/src/common/error/code.ts | 3 + dictation_server/src/common/error/message.ts | 4 + .../src/features/tasks/test/utility.ts | 10 + .../templates/templates.controller.ts | 10 +- .../features/templates/templates.module.ts | 7 +- .../templates/templates.service.spec.ts | 369 +++++++++++++++++- .../features/templates/templates.service.ts | 107 +++++ .../src/features/templates/test/utility.ts | 16 + .../template_files/errors/types.ts | 14 + .../template_files.repository.service.ts | 134 ++++++- 11 files changed, 671 insertions(+), 8 deletions(-) create mode 100644 dictation_server/db/migrations/059-add_tasks_template_index.sql diff --git a/dictation_server/db/migrations/059-add_tasks_template_index.sql b/dictation_server/db/migrations/059-add_tasks_template_index.sql new file mode 100644 index 0000000..17a871c --- /dev/null +++ b/dictation_server/db/migrations/059-add_tasks_template_index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `tasks` ADD INDEX `idx_tasks_template_file_id` (template_file_id); + +-- +migrate Down +ALTER TABLE `tasks` DROP INDEX `idx_tasks_template_file_id`; \ No newline at end of file diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index b6ad2af..a789ab7 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -79,4 +79,7 @@ export const ErrorCodes = [ 'E015001', // タイピストグループ削除エラー(削除しようとしたタイピストグループがすでに削除済みだった) 'E015002', // タイピストグループ削除エラー(削除しようとしたタイピストグループがWorkflowのTypist候補として指定されていた) 'E015003', // タイピストグループ削除エラー(削除しようとしたタイピストグループがチェックアウト可能なタスクが存在した) + 'E016001', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) + 'E016002', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) + 'E016003', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 20a176f..793cdf5 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -68,4 +68,8 @@ export const errors: Errors = { E015001: 'Typist Group delete failed Error: already deleted', E015002: 'Typist Group delete failed Error: workflow assigned', E015003: 'Typist Group delete failed Error: checkout permission existed', + E016001: 'Template file delete failed Error: already deleted', + E016002: 'Template file delete failed Error: workflow assigned', + E016003: + 'Template file delete failed Error: not finished task has template file', }; diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 1d8efc9..d9e6d8f 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -273,6 +273,16 @@ export const getTask = async ( return task; }; +export const getTasks = async ( + datasource: DataSource, + account_id: number, +): Promise => { + const tasks = await datasource.getRepository(Task).find({ + where: { account_id: account_id }, + }); + return tasks; +}; + export const getCheckoutPermissions = async ( datasource: DataSource, task_id: number, diff --git a/dictation_server/src/features/templates/templates.controller.ts b/dictation_server/src/features/templates/templates.controller.ts index f4dce06..e98bcdf 100644 --- a/dictation_server/src/features/templates/templates.controller.ts +++ b/dictation_server/src/features/templates/templates.controller.ts @@ -18,7 +18,11 @@ import { import jwt from 'jsonwebtoken'; import { AccessToken } from '../../common/token'; import { ErrorResponse } from '../../common/error/types/types'; -import { DeleteTemplateRequestParam, DeleteTemplateResponse, GetTemplatesResponse } from './types/types'; +import { + DeleteTemplateRequestParam, + DeleteTemplateResponse, + GetTemplatesResponse, +} from './types/types'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES } from '../../constants'; @@ -132,7 +136,7 @@ export class TemplatesController { RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), ) @Post(':templateFileId/delete') - async deleteTypistGroup( + async deleteTemplateFile( @Req() req: Request, @Param() param: DeleteTemplateRequestParam, ): Promise { @@ -174,7 +178,7 @@ export class TemplatesController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: service層呼び出し + await this.templatesService.deleteTemplate(context, userId, templateFileId); return {}; } diff --git a/dictation_server/src/features/templates/templates.module.ts b/dictation_server/src/features/templates/templates.module.ts index ac14580..fb93de7 100644 --- a/dictation_server/src/features/templates/templates.module.ts +++ b/dictation_server/src/features/templates/templates.module.ts @@ -3,9 +3,14 @@ import { TemplatesController } from './templates.controller'; import { TemplatesService } from './templates.service'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { TemplateFilesRepositoryModule } from '../../repositories/template_files/template_files.repository.module'; +import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; @Module({ - imports: [UsersRepositoryModule, TemplateFilesRepositoryModule], + imports: [ + UsersRepositoryModule, + TemplateFilesRepositoryModule, + BlobstorageModule, + ], providers: [TemplatesService], controllers: [TemplatesController], }) diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index 8ef101f..46dadf7 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -1,13 +1,26 @@ import { DataSource } from 'typeorm'; import { makeTestingModule } from '../../common/test/modules'; import { TemplatesService } from './templates.service'; -import { createTemplateFile } from './test/utility'; -import { makeTestAccount } from '../../common/test/utility'; +import { + createTemplateFile, + getTemplateFiles, + updateTaskTemplateFile, +} from './test/utility'; +import { makeTestAccount, makeTestUser } from '../../common/test/utility'; import { makeContext } from '../../common/log'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; +import { overrideBlobstorageService } from '../../common/test/overrides'; +import { TASK_STATUS, USER_ROLES } from '../../constants'; +import { createTask, getTasks } from '../tasks/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflow, +} from '../workflows/test/utility'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; describe('getTemplates', () => { let source: DataSource | null = null; @@ -129,3 +142,355 @@ describe('getTemplates', () => { } }); }); + +describe('deleteTemplate', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('指定したテンプレートファイルを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + const blobStorageService = + module.get(BlobstorageService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + const template2 = await createTemplateFile( + source, + account.id, + 'test2', + 'https://url2/test2', + ); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000001', + TASK_STATUS.FINISHED, + ); + await updateTaskTemplateFile(source, taskId1, template1.id); + + const { taskId: taskId2 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000002', + TASK_STATUS.BACKUP, + ); + await updateTaskTemplateFile(source, taskId2, template1.id); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(2); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + expect(templates[1].id).toBe(template2.id); + expect(templates[1].file_name).toBe(template2.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(2); + expect(tasks[0].template_file_id).toBe(template1.id); + expect(tasks[1].template_file_id).toBe(template1.id); + } + + await service.deleteTemplate(context, admin.external_id, template1.id); + + //実行結果を確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template2.id); + expect(templates[0].file_name).toBe(template2.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(2); + expect(tasks[0].template_file_id).toBe(null); + expect(tasks[1].template_file_id).toBe(null); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'Templates/test1', + ); + } + }); + + it('指定したテンプレートファイルが存在しない場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, 9999); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016001')); + } else { + fail(); + } + } + }); + + it('指定したテンプレートファイルがルーティングルールに紐づく場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const { id: typistId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.TYPIST, + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + const { id: workflowId } = await createWorkflow( + source, + account.id, + authorId, + undefined, + template1.id, + ); + await createWorkflowTypist(source, workflowId, typistId); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + + const workflow = await getWorkflow(source, account.id, workflowId); + expect(workflow?.template_id).toBe(template1.id); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016002')); + } else { + fail(); + } + } + }); + + it('指定したテンプレートファイルが未完了のタスクに紐づく場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await updateTaskTemplateFile(source, taskId1, template1.id); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(1); + expect(tasks[0].template_file_id).toBe(template1.id); + expect(tasks[0].status).toBe(TASK_STATUS.UPLOADED); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + } + + //DBアクセスに失敗するようにする + const templateFilesRepositoryService = + module.get( + TemplateFilesRepositoryService, + ); + templateFilesRepositoryService.getTemplateFiles = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/templates/templates.service.ts b/dictation_server/src/features/templates/templates.service.ts index ae36afe..36b2549 100644 --- a/dictation_server/src/features/templates/templates.service.ts +++ b/dictation_server/src/features/templates/templates.service.ts @@ -4,6 +4,13 @@ import { TemplateFile } from './types/types'; import { Context } from '../../common/log'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; +import { + NotFinishedTaskHasTemplateDeleteFailedError, + TemplateFileNotExistError, + WorkflowHasTemplateDeleteFailedError, +} from '../../repositories/template_files/errors/types'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; +import { MANUAL_RECOVERY_REQUIRED } from '../../constants'; @Injectable() export class TemplatesService { @@ -11,6 +18,7 @@ export class TemplatesService { constructor( private readonly usersRepository: UsersRepositoryService, private readonly templateFilesRepository: TemplateFilesRepositoryService, + private readonly blobStorageService: BlobstorageService, ) {} /** @@ -55,4 +63,103 @@ export class TemplatesService { ); } } + + /** + * アカウント内の指定されたテンプレートファイルを削除する + * @param context + * @param externalId + * @param templateFileId + * @returns template + */ + async deleteTemplate( + context: Context, + externalId: string, + templateFileId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTemplate.name + } | params: { externalId: ${externalId}, templateFileId: ${templateFileId} };`, + ); + + try { + const { account } = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + if (!account) { + throw new Error(`account not found. externalId: ${externalId}`); + } + + // テンプレートファイルの取得 + const templateFile = await this.templateFilesRepository.getTemplateFile( + context, + account.id, + templateFileId, + ); + + // DBからのテンプレートファイルの削除 + await this.templateFilesRepository.deleteTemplateFile( + context, + account.id, + templateFileId, + ); + + try { + // Blob Storageからのテンプレートファイルの削除 + await this.blobStorageService.deleteFile( + context, + account.id, + account.country, + `Templates/${templateFile.file_name}`, + ); + } catch (e) { + // Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 + this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete Blob: accountId: ${ + account.id + }, fileName: ${templateFile.file_name}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + // 指定されたIDのテンプレートファイルが存在しない + case TemplateFileNotExistError: + throw new HttpException( + makeErrorResponse('E016001'), + HttpStatus.BAD_REQUEST, + ); + // 指定されたIDのテンプレートファイルがルーティングルールに設定されている + case WorkflowHasTemplateDeleteFailedError: + throw new HttpException( + makeErrorResponse('E016002'), + HttpStatus.BAD_REQUEST, + ); + // 指定されたIDのテンプレートファイルが未完了タスクに紐づいている + case NotFinishedTaskHasTemplateDeleteFailedError: + throw new HttpException( + makeErrorResponse('E016003'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteTemplate.name}`, + ); + } + } } diff --git a/dictation_server/src/features/templates/test/utility.ts b/dictation_server/src/features/templates/test/utility.ts index 224b536..1358df1 100644 --- a/dictation_server/src/features/templates/test/utility.ts +++ b/dictation_server/src/features/templates/test/utility.ts @@ -1,5 +1,6 @@ import { DataSource } from 'typeorm'; import { TemplateFile } from '../../../repositories/template_files/entity/template_file.entity'; +import { Task } from '../../../repositories/tasks/entity/task.entity'; export const createTemplateFile = async ( datasource: DataSource, @@ -41,3 +42,18 @@ export const getTemplateFiles = async ( }); return templates; }; + +export const updateTaskTemplateFile = async ( + datasource: DataSource, + taskId: number, + templateFileId: number, +): Promise => { + await datasource.getRepository(Task).update( + { id: taskId }, + { + template_file_id: templateFileId, + updated_by: 'updater', + updated_at: new Date(), + }, + ); +}; diff --git a/dictation_server/src/repositories/template_files/errors/types.ts b/dictation_server/src/repositories/template_files/errors/types.ts index d1db4d7..9cff27f 100644 --- a/dictation_server/src/repositories/template_files/errors/types.ts +++ b/dictation_server/src/repositories/template_files/errors/types.ts @@ -5,3 +5,17 @@ export class TemplateFileNotExistError extends Error { this.name = 'TemplateFileNotExistError'; } } + +export class WorkflowHasTemplateDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorkflowHasTemplateDeleteFailedError'; + } +} + +export class NotFinishedTaskHasTemplateDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'NotFinishedTaskHasTemplateDeleteFailedError'; + } +} diff --git a/dictation_server/src/repositories/template_files/template_files.repository.service.ts b/dictation_server/src/repositories/template_files/template_files.repository.service.ts index 5fc805b..c1ec96c 100644 --- a/dictation_server/src/repositories/template_files/template_files.repository.service.ts +++ b/dictation_server/src/repositories/template_files/template_files.repository.service.ts @@ -1,8 +1,20 @@ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, In } from 'typeorm'; import { TemplateFile } from './entity/template_file.entity'; -import { insertEntity, updateEntity } from '../../common/repository'; +import { + deleteEntity, + insertEntity, + updateEntity, +} from '../../common/repository'; import { Context } from '../../common/log'; +import { + NotFinishedTaskHasTemplateDeleteFailedError, + TemplateFileNotExistError, + WorkflowHasTemplateDeleteFailedError, +} from './errors/types'; +import { Workflow } from '../workflows/entity/workflow.entity'; +import { Task } from '../tasks/entity/task.entity'; +import { TASK_STATUS } from '../../constants'; @Injectable() export class TemplateFilesRepositoryService { @@ -32,6 +44,36 @@ export class TemplateFilesRepositoryService { }); } + /** + * アカウント内のIDで指定されたテンプレートファイルを取得する + * @param context + * @param accountId + * @param templateFileId + * @returns template file + */ + async getTemplateFile( + context: Context, + accountId: number, + templateFileId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const templateFilesRepo = entityManager.getRepository(TemplateFile); + + const template = await templateFilesRepo.findOne({ + where: { account_id: accountId, id: templateFileId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (!template) { + throw new TemplateFileNotExistError( + `template file not found. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + return template; + }); + } + /** * アカウント内にテンプレートファイルを追加(すでに同名ファイルがあれば更新)する * @param accountId @@ -79,4 +121,92 @@ export class TemplateFilesRepositoryService { } }); } + + /** + * アカウント内にある指定されたテンプレートファイルを削除する + * @param accountId + * @param fileName + * @param url + * @returns template file + */ + async deleteTemplateFile( + context: Context, + accountId: number, + templateFileId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const workflowRepo = entityManager.getRepository(Workflow); + // テンプレートファイルがワークフローで使用されているか確認 + const workflow = await workflowRepo.findOne({ + where: { + account_id: accountId, + template_id: templateFileId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ワークフローで使用されている場合はエラー + if (workflow) { + throw new WorkflowHasTemplateDeleteFailedError( + `workflow has template file. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + const templateFilesRepo = entityManager.getRepository(TemplateFile); + // アカウント内に指定IDファイルがあるか確認 + const template = await templateFilesRepo.findOne({ + where: { account_id: accountId, id: templateFileId }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ファイルが存在しない場合はエラー + if (!template) { + throw new TemplateFileNotExistError( + `template file not found. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + const taskRepo = entityManager.getRepository(Task); + // テンプレートファイルが未完了タスクで使用されているか確認 + const templateUsedTasks = await taskRepo.findOne({ + where: { + account_id: accountId, + template_file_id: templateFileId, + status: In([ + TASK_STATUS.UPLOADED, + TASK_STATUS.IN_PROGRESS, + TASK_STATUS.PENDING, + ]), + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 未完了のタスクでテンプレートファイルが使用されている場合はエラー + if (templateUsedTasks) { + throw new NotFinishedTaskHasTemplateDeleteFailedError( + `not finished task has template file. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + // テンプレートファイルの削除 + await deleteEntity( + templateFilesRepo, + { id: templateFileId }, + this.isCommentOut, + context, + ); + + // 完了済みのタスクからテンプレートファイルの紐づけを解除 + await updateEntity( + taskRepo, + { template_file_id: templateFileId }, + { template_file_id: null }, + this.isCommentOut, + context, + ); + }); + } } From bbbd3e757b54f5768ace6117433205b9afe9099d Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 13 Feb 2024 02:32:17 +0000 Subject: [PATCH 024/109] =?UTF-8?q?Merged=20PR=20743:=20=E3=83=86=E3=83=B3?= =?UTF-8?q?=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E5=89=8A=E9=99=A4=E7=94=BB=E9=9D=A2=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3600: テンプレートファイル削除画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3600) - テンプレートファイル削除の画面を実装しました。 ## レビューポイント - エラー処理は適切でしょうか? ## UIの変更 - [Task3600](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/Task3600?csf=1&web=1&e=E25Kf7) ## 動作確認状況 - ローカルで確認 --- dictation_client/src/api/api.ts | 73 ++++++++++++++++++ dictation_client/src/common/errors/code.ts | 3 + .../features/workflow/template/operations.ts | 75 +++++++++++++++++++ .../workflow/template/templateSlice.ts | 15 +++- .../src/pages/TemplateFilePage/index.tsx | 27 ++++++- dictation_client/src/translation/de.json | 4 + dictation_client/src/translation/en.json | 4 + dictation_client/src/translation/es.json | 4 + dictation_client/src/translation/fr.json | 4 + 9 files changed, 204 insertions(+), 5 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index d510afb..0b3aacf 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -6730,6 +6730,44 @@ export class TasksApi extends BaseAPI { */ export const TemplatesApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile: async (templateFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'templateFileId' is not null or undefined + assertParamExists('deleteTemplateFile', 'templateFileId', templateFileId) + const localVarPath = `/templates/{templateFileId}/delete` + .replace(`{${"templateFileId"}}`, encodeURIComponent(String(templateFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * アカウント内のテンプレートファイルの一覧を取得します * @summary @@ -6774,6 +6812,19 @@ export const TemplatesApiAxiosParamCreator = function (configuration?: Configura export const TemplatesApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = TemplatesApiAxiosParamCreator(configuration) return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTemplateFile(templateFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TemplatesApi.deleteTemplateFile']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * アカウント内のテンプレートファイルの一覧を取得します * @summary @@ -6796,6 +6847,16 @@ export const TemplatesApiFp = function(configuration?: Configuration) { export const TemplatesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = TemplatesApiFp(configuration) return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile(templateFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTemplateFile(templateFileId, options).then((request) => request(axios, basePath)); + }, /** * アカウント内のテンプレートファイルの一覧を取得します * @summary @@ -6815,6 +6876,18 @@ export const TemplatesApiFactory = function (configuration?: Configuration, base * @extends {BaseAPI} */ export class TemplatesApi extends BaseAPI { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApi + */ + public deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig) { + return TemplatesApiFp(this.configuration).deleteTemplateFile(templateFileId, options).then((request) => request(this.axios, this.basePath)); + } + /** * アカウント内のテンプレートファイルの一覧を取得します * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 5aa1497..25366a9 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -73,4 +73,7 @@ export const errorCodes = [ "E015001", // タイピストグループ削除済みエラー "E015002", // タイピストグループがワークフローに紐づいているエラー "E015003", // タイピストグループがルーティングされているエラー + "E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) + "E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) + "E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) ] as const; diff --git a/dictation_client/src/features/workflow/template/operations.ts b/dictation_client/src/features/workflow/template/operations.ts index 9a15ac2..ae464f3 100644 --- a/dictation_client/src/features/workflow/template/operations.ts +++ b/dictation_client/src/features/workflow/template/operations.ts @@ -115,3 +115,78 @@ export const uploadTemplateAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteTemplateAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { templateFileId: number }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/deleteTemplateAsync", async (args, thunkApi) => { + const { templateFileId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const templateApi = new TemplatesApi(config); + + try { + // ファイルを削除する + await templateApi.deleteTemplateFile(templateFileId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + if (error.code === "E016001") { + // テンプレートファイルが削除済みの場合は成功扱いとする + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + + let message = getTranslationID("common.message.internalServerError"); + + // テンプレートファイルがルーティングルールに紐づく場合はエラー + if (error.code === "E016002") { + message = getTranslationID( + "templateFilePage.message.deleteFailedWorkflowAssigned" + ); + } + // テンプレートファイルが未完了のタスクに紐づく場合はエラー + if (error.code === "E016003") { + message = getTranslationID( + "templateFilePage.message.deleteFailedTaskAssigned" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message, + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/template/templateSlice.ts b/dictation_client/src/features/workflow/template/templateSlice.ts index bbd4001..d567fe7 100644 --- a/dictation_client/src/features/workflow/template/templateSlice.ts +++ b/dictation_client/src/features/workflow/template/templateSlice.ts @@ -1,6 +1,10 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { TemplateState } from "./state"; -import { listTemplateAsync, uploadTemplateAsync } from "./operations"; +import { + deleteTemplateAsync, + listTemplateAsync, + uploadTemplateAsync, +} from "./operations"; const initialState: TemplateState = { apps: { @@ -45,6 +49,15 @@ export const templateSlice = createSlice({ builder.addCase(uploadTemplateAsync.rejected, (state) => { state.apps.isUploading = false; }); + builder.addCase(deleteTemplateAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteTemplateAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteTemplateAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/pages/TemplateFilePage/index.tsx b/dictation_client/src/pages/TemplateFilePage/index.tsx index 59ef540..f50dd0f 100644 --- a/dictation_client/src/pages/TemplateFilePage/index.tsx +++ b/dictation_client/src/pages/TemplateFilePage/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { AppDispatch } from "app/store"; import Header from "components/header"; @@ -13,6 +13,7 @@ import { selectTemplates, listTemplateAsync, selectIsLoading, + deleteTemplateAsync, } from "features/workflow/template"; import { selectDelegationAccessToken } from "features/auth/selectors"; import { DelegationBar } from "components/delegate"; @@ -35,6 +36,23 @@ export const TemplateFilePage: React.FC = () => { dispatch(listTemplateAsync()); }, [dispatch]); + const onDeleteTemplate = useCallback( + async (templateFileId: number) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch(deleteTemplateAsync({ templateFileId })); + if (meta.requestStatus === "fulfilled") { + dispatch(listTemplateAsync()); + } + }, + [dispatch, t] + ); + return ( <> {isShowAddPopup && ( @@ -101,16 +119,17 @@ export const TemplateFilePage: React.FC = () => { {template.name} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 17289e4..01f661c 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -475,6 +475,10 @@ "fileSizeTerms": "Die maximale Dateigröße, die gespeichert werden kann, beträgt 5 MB.", "fileSizeError": "Die ausgewählte Dateigröße ist zu groß. Bitte wählen Sie eine Datei mit einer Größe von 5 MB oder weniger aus.", "fileEmptyError": "Dateiauswahl ist erforderlich. Bitte wählen Sie eine Datei aus." + }, + "message": { + "deleteFailedWorkflowAssigned": "(de)テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", + "deleteFailedTaskAssigned": "(de)テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" } }, "partnerPage": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index cdd73e7..c58bd89 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -475,6 +475,10 @@ "fileSizeTerms": "The maximum file size that can be saved is 5MB.", "fileSizeError": "The selected file size is too large. Please select a file that is 5MB or less in size.", "fileEmptyError": "File selection is required. Please select a file." + }, + "message": { + "deleteFailedWorkflowAssigned": "テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", + "deleteFailedTaskAssigned": "テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" } }, "partnerPage": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 2256420..7a743a5 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -475,6 +475,10 @@ "fileSizeTerms": "El tamaño máximo de archivo que se puede guardar es de 5 MB.", "fileSizeError": "El tamaño del archivo seleccionado es demasiado grande. Seleccione un archivo que tenga un tamaño de 5 MB o menos.", "fileEmptyError": "Se requiere selección de archivos. Por favor seleccione un archivo." + }, + "message": { + "deleteFailedWorkflowAssigned": "(es)テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", + "deleteFailedTaskAssigned": "(es)テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" } }, "partnerPage": { diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 403e693..7a0e0b1 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -475,6 +475,10 @@ "fileSizeTerms": "La taille maximale du fichier pouvant être enregistré est de 5 Mo.", "fileSizeError": "La taille du fichier sélectionné est trop grande. Veuillez sélectionner un fichier d'une taille maximale de 5 Mo.", "fileEmptyError": "La sélection de fichiers est requise. Veuillez sélectionner un fichier." + }, + "message": { + "deleteFailedWorkflowAssigned": "(fr)テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", + "deleteFailedTaskAssigned": "(fr)テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" } }, "partnerPage": { From 63191a3b61465db57714de281832e35c25596f5a Mon Sep 17 00:00:00 2001 From: "SAITO-PC-3\\saito.k" Date: Wed, 14 Feb 2024 12:06:19 +0900 Subject: [PATCH 025/109] =?UTF-8?q?OptionItem=E3=81=AE=E4=BD=9C=E6=88=90?= =?UTF-8?q?=E3=81=8C=E9=87=8D=E8=A4=87=E3=81=97=E3=81=A6=E3=81=97=E3=81=BE?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F=E3=81=AE=E3=81=A7=E7=89=87?= =?UTF-8?q?=E6=96=B9=E3=82=92=E5=89=8A=E9=99=A4=20=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E5=89=8A=E9=99=A4=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92Mysql=E4=BD=BF=E7=94=A8=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/tasks/tasks.service.spec.ts | 34 ++++++++---- .../src/features/tasks/test/utility.ts | 53 ------------------- 2 files changed, 23 insertions(+), 64 deletions(-) diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 2e1a101..54e191b 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -4120,20 +4120,32 @@ describe('getNextTask', () => { describe('deleteTask', () => { let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + beforeEach(async () => { - source = new DataSource({ - type: 'sqlite', - database: ':memory:', - logging: false, - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: true, // trueにすると自動的にmigrationが行われるため注意 - }); - return source.initialize(); + if (source) { + await truncateAllTable(source); + } }); - afterEach(async () => { - if (!source) return; - await source.destroy(); + afterAll(async () => { + await source?.destroy(); source = null; }); diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index eb3938e..6a1e024 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -158,59 +158,6 @@ export const createTask = async ( }); const task = taskIdentifiers.pop() as Task; - await datasource.getRepository(AudioOptionItem).insert([ - { - audio_file_id: audioFile.id, - label: 'label01', - value: 'value01', - }, - { - audio_file_id: audioFile.id, - label: 'label02', - value: 'value02', - }, - { - audio_file_id: audioFile.id, - label: 'label03', - value: 'value03', - }, - { - audio_file_id: audioFile.id, - label: 'label04', - value: 'value04', - }, - { - audio_file_id: audioFile.id, - label: 'label05', - value: 'value05', - }, - { - audio_file_id: audioFile.id, - label: 'label06', - value: 'value06', - }, - { - audio_file_id: audioFile.id, - label: 'label07', - value: 'value07', - }, - { - audio_file_id: audioFile.id, - label: 'label08', - value: 'value08', - }, - { - audio_file_id: audioFile.id, - label: 'label09', - value: 'value09', - }, - { - audio_file_id: audioFile.id, - label: 'label10', - value: 'value10', - }, - ]); - return { taskId: task.id, audioFileId: audioFile.id }; }; From aef30c8cbe5fe71c2a4f6947a0712479e41d160e Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Fri, 16 Feb 2024 02:11:34 +0000 Subject: [PATCH 026/109] =?UTF-8?q?Merged=20PR=20748:=20=E7=AC=AC=E4=BA=94?= =?UTF-8?q?=E9=9A=8E=E5=B1=A4=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E5=8F=96=E5=BE=97API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3655: 第五階層ライセンス情報取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3655) - 第五階層ライセンス情報取得APIに、ストレージ上限とストレージ使用量を取得する処理を追加しました。 - 既存の「割り当て済みライセンス取得処理」と「再利用可能ライセンス取得処理」に不要な条件があったため削除しました ## レビューポイント - 上限計算方法、使用量取得条件に仕様との認識齟齬はないか? - もしくはテストケースで「これもあったほうがいいのでは?」などないか - その他気になる点あれば ## 動作確認状況 - ローカルでテストが全部通ることを確認 --- dictation_server/src/constants/index.ts | 6 + .../accounts/accounts.service.spec.ts | 129 +++++++++++++++++- .../src/features/accounts/accounts.service.ts | 10 +- .../src/features/accounts/test/utility.ts | 29 ++++ .../src/features/tasks/tasks.service.spec.ts | 34 ++--- .../accounts/accounts.repository.service.ts | 36 ++--- .../licenses/licenses.repository.service.ts | 53 ++++++- 7 files changed, 249 insertions(+), 48 deletions(-) diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 979f864..cc23b64 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -327,3 +327,9 @@ export const USER_LICENSE_STATUS = { * @const {number} */ export const FILE_RETENTION_DAYS_DEFAULT = 30; + +/** + * 割り当て履歴有りライセンス1つあたりのストレージ使用可能量(GB) + * @const {number} + */ +export const STORAGE_SIZE_PER_LICENSE = 5; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 5334092..af5ac04 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -13,6 +13,7 @@ import { } from './test/accounts.service.mock'; import { makeDefaultConfigValue } from '../users/test/users.service.mock'; import { + createAudioFile, createLicense, createLicenseOrder, createLicenseSetExpiryDateAndStatus, @@ -47,6 +48,7 @@ import { LICENSE_ISSUE_STATUS, LICENSE_TYPE, OPTION_ITEM_VALUE_TYPE, + STORAGE_SIZE_PER_LICENSE, TASK_STATUS, TIERS, USER_ROLES, @@ -1912,7 +1914,7 @@ describe('getLicenseSummary', () => { await createLicenseSetExpiryDateAndStatus( source, childAccountId1, - null, + new Date(2037, 1, 1, 23, 59, 59), 'Allocated', 1, ); @@ -1937,7 +1939,7 @@ describe('getLicenseSummary', () => { expiringWithin14daysLicense: 5, issueRequesting: 100, numberOfRequesting: 1, - storageSize: 0, + storageSize: 40000000000, usedSize: 0, shortage: 2, isStorageAvailable: false, @@ -1950,12 +1952,135 @@ describe('getLicenseSummary', () => { expiringWithin14daysLicense: 5, issueRequesting: 0, numberOfRequesting: 0, + storageSize: 25000000000, + usedSize: 0, + shortage: 0, + isStorageAvailable: false, + }); + }); + + it('第五階層のストレージ使用量が取得できる(ライセンスなし、音声ファイルなし)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + const service = module.get(AccountsService); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + const result = await service.getLicenseSummary(context, accountId); + expect(result).toEqual({ + totalLicense: 0, + allocatedLicense: 0, + reusableLicense: 0, + freeLicense: 0, + expiringWithin14daysLicense: 0, + issueRequesting: 0, + numberOfRequesting: 0, storageSize: 0, usedSize: 0, shortage: 0, isStorageAvailable: false, }); }); + + it('第五階層のストレージ使用量が取得できる(ライセンスあり、音声ファイルあり)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // audioFileを作成する + const fileSize1 = 15000; + await createAudioFile(source, accountId, 1, fileSize1); + const fileSize2 = 17000; + await createAudioFile(source, accountId, 1, fileSize2); + + // ライセンスを作成する + const reusableLicense = 3; + for (let i = 0; i < reusableLicense; i++) { + await createLicenseSetExpiryDateAndStatus( + source, + accountId, + new Date(2037, 1, 1, 23, 59, 59), + LICENSE_ALLOCATED_STATUS.REUSABLE, + ); + } + + const allocatedLicense = 2; + for (let i = 0; i < allocatedLicense; i++) { + await createLicenseSetExpiryDateAndStatus( + source, + accountId, + new Date(2037, 1, 1, 23, 59, 59), + LICENSE_ALLOCATED_STATUS.ALLOCATED, + i + 1, // なんでもよい。重複しないようにインクリメントする。 + ); + } + + const unallocatedLicense = 5; + for (let i = 0; i < unallocatedLicense; i++) { + await createLicenseSetExpiryDateAndStatus( + source, + accountId, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + } + + // 自アカウントだけに絞って計算出来ていることを確認するため、別のアカウントとaudioFileとライセンス作成する。 + const { id: otherAccountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company2', + }) + ).account; + + await createAudioFile(source, otherAccountId, 1, 5000); + await createLicenseSetExpiryDateAndStatus( + source, + otherAccountId, + new Date(2037, 1, 1, 23, 59, 59), + LICENSE_ALLOCATED_STATUS.ALLOCATED, + ); + + // テスト実行 + const service = module.get(AccountsService); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + const result = await service.getLicenseSummary(context, accountId); + + const expectedStorageSize = + (reusableLicense + allocatedLicense) * + STORAGE_SIZE_PER_LICENSE * + 1000 * + 1000 * + 1000; // 5GB + expect(result).toEqual({ + totalLicense: reusableLicense + unallocatedLicense, + allocatedLicense: allocatedLicense, + reusableLicense: reusableLicense, + freeLicense: unallocatedLicense, + expiringWithin14daysLicense: 0, + issueRequesting: 0, + numberOfRequesting: 0, + storageSize: expectedStorageSize, + usedSize: fileSize1 + fileSize2, + shortage: 0, + isStorageAvailable: false, + }); + }); }); describe('getPartnerAccount', () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index bad5d49..28ee33d 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -131,6 +131,12 @@ export class AccountsService { let shortage = allocatableLicenseWithMargin - expiringSoonLicense; shortage = shortage >= 0 ? 0 : Math.abs(shortage); + const { size, used } = await this.licensesRepository.getStorageInfo( + context, + accountId, + currentDate, + ); + const licenseSummaryResponse: GetLicenseSummaryResponse = { totalLicense, allocatedLicense, @@ -139,8 +145,8 @@ export class AccountsService { expiringWithin14daysLicense: expiringSoonLicense, issueRequesting, numberOfRequesting, - storageSize: 0, // XXX PBI1201対象外 - usedSize: 0, // XXX PBI1201対象外 + storageSize: size, + usedSize: used, shortage, isStorageAvailable, }; diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index b68b5fd..ff0e436 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -10,6 +10,7 @@ import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity import { OptionItem } from '../../../repositories/worktypes/entity/option_item.entity'; import { OPTION_ITEM_VALUE_TYPE } from '../../../constants'; import { Account } from '../../../repositories/accounts/entity/account.entity'; +import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity'; /** * テスト ユーティリティ: すべてのソート条件を取得する @@ -219,3 +220,31 @@ export const getOptionItems = async ( }) : await datasource.getRepository(OptionItem).find(); }; + +export const createAudioFile = async ( + datasource: DataSource, + account_id: number, + owner_user_id: number, + fileSize: number, +): Promise<{ audioFileId: number }> => { + const { identifiers: audioFileIdentifiers } = await datasource + .getRepository(AudioFile) + .insert({ + account_id: account_id, + owner_user_id: owner_user_id, + url: '', + file_name: 'x.zip', + author_id: 'author_id', + work_type_id: '', + started_at: new Date(), + duration: '100000', + finished_at: new Date(), + uploaded_at: new Date(), + file_size: fileSize, + priority: '00', + audio_format: 'audio_format', + is_encrypted: true, + }); + const audioFile = audioFileIdentifiers.pop() as AudioFile; + return { audioFileId: audioFile.id }; +}; diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 54e191b..27ffcfe 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -4120,23 +4120,23 @@ describe('getNextTask', () => { describe('deleteTask', () => { let source: DataSource | null = null; - beforeAll(async () => { - if (source == null) { - source = await (async () => { - const s = new DataSource({ - type: 'mysql', - host: 'test_mysql_db', - port: 3306, - username: 'user', - password: 'password', - database: 'odms', - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - synchronize: false, // trueにすると自動的にmigrationが行われるため注意 - }); - return await s.initialize(); - })(); - } - }); + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); beforeEach(async () => { if (source) { diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index ee4fc2d..4a516a8 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -397,37 +397,21 @@ export class AccountsRepositoryService { // 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する const allocatedLicense = await license.count({ - where: [ - { - account_id: id, - allocated_user_id: Not(IsNull()), - expiry_date: MoreThanOrEqual(currentDate), - status: LICENSE_ALLOCATED_STATUS.ALLOCATED, - }, - { - account_id: id, - allocated_user_id: Not(IsNull()), - expiry_date: IsNull(), - status: LICENSE_ALLOCATED_STATUS.ALLOCATED, - }, - ], + where: { + account_id: id, + expiry_date: MoreThanOrEqual(currentDate), + status: LICENSE_ALLOCATED_STATUS.ALLOCATED, + }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); // 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する const reusableLicense = await license.count({ - where: [ - { - account_id: id, - expiry_date: MoreThanOrEqual(currentDate), - status: LICENSE_ALLOCATED_STATUS.REUSABLE, - }, - { - account_id: id, - expiry_date: IsNull(), - status: LICENSE_ALLOCATED_STATUS.REUSABLE, - }, - ], + where: { + account_id: id, + expiry_date: MoreThanOrEqual(currentDate), + status: LICENSE_ALLOCATED_STATUS.REUSABLE, + }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 6270b43..5d20eb4 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; +import { DataSource, In, IsNull, MoreThanOrEqual, Not } from 'typeorm'; import { LicenseOrder, License, @@ -12,6 +12,7 @@ import { LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, + STORAGE_SIZE_PER_LICENSE, SWITCH_FROM_TYPE, TIERS, USER_LICENSE_STATUS, @@ -41,6 +42,7 @@ import { import { Context } from '../../common/log'; import { User } from '../users/entity/user.entity'; import { UserNotFoundError } from '../users/errors/types'; +import { AudioFile } from '../audio_files/entity/audio_file.entity'; @Injectable() export class LicensesRepositoryService { @@ -862,4 +864,53 @@ export class LicensesRepositoryService { return { state: USER_LICENSE_STATUS.ALLOCATED }; } + /** + * ストレージ情報(上限と使用量)を取得します + * @param context + * @param accountId + * @param currentDate + * @returns size: ストレージ上限, used: 使用量 + */ + async getStorageInfo( + context: Context, + accountId: number, + currentDate: Date, + ): Promise<{ size: number; used: number }> { + return await this.dataSource.transaction(async (entityManager) => { + // ストレージ上限計算のための値を取得する。(ユーザーに一度でも割り当てたことのあるライセンス数) + const licenseRepo = entityManager.getRepository(License); + const licensesAllocatedOnce = await licenseRepo.count({ + where: { + account_id: accountId, + expiry_date: MoreThanOrEqual(currentDate), + status: In([ + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ]), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ストレージ上限を計算する + const size = + licensesAllocatedOnce * STORAGE_SIZE_PER_LICENSE * 1000 * 1000 * 1000; // GB -> B + + // 既に使用しているストレージ量を取得する + const audioFileRepo = entityManager.getRepository(AudioFile); + const usedQuery = await audioFileRepo + .createQueryBuilder('audioFile') + .select('SUM(audioFile.file_size)', 'used') + .where('audioFile.account_id = :accountId', { accountId }) + .comment(`${context.getTrackingId()}_${new Date().toUTCString()}`) + .getRawOne(); + + let used = parseInt(usedQuery?.used); + if (isNaN(used)) { + // AudioFileのレコードが存在しない場合、SUM関数がNULLを返すため、0を返す + used = 0; + } + + return { size, used }; + }); + } } From fc7d271a296009fb6880dde3040a82bd5f171a39 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 16 Feb 2024 05:15:45 +0000 Subject: [PATCH 027/109] =?UTF-8?q?Merged=20PR=20749:=20=E7=AC=AC=E4=BA=94?= =?UTF-8?q?=E9=9A=8E=E5=B1=A4=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E7=94=BB=E9=9D=A2=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3654: 第五階層ライセンス情報画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3654) - 第五階層ライセンス情報画面でストレージ情報(Storage Available、Storage Used)を表示するように画面を修正しました。 - 表示する値はGB単位となるようにし、小数点以下第三位までを表示するようにしています。 ## レビューポイント - 表示する値のフォーマットは適切でしょうか? - 小数点以下第三位までを固定で表示するようにしていますが問題ないでしょうか? ## UIの変更 - [Task3654](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/Task3654?csf=1&web=1&e=1tCGoN) ## 動作確認状況 - ローカルで確認 --- .../src/pages/LicensePage/licenseSummary.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/dictation_client/src/pages/LicensePage/licenseSummary.tsx b/dictation_client/src/pages/LicensePage/licenseSummary.tsx index a343fa5..0314a33 100644 --- a/dictation_client/src/pages/LicensePage/licenseSummary.tsx +++ b/dictation_client/src/pages/LicensePage/licenseSummary.tsx @@ -289,17 +289,31 @@ export const LicenseSummary: React.FC = ( ) )} - {/* Storage Usedの値表示をハイフンに置き換え */} - {/*
    {licenseSummaryInfo.storageSize}GB
    */} -
    -
    +
    + {/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */} + {( + licenseSummaryInfo.storageSize / + 1000 / + 1000 / + 1000 + ).toFixed(3)} + GB +
    {t( getTranslationID("LicenseSummaryPage.label.usedSize") )}
    - {/* Storage Usedの値表示をハイフンに置き換え */} - {/*
    {licenseSummaryInfo.usedSize}GB
    */} -
    -
    +
    + {/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */} + {( + licenseSummaryInfo.usedSize / + 1000 / + 1000 / + 1000 + ).toFixed(3)} + GB +
    {t( getTranslationID( From ecb28b9328a48db60b8469036c442585e6d94012 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Mon, 19 Feb 2024 04:32:59 +0000 Subject: [PATCH 028/109] =?UTF-8?q?Merged=20PR=20759:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3711: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3711) - アカウント利用制限更新APIのIFを実装し、openapi生成しました。 ## レビューポイント - メソッド名で他に案あれば。 ## 動作確認状況 - 特になし --- dictation_server/src/api/odms/openapi.json | 70 +++++++++++++++++ .../features/accounts/accounts.controller.ts | 76 +++++++++++++++++++ .../src/features/accounts/types/types.ts | 14 ++++ 3 files changed, 160 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index eb999e6..38b804a 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1609,6 +1609,61 @@ "security": [{ "bearer": [] }] } }, + "/accounts/restriction-status": { + "post": { + "operationId": "updateRestrictionStatus", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/アカウント不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -4359,6 +4414,21 @@ "properties": { "companyName": { "type": "string" } }, "required": ["companyName"] }, + "UpdateRestrictionStatusRequest": { + "type": "object", + "properties": { + "accountId": { + "type": "number", + "description": "操作対象の第五階層アカウントID" + }, + "restricted": { + "type": "boolean", + "description": "制限をかけるかどうか(trur:制限をかける)" + } + }, + "required": ["accountId", "restricted"] + }, + "UpdateRestrictionStatusResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { "type": "object", "properties": { "token": { "type": "string" } }, diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 1dacf58..ef75727 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -75,6 +75,8 @@ import { DeleteTypistGroupResponse, UpdateFileDeleteSettingRequest, UpdateFileDeleteSettingResponse, + UpdateRestrictionStatusRequest, + UpdateRestrictionStatusResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -2240,4 +2242,78 @@ export class AccountsController { ); return companyName; } + + @ApiResponse({ + status: HttpStatus.OK, + type: UpdateRestrictionStatusResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正/アカウント不在', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'updateRestrictionStatus' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER1] }), + ) + @Post('restriction-status') + async updateRestrictionStatus( + @Req() req: Request, + @Body() body: UpdateRestrictionStatusRequest, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // service層を呼び出す + const { accountId, restricted } = body; + // TODO:service層実装時に削除する + this.logger.log(`accountId: ${accountId}, restricted: ${restricted}`); + + return {}; + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 08217f8..22da103 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -357,6 +357,18 @@ export class GetCompanyNameRequest { accountId: number; } +export class UpdateRestrictionStatusRequest { + @ApiProperty({ description: '操作対象の第五階層アカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + accountId: number; + + @ApiProperty({ description: '制限をかけるかどうか(trur:制限をかける)' }) + @Type(() => Boolean) + restricted: boolean; +} + // ============================== // RESPONSE // ============================== @@ -672,6 +684,8 @@ export class UpdateFileDeleteSettingRequest { export class UpdateFileDeleteSettingResponse {} +export class UpdateRestrictionStatusResponse {} + // ============================== // Request/Response外の型 // TODO: Request/Response/その他の型を別ファイルに分ける From f8183399e28a5f16ecee32f033f820a37bfa4633 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Tue, 20 Feb 2024 11:23:04 +0000 Subject: [PATCH 029/109] =?UTF-8?q?Merged=20PR=20762:=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E5=88=A9=E7=94=A8=E5=88=B6=E9=99=90?= =?UTF-8?q?=E6=9B=B4=E6=96=B0API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3710: アカウント利用制限更新API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3710) - タイトルの通りです。 - 既存実装でログが不足している箇所あったのでちょろ修正もしました。 ## レビューポイント - これと言ってみて欲しいポイントはないので、何か気になる点あれば ## 動作確認状況 - ローカルで全テストが通ることを確認済み --- .../features/accounts/accounts.controller.ts | 9 +- .../accounts/accounts.service.spec.ts | 126 ++++++++++++++++++ .../src/features/accounts/accounts.service.ts | 48 ++++++- .../accounts/accounts.repository.service.ts | 41 ++++++ 4 files changed, 220 insertions(+), 4 deletions(-) diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index ef75727..dceff54 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2250,7 +2250,7 @@ export class AccountsController { }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'パラメータ不正/アカウント不在', + description: 'パラメータ不正', type: ErrorResponse, }) @ApiResponse({ @@ -2311,8 +2311,11 @@ export class AccountsController { // service層を呼び出す const { accountId, restricted } = body; - // TODO:service層実装時に削除する - this.logger.log(`accountId: ${accountId}, restricted: ${restricted}`); + await this.accountService.updateRestrictionStatus( + context, + accountId, + restricted, + ); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 0eb2c6f..47a1188 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -7711,3 +7711,129 @@ describe('updateFileDeleteSetting', () => { } }); }); + +describe('updateRestrictionStatus', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('アカウント利用制限のON/OFFが出来る', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第一階層のアカウントを作成する + const { admin } = await makeTestAccount(source, { + tier: 1, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 操作対象の第五階層のアカウントを作成する + const { account } = await makeTestAccount(source, { + tier: 5, + locked: false, + }); + + const service = module.get(AccountsService); + // 利用制限をかけられるか確認。 + { + const restricted = true; + await service.updateRestrictionStatus(context, account.id, restricted); + + const result = await getAccount(source, account.id); + expect(result?.locked).toBe(restricted); + } + + // 利用制限を解除できるか確認。 + { + const restricted = false; + await service.updateRestrictionStatus(context, account.id, restricted); + + const result = await getAccount(source, account.id); + expect(result?.locked).toBe(restricted); + } + }); + + it('対象アカウントが存在しない場合は500エラーを返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // アカウントを作成せずに実行する + const context = makeContext('dummy', 'requestId'); + try { + await service.updateRestrictionStatus(context, 0, false); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーを返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第一階層のアカウントを作成する + const { admin } = await makeTestAccount(source, { + tier: 1, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 操作対象の第五階層のアカウントを作成する + const { account } = await makeTestAccount(source, { + tier: 5, + locked: false, + }); + + //DBアクセスに失敗するようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + accountsRepositoryService.updateRestrictionStatus = jest + .fn() + .mockRejectedValue('DB failed'); + + // テスト実行する + const service = module.get(AccountsService); + try { + await service.updateRestrictionStatus(context, account.id, true); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 28ee33d..1cd288f 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -2584,7 +2584,53 @@ export class AccountsService { HttpStatus.INTERNAL_SERVER_ERROR, ); } finally { - this.logger.log(`[OUT] [${context.getTrackingId()}]`); + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${ + this.updateFileDeleteSetting.name + }`, + ); + } + } + + /** + * 指定したアカウントのシステム利用制限状態を更新する + * @param context + * @param accountId 更新対象アカウントID + * @param restricted 制限するかどうか(true:制限する) + */ + async updateRestrictionStatus( + context: Context, + accountId: number, + restricted: boolean, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updateRestrictionStatus.name + } | params: { ` + + `accountId: ${accountId}, ` + + `restricted: ${restricted},};`, + ); + + try { + await this.accountRepository.updateRestrictionStatus( + context, + accountId, + restricted, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // アカウントが存在しない場合のエラーもINTERNAL_SERVER_ERROR扱いのため個別の判定は行わない + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${ + this.updateRestrictionStatus.name + }`, + ); } } } diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 4a516a8..11464b1 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -1361,4 +1361,45 @@ export class AccountsRepositoryService { ); }); } + + /** + * 指定したアカウントのシステム利用制限状態を更新する + * @param context + * @param accountId 更新対象アカウントID + * @param restricted 制限するかどうか(true:制限する) + */ + async updateRestrictionStatus( + context: Context, + accountId: number, + restricted: boolean, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + + // アカウントが存在するかチェック + const account = await accountRepo.findOne({ + where: { id: accountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // アカウントが存在しない場合はエラー + if (!account) { + throw new AccountNotFoundError( + `Account is not found. id: ${accountId}`, + ); + } + + // アカウント利用制限状態の更新を行う + await updateEntity( + accountRepo, + { id: accountId }, + { + locked: restricted, + }, + this.isCommentOut, + context, + ); + }); + } } From c1f370faaf9446bb73b2bfd8a48f25f366f2c46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Mon, 26 Feb 2024 05:13:43 +0000 Subject: [PATCH 030/109] =?UTF-8?q?Merged=20PR=20766:=20API=20I/F=20&=20sy?= =?UTF-8?q?stem=E6=A8=A9=E9=99=90Token=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3764: API I/F & system権限Token実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3764) - システムが発行したトークンの型定義を追加 - AuthGuardと同等の、システムが発行したTokenである `SystemAccessToken` を検証する `SystemAccessGuard` を追加 - API I/Fを実装 ## レビューポイント - バリデーターは適切か - システムが発行したトークンの型定義は適切か - API I/Fの型は問題ないか ## 動作確認状況 - ローカルでswagger UI上で確認 --- dictation_server/src/app.module.ts | 2 + .../guards/system/accessguards.module.ts | 10 + .../src/common/guards/system/accessguards.ts | 43 ++ dictation_server/src/common/token/types.ts | 14 + .../src/features/users/types/types.ts | 102 ++++ .../features/users/users.controller.spec.ts | 442 ++++++++++++++++++ .../src/features/users/users.controller.ts | 151 ++++++ 7 files changed, 764 insertions(+) create mode 100644 dictation_server/src/common/guards/system/accessguards.module.ts create mode 100644 dictation_server/src/common/guards/system/accessguards.ts diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 1157fa4..5d76aac 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -52,6 +52,7 @@ import { WorkflowsRepositoryModule } from './repositories/workflows/workflows.re import { TermsModule } from './features/terms/terms.module'; import { RedisModule } from './gateways/redis/redis.module'; import * as redisStore from 'cache-manager-redis-store'; +import { SystemAccessGuardsModule } from './common/guards/system/accessguards.module'; @Module({ imports: [ ServeStaticModule.forRootAsync({ @@ -133,6 +134,7 @@ import * as redisStore from 'cache-manager-redis-store'; NotificationhubModule, BlobstorageModule, AuthGuardsModule, + SystemAccessGuardsModule, SortCriteriaRepositoryModule, WorktypesRepositoryModule, TermsModule, diff --git a/dictation_server/src/common/guards/system/accessguards.module.ts b/dictation_server/src/common/guards/system/accessguards.module.ts new file mode 100644 index 0000000..43ac336 --- /dev/null +++ b/dictation_server/src/common/guards/system/accessguards.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SystemAccessGuard } from './accessguards'; + +@Module({ + imports: [ConfigModule], + controllers: [], + providers: [SystemAccessGuard], +}) +export class SystemAccessGuardsModule {} diff --git a/dictation_server/src/common/guards/system/accessguards.ts b/dictation_server/src/common/guards/system/accessguards.ts new file mode 100644 index 0000000..4aa11e2 --- /dev/null +++ b/dictation_server/src/common/guards/system/accessguards.ts @@ -0,0 +1,43 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { isVerifyError, decode, verify } from '../../jwt'; +import { Request } from 'express'; +import { retrieveAuthorizationToken } from '../../../common/http/helper'; +import { makeErrorResponse } from '../../../common/error/makeErrorResponse'; +import { SystemAccessToken } from '../../token/types'; +import { ConfigService } from '@nestjs/config'; +import { getPublicKey } from '../../jwt/jwt'; +/** + * システム間通信用のトークンを検証するガード + **/ +@Injectable() +export class SystemAccessGuard implements CanActivate { + constructor(private readonly configService: ConfigService) {} + + canActivate(context: ExecutionContext): boolean | Promise { + const pubkey = getPublicKey(this.configService); + const req = context.switchToHttp().getRequest(); + + const token = retrieveAuthorizationToken(req); + if (!token) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const payload = verify(token, pubkey); + if (isVerifyError(payload)) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + return true; + } +} diff --git a/dictation_server/src/common/token/types.ts b/dictation_server/src/common/token/types.ts index b602913..02f746d 100644 --- a/dictation_server/src/common/token/types.ts +++ b/dictation_server/src/common/token/types.ts @@ -36,6 +36,20 @@ export type AccessToken = { tier: number; }; +// システムの内部で発行し、外部に公開しないトークン +// システム間通信用(例: Azure Functions→AppService)に使用する +export type SystemAccessToken = { + /** + * トークンの発行者名(ログ記録用) + */ + systemName: string; + + /** + * 付加情報を 文字情報として格納できる + */ + context?: string; +}; + export type IDToken = { emails: string[]; nonce?: string | undefined; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 1c17c52..64896eb 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -1,11 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; import { + IsArray, IsBoolean, IsEmail, IsIn, IsInt, + IsNotEmpty, IsOptional, + IsString, MaxLength, + ValidateIf, + ValidateNested, } from 'class-validator'; import { TASK_LIST_SORTABLE_ATTRIBUTES, @@ -264,6 +269,103 @@ export class PostDeleteUserRequest { export class PostDeleteUserResponse {} +export class MultipleImportUser { + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @MaxLength(256) // AzureAdB2Cの仕様上、256文字まで[https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/user-profile-attributes] + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'メールアドレス' }) + @IsEmail({ blacklisted_chars: '*' }) + @IsNotEmpty() + email: string; + + @ApiProperty({ description: '0(none)/1(author)/2(typist)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1, 2]) + role: number; + + @ApiProperty({ required: false }) + @IsAuthorIdValid() + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + authorId?: string; + + @ApiProperty({ description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + autoRenew: number; + + @ApiProperty({ description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + notification: number; + + @ApiProperty({ required: false, description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + encryption?: number; + + @ApiProperty({ required: false }) + @IsPasswordvalid() + @IsNotEmpty() + @IsString() + @ValidateIf((o) => o.role === 1 && o.encryption === 1) // roleがauthorかつencryptionがtrueの場合のみバリデーションを実施 + encryptionPassword?: string; + + @ApiProperty({ required: false, description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + prompt?: number; +} + +export class PostMultipleImportsRequest { + @ApiProperty({ type: [MultipleImportUser] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MultipleImportUser) + users: MultipleImportUser[]; +} +export class PostMultipleImportsResponse {} + +export class MultipleImportErrors { + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'メールアドレス' }) + @IsEmail({ blacklisted_chars: '*' }) + @IsNotEmpty() + email: string; + + @ApiProperty({ description: 'エラーコード' }) + @IsString() + @IsNotEmpty() + errorCode: string; +} + +export class PostMultipleImportsCompleteRequest { + @ApiProperty({ description: 'アカウントID' }) + @Type(() => Number) + @IsInt() + accountId: number; + + @ApiProperty({ type: [MultipleImportErrors] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MultipleImportErrors) + errors: MultipleImportErrors[]; +} +export class PostMultipleImportsCompleteResponse {} + export class AllocateLicenseRequest { @ApiProperty({ description: 'ユーザーID' }) @Type(() => Number) diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index d060ac2..fa0bcd3 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -3,6 +3,12 @@ import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from '../auth/auth.service'; +import { + PostMultipleImportsCompleteRequest, + PostMultipleImportsRequest, +} from './types/types'; +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; describe('UsersController', () => { let controller: UsersController; @@ -32,4 +38,440 @@ describe('UsersController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation PostMultipleImportsRequest', () => { + it('role:noneの最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 0, + autoRenew: 0, + notification: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('role:authorの最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('emailがメールアドレスではない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge', + role: 0, + autoRenew: 0, + notification: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('AuthorなのにAuthorIDがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('Authorなのにencryptionがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('Authorなのにpromptがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('Authorでencryption:trueなのに、encryptionPasswordがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('Authorでencryption:trueでencryptionPasswordが正常であれば成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcd', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('encryptionPasswordが要件外(短い)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abc', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('encryptionPasswordが要件外(長い)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('encryptionPasswordが要件外(全角が含まれる)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcあいうえお', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('AuthorIDが要件外(小文字)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'author', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abc', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + }); + + describe('valdation PostMultipleImportsCompleteRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsCompleteRequest(); + request.accountId = 1; + request.errors = []; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('エラーが存在するリクエストが成功する', async () => { + const request = new PostMultipleImportsCompleteRequest(); + request.accountId = 1; + request.errors = [ + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ]; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('名前が足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('emailが足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('errorCodeが足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + email: 'hogehoge@example.com', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('名前が空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: '', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('emailが空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + email: '', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('emailが空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: '', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + }); }); diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 02f1c68..8d448d9 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -44,6 +44,10 @@ import { GetMyUserResponse, PostDeleteUserRequest, PostDeleteUserResponse, + PostMultipleImportsRequest, + PostMultipleImportsResponse, + PostMultipleImportsCompleteRequest, + PostMultipleImportsCompleteResponse, } from './types/types'; import { UsersService } from './users.service'; import { AuthService } from '../auth/auth.service'; @@ -57,6 +61,8 @@ import { ADMIN_ROLES, TIERS } from '../../constants'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log'; import { UserRoles } from '../../common/types/role'; +import { SystemAccessGuard } from '../../common/guards/system/accessguards'; +import { SystemAccessToken } from '../../common/token/types'; @ApiTags('users') @Controller('users') @@ -992,4 +998,149 @@ export class UsersController { await this.usersService.deleteUser(context, body.userId, now); return {}; } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostMultipleImportsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'multipleImports', + description: 'ユーザーを一括登録します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('multiple-imports') + async multipleImports( + @Body() body: PostMultipleImportsRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedToken = jwt.decode(accessToken, { json: true }); + if (!decodedToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, delegateUserId } = decodedToken as AccessToken; + const context = makeContext(userId, requestId, delegateUserId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: 処理を実装 + + return {}; + } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostMultipleImportsCompleteResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'multipleImportsComplate', + description: 'ユーザー一括登録の完了を通知します', + }) + @ApiBearerAuth() + @UseGuards(SystemAccessGuard) + @Post('multiple-imports/complete') + async multipleImportsComplate( + @Body() body: PostMultipleImportsCompleteRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedToken = jwt.decode(accessToken, { json: true }); + if (!decodedToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { systemName } = decodedToken as SystemAccessToken; + const context = makeContext(systemName, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: 処理を実装 + + return {}; + } } From c95fb1e1f66f5b1927ed092e28bd4b98636f396e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Mon, 26 Feb 2024 07:48:08 +0000 Subject: [PATCH 031/109] =?UTF-8?q?Merged=20PR=20784:=20=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E5=A4=B1=E6=95=97=E4=BF=AE=E6=AD=A3=EF=BC=88I/F?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3791: テスト失敗修正(I/F実装)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3791) - AuthorIDのチェックを行うバリデータが特定リスクエストに対してしか利用できなかったので、もっと単純な同一ロジックのバリデータを追加して利用するよう変更 - 上記ケースに対するテストを追加 ## レビューポイント - 既存実装へは影響がなさそうか ## 動作確認状況 - npm run testを通過 --- .../common/validators/authorId.validator.ts | 42 +++++++++++++-- .../src/features/users/types/types.ts | 8 ++- .../features/users/users.controller.spec.ts | 53 +++++++++++++++++++ 3 files changed, 96 insertions(+), 7 deletions(-) diff --git a/dictation_server/src/common/validators/authorId.validator.ts b/dictation_server/src/common/validators/authorId.validator.ts index 23afafb..de721b9 100644 --- a/dictation_server/src/common/validators/authorId.validator.ts +++ b/dictation_server/src/common/validators/authorId.validator.ts @@ -12,10 +12,10 @@ import { import { USER_ROLES } from '../../constants'; // 大文字英数字とアンダースコアのみを許可するバリデータ -@ValidatorConstraint({ name: 'IsAuthorId', async: false }) -export class IsAuthorId implements ValidatorConstraintInterface { +@ValidatorConstraint({ name: 'IsAuthorIdValidConstraint', async: false }) +export class IsAuthorIdValidConstraint implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) { - const request = args.object as SignupRequest | PostUpdateUserRequest; + const request = args.object as SignupRequest | PostUpdateUserRequest; // requestの存在チェック if (!request) { return false; @@ -40,12 +40,44 @@ export class IsAuthorId implements ValidatorConstraintInterface { export function IsAuthorIdValid(validationOptions?: ValidationOptions) { return function (object: object, propertyName: string) { registerDecorator({ - name: 'IsAuthorId', + name: 'IsAuthorIdValidConstraint', target: object.constructor, propertyName: propertyName, constraints: [], options: validationOptions, - validator: IsAuthorId, + validator: IsAuthorIdValidConstraint, + }); + }; +} + +@ValidatorConstraint({ async: false }) +class IsAuhtorIDConstraint implements ValidatorConstraintInterface { + validate(value: any, args: ValidationArguments) { + // null or undefinedであれば不合格 + if (value == null) { + return false; + } + // 文字列型でなければ不合格 + if (typeof value !== 'string') { + return false; + } + + return /^[A-Z0-9_]*$/.test(value); + } + + defaultMessage(args: ValidationArguments) { + return `${args.property} should be uppercase alphanumeric and underscore only`; + } +} + +export function IsAuthorID(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsAuhtorIDConstraint, }); }; } diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 64896eb..822a45b 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -23,7 +23,10 @@ import { } from '../../../common/validators/encryptionPassword.validator'; import { IsRoleAuthorDataValid } from '../../../common/validators/roleAuthor.validator'; import { Type } from 'class-transformer'; -import { IsAuthorIdValid } from '../../../common/validators/authorId.validator'; +import { + IsAuthorID, + IsAuthorIdValid, +} from '../../../common/validators/authorId.validator'; export class ConfirmRequest { @ApiProperty() @@ -288,8 +291,9 @@ export class MultipleImportUser { role: number; @ApiProperty({ required: false }) - @IsAuthorIdValid() @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + @IsAuthorID() + @IsNotEmpty() authorId?: string; @ApiProperty({ description: '0(false)/1(true)' }) diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index fa0bcd3..2f93755 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -115,6 +115,59 @@ describe('UsersController', () => { const errors = await validate(valdationObject); expect(errors.length).toBeGreaterThan(0); }); + it('AuthorIDがルールに違反していた場合、バリデーションエラーが発生する', async () => { + // ルールに合致したAuthorIDではエラーが発生しない + const validAuthorIDs = ['A', '_', 'AB', 'A1', '1A', '_1', 'A_B']; + for await (const authorId of validAuthorIDs) { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: authorId, + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass( + PostMultipleImportsRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + } + + // ルールに違反したAuthorIDではエラーが発生する + const invalidAuthorIDs = ['a', '+', 'AB.', 'Ab', '1a', '_.', 'A/B', '']; + for await (const authorId of invalidAuthorIDs) { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: authorId, + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass( + PostMultipleImportsRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + } + }); it('Authorなのにencryptionがない場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); request.users = [ From 5305984b1a22bf8cfc90402e1139f029c64aaa64 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Tue, 27 Feb 2024 00:01:02 +0000 Subject: [PATCH 032/109] =?UTF-8?q?Merged=20PR=20764:=20=E7=AC=AC=E4=BA=94?= =?UTF-8?q?=E9=9A=8E=E5=B1=A4=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E7=94=BB=E9=9D=A2=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3709: 第五階層ライセンス情報画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3709) - ストレージ使用可否切り替えの画面実装をしました - 動作確認中に、既存実装でライセンスオーダーするときとカードライセンスアクティベートするときの操作不能化処理に漏れがあったのを修正しました ## レビューポイント - Redux周りの実装でお作法に違反しているところがないか。もしくは改善点ないか。 - ライセンス情報表示のAPI結果待ち部分のローディング処理で、最低限の改善にしたが現時点ではこれでよいか?(いつ修正するかも未定だけど、実害はないためひとまずこんな感じで。。。) - `licenseSummarySlice.ts` のコメント部分が該当箇所です ## 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/Task3709?csf=1&web=1&e=bJVzss ## 動作確認状況 - ローカルで動作確認しました。 --- dictation_client/src/api/api.ts | 94 +++++++++++++++++++ .../licenseCardActivateSlice.ts | 12 +++ .../licenseSummary/licenseSummarySlice.ts | 24 ++++- .../license/licenseSummary/operations.ts | 56 +++++++++++ .../license/licenseSummary/selectors.ts | 5 +- .../pages/LicensePage/licenseOrderPopup.tsx | 3 +- .../src/pages/LicensePage/licenseSummary.tsx | 63 ++++++++++++- dictation_client/src/styles/app.module.scss | 12 +++ .../src/styles/app.module.scss.d.ts | 1 + dictation_client/src/translation/de.json | 6 +- dictation_client/src/translation/en.json | 6 +- dictation_client/src/translation/es.json | 6 +- dictation_client/src/translation/fr.json | 6 +- 13 files changed, 284 insertions(+), 10 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 0b3aacf..e166e6f 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -2121,6 +2121,25 @@ export interface UpdateOptionItemsRequest { */ 'optionItems': Array; } +/** + * + * @export + * @interface UpdateRestrictionStatusRequest + */ +export interface UpdateRestrictionStatusRequest { + /** + * 操作対象の第五階層アカウントID + * @type {number} + * @memberof UpdateRestrictionStatusRequest + */ + 'accountId': number; + /** + * 制限をかけるかどうか(trur:制限をかける) + * @type {boolean} + * @memberof UpdateRestrictionStatusRequest + */ + 'restricted': boolean; +} /** * * @export @@ -3443,6 +3462,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus: async (updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateRestrictionStatusRequest' is not null or undefined + assertParamExists('updateRestrictionStatus', 'updateRestrictionStatusRequest', updateRestrictionStatusRequest) + const localVarPath = `/accounts/restriction-status`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateRestrictionStatusRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary @@ -3888,6 +3947,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.updateOptionItems']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateRestrictionStatus(updateRestrictionStatusRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateRestrictionStatus']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary @@ -4192,6 +4264,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: any): AxiosPromise { return localVarFp.updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: any): AxiosPromise { + return localVarFp.updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary @@ -4544,6 +4626,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary diff --git a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts index f4530e7..06781a3 100644 --- a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts +++ b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts @@ -1,5 +1,6 @@ import { createSlice } from "@reduxjs/toolkit"; import { LicenseCardActivateState } from "./state"; +import { activateCardLicenseAsync } from "./operations"; const initialState: LicenseCardActivateState = { apps: { @@ -14,6 +15,17 @@ export const licenseCardActivateSlice = createSlice({ state.apps = initialState.apps; }, }, + extraReducers: (builder) => { + builder.addCase(activateCardLicenseAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(activateCardLicenseAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(activateCardLicenseAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + }, }); export const { cleanupApps } = licenseCardActivateSlice.actions; diff --git a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts index e9d9816..283661a 100644 --- a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts +++ b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts @@ -1,6 +1,10 @@ import { createSlice } from "@reduxjs/toolkit"; import { LicenseSummaryState } from "./state"; -import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations"; +import { + getCompanyNameAsync, + getLicenseSummaryAsync, + updateRestrictionStatusAsync, +} from "./operations"; const initialState: LicenseSummaryState = { domain: { @@ -35,12 +39,30 @@ export const licenseSummarySlice = createSlice({ }, }, extraReducers: (builder) => { + builder.addCase(getLicenseSummaryAsync.pending, (state) => { + state.apps.isLoading = true; + }); builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => { state.domain.licenseSummaryInfo = action.payload; + state.apps.isLoading = false; }); + builder.addCase(getLicenseSummaryAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + // 画面側ではgetLicenseSummaryAsyncと並行して呼び出されているため、レーシングを考慮してこちらではisLoadingを更新しない + // 本来は両方の完了を待ってからisLoadingを更新するべきだが、現時点ではスピード重視のためケアしない。 builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => { state.domain.accountInfo.companyName = action.payload.companyName; }); + builder.addCase(updateRestrictionStatusAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateRestrictionStatusAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateRestrictionStatusAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/license/licenseSummary/operations.ts b/dictation_client/src/features/license/licenseSummary/operations.ts index a5f18f8..f36966f 100644 --- a/dictation_client/src/features/license/licenseSummary/operations.ts +++ b/dictation_client/src/features/license/licenseSummary/operations.ts @@ -8,6 +8,7 @@ import { GetCompanyNameResponse, GetLicenseSummaryResponse, PartnerLicenseInfo, + UpdateRestrictionStatusRequest, } from "../../../api/api"; import { Configuration } from "../../../api/configuration"; import { ErrorObject, createErrorObject } from "../../../common/errors"; @@ -123,3 +124,58 @@ export const getCompanyNameAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const updateRestrictionStatusAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + accountId: number; + restricted: boolean; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/updateRestrictionStatusAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + const requestParam: UpdateRestrictionStatusRequest = { + accountId: args.accountId, + restricted: args.restricted, + }; + + try { + await accountApi.updateRestrictionStatus(requestParam, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + + // このAPIでは個別のエラーメッセージは不要 + const errorMessage = getTranslationID("common.message.internalServerError"); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/license/licenseSummary/selectors.ts b/dictation_client/src/features/license/licenseSummary/selectors.ts index 79ba5e9..d760a9e 100644 --- a/dictation_client/src/features/license/licenseSummary/selectors.ts +++ b/dictation_client/src/features/license/licenseSummary/selectors.ts @@ -1,10 +1,11 @@ import { RootState } from "app/store"; // 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する -export const selecLicenseSummaryInfo = (state: RootState) => +export const selectLicenseSummaryInfo = (state: RootState) => state.licenseSummary.domain.licenseSummaryInfo; export const selectCompanyName = (state: RootState) => state.licenseSummary.domain.accountInfo.companyName; -export const selectIsLoading = (state: RootState) => state.license; +export const selectIsLoading = (state: RootState) => + state.licenseSummary.apps.isLoading; diff --git a/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx b/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx index 8d46a79..19d14ed 100644 --- a/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx +++ b/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx @@ -41,9 +41,10 @@ export const LicenseOrderPopup: React.FC = (props) => { // ポップアップを閉じる処理 const closePopup = useCallback(() => { + if (isLoading) return; setIsPushOrderButton(false); onClose(); - }, [onClose]); + }, [isLoading, onClose]); // 画面からのパラメータ const poNumber = useSelector(selectPoNumber); diff --git a/dictation_client/src/pages/LicensePage/licenseSummary.tsx b/dictation_client/src/pages/LicensePage/licenseSummary.tsx index 0314a33..586dca8 100644 --- a/dictation_client/src/pages/LicensePage/licenseSummary.tsx +++ b/dictation_client/src/pages/LicensePage/licenseSummary.tsx @@ -10,12 +10,16 @@ import { useDispatch, useSelector } from "react-redux"; import { getCompanyNameAsync, getLicenseSummaryAsync, - selecLicenseSummaryInfo, + selectLicenseSummaryInfo, selectCompanyName, + selectIsLoading, + updateRestrictionStatusAsync, } from "features/license/licenseSummary"; import { selectSelectedRow } from "features/license/partnerLicense"; import { selectDelegationAccessToken } from "features/auth/selectors"; import { DelegationBar } from "components/delegate"; +import { TIERS } from "components/auth/constants"; +import { isAdminUser, isApproveTier } from "features/auth/utils"; import postAdd from "../../assets/images/post_add.svg"; import history from "../../assets/images/history.svg"; import key from "../../assets/images/key.svg"; @@ -40,6 +44,8 @@ export const LicenseSummary: React.FC = ( // 代行操作用のトークンを取得する const delegationAccessToken = useSelector(selectDelegationAccessToken); + const isLoading = useSelector(selectIsLoading); + // popup制御関係 const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false); const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] = @@ -62,9 +68,12 @@ export const LicenseSummary: React.FC = ( }, [setIsLicenseOrderHistoryOpen]); // apiからの値取得関係 - const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo); + const licenseSummaryInfo = useSelector(selectLicenseSummaryInfo); const companyName = useSelector(selectCompanyName); + const isTier1 = isApproveTier([TIERS.TIER1]); + const isAdmin = isAdminUser(); + useEffect(() => { dispatch(getLicenseSummaryAsync({ selectedRow })); dispatch(getCompanyNameAsync({ selectedRow })); @@ -78,6 +87,35 @@ export const LicenseSummary: React.FC = ( } }, [onReturn]); + const onStorageAvailableChange = useCallback( + async (e: React.ChangeEvent) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm( + t( + getTranslationID( + "LicenseSummaryPage.message.storageUnavalableSwitchingConfirm" + ) + ) + ) + ) { + return; + } + + const restricted = e.target.checked; + const accountId = selectedRow?.accountId; + // 本関数が実行されるときはselectedRowが存在する前提のため、accountIdが存在しない場合の処理は不要 + if (!accountId) return; + const { meta } = await dispatch( + updateRestrictionStatusAsync({ accountId, restricted }) + ); + if (meta.requestStatus === "fulfilled") { + dispatch(getLicenseSummaryAsync({ selectedRow })); + } + }, + [dispatch, selectedRow, t] + ); + return ( <> {/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */} @@ -272,6 +310,27 @@ export const LicenseSummary: React.FC = (
    + {isTier1 && isAdmin && ( +

    + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +

    + )}
    diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 0753e75..8e77dfa 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1857,6 +1857,18 @@ tr.isSelected .menuInTable li a.isDisable { cursor: pointer; } +.license .checkAvail { + height: 30px; + padding: 0 0.3rem 0.3rem 0; + margin-top: -30px; + box-sizing: border-box; +} +.license .checkAvail label { + cursor: pointer; +} +.license .checkAvail label .formCheck { + vertical-align: middle; +} .license .listVertical dd img[src*="circle"] { filter: brightness(0) saturate(100%) invert(58%) sepia(41%) saturate(5814%) hue-rotate(143deg) brightness(96%) contrast(101%); diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index d6ebd8f..656d36a 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -123,6 +123,7 @@ declare const classNames: { readonly txNormal: "txNormal"; readonly manageIcon: "manageIcon"; readonly manageIconClose: "manageIconClose"; + readonly checkAvail: "checkAvail"; readonly history: "history"; readonly cardHistory: "cardHistory"; readonly partner: "partner"; diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index a61cd85..9730841 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -188,7 +188,11 @@ "usedSize": "Gebrauchter Lagerung", "storageAvailable": "Speicher nicht verfügbar (Menge überschritten)", "licenseLabel": "Lizenz", - "storageLabel": "Lagerung" + "storageLabel": "Lagerung", + "storageUnavailableCheckbox": "(de)Storage Unavailable" + }, + "message": { + "storageUnavalableSwitchingConfirm": "(de)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" } }, "licenseOrderPage": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 263b66d..959ea1d 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -188,7 +188,11 @@ "usedSize": "Storage Used", "storageAvailable": "Storage Unavailable (Exceeded Amount)", "licenseLabel": "License", - "storageLabel": "Storage" + "storageLabel": "Storage", + "storageUnavailableCheckbox": "Storage Unavailable" + }, + "message": { + "storageUnavalableSwitchingConfirm": "対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" } }, "licenseOrderPage": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index c63b5c3..f89d531 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -188,7 +188,11 @@ "usedSize": "Almacenamiento utilizado", "storageAvailable": "Almacenamiento no disponible (cantidad excedida)", "licenseLabel": "Licencia", - "storageLabel": "Almacenamiento" + "storageLabel": "Almacenamiento", + "storageUnavailableCheckbox": "(es)Storage Unavailable" + }, + "message": { + "storageUnavalableSwitchingConfirm": "(es)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" } }, "licenseOrderPage": { diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index f290e40..b313ce4 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -188,7 +188,11 @@ "usedSize": "Stockage utilisé", "storageAvailable": "Stockage indisponible (montant dépassée)", "licenseLabel": "Licence", - "storageLabel": "Stockage" + "storageLabel": "Stockage", + "storageUnavailableCheckbox": "(fr)Storage Unavailable" + }, + "message": { + "storageUnavalableSwitchingConfirm": "(fr)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" } }, "licenseOrderPage": { From dd8bddc971952c1f205bfe09f196c10ab1806fff Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Tue, 27 Feb 2024 02:49:52 +0000 Subject: [PATCH 033/109] =?UTF-8?q?Merged=20PR=20771:=20=E9=9F=B3=E5=A3=B0?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E5=AE=8C=E4=BA=86API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E3=82=B9=E3=83=88=E3=83=AC=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=87=8F=E8=B6=85=E9=81=8E=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=82=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3687: 音声ファイルアップロード完了API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3687) - 音声ファイルアップロード完了API実行時に、ストレージの使用量チェックを行い、必要ならメール送信をする実装を追加しました。 ## レビューポイント - 使用量チェックメソッドで他にいい関数名ないか? - なるべく既存実装をいじりたくなかったので自動ルーティング前にチェック機構を配置したが不都合ないか? - テストケースに過不足ないか - 自動テストの実行方法や確認方法として適切か?ほかに代替案ないか? ## 動作確認状況 - ローカルでUT通ることを確認。 - 実際のメール送信はdeveop動作確認でやります。 --- dictation_server/src/constants/index.ts | 6 + .../src/features/files/files.module.ts | 6 + .../src/features/files/files.service.spec.ts | 352 +++++++++++++++++- .../src/features/files/files.service.ts | 147 ++++++++ .../src/features/files/test/utility.ts | 6 +- .../src/gateways/sendgrid/sendgrid.service.ts | 164 ++++++++ .../accounts/accounts.repository.service.ts | 63 +++- .../src/templates/template_U_118.html | 94 +++++ .../src/templates/template_U_118.txt | 44 +++ .../templates/template_U_118_no_parent.html | 83 +++++ .../templates/template_U_118_no_parent.txt | 38 ++ .../src/templates/template_U_119.html | 94 +++++ .../src/templates/template_U_119.txt | 44 +++ .../templates/template_U_119_no_parent.html | 83 +++++ .../templates/template_U_119_no_parent.txt | 38 ++ 15 files changed, 1256 insertions(+), 6 deletions(-) create mode 100644 dictation_server/src/templates/template_U_118.html create mode 100644 dictation_server/src/templates/template_U_118.txt create mode 100644 dictation_server/src/templates/template_U_118_no_parent.html create mode 100644 dictation_server/src/templates/template_U_118_no_parent.txt create mode 100644 dictation_server/src/templates/template_U_119.html create mode 100644 dictation_server/src/templates/template_U_119.txt create mode 100644 dictation_server/src/templates/template_U_119_no_parent.html create mode 100644 dictation_server/src/templates/template_U_119_no_parent.txt diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index cc23b64..6478a26 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -333,3 +333,9 @@ export const FILE_RETENTION_DAYS_DEFAULT = 30; * @const {number} */ export const STORAGE_SIZE_PER_LICENSE = 5; + +/** + * ストレージ使用量の警告閾値(%) + * @const {number} + */ +export const STORAGE_WARNING_THRESHOLD_PERCENT = 80; diff --git a/dictation_server/src/features/files/files.module.ts b/dictation_server/src/features/files/files.module.ts index 56ba499..9d087d8 100644 --- a/dictation_server/src/features/files/files.module.ts +++ b/dictation_server/src/features/files/files.module.ts @@ -10,6 +10,9 @@ import { TemplateFilesRepositoryModule } from '../../repositories/template_files import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module'; import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module'; import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; +import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; +import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; +import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; @Module({ imports: [ @@ -22,6 +25,9 @@ import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.r UserGroupsRepositoryModule, NotificationhubModule, LicensesRepositoryModule, + SendGridModule, + AdB2cModule, + AccountsRepositoryModule, ], providers: [FilesService], controllers: [FilesController], diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index deff777..a849975 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -18,7 +18,10 @@ import { makeTestUser, } from '../../common/test/utility'; import { makeTestingModule } from '../../common/test/modules'; -import { overrideBlobstorageService } from '../../common/test/overrides'; +import { + overrideAdB2cService, + overrideBlobstorageService, +} from '../../common/test/overrides'; import { createTemplateFile, getTemplateFiles, @@ -768,6 +771,353 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { // 自動ルーティングが行われていないことを確認 expect(resultCheckoutPermission.length).toEqual(0); }); + it('第五階層アカウントのストレージ使用量が閾値と同値の場合、メール送信が行われない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + // 閾値は、10GB * 0.8 = 8GBとなる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 3つの音声ファイルを事前作成+uploadFinishedで、合計8GBのストレージ使用量状態を作成 + for (let i = 0; i < 3; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + const service = module.get(FilesService); + const spy = jest + .spyOn(service['sendGridService'], 'sendMail') + .mockImplementation(); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).not.toHaveBeenCalled(); + }); + it('第五階層アカウントのストレージ使用量が閾値+1byteの場合、U-118メール送信が行われる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + // 閾値は、10GB * 0.8 = 8GBとなる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 3つの音声ファイルを事前作成+uploadFinishedで、合計8GB+1byteのストレージ使用量状態を作成 + for (let i = 0; i < 3; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + const service = module.get(FilesService); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendGridService'], 'sendMailWithU118') + .mockImplementation(); + overrideAdB2cService(service, { + getUsers: async () => [], + }); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize + 1, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).toHaveBeenCalledTimes(1); + }); + it('第五階層アカウントのストレージ使用量が上限と同値の場合、U-118メール送信が行われる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 4つの音声ファイルを事前作成+uploadFinishedで、合計10GBのストレージ使用量状態を作成 + for (let i = 0; i < 4; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + const service = module.get(FilesService); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendGridService'], 'sendMailWithU118') + .mockImplementation(); + overrideAdB2cService(service, { + getUsers: async () => [], + }); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).toHaveBeenCalledTimes(1); + }); + it('第五階層アカウントのストレージ使用量が上限+1byteと同値の場合、U-119メール送信が行われる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 第一階層アカウントを作成する + await makeTestAccount(source, { + tier: 1, + }); + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 4つの音声ファイルを事前作成+uploadFinishedで、合計10GB+1byteのストレージ使用量状態を作成 + for (let i = 0; i < 4; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + const service = module.get(FilesService); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendGridService'], 'sendMailWithU119') + .mockImplementation(); + overrideAdB2cService(service, { + getUsers: async () => [], + }); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize + 1, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).toHaveBeenCalledTimes(1); + }); it('日付フォーマットが不正な場合、エラーを返却する', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 1f9c042..5736ac2 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -6,6 +6,7 @@ import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.servi import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types'; import { OPTION_ITEM_NUM, + STORAGE_WARNING_THRESHOLD_PERCENT, TASK_STATUS, TIERS, USER_LICENSE_STATUS, @@ -37,11 +38,18 @@ import { LicenseNotAllocatedError, } from '../../repositories/licenses/errors/types'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; +import { DateWithZeroTime } from '../licenses/types/types'; +import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; +import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; +import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; +import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; @Injectable() export class FilesService { private readonly logger = new Logger(FilesService.name); constructor( + private readonly accountsRepository: AccountsRepositoryService, + private readonly adB2cService: AdB2cService, private readonly usersRepository: UsersRepositoryService, private readonly tasksRepository: TasksRepositoryService, private readonly tasksRepositoryService: TasksRepositoryService, @@ -50,6 +58,7 @@ export class FilesService { private readonly userGroupsRepositoryService: UserGroupsRepositoryService, private readonly notificationhubService: NotificationhubService, private readonly licensesRepository: LicensesRepositoryService, + private readonly sendGridService: SendGridService, ) {} /** @@ -211,11 +220,20 @@ export class FilesService { ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.uploadFinished.name}`, + ); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } + + // 第五階層アカウントはストレージ使用量超過チェックをする。順番は自動ルーティングの前後どちらでも構わない。 + if (user.account?.tier === TIERS.TIER5) { + await this.checkAndAlertStorageUsage(context, user.account_id); + } + try { // ルーティング設定に従い、チェックアウト権限を付与する const { typistGroupIds, typistIds } = @@ -271,6 +289,135 @@ export class FilesService { } } + /** + * ストレージ使用量を確認し、上限または閾値を超えていた場合に通知を行う + * @param context + * @param accountId 確認対象のアカウントID + * @returns + */ + private async checkAndAlertStorageUsage( + context: Context, + accountId: number, + ): Promise { + try { + const currentDate = new DateWithZeroTime(); + const { size, used } = await this.licensesRepository.getStorageInfo( + context, + accountId, + currentDate, + ); + const storageShresholdSize = + (size * STORAGE_WARNING_THRESHOLD_PERCENT) / 100; + + if (used > size) { + this.logger.log( + `[${context.getTrackingId()}] ${ + this.checkAndAlertStorageUsage.name + } | Storage usage is over limit. accountId=${accountId}, size=${size}, used=${used}`, + ); + const tier1AccountId = ( + await this.accountsRepository.findTier1Account(context) + ).id; + const tire1AdminMails = ( + await this.getAccountInformation(context, tier1AccountId) + ).adminEmails; + + const dealer = await this.accountsRepository.findParentAccount( + context, + accountId, + ); + const dealerName: string | null = dealer?.company_name ?? null; + const { companyName, adminEmails } = await this.getAccountInformation( + context, + accountId, + ); + await this.sendGridService.sendMailWithU119( + context, + adminEmails, + companyName, + dealerName, + tire1AdminMails, + ); + return; + } + + if (used > storageShresholdSize) { + this.logger.log( + `[${context.getTrackingId()}] ${ + this.checkAndAlertStorageUsage.name + } | Storage usage is over shresholdSize. accountId=${accountId}, size=${size}, storageShresholdSize=${storageShresholdSize}`, + ); + const dealer = await this.accountsRepository.findParentAccount( + context, + accountId, + ); + const dealerName: string | null = dealer?.company_name ?? null; + const { companyName, adminEmails } = await this.getAccountInformation( + context, + accountId, + ); + await this.sendGridService.sendMailWithU118( + context, + adminEmails, + companyName, + dealerName, + ); + + return; + } + } catch (error) { + // uploadする度にストレージ使用量チェックするため、一連の処理に失敗しても例外は握りつぶす + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + } + } + + /** + * アカウントIDを指定して、アカウント情報と管理者情報を取得する + * @param context + * @param accountId 対象アカウントID + * @returns 企業名/管理者メールアドレス + */ + private async getAccountInformation( + context: Context, + accountId: number, + ): Promise<{ + companyName: string; + adminEmails: string[]; + }> { + // アカウントIDから企業名を取得する + const { company_name } = await this.accountsRepository.findAccountById( + context, + accountId, + ); + + // 管理者一覧を取得 + const admins = await this.usersRepository.findAdminUsers( + context, + accountId, + ); + const adminExternalIDs = admins.map((x) => x.external_id); + + // ADB2Cから管理者IDを元にメールアドレスを取得する + const usersInfo = await this.adB2cService.getUsers( + context, + adminExternalIDs, + ); + + // 生のAzure AD B2Cのユーザー情報からメールアドレスを抽出する + const adminEmails = usersInfo.map((x) => { + const { emailAddress } = getUserNameAndMailAddress(x); + if (emailAddress == null) { + throw new Error('admin email-address is not found'); + } + return emailAddress; + }); + + return { + companyName: company_name, + adminEmails: adminEmails, + }; + } + /** * Publishs upload sas * @param companyName diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index 79766a7..c15f981 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -54,6 +54,8 @@ export const createTask = async ( typist_user_id?: number | undefined, author_id?: string | undefined, owner_user_id?: number | undefined, + fileSize?: number | undefined, + jobNumber?: string | undefined, ): Promise<{ audioFileId: number }> => { const { identifiers: audioFileIdentifiers } = await datasource .getRepository(AudioFile) @@ -68,7 +70,7 @@ export const createTask = async ( duration: '100000', finished_at: new Date(), uploaded_at: new Date(), - file_size: 10000, + file_size: fileSize ?? 10000, priority: '00', audio_format: 'audio_format', is_encrypted: true, @@ -88,7 +90,7 @@ export const createTask = async ( const templateFile = templateFileIdentifiers.pop() as TemplateFile; await datasource.getRepository(Task).insert({ - job_number: '00000001', + job_number: jobNumber ?? '00000001', account_id: account_id, is_job_number_enabled: true, audio_file_id: audioFile.id, diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index af901f1..ea80ba7 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -63,6 +63,14 @@ export class SendGridService { private readonly templateU116Text: string; private readonly templateU117Html: string; private readonly templateU117Text: string; + private readonly templateU118Html: string; + private readonly templateU118Text: string; + private readonly templateU118NoParentHtml: string; + private readonly templateU118NoParentText: string; + private readonly templateU119Html: string; + private readonly templateU119Text: string; + private readonly templateU119NoParentHtml: string; + private readonly templateU119NoParentText: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); @@ -209,6 +217,44 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_117.txt`), 'utf-8', ); + this.templateU118Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_118.html`), + 'utf-8', + ); + this.templateU118Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_118.txt`), + 'utf-8', + ); + this.templateU118NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_118_no_parent.html`, + ), + 'utf-8', + ); + this.templateU118NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_118_no_parent.txt`), + 'utf-8', + ); + this.templateU119Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_119.html`), + 'utf-8', + ); + this.templateU119Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_119.txt`), + 'utf-8', + ); + this.templateU119NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_119_no_parent.html`, + ), + 'utf-8', + ); + this.templateU119NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_119_no_parent.txt`), + 'utf-8', + ); } } @@ -974,6 +1020,124 @@ export class SendGridService { } } + /** + * U-118のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @returns mail with u118 + */ + async sendMailWithU118( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU118.name}`, + ); + try { + const subject = 'Storage Usage Worning Notification [U-118]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU118NoParentHtml.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + text = this.templateU118NoParentText.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + } else { + html = this.templateU118Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + text = this.templateU118Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU118.name}`, + ); + } + } + + /** + * U-119のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param tire1AdminMails 第一階層の管理者(primary/secondary)のメールアドレス + * @returns mail with u119 + */ + async sendMailWithU119( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + tire1AdminMails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU119.name}`, + ); + try { + const subject = 'Storage Usage Exceeded Notification [U-119]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU119NoParentHtml.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + text = this.templateU119NoParentText.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + } else { + html = this.templateU119Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + text = this.templateU119Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + tire1AdminMails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU119.name}`, + ); + } + } + /** * メールを送信する * @param context diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 11464b1..a2d4ad5 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -279,6 +279,63 @@ export class AccountsRepositoryService { return account; } + /** + * OMDSTokyoのアカウント情報を取得する + * @param id + * @returns account + */ + async findTier1Account(context: Context): Promise { + const account = await this.dataSource.getRepository(Account).findOne({ + where: { + tier: TIERS.TIER1, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (!account) { + throw new AccountNotFoundError(`Account is Not Found.`); + } + return account; + } + + /** + * 指定したアカウントの親アカウント情報を取得します(存在しない場合はnullを返します) + * @param context + * @param accountId + * @returns parent account + */ + async findParentAccount( + context: Context, + accountId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountsRepo = entityManager.getRepository(Account); + const myAccount = await accountsRepo.findOne({ + where: { + id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (!myAccount) { + throw new AccountNotFoundError(`Target Account is Not Found.`); + } + + if (!myAccount.parent_account_id) { + // 親アカウントが存在しない場合は明示的にnullを返す + return null; + } + + const parentAccount = await accountsRepo.findOne({ + where: { + id: myAccount.parent_account_id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + return parentAccount; + }); + } + /** * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定 * 有効期限が現在日付からしきい値以内のライセンス数を取得する @@ -942,11 +999,11 @@ export class AccountsRepositoryService { /** * 一階層上のアカウントを取得する - * @param accountId - * @param tier + * @param accountId 自身の一階層上のアカウントID(子アカウントではない) + * @param tier 自身のアカウントの階層 * @returns account: 一階層上のアカウント */ - async getOneUpperTierAccount( + private async getOneUpperTierAccount( context: Context, accountId: number, tier: number, diff --git a/dictation_server/src/templates/template_U_118.html b/dictation_server/src/templates/template_U_118.html new file mode 100644 index 0000000..acad01a --- /dev/null +++ b/dictation_server/src/templates/template_U_118.html @@ -0,0 +1,94 @@ + + + Storage Usage Worning Notification [U-118] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    + The storage usage for your account has reached 80% of its usage limit. + Functions related to the Dictation Workfrow will be restricted until the + storage usage becomes lower than the limit. +

    +

    + Please remove Dictations files once the transcription is completed or + add capacity by assigning a license to a new user. 5GB of storage will + be provided to the account for each active user. +

    +

    + For detailed information, please sign in to ODMS Cloud and check the + "Subscription" tab. +

    +

    + If you need support regarding ODMS Cloud, please contact $DEALER_NAME$. +

    +

    + If you have received this e-mail in error, please delete this e-mail + from your system.
    + This is an automatically generated e-mail and this mailbox is not + monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

    + Die Speichernutzung Ihres Kontos hat 80 % des Nutzungslimits erreicht. + Funktionen im Zusammenhang mit dem Dictation Workfrow werden + eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. +

    +

    + Bitte entfernen Sie Diktatdateien, sobald die Transkription + abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen + Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem + Konto 5 GB Speicherplatz zur Verfügung gestellt. +

    +

    + Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an + und überprüfen Sie die Registerkarte „Abonnement“. +

    +

    + Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich + bitte an $DEALER_NAME$. +

    +

    + Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie + diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und dieses Postfach wird + nicht überwacht. Bitte nicht antworten. +

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

    + L'utilisation du stockage pour votre compte a atteint 80 % de sa limite + d'utilisation. Les fonctions liées au Workfrow de dictée seront + restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure + à la limite. +

    +

    + Veuillez supprimer les fichiers de dictées une fois la transcription + terminée ou ajouter de la capacité en attribuant une licence à un nouvel + utilisateur. 5 Go de stockage seront fournis au compte pour chaque + utilisateur actif. +

    +

    + Pour des informations détaillées, veuillez vous connecter à ODMS Cloud + et consulter l'onglet « Abonnement ». +

    +

    + Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez + contacter $DEALER_NAME$. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail + de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres + n'est pas surveillée. Merci de ne pas répondre. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_118.txt b/dictation_server/src/templates/template_U_118.txt new file mode 100644 index 0000000..4cc5cf5 --- /dev/null +++ b/dictation_server/src/templates/template_U_118.txt @@ -0,0 +1,44 @@ + + +Dear $CUSTOMER_NAME$, + +The storage usage for your account has reached 80% of its usage limit. Functions related to the Dictation Workfrow will be restricted until the storage usage becomes lower than the limit. + +Please remove Dictations files once the transcription is completed or add capacity by assigning a license to a new user. 5GB of storage will be provided to the account for each active user. + +For detailed information, please sign in to ODMS Cloud and check the "Subscription" tab. + +If you need support regarding ODMS Cloud, please contact $DEALER_NAME$. + +If you have received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Die Speichernutzung Ihres Kontos hat 80 % des Nutzungslimits erreicht. Funktionen im Zusammenhang mit dem Dictation Workfrow werden eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. + +Bitte entfernen Sie Diktatdateien, sobald die Transkription abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem Konto 5 GB Speicherplatz zur Verfügung gestellt. + +Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an und überprüfen Sie die Registerkarte Pour des informations détaillées, veuillez vous connecter à ODMS Cloud et consulter l'onglet « Abonnement ». + +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. + +Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten. + + + +Chère/Cher $CUSTOMER_NAME$, + +L'utilisation du stockage pour votre compte a atteint 80 % de sa limite d'utilisation. Les fonctions liées au Workfrow de dictée seront restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure à la limite. + +Veuillez supprimer les fichiers de dictées une fois la transcription terminée ou ajouter de la capacité en attribuant une licence à un nouvel utilisateur. 5 Go de stockage seront fournis au compte pour chaque utilisateur actif. + +Pour des informations détaillées, veuillez vous connecter à ODMS Cloud et consulter l'onglet « Abonnement ». + +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_118_no_parent.html b/dictation_server/src/templates/template_U_118_no_parent.html new file mode 100644 index 0000000..e5ce598 --- /dev/null +++ b/dictation_server/src/templates/template_U_118_no_parent.html @@ -0,0 +1,83 @@ + + + Storage Usage Worning Notification [U-118] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    + The storage usage for your account has reached 80% of its usage limit. + Functions related to the Dictation Workfrow will be restricted until the + storage usage becomes lower than the limit. +

    +

    + Please remove Dictations files once the transcription is completed or + add capacity by assigning a license to a new user. 5GB of storage will + be provided to the account for each active user. +

    +

    + For detailed information, please sign in to ODMS Cloud and check the + "Subscription" tab. +

    +

    + If you have received this e-mail in error, please delete this e-mail + from your system.
    + This is an automatically generated e-mail and this mailbox is not + monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

    + Die Speichernutzung Ihres Kontos hat 80 % des Nutzungslimits erreicht. + Funktionen im Zusammenhang mit dem Dictation Workfrow werden + eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. +

    +

    + Bitte entfernen Sie Diktatdateien, sobald die Transkription + abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen + Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem + Konto 5 GB Speicherplatz zur Verfügung gestellt. +

    +

    + Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an + und überprüfen Sie die Registerkarte „Abonnement“. +

    +

    + Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie + diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und dieses Postfach wird + nicht überwacht. Bitte nicht antworten. +

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

    + L'utilisation du stockage pour votre compte a atteint 80 % de sa limite + d'utilisation. Les fonctions liées au Workfrow de dictée seront + restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure + à la limite. +

    +

    + Veuillez supprimer les fichiers de dictées une fois la transcription + terminée ou ajouter de la capacité en attribuant une licence à un nouvel + utilisateur. 5 Go de stockage seront fournis au compte pour chaque + utilisateur actif. +

    +

    + Pour des informations détaillées, veuillez vous connecter à ODMS Cloud + et consulter l'onglet « Abonnement ». +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail + de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres + n'est pas surveillée. Merci de ne pas répondre. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_118_no_parent.txt b/dictation_server/src/templates/template_U_118_no_parent.txt new file mode 100644 index 0000000..f7378f2 --- /dev/null +++ b/dictation_server/src/templates/template_U_118_no_parent.txt @@ -0,0 +1,38 @@ + + +Dear $CUSTOMER_NAME$, + +The storage usage for your account has reached 80% of its usage limit. Functions related to the Dictation Workfrow will be restricted until the storage usage becomes lower than the limit. + +Please remove Dictations files once the transcription is completed or add capacity by assigning a license to a new user. 5GB of storage will be provided to the account for each active user. + +For detailed information, please sign in to ODMS Cloud and check the "Subscription" tab. + +If you have received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Die Speichernutzung Ihres Kontos hat 80 % des Nutzungslimits erreicht. Funktionen im Zusammenhang mit dem Dictation Workfrow werden eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. + +Bitte entfernen Sie Diktatdateien, sobald die Transkription abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem Konto 5 GB Speicherplatz zur Verfügung gestellt. + +Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an und überprüfen Sie die Registerkarte Pour des informations détaillées, veuillez vous connecter à ODMS Cloud et consulter l'onglet « Abonnement ». + +Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten. + + + +Chère/Cher $CUSTOMER_NAME$, + +L'utilisation du stockage pour votre compte a atteint 80 % de sa limite d'utilisation. Les fonctions liées au Workfrow de dictée seront restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure à la limite. + +Veuillez supprimer les fichiers de dictées une fois la transcription terminée ou ajouter de la capacité en attribuant une licence à un nouvel utilisateur. 5 Go de stockage seront fournis au compte pour chaque utilisateur actif. + +Pour des informations détaillées, veuillez vous connecter à ODMS Cloud et consulter l'onglet « Abonnement ». + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_119.html b/dictation_server/src/templates/template_U_119.html new file mode 100644 index 0000000..0264339 --- /dev/null +++ b/dictation_server/src/templates/template_U_119.html @@ -0,0 +1,94 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    + The storage usage for your account has exceeded the usage limit. + Functions related to the Dictation Workfrow will be restricted until the + storage usage becomes lower than the limit. +

    +

    + Please remove Dictations files once the transcription is completed or + add capacity by assigning a license to a new user. 5GB of storage will + be provided to the account for each active user. +

    +

    + For detailed information, please sign in to ODMS Cloud and check the + "Subscription" tab. +

    +

    + If you need support regarding ODMS Cloud, please contact $DEALER_NAME$. +

    +

    + If you have received this e-mail in error, please delete this e-mail + from your system.
    + This is an automatically generated e-mail and this mailbox is not + monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

    + Die Speichernutzung Ihres Kontos hat das Nutzungslimit überschritten. + Funktionen im Zusammenhang mit dem Dictation Workfrow werden + eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. +

    +

    + Bitte entfernen Sie Diktatdateien, sobald die Transkription + abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen + Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem + Konto 5 GB Speicherplatz zur Verfügung gestellt. +

    +

    + Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an + und überprüfen Sie die Registerkarte „Abonnement“. +

    +

    + Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich + bitte an $DEALER_NAME$. +

    +

    + Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie + diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und dieses Postfach wird + nicht überwacht. Bitte nicht antworten. +

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

    + L'utilisation du stockage pour votre compte a dépassé la limite + d'utilisation. Les fonctions liées au Workfrow de dictée seront + restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure + à la limite. +

    +

    + Veuillez supprimer les fichiers de dictées une fois la transcription + terminée ou ajouter de la capacité en attribuant une licence à un nouvel + utilisateur. 5 Go de stockage seront fournis au compte pour chaque + utilisateur actif. +

    +

    + Pour des informations détaillées, veuillez vous connecter à ODMS Cloud + et consulter l'onglet « Abonnement ». +

    +

    + Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez + contacter $DEALER_NAME$. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail + de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres + n'est pas surveillée. Merci de ne pas répondre. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_119.txt b/dictation_server/src/templates/template_U_119.txt new file mode 100644 index 0000000..d6c5802 --- /dev/null +++ b/dictation_server/src/templates/template_U_119.txt @@ -0,0 +1,44 @@ + + +Dear $CUSTOMER_NAME$, + +The storage usage for your account has exceeded the usage limit. Functions related to the Dictation Workfrow will be restricted until the storage usage becomes lower than the limit. + +Please remove Dictations files once the transcription is completed or add capacity by assigning a license to a new user. 5GB of storage will be provided to the account for each active user. + +For detailed information, please sign in to ODMS Cloud and check the "Subscription" tab. + +If you need support regarding ODMS Cloud, please contact $DEALER_NAME$. + +If you have received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Die Speichernutzung Ihres Kontos hat das Nutzungslimit überschritten. Funktionen im Zusammenhang mit dem Dictation Workfrow werden eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. + +Bitte entfernen Sie Diktatdateien, sobald die Transkription abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem Konto 5 GB Speicherplatz zur Verfügung gestellt. + +Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an und überprüfen Sie die Registerkarte „Abonnement“. + +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. + +Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten. + + + +Chère/Cher $CUSTOMER_NAME$, + +L'utilisation du stockage pour votre compte a dépassé la limite d'utilisation. Les fonctions liées au Workfrow de dictée seront restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure à la limite. + +Veuillez supprimer les fichiers de dictées une fois la transcription terminée ou ajouter de la capacité en attribuant une licence à un nouvel utilisateur. 5 Go de stockage seront fournis au compte pour chaque utilisateur actif. + +Pour des informations détaillées, veuillez vous connecter à ODMS Cloud et consulter l'onglet « Abonnement ». + +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_119_no_parent.html b/dictation_server/src/templates/template_U_119_no_parent.html new file mode 100644 index 0000000..d85a975 --- /dev/null +++ b/dictation_server/src/templates/template_U_119_no_parent.html @@ -0,0 +1,83 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    + The storage usage for your account has exceeded the usage limit. + Functions related to the Dictation Workfrow will be restricted until the + storage usage becomes lower than the limit. +

    +

    + Please remove Dictations files once the transcription is completed or + add capacity by assigning a license to a new user. 5GB of storage will + be provided to the account for each active user. +

    +

    + For detailed information, please sign in to ODMS Cloud and check the + "Subscription" tab. +

    +

    + If you have received this e-mail in error, please delete this e-mail + from your system.
    + This is an automatically generated e-mail and this mailbox is not + monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

    + Die Speichernutzung Ihres Kontos hat das Nutzungslimit überschritten. + Funktionen im Zusammenhang mit dem Dictation Workfrow werden + eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. +

    +

    + Bitte entfernen Sie Diktatdateien, sobald die Transkription + abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen + Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem + Konto 5 GB Speicherplatz zur Verfügung gestellt. +

    +

    + Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an + und überprüfen Sie die Registerkarte „Abonnement“. +

    +

    + Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie + diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und dieses Postfach wird + nicht überwacht. Bitte nicht antworten. +

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

    + L'utilisation du stockage pour votre compte a dépassé la limite + d'utilisation. Les fonctions liées au Workfrow de dictée seront + restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure + à la limite. +

    +

    + Veuillez supprimer les fichiers de dictées une fois la transcription + terminée ou ajouter de la capacité en attribuant une licence à un nouvel + utilisateur. 5 Go de stockage seront fournis au compte pour chaque + utilisateur actif. +

    +

    + Pour des informations détaillées, veuillez vous connecter à ODMS Cloud + et consulter l'onglet « Abonnement ». +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail + de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres + n'est pas surveillée. Merci de ne pas répondre. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_119_no_parent.txt b/dictation_server/src/templates/template_U_119_no_parent.txt new file mode 100644 index 0000000..21a0ff8 --- /dev/null +++ b/dictation_server/src/templates/template_U_119_no_parent.txt @@ -0,0 +1,38 @@ + + +Dear $CUSTOMER_NAME$, + +The storage usage for your account has exceeded the usage limit. Functions related to the Dictation Workfrow will be restricted until the storage usage becomes lower than the limit. + +Please remove Dictations files once the transcription is completed or add capacity by assigning a license to a new user. 5GB of storage will be provided to the account for each active user. + +For detailed information, please sign in to ODMS Cloud and check the "Subscription" tab. + +If you have received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Die Speichernutzung Ihres Kontos hat das Nutzungslimit überschritten. Funktionen im Zusammenhang mit dem Dictation Workfrow werden eingeschränkt, bis die Speichernutzung unter den Grenzwert sinkt. + +Bitte entfernen Sie Diktatdateien, sobald die Transkription abgeschlossen ist, oder erhöhen Sie die Kapazität, indem Sie einem neuen Benutzer eine Lizenz zuweisen. Für jeden aktiven Benutzer werden dem Konto 5 GB Speicherplatz zur Verfügung gestellt. + +Für detaillierte Informationen melden Sie sich bitte bei ODMS Cloud an und überprüfen Sie die Registerkarte „Abonnement“. + +Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten. + + + +Chère/Cher $CUSTOMER_NAME$, + +L'utilisation du stockage pour votre compte a dépassé la limite d'utilisation. Les fonctions liées au Workfrow de dictée seront restreintes jusqu'à ce que l'utilisation du stockage devienne inférieure à la limite. + +Veuillez supprimer les fichiers de dictées une fois la transcription terminée ou ajouter de la capacité en attribuant une licence à un nouvel utilisateur. 5 Go de stockage seront fournis au compte pour chaque utilisateur actif. + +Pour des informations détaillées, veuillez vous connecter à ODMS Cloud et consulter l'onglet « Abonnement ». + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file From 71127a6db952877d780b123edd7b3198f0bd4c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Wed, 28 Feb 2024 05:30:09 +0000 Subject: [PATCH 034/109] =?UTF-8?q?Merged=20PR=20787:=20API=20I/F=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3793: API I/F修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3793) - OMDS様とのメール文面調整の結果、csvファイル名もAPIで受け渡す必要が出てきたためAPI I/Fを修正する - 一括登録依頼API、一括登録完了APIの両方に「ファイル名」を追加 - 増えたプロパティのバリデーションをするテストを追加 ## レビューポイント - プロパティ名は妥当か - テストの内容は十分か ## 動作確認状況 - npm run testを通過 --- .../src/features/users/types/types.ts | 23 +++- .../features/users/users.controller.spec.ts | 109 +++++++++++++----- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 822a45b..2b5e8a4 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -331,6 +331,11 @@ export class MultipleImportUser { } export class PostMultipleImportsRequest { + @ApiProperty({ description: 'CSVファイル名' }) + @IsString() + @IsNotEmpty() + filename: string; + @ApiProperty({ type: [MultipleImportUser] }) @IsArray() @ValidateNested({ each: true }) @@ -345,10 +350,11 @@ export class MultipleImportErrors { @IsNotEmpty() name: string; - @ApiProperty({ description: 'メールアドレス' }) - @IsEmail({ blacklisted_chars: '*' }) + @ApiProperty({ description: 'エラー発生行数' }) + @IsInt() @IsNotEmpty() - email: string; + @Type(() => Number) + line: number; @ApiProperty({ description: 'エラーコード' }) @IsString() @@ -362,6 +368,17 @@ export class PostMultipleImportsCompleteRequest { @IsInt() accountId: number; + @ApiProperty({ description: 'CSVファイル名' }) + @IsString() + @IsNotEmpty() + filename: string; + + @ApiProperty({ description: '一括登録受付時刻(UNIXTIME/ミリ秒)' }) + @IsInt() + @IsNotEmpty() + @Type(() => Number) + requestTime: number; + @ApiProperty({ type: [MultipleImportErrors] }) @IsArray() @ValidateNested({ each: true }) diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index 2f93755..123c403 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -42,6 +42,7 @@ describe('UsersController', () => { describe('valdation PostMultipleImportsRequest', () => { it('role:noneの最低限の有効なリクエストが成功する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -60,6 +61,7 @@ describe('UsersController', () => { it('role:authorの最低限の有効なリクエストが成功する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -81,6 +83,7 @@ describe('UsersController', () => { it('emailがメールアドレスではない場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -94,10 +97,11 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('AuthorなのにAuthorIDがない場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -113,13 +117,14 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('AuthorIDがルールに違反していた場合、バリデーションエラーが発生する', async () => { // ルールに合致したAuthorIDではエラーが発生しない const validAuthorIDs = ['A', '_', 'AB', 'A1', '1A', '_1', 'A_B']; for await (const authorId of validAuthorIDs) { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -146,6 +151,7 @@ describe('UsersController', () => { const invalidAuthorIDs = ['a', '+', 'AB.', 'Ab', '1a', '_.', 'A/B', '']; for await (const authorId of invalidAuthorIDs) { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -165,11 +171,12 @@ describe('UsersController', () => { ); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); } }); it('Authorなのにencryptionがない場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -185,10 +192,11 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('Authorなのにpromptがない場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -204,10 +212,11 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('Authorでencryption:trueなのに、encryptionPasswordがない場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -224,11 +233,12 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('Authorでencryption:trueでencryptionPasswordが正常であれば成功する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -251,6 +261,7 @@ describe('UsersController', () => { it('encryptionPasswordが要件外(短い)の場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -268,10 +279,11 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('encryptionPasswordが要件外(長い)の場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -289,10 +301,11 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('encryptionPasswordが要件外(全角が含まれる)の場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -310,11 +323,12 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('AuthorIDが要件外(小文字)の場合、バリデーションエラーが発生する', async () => { const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; request.users = [ { name: 'namae', @@ -332,7 +346,7 @@ describe('UsersController', () => { const valdationObject = plainToClass(PostMultipleImportsRequest, request); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); }); @@ -340,6 +354,8 @@ describe('UsersController', () => { it('最低限の有効なリクエストが成功する', async () => { const request = new PostMultipleImportsCompleteRequest(); request.accountId = 1; + request.requestTime = new Date().getTime(); + request.filename = 'test.csv'; request.errors = []; const valdationObject = plainToClass( @@ -351,18 +367,36 @@ describe('UsersController', () => { expect(errors.length).toBe(0); }); + it('ファイル名が存在しなかった場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + requestTime: new Date().getTime(), + errors: [], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('エラーが存在するリクエストが成功する', async () => { const request = new PostMultipleImportsCompleteRequest(); request.accountId = 1; + request.requestTime = new Date().getTime(); + request.filename = 'test.csv'; request.errors = [ { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, ]; @@ -379,14 +413,16 @@ describe('UsersController', () => { it('名前が足りないエラーがある場合、バリデーションエラーが発生する', async () => { const request = { accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), errors: [ { - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, ], @@ -398,12 +434,14 @@ describe('UsersController', () => { ); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); - it('emailが足りないエラーがある場合、バリデーションエラーが発生する', async () => { + it('行指定が足りないエラーがある場合、バリデーションエラーが発生する', async () => { const request = { accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), errors: [ { name: 'namae', @@ -411,7 +449,7 @@ describe('UsersController', () => { }, { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, ], @@ -423,20 +461,22 @@ describe('UsersController', () => { ); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('errorCodeが足りないエラーがある場合、バリデーションエラーが発生する', async () => { const request = { accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), errors: [ { name: 'namae', - email: 'hogehoge@example.com', + line: 1, }, { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, ], @@ -448,21 +488,23 @@ describe('UsersController', () => { ); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); it('名前が空のエラーがある場合、バリデーションエラーが発生する', async () => { const request = { accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), errors: [ { name: '', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, ], @@ -474,20 +516,21 @@ describe('UsersController', () => { ); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); - it('emailが空のエラーがある場合、バリデーションエラーが発生する', async () => { + it('行数が空のエラーがある場合、バリデーションエラーが発生する', async () => { const request = { accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), errors: [ { name: 'namae', - email: '', errorCode: 'E1101', }, { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, ], @@ -499,20 +542,22 @@ describe('UsersController', () => { ); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); - it('emailが空のエラーがある場合、バリデーションエラーが発生する', async () => { + it('エラーコードが空のエラーがある場合、バリデーションエラーが発生する', async () => { const request = { accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), errors: [ { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: '', }, { name: 'namae', - email: 'hogehoge@example.com', + line: 1, errorCode: 'E1101', }, ], @@ -524,7 +569,7 @@ describe('UsersController', () => { ); const errors = await validate(valdationObject); - expect(errors.length).toBeGreaterThan(0); + expect(errors.length).toBe(1); }); }); }); From 363f12f86f2da3712eb4175f87cd399e475af493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Wed, 28 Feb 2024 09:03:27 +0000 Subject: [PATCH 035/109] =?UTF-8?q?Merged=20PR=20774:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88csv=E8=AA=AD=E3=81=BF=E8=BE=BC?= =?UTF-8?q?=E3=81=BF=E9=83=A8=E5=88=86=E5=88=87=E3=82=8A=E5=87=BA=E3=81=97?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [タスク 3754: 画面実装(csv読み込み部分切り出し)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3754) - CSVファイルの内容を入力として、JSONに変換する処理&テストを実装 - 画面実装時の想定としては、以下の流れと想定しており、本Taskの範囲は1のみ 1. csv->jsonへパースが出来るか?(csvの形式として合っていて読み込み可能か?) 2. json内の各パラメータは問題ないか?(データの制限や組み合わせは問題ないか?) - 使い方を示す & 動作確認のために、client側でもテストを実施できるよう修正(※pipelineでは実行されない) ## レビューポイント - テストケースを見て、使い方は分かるか - CSVの形式自体が想定とズレていた場合は入力を弾く必要がある想定だが、間違っていないか - 利用ライブラリはメジャーかつ便利そうなものを選定したが、問題なさそうか ## 動作確認状況 - npm run testを通過 --- dictation_client/jest.config.js | 5 + dictation_client/package-lock.json | 6032 +++++++++++++++-- dictation_client/package.json | 15 +- dictation_client/src/common/parser.test.ts | 134 + dictation_client/src/common/parser.ts | 57 + dictation_client/src/common/test/test_001.csv | 2 + dictation_client/src/common/test/test_002.csv | 2 + dictation_client/src/common/test/test_003.csv | 2 + dictation_client/src/common/test/test_004.csv | 2 + dictation_client/src/common/test/test_005.csv | 2 + dictation_client/src/common/test/test_006.csv | 2 + dictation_client/src/common/test/test_007.csv | 3 + 12 files changed, 5522 insertions(+), 736 deletions(-) create mode 100644 dictation_client/jest.config.js create mode 100644 dictation_client/src/common/parser.test.ts create mode 100644 dictation_client/src/common/parser.ts create mode 100644 dictation_client/src/common/test/test_001.csv create mode 100644 dictation_client/src/common/test/test_002.csv create mode 100644 dictation_client/src/common/test/test_003.csv create mode 100644 dictation_client/src/common/test/test_004.csv create mode 100644 dictation_client/src/common/test/test_005.csv create mode 100644 dictation_client/src/common/test/test_006.csv create mode 100644 dictation_client/src/common/test/test_007.csv diff --git a/dictation_client/jest.config.js b/dictation_client/jest.config.js new file mode 100644 index 0000000..b413e10 --- /dev/null +++ b/dictation_client/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/dictation_client/package-lock.json b/dictation_client/package-lock.json index 05a0af5..8e5f96d 100644 --- a/dictation_client/package-lock.json +++ b/dictation_client/package-lock.json @@ -15,7 +15,6 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.2.1", - "@types/jest": "^27.5.2", "@types/node": "^17.0.45", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.6", @@ -28,6 +27,7 @@ "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "luxon": "^3.3.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-google-recaptcha-v3": "^1.10.0", @@ -46,8 +46,10 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@mdx-js/react": "^2.1.2", "@openapitools/openapi-generator-cli": "^2.5.2", + "@types/jest": "^29.5.12", "@types/lodash": "^4.14.191", "@types/luxon": "^3.2.0", + "@types/papaparse": "^5.3.14", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/redux-mock-store": "^1.0.3", @@ -57,16 +59,18 @@ "babel-loader": "^8.2.5", "eslint": "^8.19.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", - "prettier": "^2.7.1", + "prettier": "^2.8.8", "redux-mock-store": "^1.5.4", "sass": "^1.58.3", + "ts-jest": "^29.1.2", "typescript": "^4.7.4", "vite": "^4.1.4", "vite-plugin-env-compatible": "^1.1.1", @@ -108,11 +112,6 @@ "node": ">=12.0.0" } }, - "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-auth": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", @@ -126,11 +125,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-auth/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-http": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.3.tgz", @@ -155,11 +149,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-http/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-lro": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz", @@ -174,11 +163,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-lro/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-paging": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", @@ -190,11 +174,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-paging/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-tracing": { "version": "1.0.0-preview.13", "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", @@ -207,11 +186,6 @@ "node": ">=12.0.0" } }, - "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-util": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.4.0.tgz", @@ -224,11 +198,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-util/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/logger": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", @@ -240,11 +209,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/logger/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/msal-browser": { "version": "2.33.0", "license": "MIT", @@ -291,52 +255,104 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/storage-blob/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -347,12 +363,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -387,22 +403,19 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { @@ -421,22 +434,22 @@ "dev": true }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -455,40 +468,41 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.18.6", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -506,9 +520,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { "@babel/types": "^7.22.5" @@ -518,52 +532,52 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -627,9 +641,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -638,6 +652,66 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.18.6", "dev": true, @@ -652,6 +726,108 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx": { "version": "7.18.6", "dev": true, @@ -724,34 +900,34 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -759,19 +935,25 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@cush/relative": { "version": "1.0.0", "dev": true, @@ -1201,6 +1383,462 @@ "version": "1.2.1", "license": "BSD-3-Clause" }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "dev": true, @@ -1261,12 +1899,13 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@lukeed/csprng": { @@ -1294,47 +1933,27 @@ "react": ">=16" } }, - "node_modules/@nestjs/axios": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", - "integrity": "sha512-oJyfR9/h9tVk776il0829xyj3b2e81yTu6HjPraxynwNtMNGqZBHHmAQL24yMB3tVbBM0RvG3eUXH8+pRCGwlg==", - "dev": true, - "dependencies": { - "axios": "0.27.2" - }, - "peerDependencies": { - "@nestjs/common": "^7.0.0 || ^8.0.0", - "reflect-metadata": "^0.1.12", - "rxjs": "^6.0.0 || ^7.0.0" - } - }, "node_modules/@nestjs/common": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.7.tgz", - "integrity": "sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", + "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", "dev": true, - "peer": true, "dependencies": { - "axios": "0.27.2", "iterare": "1.2.1", - "tslib": "2.4.0", - "uuid": "8.3.2" + "tslib": "2.6.2", + "uid": "2.0.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "cache-manager": "*", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, "class-transformer": { "optional": true }, @@ -1343,12 +1962,43 @@ } } }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "node_modules/@nestjs/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", + "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", "dev": true, - "peer": true + "hasInstallScript": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1401,28 +2051,29 @@ } }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.6.0.tgz", - "integrity": "sha512-M/aOpR7G+Y1nMf+ofuar8pGszajgfhs1aSPSijkcr2tHTxKAI3sA3YYcOGbszxaNRKFyvOcDq+KP9pcJvKoCHg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.9.0.tgz", + "integrity": "sha512-KQpftKeiMoH5aEI/amOVLFGkGeT3DyA7Atj7v7l8xT3O9xnIUpoDmMg0WBTEh+NHxEwEAITQNDzr+JLjkXVaKw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@nestjs/axios": "0.0.8", - "@nestjs/common": "9.3.11", - "@nestjs/core": "9.3.11", + "@nestjs/axios": "3.0.1", + "@nestjs/common": "10.3.0", + "@nestjs/core": "10.3.0", "@nuxtjs/opencollective": "0.3.2", + "axios": "1.6.5", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "10.1.0", - "glob": "7.1.6", - "inquirer": "8.2.5", + "glob": "7.2.3", + "inquirer": "8.2.6", "lodash": "4.17.21", "reflect-metadata": "0.1.13", - "rxjs": "7.8.0", - "tslib": "2.0.3" + "rxjs": "7.8.1", + "tslib": "2.6.2" }, "bin": { "openapi-generator-cli": "main.js" @@ -1435,89 +2086,29 @@ "url": "https://opencollective.com/openapi_generator" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", - "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", + "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/axios": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", "dev": true, - "dependencies": { - "iterare": "1.2.1", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, "peerDependencies": { - "cache-manager": "<=5", - "class-transformer": "*", - "class-validator": "*", + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", "reflect-metadata": "^0.1.12", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, - "class-transformer": { - "optional": true - }, - "class-validator": { - "optional": true - } + "rxjs": "^6.0.0 || ^7.0.0" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz", - "integrity": "sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ==", + "node_modules/@openapitools/openapi-generator-cli/node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@nuxtjs/opencollective": "0.3.2", - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/microservices": "^9.0.0", - "@nestjs/platform-express": "^9.0.0", - "@nestjs/websockets": "^9.0.0", - "reflect-metadata": "^0.1.12", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "@nestjs/microservices": { - "optional": true - }, - "@nestjs/platform-express": { - "optional": true - }, - "@nestjs/websockets": { - "optional": true - } + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, "node_modules/@openapitools/openapi-generator-cli/node_modules/commander": { "version": "8.3.0", "dev": true, @@ -1526,25 +2117,6 @@ "node": ">= 12" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/glob": { - "version": "7.1.6", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@opentelemetry/api": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.6.0.tgz", @@ -1594,6 +2166,29 @@ "node": ">= 8.0.0" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "8.14.0", "license": "MIT", @@ -1673,6 +2268,47 @@ "version": "4.2.2", "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/eslint": { "version": "8.4.10", "dev": true, @@ -1700,6 +2336,15 @@ "dev": true, "peer": true }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/history": { "version": "4.7.11", "license": "MIT" @@ -1712,12 +2357,58 @@ "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/jest": { - "version": "27.5.2", - "license": "MIT", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dependencies": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/json-schema": { @@ -1758,6 +2449,15 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.5", "license": "MIT" @@ -1807,6 +2507,11 @@ "version": "0.16.2", "license": "MIT" }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.5", "license": "MIT", @@ -1826,6 +2531,19 @@ "version": "0.0.3", "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.30.5", "dev": true, @@ -2510,6 +3228,27 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, "node_modules/babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -2529,6 +3268,92 @@ "webpack": ">=2" } }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -2590,7 +3415,6 @@ }, "node_modules/braces": { "version": "3.0.2", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.0.1" @@ -2600,7 +3424,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.5", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -2610,14 +3436,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -2626,6 +3455,27 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2653,8 +3503,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.2", @@ -2675,6 +3524,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "dev": true, @@ -2684,7 +3542,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001464", + "version": "1.0.30001589", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", + "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", "dev": true, "funding": [ { @@ -2694,9 +3554,12 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", @@ -2712,6 +3575,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -2764,6 +3636,26 @@ "node": ">=6.0" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, "node_modules/classnames": { "version": "2.3.2", "license": "MIT" @@ -2788,9 +3680,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", - "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -2826,6 +3718,22 @@ "node": ">=0.8" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, "node_modules/color-convert": { "version": "2.0.1", "license": "MIT", @@ -2945,11 +3853,30 @@ } }, "node_modules/convert-source-map": { - "version": "1.8.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, - "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.1" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/cross-spawn": { @@ -3028,10 +3955,33 @@ "node": ">=0.10" } }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "dev": true, @@ -3073,6 +4023,15 @@ "node": ">=6" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "dev": true, @@ -3082,13 +4041,6 @@ "wrappy": "1" } }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "dev": true, @@ -3123,9 +4075,22 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.328", + "version": "1.4.681", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz", + "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, - "license": "ISC" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -3154,6 +4119,15 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", @@ -3388,9 +4362,10 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.5.0", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, - "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3707,6 +4682,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.4.0", "license": "BSD-3-Clause", @@ -3753,6 +4741,121 @@ "node": ">=0.8.x" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expect/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -3824,6 +4927,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -3851,7 +4963,6 @@ }, "node_modules/fill-range": { "version": "7.0.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3904,14 +5015,15 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.1", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -4039,6 +5151,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "dev": true, @@ -4154,7 +5287,6 @@ }, "node_modules/graceful-fs": { "version": "4.2.10", - "dev": true, "license": "ISC" }, "node_modules/has": { @@ -4247,6 +5379,12 @@ "dev": true, "license": "ISC" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "license": "MIT", @@ -4254,6 +5392,15 @@ "void-elements": "3.1.0" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/i18next": { "version": "21.10.0", "funding": [ @@ -4348,6 +5495,25 @@ "node": ">=4" } }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "license": "MIT", @@ -4375,9 +5541,9 @@ "license": "ISC" }, "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", @@ -4394,12 +5560,26 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" }, "engines": { "node": ">=12.0.0" } }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -4428,6 +5608,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-bigint": { "version": "1.0.4", "dev": true, @@ -4517,6 +5703,15 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "license": "MIT", @@ -4549,7 +5744,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4595,6 +5789,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "dev": true, @@ -4669,6 +5875,117 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", @@ -4678,37 +5995,971 @@ "node": ">=6" } }, - "node_modules/jest-diff": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/jest-get-type": { - "version": "27.5.1", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { @@ -4823,6 +7074,15 @@ "version": "3.1.2", "license": "MIT" }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "dev": true, @@ -4836,6 +7096,15 @@ "language-subtag-registry": "~0.3.2" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "license": "MIT", @@ -4940,6 +7209,12 @@ "node": ">=4" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/loader-runner": { "version": "4.3.0", "dev": true, @@ -4983,6 +7258,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "license": "MIT" @@ -5062,12 +7343,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -5079,7 +7374,6 @@ }, "node_modules/micromatch": { "version": "4.0.5", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.2", @@ -5159,9 +7453,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -5205,10 +7499,17 @@ } } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node_modules/node-releases": { - "version": "2.0.10", - "dev": true, - "license": "MIT" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true }, "node_modules/nopt": { "version": "4.0.3", @@ -5255,6 +7556,18 @@ "dev": true, "license": "ISC" }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -5474,6 +7787,11 @@ "node": ">=4" } }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "license": "MIT", @@ -5484,6 +7802,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "3.0.0", "dev": true, @@ -5532,7 +7868,6 @@ }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5541,6 +7876,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -5624,9 +7968,9 @@ } }, "node_modules/postcss": { - "version": "8.4.30", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz", - "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -5643,7 +7987,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -5677,9 +8021,10 @@ } }, "node_modules/prettier": { - "version": "2.7.1", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, @@ -5735,6 +8080,19 @@ "node": ">= 0.6.0" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", @@ -5748,6 +8106,12 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/punycode": { "version": "2.1.1", "license": "MIT", @@ -5755,6 +8119,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -6109,6 +8489,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -6234,24 +8644,19 @@ } }, "node_modules/rxjs": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", - "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.1.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/safe-regex-test": { "version": "1.0.0", @@ -6374,9 +8779,14 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6488,6 +8898,31 @@ "spdx-ranges": "^2.0.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6517,6 +8952,19 @@ } ] }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "dev": true, @@ -6609,6 +9057,24 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "license": "MIT", @@ -6733,6 +9199,20 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "license": "MIT" @@ -6755,6 +9235,12 @@ "node": ">=0.6.0" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "node_modules/to-fast-properties": { "version": "2.0.0", "dev": true, @@ -6765,7 +9251,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6795,6 +9280,73 @@ "node": ">=0.6" } }, + "node_modules/ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.1", "dev": true, @@ -6826,9 +9378,9 @@ } }, "node_modules/tslib": { - "version": "2.0.3", - "dev": true, - "license": "0BSD" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -6867,6 +9419,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "license": "(MIT OR CC0-1.0)", @@ -6904,9 +9465,9 @@ } }, "node_modules/uid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.1.tgz", - "integrity": "sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", "dev": true, "dependencies": { "@lukeed/csprng": "^1.0.0" @@ -6939,7 +9500,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -6949,15 +9512,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -7000,6 +9566,20 @@ "version": "2.3.0", "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "dev": true, @@ -7010,9 +9590,9 @@ } }, "node_modules/vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -7125,6 +9705,15 @@ "node": ">=0.10.0" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watchpack": { "version": "2.4.0", "dev": true, @@ -7308,6 +9897,19 @@ "version": "1.0.2", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -7365,6 +9967,18 @@ "engines": { "node": ">=10" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { @@ -7387,13 +10001,6 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "requires": { "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-auth": { @@ -7404,13 +10011,6 @@ "@azure/abort-controller": "^1.0.0", "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-http": { @@ -7432,13 +10032,6 @@ "tunnel": "^0.0.6", "uuid": "^8.3.0", "xml2js": "^0.5.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-lro": { @@ -7450,13 +10043,6 @@ "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-paging": { @@ -7465,13 +10051,6 @@ "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", "requires": { "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-tracing": { @@ -7481,13 +10060,6 @@ "requires": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-util": { @@ -7497,13 +10069,6 @@ "requires": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/logger": { @@ -7512,13 +10077,6 @@ "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", "requires": { "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/msal-browser": { @@ -7547,59 +10105,99 @@ "@azure/logger": "^1.0.0", "events": "^3.0.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true }, "@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" } }, "@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "requires": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -7626,16 +10224,16 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "dependencies": { "lru-cache": { @@ -7656,19 +10254,19 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -7681,32 +10279,31 @@ } }, "@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" } }, "@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" } }, "@babel/helper-plugin-utils": { - "version": "7.18.6", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true }, "@babel/helper-simple-access": { @@ -7719,49 +10316,49 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true }, "@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" } }, "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -7812,11 +10409,56 @@ } }, "@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, "@babel/plugin-syntax-jsx": { "version": "7.18.6", "dev": true, @@ -7824,6 +10466,78 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, "@babel/plugin-transform-react-jsx": { "version": "7.18.6", "dev": true, @@ -7865,45 +10579,51 @@ } }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" } }, "@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "@cush/relative": { "version": "1.0.0", "dev": true @@ -8113,6 +10833,361 @@ "@humanwhocodes/object-schema": { "version": "1.2.1" }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "requires": { + "jest-get-type": "^29.6.3" + }, + "dependencies": { + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" + } + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, "@jridgewell/gen-mapping": { "version": "0.1.1", "dev": true, @@ -8159,11 +11234,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.17", + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@lukeed/csprng": { @@ -8180,35 +11257,29 @@ "@types/react": ">=16" } }, - "@nestjs/axios": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", - "integrity": "sha512-oJyfR9/h9tVk776il0829xyj3b2e81yTu6HjPraxynwNtMNGqZBHHmAQL24yMB3tVbBM0RvG3eUXH8+pRCGwlg==", + "@nestjs/common": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", + "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", "dev": true, "requires": { - "axios": "0.27.2" + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" } }, - "@nestjs/common": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.7.tgz", - "integrity": "sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw==", + "@nestjs/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", + "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", "dev": true, - "peer": true, "requires": { - "axios": "0.27.2", + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "tslib": "2.4.0", - "uuid": "8.3.2" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true - } + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" } }, "@nodelib/fs.scandir": { @@ -8243,85 +11314,51 @@ } }, "@openapitools/openapi-generator-cli": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.6.0.tgz", - "integrity": "sha512-M/aOpR7G+Y1nMf+ofuar8pGszajgfhs1aSPSijkcr2tHTxKAI3sA3YYcOGbszxaNRKFyvOcDq+KP9pcJvKoCHg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.9.0.tgz", + "integrity": "sha512-KQpftKeiMoH5aEI/amOVLFGkGeT3DyA7Atj7v7l8xT3O9xnIUpoDmMg0WBTEh+NHxEwEAITQNDzr+JLjkXVaKw==", "dev": true, "requires": { - "@nestjs/axios": "0.0.8", - "@nestjs/common": "9.3.11", - "@nestjs/core": "9.3.11", + "@nestjs/axios": "3.0.1", + "@nestjs/common": "10.3.0", + "@nestjs/core": "10.3.0", "@nuxtjs/opencollective": "0.3.2", + "axios": "1.6.5", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "10.1.0", - "glob": "7.1.6", - "inquirer": "8.2.5", + "glob": "7.2.3", + "inquirer": "8.2.6", "lodash": "4.17.21", "reflect-metadata": "0.1.13", - "rxjs": "7.8.0", - "tslib": "2.0.3" + "rxjs": "7.8.1", + "tslib": "2.6.2" }, "dependencies": { - "@nestjs/common": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", - "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", + "@nestjs/axios": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", "dev": true, - "requires": { - "iterare": "1.2.1", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - } - } + "requires": {} }, - "@nestjs/core": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz", - "integrity": "sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ==", + "axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, "requires": { - "@nuxtjs/opencollective": "0.3.2", - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - } + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "commander": { "version": "8.3.0", "dev": true - }, - "glob": { - "version": "7.1.6", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } } } }, @@ -8350,6 +11387,29 @@ "picomatch": "^2.2.2" } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, "@testing-library/dom": { "version": "8.14.0", "requires": { @@ -8401,6 +11461,47 @@ "@types/aria-query": { "version": "4.2.2" }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, "@types/eslint": { "version": "8.4.10", "dev": true, @@ -8426,6 +11527,15 @@ "dev": true, "peer": true }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/history": { "version": "4.7.11" }, @@ -8436,11 +11546,51 @@ "hoist-non-react-statics": "^3.3.0" } }, - "@types/jest": { - "version": "27.5.2", + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "requires": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } } }, "@types/json-schema": { @@ -8475,6 +11625,15 @@ "form-data": "^4.0.0" } }, + "@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/prop-types": { "version": "15.7.5" }, @@ -8517,6 +11676,11 @@ "@types/scheduler": { "version": "0.16.2" }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, "@types/testing-library__jest-dom": { "version": "5.14.5", "requires": { @@ -8534,6 +11698,19 @@ "@types/use-sync-external-store": { "version": "0.0.3" }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, "@typescript-eslint/eslint-plugin": { "version": "5.30.5", "dev": true, @@ -9014,6 +12191,21 @@ "dequal": "^2.0.3" } }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, "babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -9026,6 +12218,76 @@ "schema-utils": "^2.6.5" } }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "balanced-match": { "version": "1.0.2" }, @@ -9063,19 +12325,38 @@ }, "braces": { "version": "3.0.2", - "dev": true, "requires": { "fill-range": "^7.0.1" } }, "browserslist": { - "version": "4.21.5", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" } }, "buffer": { @@ -9090,8 +12371,7 @@ }, "buffer-from": { "version": "1.1.2", - "dev": true, - "peer": true + "dev": true }, "call-bind": { "version": "1.0.2", @@ -9104,12 +12384,20 @@ "callsites": { "version": "3.1.0" }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "camelcase-css": { "version": "2.0.1", "dev": true }, "caniuse-lite": { - "version": "1.0.30001464", + "version": "1.0.30001589", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", + "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", "dev": true }, "chalk": { @@ -9119,6 +12407,12 @@ "supports-color": "^7.1.0" } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -9153,6 +12447,17 @@ "dev": true, "peer": true }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, "classnames": { "version": "2.3.2" }, @@ -9172,9 +12477,9 @@ } }, "cli-spinners": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", - "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true }, "cli-width": { @@ -9196,6 +12501,18 @@ "version": "1.0.4", "dev": true }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, "color-convert": { "version": "2.0.1", "requires": { @@ -9281,10 +12598,24 @@ } }, "convert-source-map": { - "version": "1.8.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, "requires": { - "safe-buffer": "~5.1.1" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" } }, "cross-spawn": { @@ -9330,9 +12661,22 @@ "decode-uri-component": { "version": "0.2.2" }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "requires": {} + }, "deep-is": { "version": "0.1.4" }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, "defaults": { "version": "1.0.4", "dev": true, @@ -9356,6 +12700,12 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, "dezalgo": { "version": "1.0.4", "dev": true, @@ -9364,9 +12714,6 @@ "wrappy": "1" } }, - "diff-sequences": { - "version": "27.5.1" - }, "dir-glob": { "version": "3.0.1", "dev": true, @@ -9391,7 +12738,15 @@ } }, "electron-to-chromium": { - "version": "1.4.328", + "version": "1.4.681", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz", + "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true }, "emoji-regex": { @@ -9413,6 +12768,15 @@ "tapable": "^2.2.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", @@ -9604,7 +12968,9 @@ } }, "eslint-config-prettier": { - "version": "8.5.0", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, "requires": {} }, @@ -9802,6 +13168,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "esquery": { "version": "1.4.0", "requires": { @@ -9827,6 +13199,90 @@ "events": { "version": "3.3.0" }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==" + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -9884,6 +13340,15 @@ "reusify": "^1.0.4" } }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9901,7 +13366,6 @@ }, "fill-range": { "version": "7.0.1", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -9935,7 +13399,9 @@ "version": "3.2.6" }, "follow-redirects": { - "version": "1.15.1" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "for-each": { "version": "0.3.3", @@ -10016,6 +13482,18 @@ "has-symbols": "^1.0.3" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, "get-symbol-description": { "version": "1.0.0", "dev": true, @@ -10091,8 +13569,7 @@ } }, "graceful-fs": { - "version": "4.2.10", - "dev": true + "version": "4.2.10" }, "has": { "version": "1.0.3", @@ -10147,12 +13624,24 @@ "version": "2.8.9", "dev": true }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "html-parse-stringify": { "version": "3.0.1", "requires": { "void-elements": "3.1.0" } }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "i18next": { "version": "21.10.0", "requires": { @@ -10196,6 +13685,16 @@ } } }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "imurmurhash": { "version": "0.1.4" }, @@ -10213,9 +13712,9 @@ "version": "2.0.4" }, "inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", @@ -10232,7 +13731,20 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" + }, + "dependencies": { + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } } }, "internal-slot": { @@ -10257,6 +13769,12 @@ "is-typed-array": "^1.1.10" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-bigint": { "version": "1.0.4", "dev": true, @@ -10306,6 +13824,12 @@ "version": "3.0.0", "dev": true }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.3", "requires": { @@ -10323,8 +13847,7 @@ "dev": true }, "is-number": { - "version": "7.0.0", - "dev": true + "version": "7.0.0" }, "is-number-object": { "version": "1.0.7", @@ -10348,6 +13871,12 @@ "call-bind": "^1.0.2" } }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, "is-string": { "version": "1.0.7", "dev": true, @@ -10391,31 +13920,816 @@ "isexe": { "version": "2.0.0" }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", "dev": true }, - "jest-diff": { - "version": "27.5.1", + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" } }, - "jest-get-type": { - "version": "27.5.1" + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "dependencies": { + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + } + } }, - "jest-matcher-utils": { - "version": "27.5.1", + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "dependencies": { + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" } }, "jest-worker": { @@ -10496,6 +14810,12 @@ "jwt-decode": { "version": "3.1.2" }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, "language-subtag-registry": { "version": "0.3.22", "dev": true @@ -10507,6 +14827,12 @@ "language-subtag-registry": "~0.3.2" } }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, "levn": { "version": "0.4.1", "requires": { @@ -10583,6 +14909,12 @@ } } }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "loader-runner": { "version": "4.3.0", "dev": true, @@ -10612,6 +14944,12 @@ "version": "4.0.6", "dev": true }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "lodash.merge": { "version": "4.6.2" }, @@ -10662,12 +15000,26 @@ "semver": "^6.0.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "peer": true + "dev": true }, "merge2": { "version": "1.4.1", @@ -10675,7 +15027,6 @@ }, "micromatch": { "version": "4.0.5", - "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -10726,9 +15077,9 @@ "dev": true }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, "natural-compare": { @@ -10747,8 +15098,16 @@ "whatwg-url": "^5.0.0" } }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node-releases": { - "version": "2.0.10", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "nopt": { @@ -10785,6 +15144,15 @@ "version": "1.0.1", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "object-assign": { "version": "4.1.1" }, @@ -10932,12 +15300,29 @@ "version": "1.0.0", "dev": true }, + "papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "parent-module": { "version": "1.0.1", "requires": { "callsites": "^3.0.0" } }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "3.0.0", "dev": true @@ -10967,7 +15352,12 @@ "dev": true }, "picomatch": { - "version": "2.3.1", + "version": "2.3.1" + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true }, "pkg-dir": { @@ -11031,12 +15421,12 @@ } }, "postcss": { - "version": "8.4.30", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz", - "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -11052,7 +15442,9 @@ "version": "1.2.1" }, "prettier": { - "version": "2.7.1", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true }, "prettier-linter-helpers": { @@ -11083,6 +15475,16 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, "prop-types": { "version": "15.8.1", "requires": { @@ -11096,9 +15498,21 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "punycode": { "version": "2.1.1" }, + "pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "dev": true @@ -11314,6 +15728,27 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -11402,25 +15837,18 @@ } }, "rxjs": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", - "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "requires": { "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", - "dev": true - } } }, "safe-buffer": { "version": "5.1.2", - "dev": true + "dev": true, + "peer": true }, "safe-regex-test": { "version": "1.0.0", @@ -11510,10 +15938,15 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "slash": { - "version": "3.0.0", + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, + "slash": { + "version": "3.0.0" + }, "slide": { "version": "1.1.6", "dev": true @@ -11597,6 +16030,27 @@ "spdx-ranges": "^2.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11614,6 +16068,16 @@ } } }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, "string-width": { "version": "4.2.3", "dev": true, @@ -11684,6 +16148,18 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-indent": { "version": "3.0.0", "requires": { @@ -11751,6 +16227,17 @@ } } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, "text-table": { "version": "0.2.0" }, @@ -11769,13 +16256,18 @@ "os-tmpdir": "~1.0.2" } }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "dev": true }, "to-regex-range": { "version": "5.0.1", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -11793,6 +16285,39 @@ "version": "1.1.0", "dev": true }, + "ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "tsconfig-paths": { "version": "3.14.1", "dev": true, @@ -11817,8 +16342,9 @@ } }, "tslib": { - "version": "2.0.3", - "dev": true + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", @@ -11844,6 +16370,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2" }, @@ -11863,9 +16395,9 @@ "dev": true }, "uid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.1.tgz", - "integrity": "sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", "dev": true, "requires": { "@lukeed/csprng": "^1.0.0" @@ -11888,7 +16420,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.10", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -11923,6 +16457,17 @@ "v8-compile-cache": { "version": "2.3.0" }, + "v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, "validate-npm-package-license": { "version": "3.0.4", "dev": true, @@ -11932,9 +16477,9 @@ } }, "vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "requires": { "esbuild": "^0.18.10", @@ -11982,6 +16527,15 @@ "void-elements": { "version": "3.1.0" }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, "watchpack": { "version": "2.4.0", "dev": true, @@ -12110,6 +16664,16 @@ "wrappy": { "version": "1.0.2" }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, "xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -12148,6 +16712,12 @@ "yargs-parser": { "version": "20.2.9", "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/dictation_client/package.json b/dictation_client/package.json index ac8d5c0..feb1f28 100644 --- a/dictation_client/package.json +++ b/dictation_client/package.json @@ -15,7 +15,8 @@ "typecheck": "tsc --noEmit", "codegen": "sh codegen.sh", "lint": "eslint --cache . --ext .js,.ts,.tsx", - "lint:fix": "npm run lint -- --fix" + "lint:fix": "npm run lint -- --fix", + "test": "jest" }, "dependencies": { "@azure/msal-browser": "^2.33.0", @@ -25,7 +26,6 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.2.1", - "@types/jest": "^27.5.2", "@types/node": "^17.0.45", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.6", @@ -38,6 +38,7 @@ "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "luxon": "^3.3.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-google-recaptcha-v3": "^1.10.0", @@ -56,8 +57,10 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@mdx-js/react": "^2.1.2", "@openapitools/openapi-generator-cli": "^2.5.2", + "@types/jest": "^29.5.12", "@types/lodash": "^4.14.191", "@types/luxon": "^3.2.0", + "@types/papaparse": "^5.3.14", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/redux-mock-store": "^1.0.3", @@ -67,16 +70,18 @@ "babel-loader": "^8.2.5", "eslint": "^8.19.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", - "prettier": "^2.7.1", + "prettier": "^2.8.8", "redux-mock-store": "^1.5.4", "sass": "^1.58.3", + "ts-jest": "^29.1.2", "typescript": "^4.7.4", "vite": "^4.1.4", "vite-plugin-env-compatible": "^1.1.1", @@ -99,4 +104,4 @@ } ] } -} \ No newline at end of file +} diff --git a/dictation_client/src/common/parser.test.ts b/dictation_client/src/common/parser.test.ts new file mode 100644 index 0000000..d4aff39 --- /dev/null +++ b/dictation_client/src/common/parser.test.ts @@ -0,0 +1,134 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +// Jestによるparser.tsのテスト +import fs from "fs"; +import { CSVType, parseCSV } from "./parser"; + +describe("parse", () => { + it("指定形式のCSV文字列をパースできる", async () => { + const text = fs.readFileSync("src/common/test/test_001.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: "HOGE", + auto_renew: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); + it("指定形式のヘッダでない場合、例外が送出される | author_id(値がoptionial)がない", async () => { + const text = fs.readFileSync("src/common/test/test_002.csv", "utf-8"); + try { + await parseCSV(text); + fail("例外が発生しませんでした"); + } catch (e) { + expect(e).toEqual(new Error("Invalid CSV format")); + } + }); + it("指定形式のヘッダでない場合、例外が送出される | email(値が必須)がない", async () => { + const text = fs.readFileSync("src/common/test/test_003.csv", "utf-8"); + try { + await parseCSV(text); + fail("例外が発生しませんでした"); + } catch (e) { + expect(e).toEqual(new Error("Invalid CSV format")); + } + }); + it("指定形式のヘッダでない場合、例外が送出される | emailがスペルミス", async () => { + const text = fs.readFileSync("src/common/test/test_004.csv", "utf-8"); + try { + await parseCSV(text); + fail("例外が発生しませんでした"); + } catch (e) { + expect(e).toEqual(new Error("Invalid CSV format")); + } + }); + it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(文字列)はnullとなる", async () => { + const text = fs.readFileSync("src/common/test/test_005.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: null, + auto_renew: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); + it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(数値)はnullとなる", async () => { + const text = fs.readFileSync("src/common/test/test_006.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: null, + author_id: "HOGE", + auto_renew: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); + it("指定形式のCSV文字列をパースできる | 余計なパラメータがあっても問題はない", async () => { + const text = fs.readFileSync("src/common/test/test_007.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: "HOGE", + auto_renew: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + { + name: "hoge2", + email: "sample2@example.com", + role: 1, + author_id: "HOGE2", + auto_renew: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd2", + prompt: 0, + }, + ]; + expect(actualData.length).toBe(expectData.length); + + // 余計なパラメータ格納用に __parsed_extra: string[] というプロパティが作られてしまうので、既知のプロパティ毎に比較 + for (let i = 0; i < actualData.length; i += 1) { + const actualValue = actualData[i]; + const expectValue = expectData[i]; + expect(actualValue.author_id).toEqual(expectValue.author_id); + expect(actualValue.auto_renew).toEqual(expectValue.auto_renew); + expect(actualValue.email).toEqual(expectValue.email); + expect(actualValue.encryption).toEqual(expectValue.encryption); + expect(actualValue.encryption_password).toEqual( + expectValue.encryption_password + ); + expect(actualValue.name).toEqual(expectValue.name); + expect(actualValue.notification).toEqual(expectValue.notification); + expect(actualValue.prompt).toEqual(expectValue.prompt); + expect(actualValue.role).toEqual(expectValue.role); + } + }); +}); diff --git a/dictation_client/src/common/parser.ts b/dictation_client/src/common/parser.ts new file mode 100644 index 0000000..c2950c3 --- /dev/null +++ b/dictation_client/src/common/parser.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import Papa, { ParseResult } from "papaparse"; + +export type CSVType = { + name: string | null; + email: string | null; + role: number | null; + author_id: string | null; + auto_renew: number | null; + notification: number; + encryption: number | null; + encryption_password: string | null; + prompt: number | null; +}; + +// CSVTypeのプロパティ名を文字列の配列で定義する +const CSVTypeFields: (keyof CSVType)[] = [ + "name", + "email", + "role", + "author_id", + "auto_renew", + "notification", + "encryption", + "encryption_password", + "prompt", +]; + +// 2つの配列が等しいかどうかを判定する +const equals = (lhs: string[], rhs: string[]) => { + if (lhs.length !== rhs.length) return false; + for (let i = 0; i < lhs.length; i += 1) { + if (lhs[i] !== rhs[i]) return false; + } + return true; +}; + +/** CSVファイルをCSVType型に変換するパーサー */ +export const parseCSV = async (csvString: string): Promise => + new Promise((resolve, reject) => { + Papa.parse(csvString, { + download: false, + worker: true, + header: true, + dynamicTyping: true, + complete: (results: ParseResult) => { + // ヘッダーがCSVTypeFieldsと一致しない場合はエラーを返す + if (!equals(results.meta.fields ?? [], CSVTypeFields)) { + reject(new Error("Invalid CSV format")); + } + resolve(results.data); + }, + error: (error: Error) => { + reject(error); + }, + }); + }); diff --git a/dictation_client/src/common/test/test_001.csv b/dictation_client/src/common/test/test_001.csv new file mode 100644 index 0000000..7f52660 --- /dev/null +++ b/dictation_client/src/common/test/test_001.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_002.csv b/dictation_client/src/common/test/test_002.csv new file mode 100644 index 0000000..6a385a7 --- /dev/null +++ b/dictation_client/src/common/test/test_002.csv @@ -0,0 +1,2 @@ +name,email,role,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_003.csv b/dictation_client/src/common/test/test_003.csv new file mode 100644 index 0000000..4fb47b2 --- /dev/null +++ b/dictation_client/src/common/test/test_003.csv @@ -0,0 +1,2 @@ +name,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_004.csv b/dictation_client/src/common/test/test_004.csv new file mode 100644 index 0000000..fec0f30 --- /dev/null +++ b/dictation_client/src/common/test/test_004.csv @@ -0,0 +1,2 @@ +name,emeil,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_005.csv b/dictation_client/src/common/test/test_005.csv new file mode 100644 index 0000000..b8165d9 --- /dev/null +++ b/dictation_client/src/common/test/test_005.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,,1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_006.csv b/dictation_client/src/common/test/test_006.csv new file mode 100644 index 0000000..18ee411 --- /dev/null +++ b/dictation_client/src/common/test/test_006.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_007.csv b/dictation_client/src/common/test/test_007.csv new file mode 100644 index 0000000..ea049ff --- /dev/null +++ b/dictation_client/src/common/test/test_007.csv @@ -0,0 +1,3 @@ +name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0,x +hoge2,sample2@example.com,1,"HOGE2",1,1,1,abcd2,0,1,32,4,aa \ No newline at end of file From 7ff563f644335a93789a086e6f0d2516a94aabd7 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 4 Mar 2024 09:15:53 +0000 Subject: [PATCH 036/109] =?UTF-8?q?Merged=20PR=20795:=20API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E4=B8=80=E6=8B=AC=E7=99=BB=E9=8C=B2=E5=AE=8C?= =?UTF-8?q?=E4=BA=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3763: API実装(一括登録完了)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3763) - 一括登録完了APIとテストを実装しました。 - メール文面は多言語対応がまだですのですべて日本語の文面にしています。 ## レビューポイント - 送信メールの内容は認識通りでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../src/features/users/users.controller.ts | 9 +- .../src/features/users/users.service.spec.ts | 187 ++++++++++++++++ .../src/features/users/users.service.ts | 109 +++++++++- .../src/gateways/sendgrid/sendgrid.service.ts | 201 ++++++++++++++++++ dictation_server/src/templates/constants.ts | 5 + .../src/templates/template_U_121.html | 65 ++++++ .../src/templates/template_U_121.txt | 44 ++++ .../templates/template_U_121_no_parent.html | 53 +++++ .../templates/template_U_121_no_parent.txt | 38 ++++ .../src/templates/template_U_122.html | 107 ++++++++++ .../src/templates/template_U_122.txt | 74 +++++++ .../templates/template_U_122_no_parent.html | 95 +++++++++ .../templates/template_U_122_no_parent.txt | 68 ++++++ 13 files changed, 1053 insertions(+), 2 deletions(-) create mode 100644 dictation_server/src/templates/template_U_121.html create mode 100644 dictation_server/src/templates/template_U_121.txt create mode 100644 dictation_server/src/templates/template_U_121_no_parent.html create mode 100644 dictation_server/src/templates/template_U_121_no_parent.txt create mode 100644 dictation_server/src/templates/template_U_122.html create mode 100644 dictation_server/src/templates/template_U_122.txt create mode 100644 dictation_server/src/templates/template_U_122_no_parent.html create mode 100644 dictation_server/src/templates/template_U_122_no_parent.txt diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 8d448d9..531c13b 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -1139,7 +1139,14 @@ export class UsersController { const context = makeContext(systemName, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: 処理を実装 + const { accountId, filename, requestTime, errors } = body; + await this.usersService.multipleImportsComplate( + context, + accountId, + filename, + requestTime, + errors, + ); return {}; } diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 5ecb653..26f9703 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -56,6 +56,8 @@ import { import { truncateAllTable } from '../../common/test/init'; import { createTask } from '../files/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; +import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; +import { MultipleImportErrors } from './types/types'; describe('UsersService.confirmUser', () => { let source: DataSource | null = null; @@ -3653,3 +3655,188 @@ describe('UsersService.deleteUser', () => { } }); }); + +describe('UsersService.multipleImportsComplate', () => { + let source: DataSource | null = null; + + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ユーザー一括登録完了メールを送信できる(成功)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendgridService'], 'sendMailWithU121') + .mockImplementation(); + + const requestTime = Math.floor(new Date(2024, 2, 3).getTime() / 1000); + const filename = `U_20240303_000000_${admin.account_id}_${admin.id}.csv`; + const errors: MultipleImportErrors[] = []; + + // ユーザー一括登録完了 + await service.multipleImportsComplate( + context, + account.id, + filename, + requestTime, + errors, + ); + + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(spy).toHaveBeenCalledWith( + context, + ['admin@example.com'], + account.company_name, + dealer.company_name, + '2024.3.3', + filename, + ); + }); + + it('ユーザー一括登録完了メールを送信できる(失敗)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendgridService'], 'sendMailWithU122') + .mockImplementation(); + + const requestTime = Math.floor(new Date(2024, 2, 3).getTime() / 1000); + const filename = `U_20240303_000000_${admin.account_id}_${admin.id}`; + const errors: MultipleImportErrors[] = [ + { + name: 'user1', + line: 1, + errorCode: 'E010301', + }, + { + name: 'user2', + line: 2, + errorCode: 'E010302', + }, + { + name: 'user2', + line: 3, + errorCode: 'E010302', + }, + { + name: 'user3', + line: 4, + errorCode: 'E009999', + }, + ]; + + // ユーザー一括登録完了 + await service.multipleImportsComplate( + context, + account.id, + filename, + requestTime, + errors, + ); + + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(spy).toHaveBeenCalledWith( + context, + ['admin@example.com'], + account.company_name, + dealer.company_name, + [1], + [2, 3], + [4], + ); + }); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 1644a3c..2dbf943 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -23,7 +23,11 @@ import { } from '../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; -import { GetRelationsResponse, User } from './types/types'; +import { + GetRelationsResponse, + MultipleImportErrors, + User, +} from './types/types'; import { AdminDeleteFailedError, AssignedWorkflowWithAuthorDeleteFailedError, @@ -1492,6 +1496,109 @@ export class UsersService { ); } } + /** + * ユーザー一括登録完了メールを送信する + * @param context + * @param accountId + * @param fileName + * @param requestTime + * @param errors + * @returns imports complate + */ + async multipleImportsComplate( + context: Context, + accountId: number, + fileName: string, + requestTime: number, + errors: MultipleImportErrors[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.multipleImportsComplate.name + } | params: { accountId: ${accountId}, fileName: ${fileName}, requestTime: ${requestTime} };`, + ); + try { + const account = await this.accountsRepository.findAccountById( + context, + accountId, + ); + if (!account) { + throw new Error(`account not found. id=${accountId}`); + } + + const dealerId = account.parent_account_id; + let dealerName: string | null = null; + if (dealerId !== null) { + const { company_name } = await this.accountsRepository.findAccountById( + context, + dealerId, + ); + dealerName = company_name; + } + + // アカウント情報を取得 + const { companyName, adminEmails } = await this.getAccountInformation( + context, + accountId, + ); + + if (errors.length === 0) { + const requestTimeDate = new Date(requestTime * 1000); + + // 完了メールを通知する + await this.sendgridService.sendMailWithU121( + context, + adminEmails, + companyName, + dealerName, + `${requestTimeDate.getFullYear()}.${ + requestTimeDate.getMonth() + 1 + }.${requestTimeDate.getDate()}`, + fileName, + ); + } else { + const duplicateEmails: number[] = []; + const duplicateAuthorIds: number[] = []; + const otherErrors: number[] = []; + // エラーを仕分ける + for (const error of errors) { + switch (error.errorCode) { + // メールアドレス重複エラー + case 'E010301': + duplicateEmails.push(error.line); + break; + // AuthorID重複エラー + case 'E010302': + duplicateAuthorIds.push(error.line); + break; + // その他エラー + default: + otherErrors.push(error.line); + break; + } + } + + // エラーメールを通知する + await this.sendgridService.sendMailWithU122( + context, + adminEmails, + companyName, + dealerName, + duplicateEmails, + duplicateAuthorIds, + otherErrors, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${ + this.multipleImportsComplate.name + }`, + ); + } + } /** * アカウントIDを指定して、アカウント情報と管理者情報を取得する diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index ea80ba7..bfc1064 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -20,6 +20,10 @@ import { TYPIST_NAME, VERIFY_LINK, TEMPORARY_PASSWORD, + EMAIL_DUPLICATION, + AUTHOR_ID_DUPLICATION, + UNEXPECTED_ERROR, + REQUEST_TIME, } from '../../templates/constants'; import { URL } from 'node:url'; @@ -71,6 +75,14 @@ export class SendGridService { private readonly templateU119Text: string; private readonly templateU119NoParentHtml: string; private readonly templateU119NoParentText: string; + private readonly templateU121Html: string; + private readonly templateU121Text: string; + private readonly templateU121NoParentHtml: string; + private readonly templateU121NoParentText: string; + private readonly templateU122Html: string; + private readonly templateU122Text: string; + private readonly templateU122NoParentHtml: string; + private readonly templateU122NoParentText: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); @@ -255,6 +267,44 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_119_no_parent.txt`), 'utf-8', ); + this.templateU121Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_121.html`), + 'utf-8', + ); + this.templateU121Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_121.txt`), + 'utf-8', + ); + this.templateU121NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_121_no_parent.html`, + ), + 'utf-8', + ); + this.templateU121NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_121_no_parent.txt`), + 'utf-8', + ); + this.templateU122Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_122.html`), + 'utf-8', + ); + this.templateU122Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_122.txt`), + 'utf-8', + ); + this.templateU122NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_122_no_parent.html`, + ), + 'utf-8', + ); + this.templateU122NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_122_no_parent.txt`), + 'utf-8', + ); } } @@ -1138,6 +1188,157 @@ export class SendGridService { } } + /** + * U-121のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param requestTime ユーザー一括登録のリクエストを受け付けた日時 + * @param fileName ユーザー一括登録で使用されたファイル名 + * @returns mail with u121 + */ + async sendMailWithU121( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + requestTime: string, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU121.name}`, + ); + try { + const subject = 'ユーザー一括登録 完了通知 [U-121]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU121NoParentHtml + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU121NoParentText + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } else { + html = this.templateU121Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU121Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU121.name}`, + ); + } + } + + /** + * U-122のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param duplicateEmails メールアドレスの重複エラーのある行番号 + * @param duplicateAuthorIds AuthorIdの重複エラーのある行番号 + * @param otherErrors その他のエラーのある行番号 + * @returns mail with u122 + */ + async sendMailWithU122( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + duplicateEmails: number[], + duplicateAuthorIds: number[], + otherErrors: number[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU122.name}`, + ); + try { + const duplicateEmailsMsg = + duplicateEmails.length === 0 + ? 'エラーはありません' + : duplicateEmails.map((x) => `L${x}`).join(', '); + const duplicateAuthorIdsMsg = + duplicateAuthorIds.length === 0 + ? 'エラーはありません' + : duplicateAuthorIds.map((x) => `L${x}`).join(', '); + const otherErrorsMsg = + otherErrors.length === 0 + ? 'エラーはありません' + : otherErrors.map((x) => `L${x}`).join(', '); + + const subject = 'ユーザー一括登録 失敗通知 [U-122]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU122NoParentHtml + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) + .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) + .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + text = this.templateU122NoParentText + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) + .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) + .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + } else { + html = this.templateU122Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) + .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) + .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + text = this.templateU122Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) + .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) + .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU122.name}`, + ); + } + } + /** * メールを送信する * @param context diff --git a/dictation_server/src/templates/constants.ts b/dictation_server/src/templates/constants.ts index 72e8fc0..319bed8 100644 --- a/dictation_server/src/templates/constants.ts +++ b/dictation_server/src/templates/constants.ts @@ -11,3 +11,8 @@ export const AUTHOR_NAME = '$AUTHOR_NAME$'; export const FILE_NAME = '$FILE_NAME$'; export const TYPIST_NAME = '$TYPIST_NAME$'; export const TEMPORARY_PASSWORD = '$TEMPORARY_PASSWORD$'; +export const REQUEST_TIME = '$REQUEST_TIME$'; + +export const EMAIL_DUPLICATION = `$EMAIL_DUPLICATION$`; +export const AUTHOR_ID_DUPLICATION = `$AUTHOR_ID_DUPLICATION$`; +export const UNEXPECTED_ERROR = `$UNEXPECTED_ERROR$`; diff --git a/dictation_server/src/templates/template_U_121.html b/dictation_server/src/templates/template_U_121.html new file mode 100644 index 0000000..957b7ae --- /dev/null +++ b/dictation_server/src/templates/template_U_121.html @@ -0,0 +1,65 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録が完了しました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録が完了しました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Français>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録が完了しました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_121.txt b/dictation_server/src/templates/template_U_121.txt new file mode 100644 index 0000000..f1995de --- /dev/null +++ b/dictation_server/src/templates/template_U_121.txt @@ -0,0 +1,44 @@ + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録が完了しました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録が完了しました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録が完了しました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121_no_parent.html b/dictation_server/src/templates/template_U_121_no_parent.html new file mode 100644 index 0000000..0ae8b99 --- /dev/null +++ b/dictation_server/src/templates/template_U_121_no_parent.html @@ -0,0 +1,53 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録が完了しました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録が完了しました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Français>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録が完了しました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_121_no_parent.txt b/dictation_server/src/templates/template_U_121_no_parent.txt new file mode 100644 index 0000000..cc81ddf --- /dev/null +++ b/dictation_server/src/templates/template_U_121_no_parent.txt @@ -0,0 +1,38 @@ + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録が完了しました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録が完了しました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録が完了しました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122.html b/dictation_server/src/templates/template_U_122.html new file mode 100644 index 0000000..7a5de32 --- /dev/null +++ b/dictation_server/src/templates/template_U_122.html @@ -0,0 +1,107 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    CSVファイルによるユーザー一括登録に失敗しました。

    +

    + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail + addressと重複しています。
    + $EMAIL_DUPLICATION$ +

    +

    + 2. + 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + *既に登録済みのE-mail address, Author IDを再登録することはできません。 +

    +

    + 3. + 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    + $UNEXPECTED_ERROR$ +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system. This is an automatically generated e-mail, please do not + reply. +

    +
    +
    +

    <Deutsch>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    CSVファイルによるユーザー一括登録に失敗しました。

    +

    + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail + addressと重複しています。
    + $EMAIL_DUPLICATION$ +

    +

    + 2. + 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + *既に登録済みのE-mail address, Author IDを再登録することはできません。 +

    +

    + 3. + 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    + $UNEXPECTED_ERROR$ +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system. This is an automatically generated e-mail, please do not + reply. +

    +
    +
    +

    <Français>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    CSVファイルによるユーザー一括登録に失敗しました。

    +

    + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail + addressと重複しています。
    + $EMAIL_DUPLICATION$ +

    +

    + 2. + 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + *既に登録済みのE-mail address, Author IDを再登録することはできません。 +

    +

    + 3. + 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    + $UNEXPECTED_ERROR$ +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system. This is an automatically generated e-mail, please do not + reply. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_122.txt b/dictation_server/src/templates/template_U_122.txt new file mode 100644 index 0000000..f4f2710 --- /dev/null +++ b/dictation_server/src/templates/template_U_122.txt @@ -0,0 +1,74 @@ + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録に失敗しました。 + + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 + $EMAIL_DUPLICATION$ + + 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 + $AUTHOR_ID_DUPLICATION$ + + *既に登録済みのE-mail address, Author IDを再登録することはできません。 + + + 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 +   $UNEXPECTED_ERROR$ + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録に失敗しました。 + + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 + $EMAIL_DUPLICATION$ + + 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 + $AUTHOR_ID_DUPLICATION$ + + *既に登録済みのE-mail address, Author IDを再登録することはできません。 + + + 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 +   $UNEXPECTED_ERROR$ + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録に失敗しました。 + + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 + $EMAIL_DUPLICATION$ + + 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 + $AUTHOR_ID_DUPLICATION$ + + *既に登録済みのE-mail address, Author IDを再登録することはできません。 + + + 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 +   $UNEXPECTED_ERROR$ + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122_no_parent.html b/dictation_server/src/templates/template_U_122_no_parent.html new file mode 100644 index 0000000..cac57e5 --- /dev/null +++ b/dictation_server/src/templates/template_U_122_no_parent.html @@ -0,0 +1,95 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    CSVファイルによるユーザー一括登録に失敗しました。

    +

    + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail + addressと重複しています。
    + $EMAIL_DUPLICATION$ +

    +

    + 2. + 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + *既に登録済みのE-mail address, Author IDを再登録することはできません。 +

    +

    + 3. + 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    + $UNEXPECTED_ERROR$ +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system. This is an automatically generated e-mail, please do not + reply. +

    +
    +
    +

    <Deutsch>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    CSVファイルによるユーザー一括登録に失敗しました。

    +

    + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail + addressと重複しています。
    + $EMAIL_DUPLICATION$ +

    +

    + 2. + 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + *既に登録済みのE-mail address, Author IDを再登録することはできません。 +

    +

    + 3. + 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    + $UNEXPECTED_ERROR$ +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system. This is an automatically generated e-mail, please do not + reply. +

    +
    +
    +

    <Français>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    CSVファイルによるユーザー一括登録に失敗しました。

    +

    + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail + addressと重複しています。
    + $EMAIL_DUPLICATION$ +

    +

    + 2. + 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + *既に登録済みのE-mail address, Author IDを再登録することはできません。 +

    +

    + 3. + 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    + $UNEXPECTED_ERROR$ +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system. This is an automatically generated e-mail, please do not + reply. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_122_no_parent.txt b/dictation_server/src/templates/template_U_122_no_parent.txt new file mode 100644 index 0000000..3b54d39 --- /dev/null +++ b/dictation_server/src/templates/template_U_122_no_parent.txt @@ -0,0 +1,68 @@ + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録に失敗しました。 + + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 + $EMAIL_DUPLICATION$ + + 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 + $AUTHOR_ID_DUPLICATION$ + + *既に登録済みのE-mail address, Author IDを再登録することはできません。 + + + 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 +   $UNEXPECTED_ERROR$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録に失敗しました。 + + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 + $EMAIL_DUPLICATION$ + + 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 + $AUTHOR_ID_DUPLICATION$ + + *既に登録済みのE-mail address, Author IDを再登録することはできません。 + + + 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 +   $UNEXPECTED_ERROR$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録に失敗しました。 + + 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 + $EMAIL_DUPLICATION$ + + 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 + $AUTHOR_ID_DUPLICATION$ + + *既に登録済みのE-mail address, Author IDを再登録することはできません。 + + + 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 +   $UNEXPECTED_ERROR$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. \ No newline at end of file From 31de71f743a1ebd860172436b6f30621bf4091e8 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 5 Mar 2024 10:27:50 +0000 Subject: [PATCH 037/109] =?UTF-8?q?Merged=20PR=20797:=20API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E4=B8=80=E6=8B=AC=E7=99=BB=E9=8C=B2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3752: API実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3752) - ユーザー一括登録APIとテストを実装しました。 - メール文面はまだ翻訳が来ていないので日本語のものを使用しています。別タスクで多言語対応します。 ## レビューポイント - ファイル名は認識通りでしょうか? - ファイルの内容は認識通りでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/.env.test | 3 + dictation_server/src/common/test/overrides.ts | 11 + .../features/users/test/users.service.mock.ts | 3 + .../src/features/users/users.controller.ts | 5 +- .../src/features/users/users.module.ts | 2 + .../src/features/users/users.service.spec.ts | 201 ++++++++++++++++++ .../src/features/users/users.service.ts | 122 ++++++++++- .../blobstorage/blobstorage.service.ts | 50 +++++ .../src/gateways/sendgrid/sendgrid.service.ts | 89 ++++++++ dictation_server/src/templates/constants.ts | 1 - .../src/templates/template_U_120.html | 80 +++++++ .../src/templates/template_U_120.txt | 57 +++++ .../templates/template_U_120_no_parent.html | 68 ++++++ .../templates/template_U_120_no_parent.txt | 51 +++++ 14 files changed, 734 insertions(+), 9 deletions(-) create mode 100644 dictation_server/src/templates/template_U_120.html create mode 100644 dictation_server/src/templates/template_U_120.txt create mode 100644 dictation_server/src/templates/template_U_120_no_parent.html create mode 100644 dictation_server/src/templates/template_U_120_no_parent.txt diff --git a/dictation_server/.env.test b/dictation_server/.env.test index a2748df..81bb261 100644 --- a/dictation_server/.env.test +++ b/dictation_server/.env.test @@ -20,12 +20,15 @@ STORAGE_TOKEN_EXPIRE_TIME=30 STORAGE_ACCOUNT_NAME_US=saxxxxusxxx STORAGE_ACCOUNT_NAME_AU=saxxxxauxxx STORAGE_ACCOUNT_NAME_EU=saxxxxeuxxx +STORAGE_ACCOUNT_NAME_IMPORTS=saxxxximportsxxx STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== +STORAGE_ACCOUNT_KEY_IMPORTS=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== STORAGE_ACCOUNT_ENDPOINT_US=https://xxxxxxxxxxxx.blob.core.windows.net/ STORAGE_ACCOUNT_ENDPOINT_AU=https://xxxxxxxxxxxx.blob.core.windows.net/ STORAGE_ACCOUNT_ENDPOINT_EU=https://xxxxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_IMPORTS=https://xxxxxxxxxxxx.blob.core.windows.net/ ACCESS_TOKEN_LIFETIME_WEB=7200000 REFRESH_TOKEN_LIFETIME_WEB=86400000 REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index 6bb9aea..4c65c7c 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -180,6 +180,11 @@ export const overrideBlobstorageService = ( accountId: number, country: string, ) => Promise; + uploadImportsBlob?: ( + context: Context, + fileName: string, + content: string, + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -220,6 +225,12 @@ export const overrideBlobstorageService = ( writable: true, }); } + if (overrides.uploadImportsBlob) { + Object.defineProperty(obj, obj.uploadImportsBlob.name, { + value: overrides.uploadImportsBlob, + writable: true, + }); + } }; /** diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index c2da3db..f17d58e 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -19,6 +19,7 @@ import { import { AdB2cUser } from '../../../gateways/adb2c/types/types'; import { ADB2C_SIGN_IN_TYPE } from '../../../constants'; import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; +import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service'; export type SortCriteriaRepositoryMockValue = { updateSortCriteria: SortCriteria | Error; @@ -89,6 +90,8 @@ export const makeUsersServiceMock = async ( return makeSortCriteriaRepositoryMock( sortCriteriaRepositoryMockValue, ); + case BlobstorageService: + return {}; } }) .compile(); diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 531c13b..7d731cc 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -4,7 +4,6 @@ import { Get, HttpException, HttpStatus, - Ip, Logger, Post, Query, @@ -1068,7 +1067,9 @@ export class UsersController { const context = makeContext(userId, requestId, delegateUserId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: 処理を実装 + // 登録処理 + const { users, filename } = body; + await this.usersService.multipleImports(context, userId, filename, users); return {}; } diff --git a/dictation_server/src/features/users/users.module.ts b/dictation_server/src/features/users/users.module.ts index b5f5963..f8d02d9 100644 --- a/dictation_server/src/features/users/users.module.ts +++ b/dictation_server/src/features/users/users.module.ts @@ -9,6 +9,7 @@ import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { AuthService } from '../auth/auth.service'; import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; +import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r AdB2cModule, SendGridModule, ConfigModule, + BlobstorageModule, ], controllers: [UsersController], providers: [UsersService, AuthService], diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 26f9703..503329d 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -31,6 +31,7 @@ import { makeTestingModule } from '../../common/test/modules'; import { Context, makeContext } from '../../common/log'; import { overrideAdB2cService, + overrideBlobstorageService, overrideSendgridService, overrideUsersRepositoryService, } from '../../common/test/overrides'; @@ -3656,6 +3657,206 @@ describe('UsersService.deleteUser', () => { }); }); +describe('UsersService.multipleImports', () => { + let source: DataSource | null = null; + + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ユーザー一括登録ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + overrideBlobstorageService(service, { + uploadImportsBlob: async () => {}, + }); + + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendgridService'], 'sendMailWithU120') + .mockImplementation(); + + const now = new Date(); + const date = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`; + const filename = 'fileName.csv'; + + const users = [ + { + name: 'name1', + email: 'mail1@example.com', + role: 1, + authorId: 'HOGE1', + autoRenew: 0, + notification: 0, + encryption: 0, + encryptionPassword: 'string', + prompt: 0, + }, + { + name: 'name2', + email: 'mail2@example.com', + role: 2, + autoRenew: 0, + notification: 0, + }, + ]; + await service.multipleImports(context, admin.external_id, filename, users); + + // メール送信メソッドが呼ばれているか確認 + expect(spy).toHaveBeenCalledWith( + context, + ['admin@example.com'], + account.company_name, + dealer.company_name, + date, + filename, + ); + }); + + it('Blobアップロードに失敗した場合はエラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + overrideBlobstorageService(service, { + uploadImportsBlob: async () => { + throw new Error('error'); + }, + }); + + const now = new Date(); + const date = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`; + const filename = 'fileName.csv'; + + const users = [ + { + name: 'name1', + email: 'mail1@example.com', + role: 1, + authorId: 'HOGE1', + autoRenew: 0, + notification: 0, + encryption: 0, + encryptionPassword: 'string', + prompt: 0, + }, + { + name: 'name2', + email: 'mail2@example.com', + role: 2, + autoRenew: 0, + notification: 0, + }, + ]; + + try { + await service.multipleImports( + context, + admin.external_id, + filename, + users, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + describe('UsersService.multipleImportsComplate', () => { let source: DataSource | null = null; diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 2dbf943..2df3703 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -24,6 +24,7 @@ import { import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { + MultipleImportUser, GetRelationsResponse, MultipleImportErrors, User, @@ -63,12 +64,11 @@ import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; @Injectable() export class UsersService { private readonly logger = new Logger(UsersService.name); - private readonly mailFrom: string; - private readonly appDomain: string; constructor( private readonly accountsRepository: AccountsRepositoryService, private readonly usersRepository: UsersRepositoryService, @@ -77,10 +77,8 @@ export class UsersService { private readonly adB2cService: AdB2cService, private readonly configService: ConfigService, private readonly sendgridService: SendGridService, - ) { - this.mailFrom = this.configService.getOrThrow('MAIL_FROM'); - this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); - } + private readonly blobStorageService: BlobstorageService, + ) {} /** * Confirms user @@ -1600,6 +1598,118 @@ export class UsersService { } } + /** + * ユーザー一括登録用のファイルをBlobにアップロードする + * @param context + * @param externalId + * @param fileName + * @param users + * @returns imports + */ + async multipleImports( + context: Context, + externalId: string, + fileName: string, + users: MultipleImportUser[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.multipleImports.name + } | params: { externalId: ${externalId}, ` + + `fileName: ${fileName}, ` + + `users.length: ${users.length} };`, + ); + try { + // ユーザー情報を取得 + const user = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + if (user == null) { + throw new Error(`user not found. externalId=${externalId}`); + } + + const now = new Date(); + // 日時を生成(YYYYMMDD_HHMMSS) + const dateTime = + `${now.getFullYear().toString().padStart(4, '0')}` + + `${(now.getMonth() + 1).toString().padStart(2, '0')}` + // 月は0から始まるため+1する + `${now.getDate().toString().padStart(2, '0')}` + + `_${now.getHours().toString().padStart(2, '0')}` + + `${now.getMinutes().toString().padStart(2, '0')}` + + `${now.getSeconds().toString().padStart(2, '0')}`; + + // ファイル名を生成(U_YYYYMMDD_HHMMSS_アカウントID_ユーザーID.json) + const jsonFileName = `U_${dateTime}_${user.account_id}_${user.id}.json`; + + // ユーザー情報をJSON形式に変換 + const usersJson = JSON.stringify({ + account_id: user.account_id, + user_id: user.id, + user_role: user.role, + external_id: user.external_id, + file_name: fileName, + date: Math.floor(now.getTime() / 1000), + data: users, + }); + + // Blobにファイルをアップロード(ユーザー一括登録用) + await this.blobStorageService.uploadImportsBlob( + context, + jsonFileName, + usersJson, + ); + + // 受付完了メールを送信 + try { + // アカウント・管理者情報を取得 + const { adminEmails, companyName } = await this.getAccountInformation( + context, + user.account_id, + ); + + // Dealer情報を取得 + const dealer = await this.accountsRepository.findParentAccount( + context, + user.account_id, + ); + const dealerName = dealer?.company_name ?? null; + + const now = new Date(); + // 日時を生成(YYYY.MM.DD) + const date = `${now.getFullYear()}.${ + now.getMonth() + 1 // 月は0から始まるため+1する + }.${now.getDate()}`; + + await this.sendgridService.sendMailWithU120( + context, + adminEmails, + companyName, + dealerName, + date, + fileName, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + switch (e.constructor) { + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.multipleImports.name}`, + ); + } + } + /** * アカウントIDを指定して、アカウント情報と管理者情報を取得する * @param context diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index 9199ad9..baaf57e 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -22,9 +22,11 @@ export class BlobstorageService { private readonly blobServiceClientUS: BlobServiceClient; private readonly blobServiceClientEU: BlobServiceClient; private readonly blobServiceClientAU: BlobServiceClient; + private readonly blobServiceClientImports: BlobServiceClient; private readonly sharedKeyCredentialUS: StorageSharedKeyCredential; private readonly sharedKeyCredentialAU: StorageSharedKeyCredential; private readonly sharedKeyCredentialEU: StorageSharedKeyCredential; + private readonly sharedKeyCredentialImports: StorageSharedKeyCredential; private readonly sasTokenExpireHour: number; constructor(private readonly configService: ConfigService) { this.sharedKeyCredentialUS = new StorageSharedKeyCredential( @@ -39,6 +41,11 @@ export class BlobstorageService { this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_EU'), this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_EU'), ); + this.sharedKeyCredentialImports = new StorageSharedKeyCredential( + this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_IMPORTS'), + this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_IMPORTS'), + ); + this.blobServiceClientUS = new BlobServiceClient( this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_US'), this.sharedKeyCredentialUS, @@ -51,6 +58,10 @@ export class BlobstorageService { this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_EU'), this.sharedKeyCredentialEU, ); + this.blobServiceClientImports = new BlobServiceClient( + this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_IMPORTS'), + this.sharedKeyCredentialImports, + ); this.sasTokenExpireHour = this.configService.getOrThrow( 'STORAGE_TOKEN_EXPIRE_TIME', ); @@ -457,6 +468,45 @@ export class BlobstorageService { return url.toString(); } + /** + * 一括登録用のBlobを作成します + * @param context + * @param fileName + * @param content + * @returns imports blob + */ + async uploadImportsBlob( + context: Context, + fileName: string, + content: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.uploadImportsBlob.name + } | params: { fileName: ${fileName} };`, + ); + + try { + const containerClient = + this.blobServiceClientImports.getContainerClient('import-users'); + const blockBlobClient = containerClient.getBlockBlobClient(fileName); + const result = await blockBlobClient.upload(content, content.length); + + if (result.errorCode) { + throw new Error( + `upload failed. errorCode: ${result.errorCode} fileName: ${fileName}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.uploadImportsBlob.name}`, + ); + } + } + /** * Gets container client * @param companyName diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index bfc1064..e960d25 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -75,6 +75,10 @@ export class SendGridService { private readonly templateU119Text: string; private readonly templateU119NoParentHtml: string; private readonly templateU119NoParentText: string; + private readonly templateU120Html: string; + private readonly templateU120Text: string; + private readonly templateU120NoParentHtml: string; + private readonly templateU120NoParentText: string; private readonly templateU121Html: string; private readonly templateU121Text: string; private readonly templateU121NoParentHtml: string; @@ -267,6 +271,25 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_119_no_parent.txt`), 'utf-8', ); + this.templateU120Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_120.html`), + 'utf-8', + ); + this.templateU120Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_120.txt`), + 'utf-8', + ); + this.templateU120NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_120_no_parent.html`, + ), + 'utf-8', + ); + this.templateU120NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_120_no_parent.txt`), + 'utf-8', + ); this.templateU121Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_121.html`), 'utf-8', @@ -1188,6 +1211,72 @@ export class SendGridService { } } + /** + * U-120のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param tire1AdminMails 第一階層の管理者(primary/secondary)のメールアドレス + * @returns mail with u119 + */ + async sendMailWithU120( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + requestTime: string, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU120.name}`, + ); + try { + const subject = 'ユーザー一括登録 受付通知 [U-120]'; + + let html: string; + let text: string; + console.log(dealerAccountName); + + if (!dealerAccountName) { + html = this.templateU120NoParentHtml + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU120NoParentText + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } else { + html = this.templateU120Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU120Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU120.name}`, + ); + } + } + /** * U-121のテンプレートを使用したメールを送信する * @param context diff --git a/dictation_server/src/templates/constants.ts b/dictation_server/src/templates/constants.ts index 319bed8..d6650a6 100644 --- a/dictation_server/src/templates/constants.ts +++ b/dictation_server/src/templates/constants.ts @@ -12,7 +12,6 @@ export const FILE_NAME = '$FILE_NAME$'; export const TYPIST_NAME = '$TYPIST_NAME$'; export const TEMPORARY_PASSWORD = '$TEMPORARY_PASSWORD$'; export const REQUEST_TIME = '$REQUEST_TIME$'; - export const EMAIL_DUPLICATION = `$EMAIL_DUPLICATION$`; export const AUTHOR_ID_DUPLICATION = `$AUTHOR_ID_DUPLICATION$`; export const UNEXPECTED_ERROR = `$UNEXPECTED_ERROR$`; diff --git a/dictation_server/src/templates/template_U_120.html b/dictation_server/src/templates/template_U_120.html new file mode 100644 index 0000000..b4b4e75 --- /dev/null +++ b/dictation_server/src/templates/template_U_120.html @@ -0,0 +1,80 @@ + + + ユーザー一括登録 受付通知 [U-120] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録を受け付けました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ・登録完了には時間がかかる場合がありますので少々お待ちください。
    + ・登録完了通知は別途お送りします。
    + ・CSVファイルの内容に間違いがある場合は登録を完了できません。 +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録を受け付けました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ・登録完了には時間がかかる場合がありますので少々お待ちください。
    + ・登録完了通知は別途お送りします。
    + ・CSVファイルの内容に間違いがある場合は登録を完了できません。 +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Français>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録を受け付けました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ・登録完了には時間がかかる場合がありますので少々お待ちください。
    + ・登録完了通知は別途お送りします。
    + ・CSVファイルの内容に間違いがある場合は登録を完了できません。 +

    +

    + ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ + にお問い合わせください。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_120.txt b/dictation_server/src/templates/template_U_120.txt new file mode 100644 index 0000000..e8c68ec --- /dev/null +++ b/dictation_server/src/templates/template_U_120.txt @@ -0,0 +1,57 @@ + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録を受け付けました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +・登録完了には時間がかかる場合がありますので少々お待ちください。 +・登録完了通知は別途お送りします。 +・CSVファイルの内容に間違いがある場合は登録を完了できません。 + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録を受け付けました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +・登録完了には時間がかかる場合がありますので少々お待ちください。 +・登録完了通知は別途お送りします。 +・CSVファイルの内容に間違いがある場合は登録を完了できません。 + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録を受け付けました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +・登録完了には時間がかかる場合がありますので少々お待ちください。 +・登録完了通知は別途お送りします。 +・CSVファイルの内容に間違いがある場合は登録を完了できません。 + +ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_120_no_parent.html b/dictation_server/src/templates/template_U_120_no_parent.html new file mode 100644 index 0000000..692411d --- /dev/null +++ b/dictation_server/src/templates/template_U_120_no_parent.html @@ -0,0 +1,68 @@ + + + ユーザー一括登録 受付通知 [U-120] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録を受け付けました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ・登録完了には時間がかかる場合がありますので少々お待ちください。
    + ・登録完了通知は別途お送りします。
    + ・CSVファイルの内容に間違いがある場合は登録を完了できません。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録を受け付けました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ・登録完了には時間がかかる場合がありますので少々お待ちください。
    + ・登録完了通知は別途お送りします。
    + ・CSVファイルの内容に間違いがある場合は登録を完了できません。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    +
    +

    <Français>

    +

    Dear $CUSTOMER_NAME$,

    +

    ODMS Cloudをご利用いただきありがとうございます。

    +

    + CSVファイルによるユーザー一括登録を受け付けました。
    + - リクエスト日時:$REQUEST_TIME$
    + - SCVファイル名:$FILE_NAME$ +

    +

    + ・登録完了には時間がかかる場合がありますので少々お待ちください。
    + ・登録完了通知は別途お送りします。
    + ・CSVファイルの内容に間違いがある場合は登録を完了できません。 +

    +

    + If you received this e-mail in error, please delete this e-mail from + your system.
    + This is an automatically generated e-mail, please do not reply. +

    +
    + + diff --git a/dictation_server/src/templates/template_U_120_no_parent.txt b/dictation_server/src/templates/template_U_120_no_parent.txt new file mode 100644 index 0000000..fca8bcf --- /dev/null +++ b/dictation_server/src/templates/template_U_120_no_parent.txt @@ -0,0 +1,51 @@ + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録を受け付けました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +・登録完了には時間がかかる場合がありますので少々お待ちください。 +・登録完了通知は別途お送りします。 +・CSVファイルの内容に間違いがある場合は登録を完了できません。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録を受け付けました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +・登録完了には時間がかかる場合がありますので少々お待ちください。 +・登録完了通知は別途お送りします。 +・CSVファイルの内容に間違いがある場合は登録を完了できません。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, + +ODMS Cloudをご利用いただきありがとうございます。 + +CSVファイルによるユーザー一括登録を受け付けました。 + - リクエスト日時:$REQUEST_TIME$ + - SCVファイル名:$FILE_NAME$ + +・登録完了には時間がかかる場合がありますので少々お待ちください。 +・登録完了通知は別途お送りします。 +・CSVファイルの内容に間違いがある場合は登録を完了できません。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. \ No newline at end of file From d6a47932e708c52f5b35eff3bfe6f89d7932cd09 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 6 Mar 2024 01:48:02 +0000 Subject: [PATCH 038/109] =?UTF-8?q?Merged=20PR=20786:=20Azure=20Functions?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E4=B8=80=E6=8B=AC=E7=99=BB=E9=8C=B2?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3756: Azure Functions実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3756) - ユーザー一括登録用のAzure Functionを実装しました。 ## レビューポイント - 処理の流れがラフスケッチと認識通りでしょうか? - JSONファイルの内容はイメージ通りでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 - テスト実行 実際の詳細な動作についてはdevelop環境で確認します。 --- dictation_function/codegen.sh | 2 + dictation_function/openapitools.json | 7 + dictation_function/package-lock.json | 1118 ++- dictation_function/package.json | 6 +- dictation_function/src/adb2c/types/types.ts | 2 +- dictation_function/src/api/.gitignore | 4 + dictation_function/src/api/.npmignore | 1 + .../src/api/.openapi-generator-ignore | 23 + .../src/api/.openapi-generator/FILES | 8 + .../src/api/.openapi-generator/VERSION | 1 + dictation_function/src/api/api.ts | 8686 +++++++++++++++++ dictation_function/src/api/base.ts | 86 + dictation_function/src/api/common.ts | 150 + dictation_function/src/api/configuration.ts | 110 + dictation_function/src/api/git_push.sh | 57 + dictation_function/src/api/index.ts | 18 + dictation_function/src/api/odms/openapi.json | 5357 ++++++++++ .../src/blobstorage/blobstorage.service.ts | 184 + .../src/blobstorage/types/guards.ts | 155 + .../src/blobstorage/types/types.ts | 56 + dictation_function/src/common/errors/code.ts | 79 + dictation_function/src/common/errors/index.ts | 3 + dictation_function/src/common/errors/types.ts | 9 + dictation_function/src/common/errors/utils.ts | 101 + dictation_function/src/common/jwt/index.ts | 3 + dictation_function/src/common/jwt/jwt.spec.ts | 250 + dictation_function/src/common/jwt/jwt.ts | 130 + dictation_function/src/common/jwt/types.ts | 32 + dictation_function/src/constants/index.ts | 42 + .../src/functions/importUsers.ts | 510 + .../src/test/importUsers.spec.ts | 90 + dictation_server/src/api/odms/openapi.json | 176 +- 32 files changed, 17441 insertions(+), 15 deletions(-) create mode 100644 dictation_function/codegen.sh create mode 100644 dictation_function/openapitools.json create mode 100644 dictation_function/src/api/.gitignore create mode 100644 dictation_function/src/api/.npmignore create mode 100644 dictation_function/src/api/.openapi-generator-ignore create mode 100644 dictation_function/src/api/.openapi-generator/FILES create mode 100644 dictation_function/src/api/.openapi-generator/VERSION create mode 100644 dictation_function/src/api/api.ts create mode 100644 dictation_function/src/api/base.ts create mode 100644 dictation_function/src/api/common.ts create mode 100644 dictation_function/src/api/configuration.ts create mode 100644 dictation_function/src/api/git_push.sh create mode 100644 dictation_function/src/api/index.ts create mode 100644 dictation_function/src/api/odms/openapi.json create mode 100644 dictation_function/src/blobstorage/blobstorage.service.ts create mode 100644 dictation_function/src/blobstorage/types/guards.ts create mode 100644 dictation_function/src/blobstorage/types/types.ts create mode 100644 dictation_function/src/common/errors/code.ts create mode 100644 dictation_function/src/common/errors/index.ts create mode 100644 dictation_function/src/common/errors/types.ts create mode 100644 dictation_function/src/common/errors/utils.ts create mode 100644 dictation_function/src/common/jwt/index.ts create mode 100644 dictation_function/src/common/jwt/jwt.spec.ts create mode 100644 dictation_function/src/common/jwt/jwt.ts create mode 100644 dictation_function/src/common/jwt/types.ts create mode 100644 dictation_function/src/functions/importUsers.ts create mode 100644 dictation_function/src/test/importUsers.spec.ts diff --git a/dictation_function/codegen.sh b/dictation_function/codegen.sh new file mode 100644 index 0000000..19b9e9a --- /dev/null +++ b/dictation_function/codegen.sh @@ -0,0 +1,2 @@ +npx openapi-generator-cli version-manager set 7.1.0 +npx openapi-generator-cli generate -g typescript-axios -i /app/dictation_function/src/api/odms/openapi.json -o /app/dictation_function/src/api/ diff --git a/dictation_function/openapitools.json b/dictation_function/openapitools.json new file mode 100644 index 0000000..15fef60 --- /dev/null +++ b/dictation_function/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.1.0" + } +} diff --git a/dictation_function/package-lock.json b/dictation_function/package-lock.json index 8ebaf00..499c39b 100644 --- a/dictation_function/package-lock.json +++ b/dictation_function/package-lock.json @@ -9,6 +9,7 @@ "dependencies": { "@azure/functions": "^4.0.0", "@azure/identity": "^3.1.3", + "@azure/storage-blob": "^12.17.0", "@microsoft/microsoft-graph-client": "^3.0.5", "@sendgrid/mail": "^7.7.0", "dotenv": "^16.0.3", @@ -17,11 +18,13 @@ "typeorm": "^0.3.10" }, "devDependencies": { + "@openapitools/openapi-generator-cli": "^2.9.0", "@types/jest": "^27.5.0", "@types/node": "18.x", "@types/redis": "^2.8.13", "@types/redis-mock": "^0.17.3", "azure-functions-core-tools": "^4.x", + "base64url": "^3.0.1", "jest": "^28.0.3", "redis-mock": "^0.56.3", "rimraf": "^5.0.0", @@ -85,6 +88,103 @@ "node": ">=14.0.0" } }, + "node_modules/@azure/core-http": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz", + "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@azure/core-http/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.6.0.tgz", + "integrity": "sha512-PyRNcaIOfMgoUC01/24NoG+k8O81VrKxYARnDlo+Q2xji0/0/j2nIt8BwQh294pb1c5QnXTDPbNR4KzoDKXEoQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", + "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@azure/core-rest-pipeline": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.12.2.tgz", @@ -261,6 +361,36 @@ "node": ">=0.8.0" } }, + "node_modules/@azure/storage-blob": { + "version": "12.17.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz", + "integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^3.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -1454,6 +1584,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -1608,6 +1747,73 @@ } } }, + "node_modules/@nestjs/common": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", + "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", + "dev": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", + "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -1724,6 +1930,147 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@openapitools/openapi-generator-cli": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.9.0.tgz", + "integrity": "sha512-KQpftKeiMoH5aEI/amOVLFGkGeT3DyA7Atj7v7l8xT3O9xnIUpoDmMg0WBTEh+NHxEwEAITQNDzr+JLjkXVaKw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nestjs/axios": "3.0.1", + "@nestjs/common": "10.3.0", + "@nestjs/core": "10.3.0", + "@nuxtjs/opencollective": "0.3.2", + "axios": "1.6.5", + "chalk": "4.1.2", + "commander": "8.3.0", + "compare-versions": "4.1.4", + "concurrently": "6.5.1", + "console.table": "0.10.0", + "fs-extra": "10.1.0", + "glob": "7.2.3", + "inquirer": "8.2.6", + "lodash": "4.17.21", + "reflect-metadata": "0.1.13", + "rxjs": "7.8.1", + "tslib": "2.6.2" + }, + "bin": { + "openapi-generator-cli": "main.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openapi_generator" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/axios": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", + "dev": true, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1984,8 +2331,29 @@ "node_modules/@types/node": { "version": "18.18.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", - "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", - "dev": true + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/@types/prettier": { "version": "2.7.3", @@ -2017,6 +2385,14 @@ "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", "dev": true }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.29", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", @@ -2662,6 +3038,50 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2977,6 +3397,12 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -3017,6 +3443,18 @@ "node": ">=6" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-highlight": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", @@ -3139,6 +3577,27 @@ "node": ">=10" } }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3219,6 +3678,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -3281,6 +3749,21 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/compare-versions": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.4.tgz", + "integrity": "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==", + "dev": true + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -3292,12 +3775,196 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concurrently": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", + "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "date-fns": "^2.16.1", + "lodash": "^4.17.21", + "rxjs": "^6.6.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^16.2.0" + }, + "bin": { + "concurrently": "bin/concurrently.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/concurrently/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/concurrently/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/concurrently/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/concurrently/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/concurrently/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "dev": true + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "devOptional": true }, + "node_modules/console.table": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz", + "integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==", + "dev": true, + "dependencies": { + "easy-table": "1.1.0" + }, + "engines": { + "node": "> 0.10" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -3369,6 +4036,18 @@ "node": ">=0.10.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", @@ -3454,6 +4133,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/easy-table": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", + "integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==", + "dev": true, + "optionalDependencies": { + "wcwidth": ">=1.0.1" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3490,7 +4178,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -3614,6 +4301,32 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3635,6 +4348,30 @@ "bser": "2.1.1" } }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3661,9 +4398,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -3719,6 +4456,20 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -4166,6 +4917,102 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ioredis": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", @@ -4247,6 +5094,15 @@ "node": ">=6" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -4280,6 +5136,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -4363,6 +5231,15 @@ "node": ">=8" } }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -5093,6 +5970,18 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -5213,6 +6102,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -5268,6 +6163,22 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -5661,6 +6572,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "node_modules/mysql2": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", @@ -5745,7 +6662,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "devOptional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -6094,6 +7010,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6245,6 +7214,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -6320,6 +7295,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -6354,6 +7337,12 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/qs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", @@ -6513,6 +7502,25 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -6541,6 +7549,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6815,6 +7841,12 @@ "source-map": "^0.6.0" } }, + "node_modules/spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7291,6 +8323,24 @@ "node": ">=0.8" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7321,8 +8371,16 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } }, "node_modules/ts-jest": { "version": "28.0.1", @@ -7408,6 +8466,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -7588,6 +8654,18 @@ "node": ">=4.2.0" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dev": true, + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/undici": { "version": "5.26.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", @@ -7619,6 +8697,15 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -7692,17 +8779,24 @@ "makeerror": "1.0.12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "devOptional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/dictation_function/package.json b/dictation_function/package.json index 33f2287..fce3022 100644 --- a/dictation_function/package.json +++ b/dictation_function/package.json @@ -9,11 +9,13 @@ "clean": "rimraf dist", "prestart": "npm run clean && npm run build", "start": "func start", - "test": "jest" + "test": "jest", + "codegen": "sh codegen.sh" }, "dependencies": { "@azure/functions": "^4.0.0", "@azure/identity": "^3.1.3", + "@azure/storage-blob": "^12.17.0", "@microsoft/microsoft-graph-client": "^3.0.5", "@sendgrid/mail": "^7.7.0", "dotenv": "^16.0.3", @@ -22,11 +24,13 @@ "typeorm": "^0.3.10" }, "devDependencies": { + "@openapitools/openapi-generator-cli": "^2.9.0", "@types/jest": "^27.5.0", "@types/node": "18.x", "@types/redis": "^2.8.13", "@types/redis-mock": "^0.17.3", "azure-functions-core-tools": "^4.x", + "base64url": "^3.0.1", "jest": "^28.0.3", "redis-mock": "^0.56.3", "rimraf": "^5.0.0", diff --git a/dictation_function/src/adb2c/types/types.ts b/dictation_function/src/adb2c/types/types.ts index a7261ef..1c5d0f8 100644 --- a/dictation_function/src/adb2c/types/types.ts +++ b/dictation_function/src/adb2c/types/types.ts @@ -1,5 +1,5 @@ export type AdB2cResponse = { - '@odata.context': string; + "@odata.context": string; value: AdB2cUser[]; }; export type AdB2cUser = { diff --git a/dictation_function/src/api/.gitignore b/dictation_function/src/api/.gitignore new file mode 100644 index 0000000..149b576 --- /dev/null +++ b/dictation_function/src/api/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/dictation_function/src/api/.npmignore b/dictation_function/src/api/.npmignore new file mode 100644 index 0000000..999d88d --- /dev/null +++ b/dictation_function/src/api/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/dictation_function/src/api/.openapi-generator-ignore b/dictation_function/src/api/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/dictation_function/src/api/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/dictation_function/src/api/.openapi-generator/FILES b/dictation_function/src/api/.openapi-generator/FILES new file mode 100644 index 0000000..a80cd4f --- /dev/null +++ b/dictation_function/src/api/.openapi-generator/FILES @@ -0,0 +1,8 @@ +.gitignore +.npmignore +api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts diff --git a/dictation_function/src/api/.openapi-generator/VERSION b/dictation_function/src/api/.openapi-generator/VERSION new file mode 100644 index 0000000..3769235 --- /dev/null +++ b/dictation_function/src/api/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.1.0 \ No newline at end of file diff --git a/dictation_function/src/api/api.ts b/dictation_function/src/api/api.ts new file mode 100644 index 0000000..b68f729 --- /dev/null +++ b/dictation_function/src/api/api.ts @@ -0,0 +1,8686 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import type { RequestArgs } from './base'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base'; + +/** + * + * @export + * @interface AccessTokenResponse + */ +export interface AccessTokenResponse { + /** + * + * @type {string} + * @memberof AccessTokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface Account + */ +export interface Account { + /** + * + * @type {number} + * @memberof Account + */ + 'accountId': number; + /** + * + * @type {string} + * @memberof Account + */ + 'companyName': string; + /** + * + * @type {number} + * @memberof Account + */ + 'tier': number; + /** + * + * @type {string} + * @memberof Account + */ + 'country': string; + /** + * + * @type {number} + * @memberof Account + */ + 'parentAccountId'?: number; + /** + * + * @type {boolean} + * @memberof Account + */ + 'delegationPermission': boolean; + /** + * + * @type {boolean} + * @memberof Account + */ + 'autoFileDelete': boolean; + /** + * + * @type {number} + * @memberof Account + */ + 'fileRetentionDays': number; + /** + * + * @type {number} + * @memberof Account + */ + 'primaryAdminUserId'?: number; + /** + * + * @type {number} + * @memberof Account + */ + 'secondryAdminUserId'?: number; + /** + * + * @type {string} + * @memberof Account + */ + 'parentAccountName'?: string; +} +/** + * + * @export + * @interface ActivateCardLicensesRequest + */ +export interface ActivateCardLicensesRequest { + /** + * + * @type {string} + * @memberof ActivateCardLicensesRequest + */ + 'cardLicenseKey': string; +} +/** + * + * @export + * @interface AllocatableLicenseInfo + */ +export interface AllocatableLicenseInfo { + /** + * + * @type {number} + * @memberof AllocatableLicenseInfo + */ + 'licenseId': number; + /** + * + * @type {string} + * @memberof AllocatableLicenseInfo + */ + 'expiryDate'?: string; +} +/** + * + * @export + * @interface AllocateLicenseRequest + */ +export interface AllocateLicenseRequest { + /** + * ユーザーID + * @type {number} + * @memberof AllocateLicenseRequest + */ + 'userId': number; + /** + * 割り当てるライセンスのID + * @type {number} + * @memberof AllocateLicenseRequest + */ + 'newLicenseId': number; +} +/** + * + * @export + * @interface Assignee + */ +export interface Assignee { + /** + * TypistID(TypistIDかTypistGroupIDのどちらかに値が入る) + * @type {number} + * @memberof Assignee + */ + 'typistUserId'?: number; + /** + * TypistGroupID(TypistGroupIDかTypistIDのどちらかに値が入る) + * @type {number} + * @memberof Assignee + */ + 'typistGroupId'?: number; + /** + * Typist名 / TypistGroup名 + * @type {string} + * @memberof Assignee + */ + 'typistName': string; +} +/** + * + * @export + * @interface AudioDownloadLocationResponse + */ +export interface AudioDownloadLocationResponse { + /** + * Blob StorageにアクセスするためのSASトークン入りのアクセスURL + * @type {string} + * @memberof AudioDownloadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface AudioNextResponse + */ +export interface AudioNextResponse { + /** + * ODMS Cloud上の次の音声ファイルID(存在しなければundefind) + * @type {number} + * @memberof AudioNextResponse + */ + 'nextFileId'?: number; +} +/** + * + * @export + * @interface AudioOptionItem + */ +export interface AudioOptionItem { + /** + * + * @type {string} + * @memberof AudioOptionItem + */ + 'optionItemLabel': string; + /** + * + * @type {string} + * @memberof AudioOptionItem + */ + 'optionItemValue': string; +} +/** + * + * @export + * @interface AudioUploadFinishedRequest + */ +export interface AudioUploadFinishedRequest { + /** + * アップロード先Blob Storage(ファイル名含む) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'url': string; + /** + * 自分自身(ログイン認証)したAuthorID + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'authorId': string; + /** + * 音声ファイル名 + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'fileName': string; + /** + * 音声ファイルの録音時間(ミリ秒の整数値) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'duration': string; + /** + * 音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'createdDate': string; + /** + * 音声ファイルの録音作成終了日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'finishedDate': string; + /** + * 音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'uploadedDate': string; + /** + * 音声ファイルのファイルサイズ(Byte) + * @type {number} + * @memberof AudioUploadFinishedRequest + */ + 'fileSize': number; + /** + * 優先度 \"00\":Normal / \"01\":High + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'priority': string; + /** + * 録音形式: DSS/DS2(SP)/DS2(QP) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'audioFormat': string; + /** + * + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'comment': string; + /** + * + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'workType': string; + /** + * 音声ファイルに紐づくOption Itemの一覧(10個固定) + * @type {Array} + * @memberof AudioUploadFinishedRequest + */ + 'optionItemList': Array; + /** + * + * @type {boolean} + * @memberof AudioUploadFinishedRequest + */ + 'isEncrypted': boolean; +} +/** + * + * @export + * @interface AudioUploadFinishedResponse + */ +export interface AudioUploadFinishedResponse { + /** + * 8桁固定の数字 + * @type {string} + * @memberof AudioUploadFinishedResponse + */ + 'jobNumber': string; +} +/** + * + * @export + * @interface AudioUploadLocationResponse + */ +export interface AudioUploadLocationResponse { + /** + * Blob StorageにアクセスするためのSASトークン入りのアクセスURL + * @type {string} + * @memberof AudioUploadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface Author + */ +export interface Author { + /** + * Authorユーザーの内部ID + * @type {number} + * @memberof Author + */ + 'id': number; + /** + * AuthorID + * @type {string} + * @memberof Author + */ + 'authorId': string; +} +/** + * + * @export + * @interface CancelIssueRequest + */ +export interface CancelIssueRequest { + /** + * 注文元アカウントID + * @type {number} + * @memberof CancelIssueRequest + */ + 'orderedAccountId': number; + /** + * POナンバー + * @type {string} + * @memberof CancelIssueRequest + */ + 'poNumber': string; +} +/** + * + * @export + * @interface CancelOrderRequest + */ +export interface CancelOrderRequest { + /** + * + * @type {string} + * @memberof CancelOrderRequest + */ + 'poNumber': string; +} +/** + * + * @export + * @interface ConfirmRequest + */ +export interface ConfirmRequest { + /** + * + * @type {string} + * @memberof ConfirmRequest + */ + 'token': string; +} +/** + * + * @export + * @interface CreateAccountRequest + */ +export interface CreateAccountRequest { + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'companyName': string; + /** + * 国名(ISO 3166-1 alpha-2) + * @type {string} + * @memberof CreateAccountRequest + */ + 'country': string; + /** + * + * @type {number} + * @memberof CreateAccountRequest + */ + 'dealerAccountId'?: number; + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'adminName': string; + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'adminMail': string; + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'adminPassword': string; + /** + * 同意済み利用規約のバージョン(EULA) + * @type {string} + * @memberof CreateAccountRequest + */ + 'acceptedEulaVersion': string; + /** + * 同意済みプライバシーポリシーのバージョン + * @type {string} + * @memberof CreateAccountRequest + */ + 'acceptedPrivacyNoticeVersion': string; + /** + * 同意済み利用規約のバージョン(DPA) + * @type {string} + * @memberof CreateAccountRequest + */ + 'acceptedDpaVersion': string; + /** + * reCAPTCHA Token + * @type {string} + * @memberof CreateAccountRequest + */ + 'token': string; +} +/** + * + * @export + * @interface CreateOrdersRequest + */ +export interface CreateOrdersRequest { + /** + * + * @type {string} + * @memberof CreateOrdersRequest + */ + 'poNumber': string; + /** + * + * @type {number} + * @memberof CreateOrdersRequest + */ + 'orderCount': number; +} +/** + * + * @export + * @interface CreatePartnerAccountRequest + */ +export interface CreatePartnerAccountRequest { + /** + * + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'companyName': string; + /** + * 国名(ISO 3166-1 alpha-2) + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'country': string; + /** + * + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'adminName': string; + /** + * + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'email': string; +} +/** + * + * @export + * @interface CreateTypistGroupRequest + */ +export interface CreateTypistGroupRequest { + /** + * + * @type {string} + * @memberof CreateTypistGroupRequest + */ + 'typistGroupName': string; + /** + * + * @type {Array} + * @memberof CreateTypistGroupRequest + */ + 'typistIds': Array; +} +/** + * + * @export + * @interface CreateWorkflowsRequest + */ +export interface CreateWorkflowsRequest { + /** + * Authorの内部ID + * @type {number} + * @memberof CreateWorkflowsRequest + */ + 'authorId': number; + /** + * Worktypeの内部ID + * @type {number} + * @memberof CreateWorkflowsRequest + */ + 'worktypeId'?: number; + /** + * テンプレートの内部ID + * @type {number} + * @memberof CreateWorkflowsRequest + */ + 'templateId'?: number; + /** + * ルーティング候補のタイピストユーザー/タイピストグループ + * @type {Array} + * @memberof CreateWorkflowsRequest + */ + 'typists': Array; +} +/** + * + * @export + * @interface CreateWorktypesRequest + */ +export interface CreateWorktypesRequest { + /** + * WorktypeID + * @type {string} + * @memberof CreateWorktypesRequest + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof CreateWorktypesRequest + */ + 'description'?: string; +} +/** + * + * @export + * @interface Dealer + */ +export interface Dealer { + /** + * アカウントID + * @type {number} + * @memberof Dealer + */ + 'id': number; + /** + * 会社名 + * @type {string} + * @memberof Dealer + */ + 'name': string; + /** + * 国名(ISO 3166-1 alpha-2) + * @type {string} + * @memberof Dealer + */ + 'country': string; +} +/** + * + * @export + * @interface DeallocateLicenseRequest + */ +export interface DeallocateLicenseRequest { + /** + * ユーザーID + * @type {number} + * @memberof DeallocateLicenseRequest + */ + 'userId': number; +} +/** + * + * @export + * @interface DelegationAccessTokenResponse + */ +export interface DelegationAccessTokenResponse { + /** + * 代行操作用のアクセストークン + * @type {string} + * @memberof DelegationAccessTokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface DelegationTokenRequest + */ +export interface DelegationTokenRequest { + /** + * 代行操作対象のアカウントID + * @type {number} + * @memberof DelegationTokenRequest + */ + 'delegatedAccountId': number; +} +/** + * + * @export + * @interface DelegationTokenResponse + */ +export interface DelegationTokenResponse { + /** + * 代行操作用のリフレッシュトークン + * @type {string} + * @memberof DelegationTokenResponse + */ + 'refreshToken': string; + /** + * 代行操作用のアクセストークン + * @type {string} + * @memberof DelegationTokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface DeleteAccountRequest + */ +export interface DeleteAccountRequest { + /** + * アカウントID + * @type {number} + * @memberof DeleteAccountRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface ErrorResponse + */ +export interface ErrorResponse { + /** + * + * @type {string} + * @memberof ErrorResponse + */ + 'message': string; + /** + * + * @type {string} + * @memberof ErrorResponse + */ + 'code': string; +} +/** + * + * @export + * @interface GetAccountInfoMinimalAccessRequest + */ +export interface GetAccountInfoMinimalAccessRequest { + /** + * idトークン + * @type {string} + * @memberof GetAccountInfoMinimalAccessRequest + */ + 'idToken': string; +} +/** + * + * @export + * @interface GetAccountInfoMinimalAccessResponse + */ +export interface GetAccountInfoMinimalAccessResponse { + /** + * 階層 + * @type {number} + * @memberof GetAccountInfoMinimalAccessResponse + */ + 'tier': number; +} +/** + * + * @export + * @interface GetAllocatableLicensesResponse + */ +export interface GetAllocatableLicensesResponse { + /** + * + * @type {Array} + * @memberof GetAllocatableLicensesResponse + */ + 'allocatableLicenses': Array; +} +/** + * + * @export + * @interface GetAuthorsResponse + */ +export interface GetAuthorsResponse { + /** + * + * @type {Array} + * @memberof GetAuthorsResponse + */ + 'authors': Array; +} +/** + * + * @export + * @interface GetCompanyNameRequest + */ +export interface GetCompanyNameRequest { + /** + * + * @type {number} + * @memberof GetCompanyNameRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetCompanyNameResponse + */ +export interface GetCompanyNameResponse { + /** + * + * @type {string} + * @memberof GetCompanyNameResponse + */ + 'companyName': string; +} +/** + * + * @export + * @interface GetDealersResponse + */ +export interface GetDealersResponse { + /** + * + * @type {Array} + * @memberof GetDealersResponse + */ + 'dealers': Array; +} +/** + * + * @export + * @interface GetLicenseSummaryRequest + */ +export interface GetLicenseSummaryRequest { + /** + * + * @type {number} + * @memberof GetLicenseSummaryRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetLicenseSummaryResponse + */ +export interface GetLicenseSummaryResponse { + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'totalLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'allocatedLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'reusableLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'freeLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'expiringWithin14daysLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'issueRequesting': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'numberOfRequesting': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'shortage': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'storageSize': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'usedSize': number; + /** + * + * @type {boolean} + * @memberof GetLicenseSummaryResponse + */ + 'isStorageAvailable': boolean; +} +/** + * + * @export + * @interface GetMyAccountResponse + */ +export interface GetMyAccountResponse { + /** + * + * @type {Account} + * @memberof GetMyAccountResponse + */ + 'account': Account; +} +/** + * + * @export + * @interface GetMyUserResponse + */ +export interface GetMyUserResponse { + /** + * ユーザー名 + * @type {string} + * @memberof GetMyUserResponse + */ + 'userName': string; +} +/** + * + * @export + * @interface GetOptionItemsResponse + */ +export interface GetOptionItemsResponse { + /** + * + * @type {Array} + * @memberof GetOptionItemsResponse + */ + 'optionItems': Array; +} +/** + * + * @export + * @interface GetOrderHistoriesRequest + */ +export interface GetOrderHistoriesRequest { + /** + * 取得件数 + * @type {number} + * @memberof GetOrderHistoriesRequest + */ + 'limit': number; + /** + * 開始位置 + * @type {number} + * @memberof GetOrderHistoriesRequest + */ + 'offset': number; + /** + * アカウントID + * @type {number} + * @memberof GetOrderHistoriesRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetOrderHistoriesResponse + */ +export interface GetOrderHistoriesResponse { + /** + * 合計件数 + * @type {number} + * @memberof GetOrderHistoriesResponse + */ + 'total': number; + /** + * + * @type {Array} + * @memberof GetOrderHistoriesResponse + */ + 'orderHistories': Array; +} +/** + * + * @export + * @interface GetPartnerLicensesRequest + */ +export interface GetPartnerLicensesRequest { + /** + * + * @type {number} + * @memberof GetPartnerLicensesRequest + */ + 'limit': number; + /** + * + * @type {number} + * @memberof GetPartnerLicensesRequest + */ + 'offset': number; + /** + * + * @type {number} + * @memberof GetPartnerLicensesRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetPartnerLicensesResponse + */ +export interface GetPartnerLicensesResponse { + /** + * + * @type {number} + * @memberof GetPartnerLicensesResponse + */ + 'total': number; + /** + * + * @type {PartnerLicenseInfo} + * @memberof GetPartnerLicensesResponse + */ + 'ownPartnerLicense': PartnerLicenseInfo; + /** + * + * @type {Array} + * @memberof GetPartnerLicensesResponse + */ + 'childrenPartnerLicenses': Array; +} +/** + * + * @export + * @interface GetPartnersResponse + */ +export interface GetPartnersResponse { + /** + * 合計件数 + * @type {number} + * @memberof GetPartnersResponse + */ + 'total': number; + /** + * + * @type {Array} + * @memberof GetPartnersResponse + */ + 'partners': Array; +} +/** + * + * @export + * @interface GetRelationsResponse + */ +export interface GetRelationsResponse { + /** + * ログインしたユーザーのAuthorID(Authorでない場合はundefined) + * @type {string} + * @memberof GetRelationsResponse + */ + 'authorId'?: string; + /** + * 属しているアカウントのAuthorID List(全て) + * @type {Array} + * @memberof GetRelationsResponse + */ + 'authorIdList': Array; + /** + * アカウントに設定されているWorktypeIDのリスト(最大20個) + * @type {Array} + * @memberof GetRelationsResponse + */ + 'workTypeList': Array; + /** + * ユーザーが音声ファイルを暗号化するかどうか + * @type {boolean} + * @memberof GetRelationsResponse + */ + 'isEncrypted': boolean; + /** + * ユーザーが暗号化を掛ける場合のパスワード + * @type {string} + * @memberof GetRelationsResponse + */ + 'encryptionPassword'?: string; + /** + * アカウントがデフォルトで利用するWorkTypeID(アカウントに紐づくWorkTypeIDから一つ指定。activeWorktypeがなければ空文字を返却する) + * @type {string} + * @memberof GetRelationsResponse + */ + 'activeWorktype': string; + /** + * 録音形式: DSS/DS2(SP)/DS2(QP): DS2固定 + * @type {string} + * @memberof GetRelationsResponse + */ + 'audioFormat': string; + /** + * デバイス上で自動的にWorkTypeの選択画面を表示するかどうかのユーザーごとの設定(Authorでない場合はfalse) + * @type {boolean} + * @memberof GetRelationsResponse + */ + 'prompt': boolean; +} +/** + * + * @export + * @interface GetSortCriteriaResponse + */ +export interface GetSortCriteriaResponse { + /** + * ASC/DESC + * @type {string} + * @memberof GetSortCriteriaResponse + */ + 'direction': string; + /** + * JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @type {string} + * @memberof GetSortCriteriaResponse + */ + 'paramName': string; +} +/** + * + * @export + * @interface GetTemplatesResponse + */ +export interface GetTemplatesResponse { + /** + * テンプレートファイルの一覧 + * @type {Array} + * @memberof GetTemplatesResponse + */ + 'templates': Array; +} +/** + * + * @export + * @interface GetTermsInfoResponse + */ +export interface GetTermsInfoResponse { + /** + * + * @type {Array} + * @memberof GetTermsInfoResponse + */ + 'termsInfo': Array; +} +/** + * + * @export + * @interface GetTypistGroupResponse + */ +export interface GetTypistGroupResponse { + /** + * + * @type {string} + * @memberof GetTypistGroupResponse + */ + 'typistGroupName': string; + /** + * + * @type {Array} + * @memberof GetTypistGroupResponse + */ + 'typistIds': Array; +} +/** + * + * @export + * @interface GetTypistGroupsResponse + */ +export interface GetTypistGroupsResponse { + /** + * + * @type {Array} + * @memberof GetTypistGroupsResponse + */ + 'typistGroups': Array; +} +/** + * + * @export + * @interface GetTypistsResponse + */ +export interface GetTypistsResponse { + /** + * + * @type {Array} + * @memberof GetTypistsResponse + */ + 'typists': Array; +} +/** + * + * @export + * @interface GetUsersResponse + */ +export interface GetUsersResponse { + /** + * + * @type {Array} + * @memberof GetUsersResponse + */ + 'users': Array; +} +/** + * + * @export + * @interface GetWorkflowsResponse + */ +export interface GetWorkflowsResponse { + /** + * ワークフローの一覧 + * @type {Array} + * @memberof GetWorkflowsResponse + */ + 'workflows': Array; +} +/** + * + * @export + * @interface GetWorktypeOptionItem + */ +export interface GetWorktypeOptionItem { + /** + * + * @type {string} + * @memberof GetWorktypeOptionItem + */ + 'itemLabel': string; + /** + * Default / Blank / LastInput + * @type {string} + * @memberof GetWorktypeOptionItem + */ + 'defaultValueType': string; + /** + * + * @type {string} + * @memberof GetWorktypeOptionItem + */ + 'initialValue': string; + /** + * + * @type {number} + * @memberof GetWorktypeOptionItem + */ + 'id': number; +} +/** + * + * @export + * @interface GetWorktypesResponse + */ +export interface GetWorktypesResponse { + /** + * + * @type {Array} + * @memberof GetWorktypesResponse + */ + 'worktypes': Array; + /** + * Active WorktypeIDに設定されているWorkTypeの内部ID + * @type {number} + * @memberof GetWorktypesResponse + */ + 'active'?: number; +} +/** + * + * @export + * @interface IssueCardLicensesRequest + */ +export interface IssueCardLicensesRequest { + /** + * + * @type {number} + * @memberof IssueCardLicensesRequest + */ + 'createCount': number; +} +/** + * + * @export + * @interface IssueCardLicensesResponse + */ +export interface IssueCardLicensesResponse { + /** + * + * @type {Array} + * @memberof IssueCardLicensesResponse + */ + 'cardLicenseKeys': Array; +} +/** + * + * @export + * @interface IssueLicenseRequest + */ +export interface IssueLicenseRequest { + /** + * 注文元アカウントID + * @type {number} + * @memberof IssueLicenseRequest + */ + 'orderedAccountId': number; + /** + * POナンバー + * @type {string} + * @memberof IssueLicenseRequest + */ + 'poNumber': string; +} +/** + * + * @export + * @interface LicenseOrder + */ +export interface LicenseOrder { + /** + * 注文日付 + * @type {string} + * @memberof LicenseOrder + */ + 'orderDate': string; + /** + * 発行日付 + * @type {string} + * @memberof LicenseOrder + */ + 'issueDate'?: string; + /** + * 注文数 + * @type {number} + * @memberof LicenseOrder + */ + 'numberOfOrder': number; + /** + * POナンバー + * @type {string} + * @memberof LicenseOrder + */ + 'poNumber': string; + /** + * 注文状態 + * @type {string} + * @memberof LicenseOrder + */ + 'status': string; +} +/** + * + * @export + * @interface MultipleImportErrors + */ +export interface MultipleImportErrors { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportErrors + */ + 'name': string; + /** + * エラー発生行数 + * @type {number} + * @memberof MultipleImportErrors + */ + 'line': number; + /** + * エラーコード + * @type {string} + * @memberof MultipleImportErrors + */ + 'errorCode': string; +} +/** + * + * @export + * @interface MultipleImportUser + */ +export interface MultipleImportUser { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof MultipleImportUser + */ + 'email': string; + /** + * 0(none)/1(author)/2(typist) + * @type {number} + * @memberof MultipleImportUser + */ + 'role': number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'authorId'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'autoRenew': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'notification': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'encryption'?: number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'encryptionPassword'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'prompt'?: number; +} +/** + * + * @export + * @interface OptionItem + */ +export interface OptionItem { + /** + * Option Itemのラベル + * @type {string} + * @memberof OptionItem + */ + 'label': string; + /** + * 項目タイプ 1:Blank/2:Default/3:前の値 + * @type {number} + * @memberof OptionItem + */ + 'initialValueType': number; + /** + * typeでDefaultを選択した場合のデフォルト値 + * @type {string} + * @memberof OptionItem + */ + 'defaultValue': string; +} +/** + * + * @export + * @interface OptionItemList + */ +export interface OptionItemList { + /** + * + * @type {string} + * @memberof OptionItemList + */ + 'workTypeId': string; + /** + * 1WorkTypeIDにつき、10個まで登録可能 + * @type {Array} + * @memberof OptionItemList + */ + 'optionItemList': Array; +} +/** + * + * @export + * @interface Partner + */ +export interface Partner { + /** + * 会社名 + * @type {string} + * @memberof Partner + */ + 'name': string; + /** + * 階層 + * @type {number} + * @memberof Partner + */ + 'tier': number; + /** + * アカウントID + * @type {number} + * @memberof Partner + */ + 'accountId': number; + /** + * 国 + * @type {string} + * @memberof Partner + */ + 'country': string; + /** + * プライマリ管理者 + * @type {string} + * @memberof Partner + */ + 'primaryAdmin': string; + /** + * プライマリ管理者メールアドレス + * @type {string} + * @memberof Partner + */ + 'email': string; + /** + * 代行操作許可 + * @type {boolean} + * @memberof Partner + */ + 'dealerManagement': boolean; +} +/** + * + * @export + * @interface PartnerLicenseInfo + */ +export interface PartnerLicenseInfo { + /** + * アカウントID + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'accountId': number; + /** + * 階層 + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'tier': number; + /** + * アカウント名 + * @type {string} + * @memberof PartnerLicenseInfo + */ + 'companyName': string; + /** + * 保有している有効期限が未設定あるいは有効期限内のライセンス数 + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'stockLicense': number; + /** + * 子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数 + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'issuedRequested': number; + /** + * 不足数({Stock license} - {Issue Requested}) + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'shortage': number; + /** + * 未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数) + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'issueRequesting': number; +} +/** + * + * @export + * @interface PostActiveWorktypeRequest + */ +export interface PostActiveWorktypeRequest { + /** + * Active WorkTypeIDにするWorktypeの内部ID + * @type {number} + * @memberof PostActiveWorktypeRequest + */ + 'id'?: number; +} +/** + * + * @export + * @interface PostCheckoutPermissionRequest + */ +export interface PostCheckoutPermissionRequest { + /** + * 文字起こしに着手可能(チェックアウト可能)にしたい、グループ個人の一覧 + * @type {Array} + * @memberof PostCheckoutPermissionRequest + */ + 'assignees': Array; +} +/** + * + * @export + * @interface PostDeleteUserRequest + */ +export interface PostDeleteUserRequest { + /** + * 削除対象のユーザーID + * @type {number} + * @memberof PostDeleteUserRequest + */ + 'userId': number; +} +/** + * + * @export + * @interface PostMultipleImportsCompleteRequest + */ +export interface PostMultipleImportsCompleteRequest { + /** + * アカウントID + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'accountId': number; + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsCompleteRequest + */ + 'filename': string; + /** + * 一括登録受付時刻(UNIXTIME/ミリ秒) + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'requestTime': number; + /** + * + * @type {Array} + * @memberof PostMultipleImportsCompleteRequest + */ + 'errors': Array; +} +/** + * + * @export + * @interface PostMultipleImportsRequest + */ +export interface PostMultipleImportsRequest { + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsRequest + */ + 'filename': string; + /** + * + * @type {Array} + * @memberof PostMultipleImportsRequest + */ + 'users': Array; +} +/** + * + * @export + * @interface PostSortCriteriaRequest + */ +export interface PostSortCriteriaRequest { + /** + * ASC/DESC + * @type {string} + * @memberof PostSortCriteriaRequest + */ + 'direction': string; + /** + * JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @type {string} + * @memberof PostSortCriteriaRequest + */ + 'paramName': string; +} +/** + * + * @export + * @interface PostUpdateUserRequest + */ +export interface PostUpdateUserRequest { + /** + * + * @type {number} + * @memberof PostUpdateUserRequest + */ + 'id': number; + /** + * none/author/typist + * @type {string} + * @memberof PostUpdateUserRequest + */ + 'role': string; + /** + * + * @type {string} + * @memberof PostUpdateUserRequest + */ + 'authorId'?: string; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'notification': boolean; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'encryption'?: boolean; + /** + * + * @type {string} + * @memberof PostUpdateUserRequest + */ + 'encryptionPassword'?: string; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'prompt'?: boolean; +} +/** + * + * @export + * @interface PostWorktypeOptionItem + */ +export interface PostWorktypeOptionItem { + /** + * + * @type {string} + * @memberof PostWorktypeOptionItem + */ + 'itemLabel': string; + /** + * Default / Blank / LastInput + * @type {string} + * @memberof PostWorktypeOptionItem + */ + 'defaultValueType': string; + /** + * + * @type {string} + * @memberof PostWorktypeOptionItem + */ + 'initialValue': string; +} +/** + * + * @export + * @interface RegisterRequest + */ +export interface RegisterRequest { + /** + * wns or apns + * @type {string} + * @memberof RegisterRequest + */ + 'pns': string; + /** + * wnsのチャネルURI or apnsのデバイストークン + * @type {string} + * @memberof RegisterRequest + */ + 'handler': string; +} +/** + * + * @export + * @interface SignupRequest + */ +export interface SignupRequest { + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'name': string; + /** + * none/author/typist + * @type {string} + * @memberof SignupRequest + */ + 'role': string; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'authorId'?: string; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'email': string; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'notification': boolean; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'encryption'?: boolean; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'encryptionPassword'?: string; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'prompt'?: boolean; +} +/** + * + * @export + * @interface Task + */ +export interface Task { + /** + * ODMS Cloud上の音声ファイルID + * @type {number} + * @memberof Task + */ + 'audioFileId': number; + /** + * AuthorID + * @type {string} + * @memberof Task + */ + 'authorId': string; + /** + * + * @type {string} + * @memberof Task + */ + 'workType': string; + /** + * 音声ファイルに紐づくOption Itemの一覧(10個固定) + * @type {Array} + * @memberof Task + */ + 'optionItemList': Array; + /** + * 音声ファイルのBlob Storage上での保存場所(ファイル名含む)のURL + * @type {string} + * @memberof Task + */ + 'url': string; + /** + * 音声ファイル名 + * @type {string} + * @memberof Task + */ + 'fileName': string; + /** + * 音声ファイルの録音時間(ミリ秒の整数値) + * @type {string} + * @memberof Task + */ + 'audioDuration': string; + /** + * 音声ファイルの録音開始日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'audioCreatedDate': string; + /** + * 音声ファイルの録音終了日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'audioFinishedDate': string; + /** + * 音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'audioUploadedDate': string; + /** + * 音声ファイルのファイルサイズ(Byte) + * @type {number} + * @memberof Task + */ + 'fileSize': number; + /** + * 音声ファイルの優先度 \"00\":Normal / \"01\":High + * @type {string} + * @memberof Task + */ + 'priority': string; + /** + * 録音形式: DSS/DS2(SP)/DS2(QP) + * @type {string} + * @memberof Task + */ + 'audioFormat': string; + /** + * コメント + * @type {string} + * @memberof Task + */ + 'comment': string; + /** + * + * @type {boolean} + * @memberof Task + */ + 'isEncrypted': boolean; + /** + * JOBナンバー + * @type {string} + * @memberof Task + */ + 'jobNumber': string; + /** + * + * @type {Typist} + * @memberof Task + */ + 'typist'?: Typist; + /** + * 文字起こしに着手できる(チェックアウト可能な)、タスクにアサインされているグループ/個人の一覧 + * @type {Array} + * @memberof Task + */ + 'assignees': Array; + /** + * 音声ファイルのファイルステータス Uploaded / Pending / InProgress / Finished / Backup + * @type {string} + * @memberof Task + */ + 'status': string; + /** + * 文字起こし開始日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'transcriptionStartedDate'?: string; + /** + * 文字起こし終了日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'transcriptionFinishedDate'?: string; +} +/** + * + * @export + * @interface TasksResponse + */ +export interface TasksResponse { + /** + * タスクの取得件数(指定しない場合はデフォルト値) + * @type {number} + * @memberof TasksResponse + */ + 'limit': number; + /** + * オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @type {number} + * @memberof TasksResponse + */ + 'offset': number; + /** + * タスクの総件数 + * @type {number} + * @memberof TasksResponse + */ + 'total': number; + /** + * 音声ファイル/タスク一覧 + * @type {Array} + * @memberof TasksResponse + */ + 'tasks': Array; +} +/** + * + * @export + * @interface TemplateDownloadLocationResponse + */ +export interface TemplateDownloadLocationResponse { + /** + * + * @type {string} + * @memberof TemplateDownloadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface TemplateFile + */ +export interface TemplateFile { + /** + * テンプレートファイルのID + * @type {number} + * @memberof TemplateFile + */ + 'id': number; + /** + * テンプレートファイルのファイル名 + * @type {string} + * @memberof TemplateFile + */ + 'name': string; +} +/** + * + * @export + * @interface TemplateUploadFinishedRequest + */ +export interface TemplateUploadFinishedRequest { + /** + * テンプレートファイルのファイル名 + * @type {string} + * @memberof TemplateUploadFinishedRequest + */ + 'name': string; + /** + * テンプレートファイルのアップロード先URL + * @type {string} + * @memberof TemplateUploadFinishedRequest + */ + 'url': string; +} +/** + * + * @export + * @interface TemplateUploadLocationResponse + */ +export interface TemplateUploadLocationResponse { + /** + * + * @type {string} + * @memberof TemplateUploadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface TermInfo + */ +export interface TermInfo { + /** + * 利用規約種別 + * @type {string} + * @memberof TermInfo + */ + 'documentType': string; + /** + * バージョン + * @type {string} + * @memberof TermInfo + */ + 'version': string; +} +/** + * + * @export + * @interface TokenRequest + */ +export interface TokenRequest { + /** + * + * @type {string} + * @memberof TokenRequest + */ + 'idToken': string; + /** + * web or mobile or desktop + * @type {string} + * @memberof TokenRequest + */ + 'type': string; +} +/** + * + * @export + * @interface TokenResponse + */ +export interface TokenResponse { + /** + * + * @type {string} + * @memberof TokenResponse + */ + 'refreshToken': string; + /** + * + * @type {string} + * @memberof TokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface Typist + */ +export interface Typist { + /** + * TypistのユーザーID + * @type {number} + * @memberof Typist + */ + 'id': number; + /** + * Typistのユーザー名 + * @type {string} + * @memberof Typist + */ + 'name': string; +} +/** + * + * @export + * @interface TypistGroup + */ +export interface TypistGroup { + /** + * TypistGroupのID + * @type {number} + * @memberof TypistGroup + */ + 'id': number; + /** + * TypistGroup名 + * @type {string} + * @memberof TypistGroup + */ + 'name': string; +} +/** + * + * @export + * @interface UpdateAcceptedVersionRequest + */ +export interface UpdateAcceptedVersionRequest { + /** + * IDトークン + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'idToken': string; + /** + * 更新バージョン(EULA) + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'acceptedEULAVersion': string; + /** + * 更新バージョン(PrivacyNotice) + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'acceptedPrivacyNoticeVersion': string; + /** + * 更新バージョン(DPA) + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'acceptedDPAVersion'?: string; +} +/** + * + * @export + * @interface UpdateAccountInfoRequest + */ +export interface UpdateAccountInfoRequest { + /** + * 親アカウントのID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'parentAccountId'?: number; + /** + * 代行操作許可 + * @type {boolean} + * @memberof UpdateAccountInfoRequest + */ + 'delegationPermission': boolean; + /** + * プライマリ管理者ID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'primaryAdminUserId': number; + /** + * セカンダリ管理者ID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'secondryAdminUserId'?: number; +} +/** + * + * @export + * @interface UpdateFileDeleteSettingRequest + */ +export interface UpdateFileDeleteSettingRequest { + /** + * 自動ファイル削除をするかどうか + * @type {boolean} + * @memberof UpdateFileDeleteSettingRequest + */ + 'autoFileDelete': boolean; + /** + * 文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間 + * @type {number} + * @memberof UpdateFileDeleteSettingRequest + */ + 'retentionDays': number; +} +/** + * + * @export + * @interface UpdateOptionItemsRequest + */ +export interface UpdateOptionItemsRequest { + /** + * + * @type {Array} + * @memberof UpdateOptionItemsRequest + */ + 'optionItems': Array; +} +/** + * + * @export + * @interface UpdateRestrictionStatusRequest + */ +export interface UpdateRestrictionStatusRequest { + /** + * 操作対象の第五階層アカウントID + * @type {number} + * @memberof UpdateRestrictionStatusRequest + */ + 'accountId': number; + /** + * 制限をかけるかどうか(trur:制限をかける) + * @type {boolean} + * @memberof UpdateRestrictionStatusRequest + */ + 'restricted': boolean; +} +/** + * + * @export + * @interface UpdateTypistGroupRequest + */ +export interface UpdateTypistGroupRequest { + /** + * + * @type {string} + * @memberof UpdateTypistGroupRequest + */ + 'typistGroupName': string; + /** + * + * @type {Array} + * @memberof UpdateTypistGroupRequest + */ + 'typistIds': Array; +} +/** + * + * @export + * @interface UpdateWorkflowRequest + */ +export interface UpdateWorkflowRequest { + /** + * Authorの内部ID + * @type {number} + * @memberof UpdateWorkflowRequest + */ + 'authorId': number; + /** + * Worktypeの内部ID + * @type {number} + * @memberof UpdateWorkflowRequest + */ + 'worktypeId'?: number; + /** + * テンプレートの内部ID + * @type {number} + * @memberof UpdateWorkflowRequest + */ + 'templateId'?: number; + /** + * ルーティング候補のタイピストユーザー/タイピストグループ + * @type {Array} + * @memberof UpdateWorkflowRequest + */ + 'typists': Array; +} +/** + * + * @export + * @interface UpdateWorktypesRequest + */ +export interface UpdateWorktypesRequest { + /** + * WorktypeID + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'description'?: string; +} +/** + * + * @export + * @interface User + */ +export interface User { + /** + * + * @type {number} + * @memberof User + */ + 'id': number; + /** + * + * @type {string} + * @memberof User + */ + 'name': string; + /** + * none/author/typist + * @type {string} + * @memberof User + */ + 'role': string; + /** + * + * @type {string} + * @memberof User + */ + 'authorId'?: string; + /** + * + * @type {Array} + * @memberof User + */ + 'typistGroupName': Array; + /** + * + * @type {string} + * @memberof User + */ + 'email': string; + /** + * + * @type {boolean} + * @memberof User + */ + 'emailVerified': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'notification': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'encryption': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'prompt': boolean; + /** + * + * @type {string} + * @memberof User + */ + 'expiration'?: string; + /** + * + * @type {number} + * @memberof User + */ + 'remaining'?: number; + /** + * Normal/NoLicense/Alert/Renew + * @type {string} + * @memberof User + */ + 'licenseStatus': string; +} +/** + * + * @export + * @interface Workflow + */ +export interface Workflow { + /** + * ワークフローの内部ID + * @type {number} + * @memberof Workflow + */ + 'id': number; + /** + * + * @type {Author} + * @memberof Workflow + */ + 'author': Author; + /** + * + * @type {WorkflowWorktype} + * @memberof Workflow + */ + 'worktype'?: WorkflowWorktype; + /** + * + * @type {WorkflowTemplate} + * @memberof Workflow + */ + 'template'?: WorkflowTemplate; + /** + * ルーティング候補のタイピストユーザー/タイピストグループ + * @type {Array} + * @memberof Workflow + */ + 'typists': Array; +} +/** + * + * @export + * @interface WorkflowTemplate + */ +export interface WorkflowTemplate { + /** + * テンプレートの内部ID + * @type {number} + * @memberof WorkflowTemplate + */ + 'id': number; + /** + * テンプレートのファイル名 + * @type {string} + * @memberof WorkflowTemplate + */ + 'fileName': string; +} +/** + * + * @export + * @interface WorkflowTypist + */ +export interface WorkflowTypist { + /** + * タイピストユーザーの内部ID + * @type {number} + * @memberof WorkflowTypist + */ + 'typistId'?: number; + /** + * タイピストグループの内部ID + * @type {number} + * @memberof WorkflowTypist + */ + 'typistGroupId'?: number; +} +/** + * + * @export + * @interface WorkflowWorktype + */ +export interface WorkflowWorktype { + /** + * Worktypeの内部ID + * @type {number} + * @memberof WorkflowWorktype + */ + 'id': number; + /** + * WorktypeID + * @type {string} + * @memberof WorkflowWorktype + */ + 'worktypeId': string; +} +/** + * + * @export + * @interface Worktype + */ +export interface Worktype { + /** + * WorktypeのID + * @type {number} + * @memberof Worktype + */ + 'id': number; + /** + * WorktypeID + * @type {string} + * @memberof Worktype + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof Worktype + */ + 'description'?: string; +} + +/** + * AccountsApi - axios parameter creator + * @export + */ +export const AccountsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activeWorktype: async (postActiveWorktypeRequest: PostActiveWorktypeRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postActiveWorktypeRequest' is not null or undefined + assertParamExists('activeWorktype', 'postActiveWorktypeRequest', postActiveWorktypeRequest) + const localVarPath = `/accounts/active-worktype`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postActiveWorktypeRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelIssue: async (cancelIssueRequest: CancelIssueRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'cancelIssueRequest' is not null or undefined + assertParamExists('cancelIssue', 'cancelIssueRequest', cancelIssueRequest) + const localVarPath = `/accounts/issue/cancel`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(cancelIssueRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createAccount: async (createAccountRequest: CreateAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createAccountRequest' is not null or undefined + assertParamExists('createAccount', 'createAccountRequest', createAccountRequest) + const localVarPath = `/accounts`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPartnerAccount: async (createPartnerAccountRequest: CreatePartnerAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createPartnerAccountRequest' is not null or undefined + assertParamExists('createPartnerAccount', 'createPartnerAccountRequest', createPartnerAccountRequest) + const localVarPath = `/accounts/partner`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createPartnerAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createTypistGroup: async (createTypistGroupRequest: CreateTypistGroupRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createTypistGroupRequest' is not null or undefined + assertParamExists('createTypistGroup', 'createTypistGroupRequest', createTypistGroupRequest) + const localVarPath = `/accounts/typist-groups`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createTypistGroupRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorktype: async (createWorktypesRequest: CreateWorktypesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createWorktypesRequest' is not null or undefined + assertParamExists('createWorktype', 'createWorktypesRequest', createWorktypesRequest) + const localVarPath = `/accounts/worktypes`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createWorktypesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteAccountAndData: async (deleteAccountRequest: DeleteAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deleteAccountRequest' is not null or undefined + assertParamExists('deleteAccountAndData', 'deleteAccountRequest', deleteAccountRequest) + const localVarPath = `/accounts/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deleteAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup: async (typistGroupId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('deleteTypistGroup', 'typistGroupId', typistGroupId) + const localVarPath = `/accounts/typist-groups/{typistGroupId}/delete` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorktype: async (id: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('deleteWorktype', 'id', id) + const localVarPath = `/accounts/worktypes/{id}/delete` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAccountInfoMinimalAccess: async (getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getAccountInfoMinimalAccessRequest' is not null or undefined + assertParamExists('getAccountInfoMinimalAccess', 'getAccountInfoMinimalAccessRequest', getAccountInfoMinimalAccessRequest) + const localVarPath = `/accounts/minimal-access`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getAccountInfoMinimalAccessRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuthors: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/authors`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCompanyName: async (getCompanyNameRequest: GetCompanyNameRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getCompanyNameRequest' is not null or undefined + assertParamExists('getCompanyName', 'getCompanyNameRequest', getCompanyNameRequest) + const localVarPath = `/accounts/company-name`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getCompanyNameRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDealers: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/dealers`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getLicenseSummary: async (getLicenseSummaryRequest: GetLicenseSummaryRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getLicenseSummaryRequest' is not null or undefined + assertParamExists('getLicenseSummary', 'getLicenseSummaryRequest', getLicenseSummaryRequest) + const localVarPath = `/accounts/licenses/summary`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getLicenseSummaryRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyAccount: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOptionItems: async (id: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('getOptionItems', 'id', id) + const localVarPath = `/accounts/worktypes/{id}/option-items` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOrderHistories: async (getOrderHistoriesRequest: GetOrderHistoriesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getOrderHistoriesRequest' is not null or undefined + assertParamExists('getOrderHistories', 'getOrderHistoriesRequest', getOrderHistoriesRequest) + const localVarPath = `/accounts/order-histories`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getOrderHistoriesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerLicenses: async (getPartnerLicensesRequest: GetPartnerLicensesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getPartnerLicensesRequest' is not null or undefined + assertParamExists('getPartnerLicenses', 'getPartnerLicensesRequest', getPartnerLicensesRequest) + const localVarPath = `/accounts/partner-licenses`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getPartnerLicensesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners: async (limit: number, offset: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'limit' is not null or undefined + assertParamExists('getPartners', 'limit', limit) + // verify required parameter 'offset' is not null or undefined + assertParamExists('getPartners', 'offset', offset) + const localVarPath = `/accounts/partners`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (offset !== undefined) { + localVarQueryParameter['offset'] = offset; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroup: async (typistGroupId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('getTypistGroup', 'typistGroupId', typistGroupId) + const localVarPath = `/accounts/typist-groups/{typistGroupId}` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroups: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/typist-groups`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypists: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/typists`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorktypes: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/worktypes`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueLicense: async (issueLicenseRequest: IssueLicenseRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'issueLicenseRequest' is not null or undefined + assertParamExists('issueLicense', 'issueLicenseRequest', issueLicenseRequest) + const localVarPath = `/accounts/licenses/issue`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(issueLicenseRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAccountInfo: async (updateAccountInfoRequest: UpdateAccountInfoRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateAccountInfoRequest' is not null or undefined + assertParamExists('updateAccountInfo', 'updateAccountInfoRequest', updateAccountInfoRequest) + const localVarPath = `/accounts/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateAccountInfoRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting: async (updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateFileDeleteSettingRequest' is not null or undefined + assertParamExists('updateFileDeleteSetting', 'updateFileDeleteSettingRequest', updateFileDeleteSettingRequest) + const localVarPath = `/accounts/me/file-delete-setting`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateFileDeleteSettingRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateOptionItems: async (id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateOptionItems', 'id', id) + // verify required parameter 'updateOptionItemsRequest' is not null or undefined + assertParamExists('updateOptionItems', 'updateOptionItemsRequest', updateOptionItemsRequest) + const localVarPath = `/accounts/worktypes/{id}/option-items` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateOptionItemsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus: async (updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateRestrictionStatusRequest' is not null or undefined + assertParamExists('updateRestrictionStatus', 'updateRestrictionStatusRequest', updateRestrictionStatusRequest) + const localVarPath = `/accounts/restriction-status`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateRestrictionStatusRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateTypistGroup: async (typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('updateTypistGroup', 'typistGroupId', typistGroupId) + // verify required parameter 'updateTypistGroupRequest' is not null or undefined + assertParamExists('updateTypistGroup', 'updateTypistGroupRequest', updateTypistGroupRequest) + const localVarPath = `/accounts/typist-groups/{typistGroupId}` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateTypistGroupRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype: async (id: number, updateWorktypesRequest: UpdateWorktypesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateWorktype', 'id', id) + // verify required parameter 'updateWorktypesRequest' is not null or undefined + assertParamExists('updateWorktype', 'updateWorktypesRequest', updateWorktypesRequest) + const localVarPath = `/accounts/worktypes/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateWorktypesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AccountsApi - functional programming interface + * @export + */ +export const AccountsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AccountsApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async activeWorktype(postActiveWorktypeRequest: PostActiveWorktypeRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.activeWorktype(postActiveWorktypeRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.activeWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancelIssue(cancelIssueRequest: CancelIssueRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancelIssue(cancelIssueRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.cancelIssue']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createAccount(createAccountRequest: CreateAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createAccount(createAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createPartnerAccount(createPartnerAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createPartnerAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createTypistGroup(createTypistGroupRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createWorktype(createWorktypesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccountAndData(deleteAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteAccountAndData']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTypistGroup(typistGroupId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteWorktype(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorktype(id, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getAccountInfoMinimalAccess']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAuthors(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAuthors(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getAuthors']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCompanyName(getCompanyNameRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getCompanyName']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getDealers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getDealers(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getDealers']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getLicenseSummary(getLicenseSummaryRequest: GetLicenseSummaryRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getLicenseSummary(getLicenseSummaryRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getLicenseSummary']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMyAccount(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMyAccount(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getMyAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOptionItems(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOptionItems(id, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getOptionItems']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOrderHistories(getOrderHistoriesRequest: GetOrderHistoriesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOrderHistories(getOrderHistoriesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getOrderHistories']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartnerLicenses(getPartnerLicensesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getPartnerLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartners(limit: number, offset: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(limit, offset, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getPartners']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTypistGroup(typistGroupId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTypistGroup(typistGroupId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTypistGroups(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTypistGroups(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getTypistGroups']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTypists(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTypists(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getTypists']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getWorktypes(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getWorktypes(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getWorktypes']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.issueLicense(issueLicenseRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.issueLicense']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateAccountInfo(updateAccountInfoRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateAccountInfo']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateFileDeleteSetting(updateFileDeleteSettingRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateFileDeleteSetting']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateOptionItems(id, updateOptionItemsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateOptionItems']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateRestrictionStatus(updateRestrictionStatusRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateRestrictionStatus']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateWorktype(id, updateWorktypesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * AccountsApi - factory interface + * @export + */ +export const AccountsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AccountsApiFp(configuration) + return { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activeWorktype(postActiveWorktypeRequest: PostActiveWorktypeRequest, options?: any): AxiosPromise { + return localVarFp.activeWorktype(postActiveWorktypeRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelIssue(cancelIssueRequest: CancelIssueRequest, options?: any): AxiosPromise { + return localVarFp.cancelIssue(cancelIssueRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createAccount(createAccountRequest: CreateAccountRequest, options?: any): AxiosPromise { + return localVarFp.createAccount(createAccountRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: any): AxiosPromise { + return localVarFp.createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: any): AxiosPromise { + return localVarFp.createTypistGroup(createTypistGroupRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: any): AxiosPromise { + return localVarFp.createWorktype(createWorktypesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise { + return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup(typistGroupId: number, options?: any): AxiosPromise { + return localVarFp.deleteTypistGroup(typistGroupId, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorktype(id: number, options?: any): AxiosPromise { + return localVarFp.deleteWorktype(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: any): AxiosPromise { + return localVarFp.getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuthors(options?: any): AxiosPromise { + return localVarFp.getAuthors(options).then((request) => request(axios, basePath)); + }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: any): AxiosPromise { + return localVarFp.getCompanyName(getCompanyNameRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDealers(options?: any): AxiosPromise { + return localVarFp.getDealers(options).then((request) => request(axios, basePath)); + }, + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getLicenseSummary(getLicenseSummaryRequest: GetLicenseSummaryRequest, options?: any): AxiosPromise { + return localVarFp.getLicenseSummary(getLicenseSummaryRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyAccount(options?: any): AxiosPromise { + return localVarFp.getMyAccount(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOptionItems(id: number, options?: any): AxiosPromise { + return localVarFp.getOptionItems(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOrderHistories(getOrderHistoriesRequest: GetOrderHistoriesRequest, options?: any): AxiosPromise { + return localVarFp.getOrderHistories(getOrderHistoriesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: any): AxiosPromise { + return localVarFp.getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners(limit: number, offset: number, options?: any): AxiosPromise { + return localVarFp.getPartners(limit, offset, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroup(typistGroupId: number, options?: any): AxiosPromise { + return localVarFp.getTypistGroup(typistGroupId, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroups(options?: any): AxiosPromise { + return localVarFp.getTypistGroups(options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypists(options?: any): AxiosPromise { + return localVarFp.getTypists(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorktypes(options?: any): AxiosPromise { + return localVarFp.getWorktypes(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: any): AxiosPromise { + return localVarFp.issueLicense(issueLicenseRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: any): AxiosPromise { + return localVarFp.updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: any): AxiosPromise { + return localVarFp.updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: any): AxiosPromise { + return localVarFp.updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: any): AxiosPromise { + return localVarFp.updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: any): AxiosPromise { + return localVarFp.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: any): AxiosPromise { + return localVarFp.updateWorktype(id, updateWorktypesRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * AccountsApi - object-oriented interface + * @export + * @class AccountsApi + * @extends {BaseAPI} + */ +export class AccountsApi extends BaseAPI { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public activeWorktype(postActiveWorktypeRequest: PostActiveWorktypeRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).activeWorktype(postActiveWorktypeRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public cancelIssue(cancelIssueRequest: CancelIssueRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).cancelIssue(cancelIssueRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createAccount(createAccountRequest: CreateAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createAccount(createAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createTypistGroup(createTypistGroupRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createWorktype(createWorktypesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteTypistGroup(typistGroupId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteWorktype(id: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteWorktype(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getAuthors(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getAuthors(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getCompanyName(getCompanyNameRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getDealers(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getDealers(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getLicenseSummary(getLicenseSummaryRequest: GetLicenseSummaryRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getLicenseSummary(getLicenseSummaryRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getMyAccount(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getMyAccount(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getOptionItems(id: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getOptionItems(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getOrderHistories(getOrderHistoriesRequest: GetOrderHistoriesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getOrderHistories(getOrderHistoriesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartners(limit: number, offset: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartners(limit, offset, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getTypistGroup(typistGroupId: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getTypistGroup(typistGroupId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getTypistGroups(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getTypistGroups(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getTypists(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getTypists(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getWorktypes(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getWorktypes(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).issueLicense(issueLicenseRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateWorktype(id, updateWorktypesRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * AuthApi - axios parameter creator + * @export + */ +export const AuthApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accessToken: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/auth/accessToken`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationAccessToken: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/auth/delegation/access-token`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationToken: async (delegationTokenRequest: DelegationTokenRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'delegationTokenRequest' is not null or undefined + assertParamExists('delegationToken', 'delegationTokenRequest', delegationTokenRequest) + const localVarPath = `/auth/delegation/token`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(delegationTokenRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + token: async (tokenRequest: TokenRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'tokenRequest' is not null or undefined + assertParamExists('token', 'tokenRequest', tokenRequest) + const localVarPath = `/auth/token`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(tokenRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AuthApi - functional programming interface + * @export + */ +export const AuthApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AuthApiAxiosParamCreator(configuration) + return { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async accessToken(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.accessToken(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.accessToken']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async delegationAccessToken(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.delegationAccessToken(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.delegationAccessToken']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async delegationToken(delegationTokenRequest: DelegationTokenRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.delegationToken(delegationTokenRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.delegationToken']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async token(tokenRequest: TokenRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.token(tokenRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.token']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * AuthApi - factory interface + * @export + */ +export const AuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AuthApiFp(configuration) + return { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accessToken(options?: any): AxiosPromise { + return localVarFp.accessToken(options).then((request) => request(axios, basePath)); + }, + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationAccessToken(options?: any): AxiosPromise { + return localVarFp.delegationAccessToken(options).then((request) => request(axios, basePath)); + }, + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationToken(delegationTokenRequest: DelegationTokenRequest, options?: any): AxiosPromise { + return localVarFp.delegationToken(delegationTokenRequest, options).then((request) => request(axios, basePath)); + }, + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + token(tokenRequest: TokenRequest, options?: any): AxiosPromise { + return localVarFp.token(tokenRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * AuthApi - object-oriented interface + * @export + * @class AuthApi + * @extends {BaseAPI} + */ +export class AuthApi extends BaseAPI { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public accessToken(options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).accessToken(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public delegationAccessToken(options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).delegationAccessToken(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public delegationToken(delegationTokenRequest: DelegationTokenRequest, options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).delegationToken(delegationTokenRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public token(tokenRequest: TokenRequest, options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).token(tokenRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkHealth: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/health`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async checkHealth(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.checkHealth(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['DefaultApi.checkHealth']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * DefaultApi - factory interface + * @export + */ +export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = DefaultApiFp(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkHealth(options?: any): AxiosPromise { + return localVarFp.checkHealth(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public checkHealth(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).checkHealth(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * FilesApi - axios parameter creator + * @export + */ +export const FilesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadLocation: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('downloadLocation', 'audioFileId', audioFileId) + const localVarPath = `/files/audio/download-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (audioFileId !== undefined) { + localVarQueryParameter['audioFileId'] = audioFileId; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadTemplateLocation: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('downloadTemplateLocation', 'audioFileId', audioFileId) + const localVarPath = `/files/template/download-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (audioFileId !== undefined) { + localVarQueryParameter['audioFileId'] = audioFileId; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadFinished: async (audioUploadFinishedRequest: AudioUploadFinishedRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioUploadFinishedRequest' is not null or undefined + assertParamExists('uploadFinished', 'audioUploadFinishedRequest', audioUploadFinishedRequest) + const localVarPath = `/files/audio/upload-finished`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(audioUploadFinishedRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadLocation: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/files/audio/upload-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateFinished: async (templateUploadFinishedRequest: TemplateUploadFinishedRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'templateUploadFinishedRequest' is not null or undefined + assertParamExists('uploadTemplateFinished', 'templateUploadFinishedRequest', templateUploadFinishedRequest) + const localVarPath = `/files/template/upload-finished`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(templateUploadFinishedRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateLocation: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/files/template/upload-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * FilesApi - functional programming interface + * @export + */ +export const FilesApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = FilesApiAxiosParamCreator(configuration) + return { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async downloadLocation(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLocation(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.downloadLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async downloadTemplateLocation(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.downloadTemplateLocation(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.downloadTemplateLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFinished(audioUploadFinishedRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadFinished']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadLocation(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadLocation(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadTemplateFinished(templateUploadFinishedRequest: TemplateUploadFinishedRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadTemplateFinished(templateUploadFinishedRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadTemplateFinished']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadTemplateLocation(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadTemplateLocation(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadTemplateLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * FilesApi - factory interface + * @export + */ +export const FilesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = FilesApiFp(configuration) + return { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadLocation(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.downloadLocation(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadTemplateLocation(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.downloadTemplateLocation(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: any): AxiosPromise { + return localVarFp.uploadFinished(audioUploadFinishedRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadLocation(options?: any): AxiosPromise { + return localVarFp.uploadLocation(options).then((request) => request(axios, basePath)); + }, + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateFinished(templateUploadFinishedRequest: TemplateUploadFinishedRequest, options?: any): AxiosPromise { + return localVarFp.uploadTemplateFinished(templateUploadFinishedRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateLocation(options?: any): AxiosPromise { + return localVarFp.uploadTemplateLocation(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * FilesApi - object-oriented interface + * @export + * @class FilesApi + * @extends {BaseAPI} + */ +export class FilesApi extends BaseAPI { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public downloadLocation(audioFileId: number, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).downloadLocation(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public downloadTemplateLocation(audioFileId: number, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).downloadTemplateLocation(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadFinished(audioUploadFinishedRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadLocation(options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadLocation(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadTemplateFinished(templateUploadFinishedRequest: TemplateUploadFinishedRequest, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadTemplateFinished(templateUploadFinishedRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadTemplateLocation(options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadTemplateLocation(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * LicensesApi - axios parameter creator + * @export + */ +export const LicensesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activateCardLicenses: async (activateCardLicensesRequest: ActivateCardLicensesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'activateCardLicensesRequest' is not null or undefined + assertParamExists('activateCardLicenses', 'activateCardLicensesRequest', activateCardLicensesRequest) + const localVarPath = `/licenses/cards/activate`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(activateCardLicensesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelOrder: async (cancelOrderRequest: CancelOrderRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'cancelOrderRequest' is not null or undefined + assertParamExists('cancelOrder', 'cancelOrderRequest', cancelOrderRequest) + const localVarPath = `/licenses/orders/cancel`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(cancelOrderRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createOrders: async (createOrdersRequest: CreateOrdersRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createOrdersRequest' is not null or undefined + assertParamExists('createOrders', 'createOrdersRequest', createOrdersRequest) + const localVarPath = `/licenses/orders`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createOrdersRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllocatableLicenses: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/licenses/allocatable`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueCardLicenses: async (issueCardLicensesRequest: IssueCardLicensesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'issueCardLicensesRequest' is not null or undefined + assertParamExists('issueCardLicenses', 'issueCardLicensesRequest', issueCardLicensesRequest) + const localVarPath = `/licenses/cards`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(issueCardLicensesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * LicensesApi - functional programming interface + * @export + */ +export const LicensesApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = LicensesApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async activateCardLicenses(activateCardLicensesRequest: ActivateCardLicensesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.activateCardLicenses(activateCardLicensesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.activateCardLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancelOrder(cancelOrderRequest: CancelOrderRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancelOrder(cancelOrderRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.cancelOrder']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createOrders(createOrdersRequest: CreateOrdersRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createOrders(createOrdersRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.createOrders']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllocatableLicenses(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllocatableLicenses(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.getAllocatableLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async issueCardLicenses(issueCardLicensesRequest: IssueCardLicensesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.issueCardLicenses(issueCardLicensesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.issueCardLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * LicensesApi - factory interface + * @export + */ +export const LicensesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = LicensesApiFp(configuration) + return { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activateCardLicenses(activateCardLicensesRequest: ActivateCardLicensesRequest, options?: any): AxiosPromise { + return localVarFp.activateCardLicenses(activateCardLicensesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelOrder(cancelOrderRequest: CancelOrderRequest, options?: any): AxiosPromise { + return localVarFp.cancelOrder(cancelOrderRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createOrders(createOrdersRequest: CreateOrdersRequest, options?: any): AxiosPromise { + return localVarFp.createOrders(createOrdersRequest, options).then((request) => request(axios, basePath)); + }, + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllocatableLicenses(options?: any): AxiosPromise { + return localVarFp.getAllocatableLicenses(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueCardLicenses(issueCardLicensesRequest: IssueCardLicensesRequest, options?: any): AxiosPromise { + return localVarFp.issueCardLicenses(issueCardLicensesRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * LicensesApi - object-oriented interface + * @export + * @class LicensesApi + * @extends {BaseAPI} + */ +export class LicensesApi extends BaseAPI { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public activateCardLicenses(activateCardLicensesRequest: ActivateCardLicensesRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).activateCardLicenses(activateCardLicensesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public cancelOrder(cancelOrderRequest: CancelOrderRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).cancelOrder(cancelOrderRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public createOrders(createOrdersRequest: CreateOrdersRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).createOrders(createOrdersRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public getAllocatableLicenses(options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).getAllocatableLicenses(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public issueCardLicenses(issueCardLicensesRequest: IssueCardLicensesRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).issueCardLicenses(issueCardLicensesRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * NotificationApi - axios parameter creator + * @export + */ +export const NotificationApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + register: async (registerRequest: RegisterRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'registerRequest' is not null or undefined + assertParamExists('register', 'registerRequest', registerRequest) + const localVarPath = `/notification/register`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(registerRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * NotificationApi - functional programming interface + * @export + */ +export const NotificationApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = NotificationApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async register(registerRequest: RegisterRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.register(registerRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['NotificationApi.register']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * NotificationApi - factory interface + * @export + */ +export const NotificationApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = NotificationApiFp(configuration) + return { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + register(registerRequest: RegisterRequest, options?: any): AxiosPromise { + return localVarFp.register(registerRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * NotificationApi - object-oriented interface + * @export + * @class NotificationApi + * @extends {BaseAPI} + */ +export class NotificationApi extends BaseAPI { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof NotificationApi + */ + public register(registerRequest: RegisterRequest, options?: AxiosRequestConfig) { + return NotificationApiFp(this.configuration).register(registerRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * TasksApi - axios parameter creator + * @export + */ +export const TasksApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + backup: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('backup', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/backup` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancel: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('cancel', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/cancel` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + changeCheckoutPermission: async (audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('changeCheckoutPermission', 'audioFileId', audioFileId) + // verify required parameter 'postCheckoutPermissionRequest' is not null or undefined + assertParamExists('changeCheckoutPermission', 'postCheckoutPermissionRequest', postCheckoutPermissionRequest) + const localVarPath = `/tasks/{audioFileId}/checkout-permission` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postCheckoutPermissionRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkin: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('checkin', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/checkin` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkout: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('checkout', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/checkout` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('deleteTask', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/delete` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNextAudioFile: async (endedFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'endedFileId' is not null or undefined + assertParamExists('getNextAudioFile', 'endedFileId', endedFileId) + const localVarPath = `/tasks/next`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (endedFileId !== undefined) { + localVarQueryParameter['endedFileId'] = endedFileId; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTasks: async (limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/tasks`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (offset !== undefined) { + localVarQueryParameter['offset'] = offset; + } + + if (status !== undefined) { + localVarQueryParameter['status'] = status; + } + + if (direction !== undefined) { + localVarQueryParameter['direction'] = direction; + } + + if (paramName !== undefined) { + localVarQueryParameter['paramName'] = paramName; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + suspend: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('suspend', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/suspend` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TasksApi - functional programming interface + * @export + */ +export const TasksApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TasksApiAxiosParamCreator(configuration) + return { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async backup(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.backup(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.backup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancel(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancel(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.cancel']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async changeCheckoutPermission(audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.changeCheckoutPermission(audioFileId, postCheckoutPermissionRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.changeCheckoutPermission']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async checkin(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.checkin(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.checkin']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async checkout(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.checkout(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.checkout']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTask(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTask(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.deleteTask']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getNextAudioFile(endedFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getNextAudioFile(endedFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.getNextAudioFile']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTasks(limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTasks(limit, offset, status, direction, paramName, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.getTasks']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async suspend(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.suspend(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.suspend']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * TasksApi - factory interface + * @export + */ +export const TasksApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TasksApiFp(configuration) + return { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + backup(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.backup(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancel(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.cancel(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + changeCheckoutPermission(audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options?: any): AxiosPromise { + return localVarFp.changeCheckoutPermission(audioFileId, postCheckoutPermissionRequest, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkin(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.checkin(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkout(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.checkout(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTask(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNextAudioFile(endedFileId: number, options?: any): AxiosPromise { + return localVarFp.getNextAudioFile(endedFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTasks(limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options?: any): AxiosPromise { + return localVarFp.getTasks(limit, offset, status, direction, paramName, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + suspend(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.suspend(audioFileId, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TasksApi - object-oriented interface + * @export + * @class TasksApi + * @extends {BaseAPI} + */ +export class TasksApi extends BaseAPI { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public backup(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).backup(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public cancel(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).cancel(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public changeCheckoutPermission(audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).changeCheckoutPermission(audioFileId, postCheckoutPermissionRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public checkin(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).checkin(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public checkout(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).checkout(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public deleteTask(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).deleteTask(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public getNextAudioFile(endedFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).getNextAudioFile(endedFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public getTasks(limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).getTasks(limit, offset, status, direction, paramName, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public suspend(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).suspend(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * TemplatesApi - axios parameter creator + * @export + */ +export const TemplatesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile: async (templateFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'templateFileId' is not null or undefined + assertParamExists('deleteTemplateFile', 'templateFileId', templateFileId) + const localVarPath = `/templates/{templateFileId}/delete` + .replace(`{${"templateFileId"}}`, encodeURIComponent(String(templateFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTemplates: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/templates`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TemplatesApi - functional programming interface + * @export + */ +export const TemplatesApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TemplatesApiAxiosParamCreator(configuration) + return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTemplateFile(templateFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TemplatesApi.deleteTemplateFile']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTemplates(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTemplates(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TemplatesApi.getTemplates']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * TemplatesApi - factory interface + * @export + */ +export const TemplatesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TemplatesApiFp(configuration) + return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile(templateFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTemplateFile(templateFileId, options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTemplates(options?: any): AxiosPromise { + return localVarFp.getTemplates(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TemplatesApi - object-oriented interface + * @export + * @class TemplatesApi + * @extends {BaseAPI} + */ +export class TemplatesApi extends BaseAPI { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApi + */ + public deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig) { + return TemplatesApiFp(this.configuration).deleteTemplateFile(templateFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApi + */ + public getTemplates(options?: AxiosRequestConfig) { + return TemplatesApiFp(this.configuration).getTemplates(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * TermsApi - axios parameter creator + * @export + */ +export const TermsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTermsInfo: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/terms`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TermsApi - functional programming interface + * @export + */ +export const TermsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TermsApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTermsInfo(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTermsInfo(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TermsApi.getTermsInfo']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * TermsApi - factory interface + * @export + */ +export const TermsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TermsApiFp(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTermsInfo(options?: any): AxiosPromise { + return localVarFp.getTermsInfo(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TermsApi - object-oriented interface + * @export + * @class TermsApi + * @extends {BaseAPI} + */ +export class TermsApi extends BaseAPI { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TermsApi + */ + public getTermsInfo(options?: AxiosRequestConfig) { + return TermsApiFp(this.configuration).getTermsInfo(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * UsersApi - axios parameter creator + * @export + */ +export const UsersApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + allocateLicense: async (allocateLicenseRequest: AllocateLicenseRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'allocateLicenseRequest' is not null or undefined + assertParamExists('allocateLicense', 'allocateLicenseRequest', allocateLicenseRequest) + const localVarPath = `/users/license/allocate`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(allocateLicenseRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUser: async (confirmRequest: ConfirmRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'confirmRequest' is not null or undefined + assertParamExists('confirmUser', 'confirmRequest', confirmRequest) + const localVarPath = `/users/confirm`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(confirmRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUserAndInitPassword: async (confirmRequest: ConfirmRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'confirmRequest' is not null or undefined + assertParamExists('confirmUserAndInitPassword', 'confirmRequest', confirmRequest) + const localVarPath = `/users/confirm/initpassword`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(confirmRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deallocateLicense: async (deallocateLicenseRequest: DeallocateLicenseRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deallocateLicenseRequest' is not null or undefined + assertParamExists('deallocateLicense', 'deallocateLicenseRequest', deallocateLicenseRequest) + const localVarPath = `/users/license/deallocate`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deallocateLicenseRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser: async (postDeleteUserRequest: PostDeleteUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postDeleteUserRequest' is not null or undefined + assertParamExists('deleteUser', 'postDeleteUserRequest', postDeleteUserRequest) + const localVarPath = `/users/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postDeleteUserRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyUser: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getRelations: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users/relations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSortCriteria: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users/sort-criteria`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUsers: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports: async (postMultipleImportsRequest: PostMultipleImportsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsRequest' is not null or undefined + assertParamExists('multipleImports', 'postMultipleImportsRequest', postMultipleImportsRequest) + const localVarPath = `/users/multiple-imports`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate: async (postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsCompleteRequest' is not null or undefined + assertParamExists('multipleImportsComplate', 'postMultipleImportsCompleteRequest', postMultipleImportsCompleteRequest) + const localVarPath = `/users/multiple-imports/complete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsCompleteRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signup: async (signupRequest: SignupRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'signupRequest' is not null or undefined + assertParamExists('signup', 'signupRequest', signupRequest) + const localVarPath = `/users/signup`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(signupRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAcceptedVersion: async (updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateAcceptedVersionRequest' is not null or undefined + assertParamExists('updateAcceptedVersion', 'updateAcceptedVersionRequest', updateAcceptedVersionRequest) + const localVarPath = `/users/accepted-version`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateAcceptedVersionRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSortCriteria: async (postSortCriteriaRequest: PostSortCriteriaRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postSortCriteriaRequest' is not null or undefined + assertParamExists('updateSortCriteria', 'postSortCriteriaRequest', postSortCriteriaRequest) + const localVarPath = `/users/sort-criteria`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postSortCriteriaRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateUser: async (postUpdateUserRequest: PostUpdateUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postUpdateUserRequest' is not null or undefined + assertParamExists('updateUser', 'postUpdateUserRequest', postUpdateUserRequest) + const localVarPath = `/users/update`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postUpdateUserRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * UsersApi - functional programming interface + * @export + */ +export const UsersApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration) + return { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async allocateLicense(allocateLicenseRequest: AllocateLicenseRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.allocateLicense(allocateLicenseRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.allocateLicense']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async confirmUser(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.confirmUser(confirmRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.confirmUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.confirmUserAndInitPassword(confirmRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.confirmUserAndInitPassword']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deallocateLicense(deallocateLicenseRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.deallocateLicense']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(postDeleteUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.deleteUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMyUser(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMyUser(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getMyUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getRelations(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getRelations(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getRelations']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getSortCriteria(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getSortCriteria(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getSortCriteria']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getUsers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getUsers(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getUsers']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImports(postMultipleImportsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImports']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImportsComplate(postMultipleImportsCompleteRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImportsComplate']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async signup(signupRequest: SignupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.signup(signupRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.signup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateAcceptedVersion(updateAcceptedVersionRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.updateAcceptedVersion']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateSortCriteria(postSortCriteriaRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.updateSortCriteria']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateUser(postUpdateUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.updateUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * UsersApi - factory interface + * @export + */ +export const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = UsersApiFp(configuration) + return { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + allocateLicense(allocateLicenseRequest: AllocateLicenseRequest, options?: any): AxiosPromise { + return localVarFp.allocateLicense(allocateLicenseRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUser(confirmRequest: ConfirmRequest, options?: any): AxiosPromise { + return localVarFp.confirmUser(confirmRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: any): AxiosPromise { + return localVarFp.confirmUserAndInitPassword(confirmRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: any): AxiosPromise { + return localVarFp.deallocateLicense(deallocateLicenseRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: any): AxiosPromise { + return localVarFp.deleteUser(postDeleteUserRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyUser(options?: any): AxiosPromise { + return localVarFp.getMyUser(options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getRelations(options?: any): AxiosPromise { + return localVarFp.getRelations(options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSortCriteria(options?: any): AxiosPromise { + return localVarFp.getSortCriteria(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUsers(options?: any): AxiosPromise { + return localVarFp.getUsers(options).then((request) => request(axios, basePath)); + }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: any): AxiosPromise { + return localVarFp.multipleImports(postMultipleImportsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: any): AxiosPromise { + return localVarFp.multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signup(signupRequest: SignupRequest, options?: any): AxiosPromise { + return localVarFp.signup(signupRequest, options).then((request) => request(axios, basePath)); + }, + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: any): AxiosPromise { + return localVarFp.updateAcceptedVersion(updateAcceptedVersionRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: any): AxiosPromise { + return localVarFp.updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: any): AxiosPromise { + return localVarFp.updateUser(postUpdateUserRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * UsersApi - object-oriented interface + * @export + * @class UsersApi + * @extends {BaseAPI} + */ +export class UsersApi extends BaseAPI { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public allocateLicense(allocateLicenseRequest: AllocateLicenseRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).allocateLicense(allocateLicenseRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public confirmUser(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).confirmUser(confirmRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).confirmUserAndInitPassword(confirmRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).deallocateLicense(deallocateLicenseRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).deleteUser(postDeleteUserRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getMyUser(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getMyUser(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getRelations(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getRelations(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getSortCriteria(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getSortCriteria(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getUsers(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImports(postMultipleImportsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public signup(signupRequest: SignupRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).signup(signupRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updateAcceptedVersion(updateAcceptedVersionRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updateUser(postUpdateUserRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * WorkflowsApi - axios parameter creator + * @export + */ +export const WorkflowsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorkflows: async (createWorkflowsRequest: CreateWorkflowsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createWorkflowsRequest' is not null or undefined + assertParamExists('createWorkflows', 'createWorkflowsRequest', createWorkflowsRequest) + const localVarPath = `/workflows`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createWorkflowsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorkflow: async (workflowId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'workflowId' is not null or undefined + assertParamExists('deleteWorkflow', 'workflowId', workflowId) + const localVarPath = `/workflows/{workflowId}/delete` + .replace(`{${"workflowId"}}`, encodeURIComponent(String(workflowId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorkflows: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/workflows`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorkflow: async (workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'workflowId' is not null or undefined + assertParamExists('updateWorkflow', 'workflowId', workflowId) + // verify required parameter 'updateWorkflowRequest' is not null or undefined + assertParamExists('updateWorkflow', 'updateWorkflowRequest', updateWorkflowRequest) + const localVarPath = `/workflows/{workflowId}` + .replace(`{${"workflowId"}}`, encodeURIComponent(String(workflowId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateWorkflowRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * WorkflowsApi - functional programming interface + * @export + */ +export const WorkflowsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = WorkflowsApiAxiosParamCreator(configuration) + return { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createWorkflows(createWorkflowsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.createWorkflows']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteWorkflow(workflowId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorkflow(workflowId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.deleteWorkflow']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getWorkflows(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getWorkflows(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.getWorkflows']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateWorkflow(workflowId, updateWorkflowRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.updateWorkflow']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * WorkflowsApi - factory interface + * @export + */ +export const WorkflowsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = WorkflowsApiFp(configuration) + return { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: any): AxiosPromise { + return localVarFp.createWorkflows(createWorkflowsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorkflow(workflowId: number, options?: any): AxiosPromise { + return localVarFp.deleteWorkflow(workflowId, options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorkflows(options?: any): AxiosPromise { + return localVarFp.getWorkflows(options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: any): AxiosPromise { + return localVarFp.updateWorkflow(workflowId, updateWorkflowRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * WorkflowsApi - object-oriented interface + * @export + * @class WorkflowsApi + * @extends {BaseAPI} + */ +export class WorkflowsApi extends BaseAPI { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).createWorkflows(createWorkflowsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public deleteWorkflow(workflowId: number, options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).deleteWorkflow(workflowId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public getWorkflows(options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).getWorkflows(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).updateWorkflow(workflowId, updateWorkflowRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + diff --git a/dictation_function/src/api/base.ts b/dictation_function/src/api/base.ts new file mode 100644 index 0000000..4e44af5 --- /dev/null +++ b/dictation_function/src/api/base.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: AxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath ?? basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + constructor(public field: string, msg?: string) { + super(msg); + this.name = "RequiredError" + } +} + +interface ServerMap { + [key: string]: { + url: string, + description: string, + }[]; +} + +/** + * + * @export + */ +export const operationServerMap: ServerMap = { +} diff --git a/dictation_function/src/api/common.ts b/dictation_function/src/api/common.ts new file mode 100644 index 0000000..0e1c39c --- /dev/null +++ b/dictation_function/src/api/common.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (parameter == null) return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/dictation_function/src/api/configuration.ts b/dictation_function/src/api/configuration.ts new file mode 100644 index 0000000..9941122 --- /dev/null +++ b/dictation_function/src/api/configuration.ts @@ -0,0 +1,110 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + serverIndex?: number; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * override server index + * + * @type {number} + * @memberof Configuration + */ + serverIndex?: number; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.serverIndex = param.serverIndex; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/dictation_function/src/api/git_push.sh b/dictation_function/src/api/git_push.sh new file mode 100644 index 0000000..f53a75d --- /dev/null +++ b/dictation_function/src/api/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/dictation_function/src/api/index.ts b/dictation_function/src/api/index.ts new file mode 100644 index 0000000..c982723 --- /dev/null +++ b/dictation_function/src/api/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; + diff --git a/dictation_function/src/api/odms/openapi.json b/dictation_function/src/api/odms/openapi.json new file mode 100644 index 0000000..c279e1f --- /dev/null +++ b/dictation_function/src/api/odms/openapi.json @@ -0,0 +1,5357 @@ +{ + "openapi": "3.0.0", + "paths": { + "/health": { + "get": { + "operationId": "checkHealth", + "summary": "", + "parameters": [], + "responses": { "200": { "description": "" } } + } + }, + "/auth/token": { + "post": { + "operationId": "token", + "summary": "", + "description": "AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TokenRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TokenResponse" } + } + } + }, + "401": { + "description": "認証エラー/同意済み利用規約が最新でない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"] + } + }, + "/auth/accessToken": { + "post": { + "operationId": "accessToken", + "summary": "", + "description": "リフレッシュトークンを元にアクセストークンを再生成します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AccessTokenResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"], + "security": [{ "bearer": [] }] + } + }, + "/auth/delegation/token": { + "post": { + "operationId": "delegationToken", + "summary": "", + "description": "代行操作用のリフレッシュトークン・アクセストークンを生成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationTokenRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationTokenResponse" + } + } + } + }, + "400": { + "description": "指定したアカウントが代行操作を許可していない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"], + "security": [{ "bearer": [] }] + } + }, + "/auth/delegation/access-token": { + "post": { + "operationId": "delegationAccessToken", + "summary": "", + "description": "代行操作用のアクセストークンを再生成します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationAccessTokenResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"], + "security": [{ "bearer": [] }] + } + }, + "/accounts": { + "post": { + "operationId": "createAccount", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateAccountRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAccountResponse" + } + } + } + }, + "400": { + "description": "登録済みユーザーからの登録など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"] + } + }, + "/accounts/licenses/summary": { + "post": { + "operationId": "getLicenseSummary", + "summary": "", + "description": "指定したアカウントのライセンス集計情報を取得します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetLicenseSummaryRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetLicenseSummaryResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/me": { + "get": { + "operationId": "getMyAccount", + "summary": "", + "description": "ログインしているユーザーのアカウント情報を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMyAccountResponse" + } + } + } + }, + "400": { + "description": "該当アカウントがDBに存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateAccountInfo", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAccountInfoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAccountInfoResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/アカウント・ユーザー不在/管理者ユーザ不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/authors": { + "get": { + "operationId": "getAuthors", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のAuthor一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetAuthorsResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typists": { + "get": { + "operationId": "getTypists", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のタイピスト一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetTypistsResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typist-groups": { + "get": { + "operationId": "getTypistGroups", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTypistGroupsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "createTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下にタイピストグループを追加します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTypistGroupRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTypistGroupResponse" + } + } + } + }, + "400": { + "description": "グループ名が空の場合/ユーザーが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typist-groups/{typistGroupId}": { + "get": { + "operationId": "getTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTypistGroupResponse" + } + } + } + }, + "400": { + "description": "グループが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTypistGroupRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTypistGroupResponse" + } + } + } + }, + "400": { + "description": "グループ名が空の場合/ユーザーが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typist-groups/{typistGroupId}/delete": { + "post": { + "operationId": "deleteTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTypistGroupResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / タスクの割り当て候補に設定されている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner": { + "post": { + "operationId": "createPartnerAccount", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePartnerAccountRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePartnerAccountResponse" + } + } + } + }, + "400": { + "description": "登録済みユーザーからの登録など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner-licenses": { + "post": { + "operationId": "getPartnerLicenses", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerLicensesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerLicensesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/order-histories": { + "post": { + "operationId": "getOrderHistories", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrderHistoriesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrderHistoriesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/licenses/issue": { + "post": { + "operationId": "issueLicense", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/IssueLicenseRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueLicenseResponse" + } + } + } + }, + "400": { + "description": "自身のライセンス数が不足している場合/すでに対象注文が発行済の場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/dealers": { + "get": { + "operationId": "getDealers", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetDealersResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"] + } + }, + "/accounts/issue/cancel": { + "post": { + "operationId": "cancelIssue", + "summary": "", + "description": "ライセンス発行をキャンセルします", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelIssueRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelIssueResponse" } + } + } + }, + "400": { + "description": "対象注文のステータスが発行済以外/発行日から15日以降/ライセンスをユーザに割り当てている", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes": { + "get": { + "operationId": "getWorktypes", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWorktypesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "createWorktype", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorktypesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorktypeResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが重複 / WorktypeIDが空 / WorktypeIDが20件登録済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes/{id}": { + "post": { + "operationId": "updateWorktype", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorktypesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorktypeResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが重複 / WorktypeIDが空", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes/{id}/delete": { + "post": { + "operationId": "deleteWorktype", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteWorktypeResponse" + } + } + } + }, + "400": { + "description": "指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes/{id}/option-items": { + "get": { + "operationId": "getOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/active-worktype": { + "post": { + "operationId": "activeWorktype", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostActiveWorktypeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostActiveWorktypeResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが存在しない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partners": { + "get": { + "operationId": "getPartners", + "summary": "", + "parameters": [ + { + "name": "limit", + "required": true, + "in": "query", + "description": "取得件数", + "schema": { "type": "number" } + }, + { + "name": "offset", + "required": true, + "in": "query", + "description": "開始位置", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetPartnersResponse" } + } + } + }, + "400": { + "description": "パラメータ不正", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/me/file-delete-setting": { + "post": { + "operationId": "updateFileDeleteSetting", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/アカウント・ユーザー不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/delete": { + "post": { + "operationId": "deleteAccountAndData", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/DeleteAccountRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAccountInfoResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "DBアクセスに失敗しログインできる状態で処理が終了した場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/minimal-access": { + "post": { + "operationId": "getAccountInfoMinimalAccess", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAccountInfoMinimalAccessRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAccountInfoMinimalAccessResponse" + } + } + } + }, + "400": { + "description": "対象のユーザーIDが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"] + } + }, + "/accounts/company-name": { + "post": { + "operationId": "getCompanyName", + "summary": "", + "description": "指定したアカウントの会社名を取得します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetCompanyNameRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCompanyNameResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/restriction-status": { + "post": { + "operationId": "updateRestrictionStatus", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/users/confirm": { + "post": { + "operationId": "confirmUser", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmResponse" } + } + } + }, + "400": { + "description": "不正なトークン", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"] + } + }, + "/users/confirm/initpassword": { + "post": { + "operationId": "confirmUserAndInitPassword", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmResponse" } + } + } + }, + "400": { + "description": "不正なトークン", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"] + } + }, + "/users": { + "get": { + "operationId": "getUsers", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetUsersResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/signup": { + "post": { + "operationId": "signup", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SignupRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SignupResponse" } + } + } + }, + "400": { + "description": "登録済みメールによる再登録、AuthorIDの重複など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/relations": { + "get": { + "operationId": "getRelations", + "summary": "", + "description": "ログインしているユーザーに関連する各種情報を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetRelationsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/sort-criteria": { + "post": { + "operationId": "updateSortCriteria", + "summary": "", + "description": "ログインしているユーザーのタスクソート条件を更新します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostSortCriteriaRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostSortCriteriaResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + }, + "get": { + "operationId": "getSortCriteria", + "summary": "", + "description": "ログインしているユーザーのタスクソート条件を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSortCriteriaResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/update": { + "post": { + "operationId": "updateUser", + "summary": "", + "description": "ユーザーの情報を更新します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PostUpdateUserRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostUpdateUserResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/license/allocate": { + "post": { + "operationId": "allocateLicense", + "summary": "", + "description": "ライセンスを割り当てます", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllocateLicenseRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllocateLicenseResponse" + } + } + } + }, + "400": { + "description": "割り当て失敗時", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/license/deallocate": { + "post": { + "operationId": "deallocateLicense", + "summary": "", + "description": "ライセンス割り当てを解除します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeallocateLicenseRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeallocateLicenseResponse" + } + } + } + }, + "400": { + "description": "すでにライセンスが割り当て解除されている時", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/accepted-version": { + "post": { + "operationId": "updateAcceptedVersion", + "summary": "", + "description": "利用規約同意バージョンを更新", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAcceptedVersionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAcceptedVersionResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/対象のユーザidが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"] + } + }, + "/users/me": { + "get": { + "operationId": "getMyUser", + "summary": "", + "description": "ログインしているユーザーの情報を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetMyUserResponse" } + } + } + }, + "400": { + "description": "該当ユーザーがDBに存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/delete": { + "post": { + "operationId": "deleteUser", + "summary": "", + "description": "ユーザーを削除します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PostDeleteUserRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostDeleteUserResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/multiple-imports": { + "post": { + "operationId": "multipleImports", + "summary": "", + "description": "ユーザーを一括登録します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/multiple-imports/complete": { + "post": { + "operationId": "multipleImportsComplate", + "summary": "", + "description": "ユーザー一括登録の完了を通知します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/files/audio/upload-finished": { + "post": { + "operationId": "uploadFinished", + "summary": "", + "description": "アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioUploadFinishedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioUploadFinishedResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/audio/upload-location": { + "get": { + "operationId": "uploadLocation", + "summary": "", + "description": "ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioUploadLocationResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/audio/download-location": { + "get": { + "operationId": "downloadLocation", + "summary": "", + "description": "指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "query", + "description": "ODMSCloud上で管理する音声ファイルのID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioDownloadLocationResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/template/download-location": { + "get": { + "operationId": "downloadTemplateLocation", + "summary": "", + "description": "指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "query", + "description": "文字起こし対象の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateDownloadLocationResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/template/upload-location": { + "get": { + "operationId": "uploadTemplateLocation", + "summary": "", + "description": "ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadLocationResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/template/upload-finished": { + "post": { + "operationId": "uploadTemplateFinished", + "summary": "", + "description": "アップロードが完了したテンプレートファイルの情報を登録します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadFinishedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadFinishedReqponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/tasks": { + "get": { + "operationId": "getTasks", + "summary": "", + "description": "音声ファイル・文字起こしタスク情報をページ指定して取得します", + "parameters": [ + { + "name": "limit", + "required": false, + "in": "query", + "description": "タスクの取得件数(指定しない場合はデフォルト値)", + "schema": { "default": 200, "type": "number" } + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "オフセット(何件目から取得するか 設定しない場合はデフォルト値)", + "schema": { "default": 0, "type": "number" } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup", + "example": "Uploaded,Pending,InProgress", + "schema": { "type": "string" } + }, + { + "name": "direction", + "required": false, + "in": "query", + "description": "ASC/DESC", + "schema": { "type": "string" } + }, + { + "name": "paramName", + "required": false, + "in": "query", + "description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TasksResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/next": { + "get": { + "operationId": "getNextAudioFile", + "summary": "", + "description": "指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します", + "parameters": [ + { + "name": "endedFileId", + "required": true, + "in": "query", + "description": "文字起こし完了したタスクの音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AudioNextResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/checkout": { + "post": { + "operationId": "checkout", + "summary": "", + "description": "指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/checkin": { + "post": { + "operationId": "checkin", + "summary": "", + "description": "指定した文字起こしタスクをチェックインします(ステータスをFinishedにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/cancel": { + "post": { + "operationId": "cancel", + "summary": "", + "description": "指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/suspend": { + "post": { + "operationId": "suspend", + "summary": "", + "description": "指定した文字起こしタスクを一時中断します(ステータスをPendingにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/backup": { + "post": { + "operationId": "backup", + "summary": "", + "description": "指定した文字起こしタスクをバックアップします(ステータスをBackupにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/checkout-permission": { + "post": { + "operationId": "changeCheckoutPermission", + "summary": "", + "description": "指定した文字起こしタスクのチェックアウト候補を変更します。", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCheckoutPermissionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCheckoutPermissionResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ(タスクのステータス不正、指定ユーザー不正など)", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/delete": { + "post": { + "operationId": "deleteTask", + "summary": "", + "description": "指定した文字起こしタスクを削除します。", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostDeleteTaskResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/orders": { + "post": { + "operationId": "createOrders", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateOrdersRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrdersResponse" + } + } + } + }, + "400": { + "description": "同一PONumberの注文がすでに存在する場合など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/cards": { + "post": { + "operationId": "issueCardLicenses", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueCardLicensesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueCardLicensesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/cards/activate": { + "post": { + "operationId": "activateCardLicenses", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivateCardLicensesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivateCardLicensesResponse" + } + } + } + }, + "400": { + "description": "パラメータのライセンスキーが不正な内容の場合/存在しない場合/登録済みの場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/allocatable": { + "get": { + "operationId": "getAllocatableLicenses", + "summary": "", + "description": "割り当て可能なライセンスを取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllocatableLicensesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/orders/cancel": { + "post": { + "operationId": "cancelOrder", + "summary": "", + "description": "ライセンス注文をキャンセルします", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelOrderRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelOrderResponse" } + } + } + }, + "400": { + "description": "対象注文のステータスが発行待ち状態でないとき", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/templates": { + "get": { + "operationId": "getTemplates", + "summary": "", + "description": "アカウント内のテンプレートファイルの一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTemplatesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["templates"], + "security": [{ "bearer": [] }] + } + }, + "/templates/{templateFileId}/delete": { + "post": { + "operationId": "deleteTemplateFile", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します", + "parameters": [ + { + "name": "templateFileId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTemplateResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / 未完了タスクに紐づいている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["templates"], + "security": [{ "bearer": [] }] + } + }, + "/workflows": { + "get": { + "operationId": "getWorkflows", + "summary": "", + "description": "アカウント内のワークフローの一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWorkflowsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "createWorkflows", + "summary": "", + "description": "アカウント内にワークフローを新規作成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkflowsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkflowsResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + } + }, + "/workflows/{workflowId}": { + "post": { + "operationId": "updateWorkflow", + "summary": "", + "description": "アカウント内のワークフローを編集します", + "parameters": [ + { + "name": "workflowId", + "required": true, + "in": "path", + "description": "ワークフローの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateWorkflowRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkflowResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + } + }, + "/workflows/{workflowId}/delete": { + "post": { + "operationId": "deleteWorkflow", + "summary": "", + "description": "アカウント内のワークフローを削除します", + "parameters": [ + { + "name": "workflowId", + "required": true, + "in": "path", + "description": "ワークフローの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteWorkflowResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + } + }, + "/notification/register": { + "post": { + "operationId": "register", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RegisterRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RegisterResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["notification"], + "security": [{ "bearer": [] }] + } + }, + "/terms": { + "get": { + "operationId": "getTermsInfo", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTermsInfoResponse" + } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["terms"] + } + } + }, + "info": { + "title": "ODMSOpenAPI", + "description": "", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "securitySchemes": { + "bearer": { "scheme": "bearer", "bearerFormat": "JWT", "type": "http" } + }, + "schemas": { + "TokenRequest": { + "type": "object", + "properties": { + "idToken": { "type": "string" }, + "type": { + "type": "string", + "description": "web or mobile or desktop" + } + }, + "required": ["idToken", "type"] + }, + "TokenResponse": { + "type": "object", + "properties": { + "refreshToken": { "type": "string" }, + "accessToken": { "type": "string" } + }, + "required": ["refreshToken", "accessToken"] + }, + "ErrorResponse": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "code": { "type": "string" } + }, + "required": ["message", "code"] + }, + "AccessTokenResponse": { + "type": "object", + "properties": { "accessToken": { "type": "string" } }, + "required": ["accessToken"] + }, + "DelegationTokenRequest": { + "type": "object", + "properties": { + "delegatedAccountId": { + "type": "number", + "description": "代行操作対象のアカウントID" + } + }, + "required": ["delegatedAccountId"] + }, + "DelegationTokenResponse": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string", + "description": "代行操作用のリフレッシュトークン" + }, + "accessToken": { + "type": "string", + "description": "代行操作用のアクセストークン" + } + }, + "required": ["refreshToken", "accessToken"] + }, + "DelegationAccessTokenResponse": { + "type": "object", + "properties": { + "accessToken": { + "type": "string", + "description": "代行操作用のアクセストークン" + } + }, + "required": ["accessToken"] + }, + "CreateAccountRequest": { + "type": "object", + "properties": { + "companyName": { "type": "string" }, + "country": { + "type": "string", + "description": "国名(ISO 3166-1 alpha-2)", + "minLength": 2, + "maxLength": 2 + }, + "dealerAccountId": { "type": "number" }, + "adminName": { "type": "string" }, + "adminMail": { "type": "string" }, + "adminPassword": { "type": "string" }, + "acceptedEulaVersion": { + "type": "string", + "description": "同意済み利用規約のバージョン(EULA)" + }, + "acceptedPrivacyNoticeVersion": { + "type": "string", + "description": "同意済みプライバシーポリシーのバージョン" + }, + "acceptedDpaVersion": { + "type": "string", + "description": "同意済み利用規約のバージョン(DPA)" + }, + "token": { "type": "string", "description": "reCAPTCHA Token" } + }, + "required": [ + "companyName", + "country", + "adminName", + "adminMail", + "adminPassword", + "acceptedEulaVersion", + "acceptedPrivacyNoticeVersion", + "acceptedDpaVersion", + "token" + ] + }, + "CreateAccountResponse": { "type": "object", "properties": {} }, + "GetLicenseSummaryRequest": { + "type": "object", + "properties": { "accountId": { "type": "number" } }, + "required": ["accountId"] + }, + "GetLicenseSummaryResponse": { + "type": "object", + "properties": { + "totalLicense": { "type": "number" }, + "allocatedLicense": { "type": "number" }, + "reusableLicense": { "type": "number" }, + "freeLicense": { "type": "number" }, + "expiringWithin14daysLicense": { "type": "number" }, + "issueRequesting": { "type": "number" }, + "numberOfRequesting": { "type": "number" }, + "shortage": { "type": "number" }, + "storageSize": { "type": "number" }, + "usedSize": { "type": "number" }, + "isStorageAvailable": { "type": "boolean" } + }, + "required": [ + "totalLicense", + "allocatedLicense", + "reusableLicense", + "freeLicense", + "expiringWithin14daysLicense", + "issueRequesting", + "numberOfRequesting", + "shortage", + "storageSize", + "usedSize", + "isStorageAvailable" + ] + }, + "Account": { + "type": "object", + "properties": { + "accountId": { "type": "number" }, + "companyName": { "type": "string" }, + "tier": { "type": "number" }, + "country": { "type": "string" }, + "parentAccountId": { "type": "number" }, + "delegationPermission": { "type": "boolean" }, + "autoFileDelete": { "type": "boolean" }, + "fileRetentionDays": { "type": "number" }, + "primaryAdminUserId": { "type": "number" }, + "secondryAdminUserId": { "type": "number" }, + "parentAccountName": { "type": "string" } + }, + "required": [ + "accountId", + "companyName", + "tier", + "country", + "delegationPermission", + "autoFileDelete", + "fileRetentionDays" + ] + }, + "GetMyAccountResponse": { + "type": "object", + "properties": { "account": { "$ref": "#/components/schemas/Account" } }, + "required": ["account"] + }, + "Author": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "Authorユーザーの内部ID" }, + "authorId": { "type": "string", "description": "AuthorID" } + }, + "required": ["id", "authorId"] + }, + "GetAuthorsResponse": { + "type": "object", + "properties": { + "authors": { + "type": "array", + "items": { "$ref": "#/components/schemas/Author" } + } + }, + "required": ["authors"] + }, + "Typist": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "TypistのユーザーID" }, + "name": { "type": "string", "description": "Typistのユーザー名" } + }, + "required": ["id", "name"] + }, + "GetTypistsResponse": { + "type": "object", + "properties": { + "typists": { + "type": "array", + "items": { "$ref": "#/components/schemas/Typist" } + } + }, + "required": ["typists"] + }, + "TypistGroup": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "TypistGroupのID" }, + "name": { "type": "string", "description": "TypistGroup名" } + }, + "required": ["id", "name"] + }, + "GetTypistGroupsResponse": { + "type": "object", + "properties": { + "typistGroups": { + "type": "array", + "items": { "$ref": "#/components/schemas/TypistGroup" } + } + }, + "required": ["typistGroups"] + }, + "GetTypistGroupResponse": { + "type": "object", + "properties": { + "typistGroupName": { "type": "string" }, + "typistIds": { "type": "array", "items": { "type": "integer" } } + }, + "required": ["typistGroupName", "typistIds"] + }, + "CreateTypistGroupRequest": { + "type": "object", + "properties": { + "typistGroupName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "typistIds": { + "minItems": 1, + "type": "array", + "items": { "type": "integer" } + } + }, + "required": ["typistGroupName", "typistIds"] + }, + "CreateTypistGroupResponse": { "type": "object", "properties": {} }, + "UpdateTypistGroupRequest": { + "type": "object", + "properties": { + "typistGroupName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "typistIds": { + "minItems": 1, + "type": "array", + "items": { "type": "integer" } + } + }, + "required": ["typistGroupName", "typistIds"] + }, + "DeleteTypistGroupResponse": { "type": "object", "properties": {} }, + "CreatePartnerAccountRequest": { + "type": "object", + "properties": { + "companyName": { "type": "string" }, + "country": { + "type": "string", + "description": "国名(ISO 3166-1 alpha-2)", + "minLength": 2, + "maxLength": 2 + }, + "adminName": { "type": "string" }, + "email": { "type": "string" } + }, + "required": ["companyName", "country", "adminName", "email"] + }, + "CreatePartnerAccountResponse": { "type": "object", "properties": {} }, + "GetPartnerLicensesRequest": { + "type": "object", + "properties": { + "limit": { "type": "number" }, + "offset": { "type": "number" }, + "accountId": { "type": "number" } + }, + "required": ["limit", "offset", "accountId"] + }, + "PartnerLicenseInfo": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" }, + "tier": { "type": "number", "description": "階層" }, + "companyName": { "type": "string", "description": "アカウント名" }, + "stockLicense": { + "type": "number", + "description": "保有している有効期限が未設定あるいは有効期限内のライセンス数" + }, + "issuedRequested": { + "type": "number", + "description": "子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数" + }, + "shortage": { + "type": "number", + "description": "不足数({Stock license} - {Issue Requested})" + }, + "issueRequesting": { + "type": "number", + "description": "未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数)" + } + }, + "required": [ + "accountId", + "tier", + "companyName", + "stockLicense", + "issuedRequested", + "shortage", + "issueRequesting" + ] + }, + "GetPartnerLicensesResponse": { + "type": "object", + "properties": { + "total": { "type": "number" }, + "ownPartnerLicense": { + "$ref": "#/components/schemas/PartnerLicenseInfo" + }, + "childrenPartnerLicenses": { + "type": "array", + "items": { "$ref": "#/components/schemas/PartnerLicenseInfo" } + } + }, + "required": ["total", "ownPartnerLicense", "childrenPartnerLicenses"] + }, + "GetOrderHistoriesRequest": { + "type": "object", + "properties": { + "limit": { "type": "number", "description": "取得件数" }, + "offset": { "type": "number", "description": "開始位置" }, + "accountId": { "type": "number", "description": "アカウントID" } + }, + "required": ["limit", "offset", "accountId"] + }, + "LicenseOrder": { + "type": "object", + "properties": { + "orderDate": { "type": "string", "description": "注文日付" }, + "issueDate": { "type": "string", "description": "発行日付" }, + "numberOfOrder": { "type": "number", "description": "注文数" }, + "poNumber": { "type": "string", "description": "POナンバー" }, + "status": { "type": "string", "description": "注文状態" } + }, + "required": ["orderDate", "numberOfOrder", "poNumber", "status"] + }, + "GetOrderHistoriesResponse": { + "type": "object", + "properties": { + "total": { "type": "number", "description": "合計件数" }, + "orderHistories": { + "type": "array", + "items": { "$ref": "#/components/schemas/LicenseOrder" } + } + }, + "required": ["total", "orderHistories"] + }, + "IssueLicenseRequest": { + "type": "object", + "properties": { + "orderedAccountId": { + "type": "number", + "description": "注文元アカウントID" + }, + "poNumber": { "type": "string", "description": "POナンバー" } + }, + "required": ["orderedAccountId", "poNumber"] + }, + "IssueLicenseResponse": { "type": "object", "properties": {} }, + "Dealer": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "アカウントID" }, + "name": { "type": "string", "description": "会社名" }, + "country": { + "type": "string", + "description": "国名(ISO 3166-1 alpha-2)" + } + }, + "required": ["id", "name", "country"] + }, + "GetDealersResponse": { + "type": "object", + "properties": { + "dealers": { + "type": "array", + "items": { "$ref": "#/components/schemas/Dealer" } + } + }, + "required": ["dealers"] + }, + "CancelIssueRequest": { + "type": "object", + "properties": { + "orderedAccountId": { + "type": "number", + "description": "注文元アカウントID" + }, + "poNumber": { "type": "string", "description": "POナンバー" } + }, + "required": ["orderedAccountId", "poNumber"] + }, + "CancelIssueResponse": { "type": "object", "properties": {} }, + "Worktype": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "WorktypeのID" }, + "worktypeId": { "type": "string", "description": "WorktypeID" }, + "description": { "type": "string", "description": "Worktypeの説明" } + }, + "required": ["id", "worktypeId"] + }, + "GetWorktypesResponse": { + "type": "object", + "properties": { + "worktypes": { + "type": "array", + "items": { "$ref": "#/components/schemas/Worktype" } + }, + "active": { + "type": "number", + "description": "Active WorktypeIDに設定されているWorkTypeの内部ID" + } + }, + "required": ["worktypes"] + }, + "CreateWorktypesRequest": { + "type": "object", + "properties": { + "worktypeId": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "WorktypeID" + }, + "description": { "type": "string", "description": "Worktypeの説明" } + }, + "required": ["worktypeId"] + }, + "CreateWorktypeResponse": { "type": "object", "properties": {} }, + "UpdateWorktypesRequest": { + "type": "object", + "properties": { + "worktypeId": { + "type": "string", + "minLength": 1, + "description": "WorktypeID" + }, + "description": { "type": "string", "description": "Worktypeの説明" } + }, + "required": ["worktypeId"] + }, + "UpdateWorktypeResponse": { "type": "object", "properties": {} }, + "DeleteWorktypeResponse": { "type": "object", "properties": {} }, + "GetWorktypeOptionItem": { + "type": "object", + "properties": { + "itemLabel": { "type": "string", "maxLength": 16 }, + "defaultValueType": { + "type": "string", + "maxLength": 20, + "description": "Default / Blank / LastInput" + }, + "initialValue": { "type": "string", "maxLength": 20 }, + "id": { "type": "number" } + }, + "required": ["itemLabel", "defaultValueType", "initialValue", "id"] + }, + "GetOptionItemsResponse": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/GetWorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "PostWorktypeOptionItem": { + "type": "object", + "properties": { + "itemLabel": { "type": "string", "maxLength": 16 }, + "defaultValueType": { + "type": "string", + "maxLength": 20, + "description": "Default / Blank / LastInput" + }, + "initialValue": { "type": "string", "maxLength": 20 } + }, + "required": ["itemLabel", "defaultValueType", "initialValue"] + }, + "UpdateOptionItemsRequest": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/PostWorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "UpdateOptionItemsResponse": { "type": "object", "properties": {} }, + "PostActiveWorktypeRequest": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Active WorkTypeIDにするWorktypeの内部ID" + } + } + }, + "PostActiveWorktypeResponse": { "type": "object", "properties": {} }, + "Partner": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "会社名" }, + "tier": { "type": "number", "description": "階層" }, + "accountId": { "type": "number", "description": "アカウントID" }, + "country": { "type": "string", "description": "国" }, + "primaryAdmin": { + "type": "string", + "description": "プライマリ管理者" + }, + "email": { + "type": "string", + "description": "プライマリ管理者メールアドレス" + }, + "dealerManagement": { + "type": "boolean", + "description": "代行操作許可" + } + }, + "required": [ + "name", + "tier", + "accountId", + "country", + "primaryAdmin", + "email", + "dealerManagement" + ] + }, + "GetPartnersResponse": { + "type": "object", + "properties": { + "total": { "type": "number", "description": "合計件数" }, + "partners": { + "type": "array", + "items": { "$ref": "#/components/schemas/Partner" } + } + }, + "required": ["total", "partners"] + }, + "UpdateAccountInfoRequest": { + "type": "object", + "properties": { + "parentAccountId": { + "type": "number", + "description": "親アカウントのID" + }, + "delegationPermission": { + "type": "boolean", + "description": "代行操作許可" + }, + "primaryAdminUserId": { + "type": "number", + "description": "プライマリ管理者ID" + }, + "secondryAdminUserId": { + "type": "number", + "description": "セカンダリ管理者ID" + } + }, + "required": ["delegationPermission", "primaryAdminUserId"] + }, + "UpdateAccountInfoResponse": { "type": "object", "properties": {} }, + "UpdateFileDeleteSettingRequest": { + "type": "object", + "properties": { + "autoFileDelete": { + "type": "boolean", + "description": "自動ファイル削除をするかどうか" + }, + "retentionDays": { + "type": "number", + "description": "文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間" + } + }, + "required": ["autoFileDelete", "retentionDays"] + }, + "UpdateFileDeleteSettingResponse": { "type": "object", "properties": {} }, + "DeleteAccountRequest": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" } + }, + "required": ["accountId"] + }, + "GetAccountInfoMinimalAccessRequest": { + "type": "object", + "properties": { + "idToken": { "type": "string", "description": "idトークン" } + }, + "required": ["idToken"] + }, + "GetAccountInfoMinimalAccessResponse": { + "type": "object", + "properties": { "tier": { "type": "number", "description": "階層" } }, + "required": ["tier"] + }, + "GetCompanyNameRequest": { + "type": "object", + "properties": { "accountId": { "type": "number" } }, + "required": ["accountId"] + }, + "GetCompanyNameResponse": { + "type": "object", + "properties": { "companyName": { "type": "string" } }, + "required": ["companyName"] + }, + "UpdateRestrictionStatusRequest": { + "type": "object", + "properties": { + "accountId": { + "type": "number", + "description": "操作対象の第五階層アカウントID" + }, + "restricted": { + "type": "boolean", + "description": "制限をかけるかどうか(trur:制限をかける)" + } + }, + "required": ["accountId", "restricted"] + }, + "UpdateRestrictionStatusResponse": { "type": "object", "properties": {} }, + "ConfirmRequest": { + "type": "object", + "properties": { "token": { "type": "string" } }, + "required": ["token"] + }, + "ConfirmResponse": { "type": "object", "properties": {} }, + "User": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "name": { "type": "string" }, + "role": { "type": "string", "description": "none/author/typist" }, + "authorId": { "type": "string" }, + "typistGroupName": { "type": "array", "items": { "type": "string" } }, + "email": { "type": "string" }, + "emailVerified": { "type": "boolean" }, + "autoRenew": { "type": "boolean" }, + "notification": { "type": "boolean" }, + "encryption": { "type": "boolean" }, + "prompt": { "type": "boolean" }, + "expiration": { "type": "string" }, + "remaining": { "type": "number" }, + "licenseStatus": { + "type": "string", + "description": "Normal/NoLicense/Alert/Renew" + } + }, + "required": [ + "id", + "name", + "role", + "typistGroupName", + "email", + "emailVerified", + "autoRenew", + "notification", + "encryption", + "prompt", + "licenseStatus" + ] + }, + "GetUsersResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } + } + }, + "required": ["users"] + }, + "SignupRequest": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "role": { "type": "string", "description": "none/author/typist" }, + "authorId": { "type": "string" }, + "email": { "type": "string" }, + "autoRenew": { "type": "boolean" }, + "notification": { "type": "boolean" }, + "encryption": { "type": "boolean" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "boolean" } + }, + "required": ["name", "role", "email", "autoRenew", "notification"] + }, + "SignupResponse": { "type": "object", "properties": {} }, + "OptionItem": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "description": "Option Itemのラベル" + }, + "initialValueType": { + "type": "number", + "description": "項目タイプ 1:Blank/2:Default/3:前の値" + }, + "defaultValue": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "typeでDefaultを選択した場合のデフォルト値" + } + }, + "required": ["label", "initialValueType", "defaultValue"] + }, + "OptionItemList": { + "type": "object", + "properties": { + "workTypeId": { "type": "string" }, + "optionItemList": { + "maxItems": 10, + "description": "1WorkTypeIDにつき、10個まで登録可能", + "type": "array", + "items": { "$ref": "#/components/schemas/OptionItem" } + } + }, + "required": ["workTypeId", "optionItemList"] + }, + "GetRelationsResponse": { + "type": "object", + "properties": { + "authorId": { + "type": "string", + "description": "ログインしたユーザーのAuthorID(Authorでない場合はundefined)" + }, + "authorIdList": { + "description": "属しているアカウントのAuthorID List(全て)", + "type": "array", + "items": { "type": "string" } + }, + "workTypeList": { + "maxItems": 20, + "description": "アカウントに設定されているWorktypeIDのリスト(最大20個)", + "type": "array", + "items": { "$ref": "#/components/schemas/OptionItemList" } + }, + "isEncrypted": { + "type": "boolean", + "description": "ユーザーが音声ファイルを暗号化するかどうか" + }, + "encryptionPassword": { + "type": "string", + "description": "ユーザーが暗号化を掛ける場合のパスワード" + }, + "activeWorktype": { + "type": "string", + "description": "アカウントがデフォルトで利用するWorkTypeID(アカウントに紐づくWorkTypeIDから一つ指定。activeWorktypeがなければ空文字を返却する)" + }, + "audioFormat": { + "type": "string", + "description": "録音形式: DSS/DS2(SP)/DS2(QP): DS2固定" + }, + "prompt": { + "type": "boolean", + "description": "デバイス上で自動的にWorkTypeの選択画面を表示するかどうかのユーザーごとの設定(Authorでない場合はfalse)" + } + }, + "required": [ + "authorIdList", + "workTypeList", + "isEncrypted", + "activeWorktype", + "audioFormat", + "prompt" + ] + }, + "PostSortCriteriaRequest": { + "type": "object", + "properties": { + "direction": { "type": "string", "description": "ASC/DESC" }, + "paramName": { + "type": "string", + "description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE" + } + }, + "required": ["direction", "paramName"] + }, + "PostSortCriteriaResponse": { "type": "object", "properties": {} }, + "GetSortCriteriaResponse": { + "type": "object", + "properties": { + "direction": { "type": "string", "description": "ASC/DESC" }, + "paramName": { + "type": "string", + "description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE" + } + }, + "required": ["direction", "paramName"] + }, + "PostUpdateUserRequest": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "role": { "type": "string", "description": "none/author/typist" }, + "authorId": { "type": "string" }, + "autoRenew": { "type": "boolean" }, + "notification": { "type": "boolean" }, + "encryption": { "type": "boolean" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "boolean" } + }, + "required": ["id", "role", "autoRenew", "notification"] + }, + "PostUpdateUserResponse": { "type": "object", "properties": {} }, + "AllocateLicenseRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "ユーザーID" }, + "newLicenseId": { + "type": "number", + "description": "割り当てるライセンスのID" + } + }, + "required": ["userId", "newLicenseId"] + }, + "AllocateLicenseResponse": { "type": "object", "properties": {} }, + "DeallocateLicenseRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "ユーザーID" } + }, + "required": ["userId"] + }, + "DeallocateLicenseResponse": { "type": "object", "properties": {} }, + "UpdateAcceptedVersionRequest": { + "type": "object", + "properties": { + "idToken": { "type": "string", "description": "IDトークン" }, + "acceptedEULAVersion": { + "type": "string", + "description": "更新バージョン(EULA)" + }, + "acceptedPrivacyNoticeVersion": { + "type": "string", + "description": "更新バージョン(PrivacyNotice)" + }, + "acceptedDPAVersion": { + "type": "string", + "description": "更新バージョン(DPA)" + } + }, + "required": [ + "idToken", + "acceptedEULAVersion", + "acceptedPrivacyNoticeVersion" + ] + }, + "UpdateAcceptedVersionResponse": { "type": "object", "properties": {} }, + "GetMyUserResponse": { + "type": "object", + "properties": { + "userName": { "type": "string", "description": "ユーザー名" } + }, + "required": ["userName"] + }, + "PostDeleteUserRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "削除対象のユーザーID" } + }, + "required": ["userId"] + }, + "PostDeleteUserResponse": { "type": "object", "properties": {} }, + "MultipleImportUser": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "email": { "type": "string", "description": "メールアドレス" }, + "role": { + "type": "number", + "description": "0(none)/1(author)/2(typist)" + }, + "authorId": { "type": "string" }, + "autoRenew": { "type": "number", "description": "0(false)/1(true)" }, + "notification": { + "type": "number", + "description": "0(false)/1(true)" + }, + "encryption": { "type": "number", "description": "0(false)/1(true)" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "number", "description": "0(false)/1(true)" } + }, + "required": ["name", "email", "role", "autoRenew", "notification"] + }, + "PostMultipleImportsRequest": { + "type": "object", + "properties": { + "filename": { "type": "string", "description": "CSVファイル名" }, + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportUser" } + } + }, + "required": ["filename", "users"] + }, + "PostMultipleImportsResponse": { "type": "object", "properties": {} }, + "MultipleImportErrors": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "line": { "type": "number", "description": "エラー発生行数" }, + "errorCode": { "type": "string", "description": "エラーコード" } + }, + "required": ["name", "line", "errorCode"] + }, + "PostMultipleImportsCompleteRequest": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" }, + "filename": { "type": "string", "description": "CSVファイル名" }, + "requestTime": { + "type": "number", + "description": "一括登録受付時刻(UNIXTIME/ミリ秒)" + }, + "errors": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportErrors" } + } + }, + "required": ["accountId", "filename", "requestTime", "errors"] + }, + "PostMultipleImportsCompleteResponse": { + "type": "object", + "properties": {} + }, + "AudioOptionItem": { + "type": "object", + "properties": { + "optionItemLabel": { + "type": "string", + "minLength": 1, + "maxLength": 16 + }, + "optionItemValue": { + "type": "string", + "minLength": 1, + "maxLength": 20 + } + }, + "required": ["optionItemLabel", "optionItemValue"] + }, + "AudioUploadFinishedRequest": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "アップロード先Blob Storage(ファイル名含む)" + }, + "authorId": { + "type": "string", + "description": "自分自身(ログイン認証)したAuthorID" + }, + "fileName": { "type": "string", "description": "音声ファイル名" }, + "duration": { + "type": "string", + "description": "音声ファイルの録音時間(ミリ秒の整数値)" + }, + "createdDate": { + "type": "string", + "description": "音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss)" + }, + "finishedDate": { + "type": "string", + "description": "音声ファイルの録音作成終了日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "uploadedDate": { + "type": "string", + "description": "音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "fileSize": { + "type": "number", + "description": "音声ファイルのファイルサイズ(Byte)" + }, + "priority": { + "type": "string", + "description": "優先度 \"00\":Normal / \"01\":High" + }, + "audioFormat": { + "type": "string", + "description": "録音形式: DSS/DS2(SP)/DS2(QP)" + }, + "comment": { "type": "string" }, + "workType": { "type": "string" }, + "optionItemList": { + "maxItems": 10, + "minItems": 10, + "description": "音声ファイルに紐づくOption Itemの一覧(10個固定)", + "type": "array", + "items": { "$ref": "#/components/schemas/AudioOptionItem" } + }, + "isEncrypted": { "type": "boolean" } + }, + "required": [ + "url", + "authorId", + "fileName", + "duration", + "createdDate", + "finishedDate", + "uploadedDate", + "fileSize", + "priority", + "audioFormat", + "comment", + "workType", + "optionItemList", + "isEncrypted" + ] + }, + "AudioUploadFinishedResponse": { + "type": "object", + "properties": { + "jobNumber": { "type": "string", "description": "8桁固定の数字" } + }, + "required": ["jobNumber"] + }, + "AudioUploadLocationResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Blob StorageにアクセスするためのSASトークン入りのアクセスURL" + } + }, + "required": ["url"] + }, + "AudioDownloadLocationResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Blob StorageにアクセスするためのSASトークン入りのアクセスURL" + } + }, + "required": ["url"] + }, + "TemplateDownloadLocationResponse": { + "type": "object", + "properties": { "url": { "type": "string" } }, + "required": ["url"] + }, + "TemplateUploadLocationResponse": { + "type": "object", + "properties": { "url": { "type": "string" } }, + "required": ["url"] + }, + "TemplateUploadFinishedRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "テンプレートファイルのファイル名" + }, + "url": { + "type": "string", + "description": "テンプレートファイルのアップロード先URL" + } + }, + "required": ["name", "url"] + }, + "TemplateUploadFinishedReqponse": { "type": "object", "properties": {} }, + "Assignee": { + "type": "object", + "properties": { + "typistUserId": { + "type": "number", + "description": "TypistID(TypistIDかTypistGroupIDのどちらかに値が入る)" + }, + "typistGroupId": { + "type": "number", + "description": "TypistGroupID(TypistGroupIDかTypistIDのどちらかに値が入る)" + }, + "typistName": { + "type": "string", + "description": "Typist名 / TypistGroup名" + } + }, + "required": ["typistName"] + }, + "Task": { + "type": "object", + "properties": { + "audioFileId": { + "type": "number", + "description": "ODMS Cloud上の音声ファイルID" + }, + "authorId": { "type": "string", "description": "AuthorID" }, + "workType": { "type": "string" }, + "optionItemList": { + "maxItems": 10, + "minItems": 10, + "description": "音声ファイルに紐づくOption Itemの一覧(10個固定)", + "type": "array", + "items": { "$ref": "#/components/schemas/AudioOptionItem" } + }, + "url": { + "type": "string", + "description": "音声ファイルのBlob Storage上での保存場所(ファイル名含む)のURL" + }, + "fileName": { "type": "string", "description": "音声ファイル名" }, + "audioDuration": { + "type": "string", + "description": "音声ファイルの録音時間(ミリ秒の整数値)" + }, + "audioCreatedDate": { + "type": "string", + "description": "音声ファイルの録音開始日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "audioFinishedDate": { + "type": "string", + "description": "音声ファイルの録音終了日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "audioUploadedDate": { + "type": "string", + "description": "音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "fileSize": { + "type": "number", + "description": "音声ファイルのファイルサイズ(Byte)" + }, + "priority": { + "type": "string", + "description": "音声ファイルの優先度 \"00\":Normal / \"01\":High" + }, + "audioFormat": { + "type": "string", + "description": "録音形式: DSS/DS2(SP)/DS2(QP)" + }, + "comment": { "type": "string", "description": "コメント" }, + "isEncrypted": { "type": "boolean" }, + "jobNumber": { "type": "string", "description": "JOBナンバー" }, + "typist": { + "description": "割り当てられたユーザー", + "allOf": [{ "$ref": "#/components/schemas/Typist" }] + }, + "assignees": { + "description": "文字起こしに着手できる(チェックアウト可能な)、タスクにアサインされているグループ/個人の一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Assignee" } + }, + "status": { + "type": "string", + "description": "音声ファイルのファイルステータス Uploaded / Pending / InProgress / Finished / Backup" + }, + "transcriptionStartedDate": { + "type": "string", + "description": "文字起こし開始日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "transcriptionFinishedDate": { + "type": "string", + "description": "文字起こし終了日時(yyyy-mm-ddThh:mm:ss.sss)" + } + }, + "required": [ + "audioFileId", + "authorId", + "workType", + "optionItemList", + "url", + "fileName", + "audioDuration", + "audioCreatedDate", + "audioFinishedDate", + "audioUploadedDate", + "fileSize", + "priority", + "audioFormat", + "comment", + "isEncrypted", + "jobNumber", + "assignees", + "status" + ] + }, + "TasksResponse": { + "type": "object", + "properties": { + "limit": { + "type": "number", + "description": "タスクの取得件数(指定しない場合はデフォルト値)" + }, + "offset": { + "type": "number", + "description": "オフセット(何件目から取得するか 設定しない場合はデフォルト値)" + }, + "total": { "type": "number", "description": "タスクの総件数" }, + "tasks": { + "description": "音声ファイル/タスク一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Task" } + } + }, + "required": ["limit", "offset", "total", "tasks"] + }, + "AudioNextResponse": { + "type": "object", + "properties": { + "nextFileId": { + "type": "number", + "description": "ODMS Cloud上の次の音声ファイルID(存在しなければundefind)" + } + } + }, + "ChangeStatusResponse": { "type": "object", "properties": {} }, + "PostCheckoutPermissionRequest": { + "type": "object", + "properties": { + "assignees": { + "description": "文字起こしに着手可能(チェックアウト可能)にしたい、グループ個人の一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Assignee" } + } + }, + "required": ["assignees"] + }, + "PostCheckoutPermissionResponse": { "type": "object", "properties": {} }, + "PostDeleteTaskResponse": { "type": "object", "properties": {} }, + "CreateOrdersRequest": { + "type": "object", + "properties": { + "poNumber": { "type": "string" }, + "orderCount": { "type": "number" } + }, + "required": ["poNumber", "orderCount"] + }, + "CreateOrdersResponse": { "type": "object", "properties": {} }, + "IssueCardLicensesRequest": { + "type": "object", + "properties": { "createCount": { "type": "number" } }, + "required": ["createCount"] + }, + "IssueCardLicensesResponse": { + "type": "object", + "properties": { + "cardLicenseKeys": { "type": "array", "items": { "type": "string" } } + }, + "required": ["cardLicenseKeys"] + }, + "ActivateCardLicensesRequest": { + "type": "object", + "properties": { "cardLicenseKey": { "type": "string" } }, + "required": ["cardLicenseKey"] + }, + "ActivateCardLicensesResponse": { "type": "object", "properties": {} }, + "AllocatableLicenseInfo": { + "type": "object", + "properties": { + "licenseId": { "type": "number" }, + "expiryDate": { "format": "date-time", "type": "string" } + }, + "required": ["licenseId"] + }, + "GetAllocatableLicensesResponse": { + "type": "object", + "properties": { + "allocatableLicenses": { + "type": "array", + "items": { "$ref": "#/components/schemas/AllocatableLicenseInfo" } + } + }, + "required": ["allocatableLicenses"] + }, + "CancelOrderRequest": { + "type": "object", + "properties": { "poNumber": { "type": "string" } }, + "required": ["poNumber"] + }, + "CancelOrderResponse": { "type": "object", "properties": {} }, + "TemplateFile": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "テンプレートファイルのID" }, + "name": { + "type": "string", + "description": "テンプレートファイルのファイル名" + } + }, + "required": ["id", "name"] + }, + "GetTemplatesResponse": { + "type": "object", + "properties": { + "templates": { + "description": "テンプレートファイルの一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/TemplateFile" } + } + }, + "required": ["templates"] + }, + "DeleteTemplateResponse": { "type": "object", "properties": {} }, + "WorkflowWorktype": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "Worktypeの内部ID" }, + "worktypeId": { "type": "string", "description": "WorktypeID" } + }, + "required": ["id", "worktypeId"] + }, + "WorkflowTemplate": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "テンプレートの内部ID" }, + "fileName": { + "type": "string", + "description": "テンプレートのファイル名" + } + }, + "required": ["id", "fileName"] + }, + "Workflow": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "ワークフローの内部ID" }, + "author": { + "description": "Author情報", + "allOf": [{ "$ref": "#/components/schemas/Author" }] + }, + "worktype": { + "description": "Worktype情報", + "allOf": [{ "$ref": "#/components/schemas/WorkflowWorktype" }] + }, + "template": { + "description": "テンプレート情報", + "allOf": [{ "$ref": "#/components/schemas/WorkflowTemplate" }] + }, + "typists": { + "description": "ルーティング候補のタイピストユーザー/タイピストグループ", + "type": "array", + "items": { "$ref": "#/components/schemas/Assignee" } + } + }, + "required": ["id", "author", "typists"] + }, + "GetWorkflowsResponse": { + "type": "object", + "properties": { + "workflows": { + "description": "ワークフローの一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Workflow" } + } + }, + "required": ["workflows"] + }, + "WorkflowTypist": { + "type": "object", + "properties": { + "typistId": { + "type": "number", + "description": "タイピストユーザーの内部ID" + }, + "typistGroupId": { + "type": "number", + "description": "タイピストグループの内部ID" + } + } + }, + "CreateWorkflowsRequest": { + "type": "object", + "properties": { + "authorId": { "type": "number", "description": "Authorの内部ID" }, + "worktypeId": { "type": "number", "description": "Worktypeの内部ID" }, + "templateId": { + "type": "number", + "description": "テンプレートの内部ID" + }, + "typists": { + "description": "ルーティング候補のタイピストユーザー/タイピストグループ", + "minItems": 1, + "type": "array", + "items": { "$ref": "#/components/schemas/WorkflowTypist" } + } + }, + "required": ["authorId", "typists"] + }, + "CreateWorkflowsResponse": { "type": "object", "properties": {} }, + "UpdateWorkflowRequest": { + "type": "object", + "properties": { + "authorId": { "type": "number", "description": "Authorの内部ID" }, + "worktypeId": { "type": "number", "description": "Worktypeの内部ID" }, + "templateId": { + "type": "number", + "description": "テンプレートの内部ID" + }, + "typists": { + "description": "ルーティング候補のタイピストユーザー/タイピストグループ", + "minItems": 1, + "type": "array", + "items": { "$ref": "#/components/schemas/WorkflowTypist" } + } + }, + "required": ["authorId", "typists"] + }, + "UpdateWorkflowResponse": { "type": "object", "properties": {} }, + "DeleteWorkflowResponse": { "type": "object", "properties": {} }, + "RegisterRequest": { + "type": "object", + "properties": { + "pns": { "type": "string", "description": "wns or apns" }, + "handler": { + "type": "string", + "description": "wnsのチャネルURI or apnsのデバイストークン" + } + }, + "required": ["pns", "handler"] + }, + "RegisterResponse": { "type": "object", "properties": {} }, + "TermInfo": { + "type": "object", + "properties": { + "documentType": { "type": "string", "description": "利用規約種別" }, + "version": { "type": "string", "description": "バージョン" } + }, + "required": ["documentType", "version"] + }, + "GetTermsInfoResponse": { + "type": "object", + "properties": { + "termsInfo": { + "type": "array", + "items": { "$ref": "#/components/schemas/TermInfo" } + } + }, + "required": ["termsInfo"] + } + } + } +} diff --git a/dictation_function/src/blobstorage/blobstorage.service.ts b/dictation_function/src/blobstorage/blobstorage.service.ts new file mode 100644 index 0000000..d9b4472 --- /dev/null +++ b/dictation_function/src/blobstorage/blobstorage.service.ts @@ -0,0 +1,184 @@ +import { + BlobServiceClient, + StorageSharedKeyCredential, +} from "@azure/storage-blob"; +import { IMPORT_USERS_CONTAINER_NAME } from "../constants"; +import { InvocationContext } from "@azure/functions"; + +export class BlobstorageService { + private readonly blobServiceClient: BlobServiceClient; + private readonly sharedKeyCredential: StorageSharedKeyCredential; + constructor() { + if ( + !process.env.STORAGE_ACCOUNT_NAME_IMPORT || + !process.env.STORAGE_ACCOUNT_KEY_IMPORT || + !process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT + ) { + throw new Error("Storage account information is missing"); + } + + this.sharedKeyCredential = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_IMPORT, + process.env.STORAGE_ACCOUNT_KEY_IMPORT + ); + this.blobServiceClient = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT, + this.sharedKeyCredential + ); + } + /** + * Lists blobs + * @returns blobs + */ + async listBlobs(context: InvocationContext): Promise { + context.log(`[IN] ${this.listBlobs.name}`); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + + const blobNames: string[] = []; + // stage.json以外のファイルを取得 + for await (const blob of containerClient.listBlobsFlat({ + prefix: "U_", + })) { + blobNames.push(blob.name); + } + + return blobNames; + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.listBlobs.name}`); + } + } + + /** + * Downloads a blob + * @param context + * @param filename + * @returns file buffer + */ + public async downloadFileData( + context: InvocationContext, + filename: string + ): Promise { + context.log( + `[IN] ${this.downloadFileData.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + const blobClient = containerClient.getBlobClient(filename); + try { + const downloadBlockBlobResponse = await blobClient.downloadToBuffer(); + return downloadBlockBlobResponse.toString(); + } catch (error) { + // ファイルが存在しない場合はundefinedを返す + if (error?.statusCode === 404) { + return undefined; + } + throw error; + } + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.downloadFileData.name}`); + } + } + + /** + * Updates file + * @param context + * @param filename + * @param data + * @returns file + */ + public async updateFile( + context: InvocationContext, + filename: string, + data: string + ): Promise { + context.log( + `[IN] ${this.updateFile.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + + const { response } = await containerClient.uploadBlockBlob( + filename, + data, + data.length + ); + if (response.errorCode) { + context.log(`update failed. response errorCode: ${response.errorCode}`); + return false; + } + return true; + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.updateFile.name}`); + } + } + + /** + * Deletes file + * @param context + * @param filename + * @returns file + */ + public async deleteFile( + context: InvocationContext, + filename: string + ): Promise { + context.log( + `[IN] ${this.deleteFile.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + const blobClient = containerClient.getBlobClient(filename); + await blobClient.deleteIfExists(); + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.deleteFile.name}`); + } + } + + /** + * Determines whether file exists is + * @param context + * @param filename + * @returns file exists + */ + public async isFileExists( + context: InvocationContext, + filename: string + ): Promise { + context.log( + `[IN] ${this.isFileExists.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + const blobClient = containerClient.getBlobClient(filename); + return await blobClient.exists(); + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.isFileExists.name}`); + } + } +} diff --git a/dictation_function/src/blobstorage/types/guards.ts b/dictation_function/src/blobstorage/types/guards.ts new file mode 100644 index 0000000..b16a241 --- /dev/null +++ b/dictation_function/src/blobstorage/types/guards.ts @@ -0,0 +1,155 @@ +import { IMPORT_USERS_STAGES } from "../../constants"; +import { ErrorRow, ImportData, ImportJson, StageJson } from "./types"; + +const isErrorRow = (obj: any): obj is ErrorRow => { + if (typeof obj !== "object") return false; + const errorRow = obj as ErrorRow; + if (errorRow.name === undefined || typeof errorRow.name !== "string") { + return false; + } + if (errorRow.row === undefined || typeof errorRow.row !== "number") { + return false; + } + if (errorRow.error === undefined || typeof errorRow.error !== "string") { + return false; + } + return true; +}; + +export const isStageJson = (obj: any): obj is StageJson => { + if (typeof obj !== "object") return false; + const stageJson = obj as StageJson; + if ( + stageJson.filename !== undefined && + typeof stageJson.filename !== "string" + ) { + return false; + } + if (stageJson.update === undefined || typeof stageJson.update !== "number") { + return false; + } + if (stageJson.row !== undefined && typeof stageJson.row !== "number") { + return false; + } + + if ( + stageJson.errors !== undefined && + (!Array.isArray(stageJson.errors) || + !stageJson.errors.every((x) => isErrorRow(x))) + ) { + return false; + } + if ( + stageJson.state === undefined || + !Object.values(IMPORT_USERS_STAGES).includes(stageJson.state) + ) { + return false; + } + return true; +}; + +const isImportData = (obj: any): obj is ImportData => { + if (typeof obj !== "object") return false; + const importData = obj as ImportData; + if (importData.name === undefined || typeof importData.name !== "string") { + return false; + } + if (importData.email === undefined || typeof importData.email !== "string") { + return false; + } + if (importData.role === undefined || typeof importData.role !== "number") { + return false; + } + if ( + importData.author_id !== undefined && + typeof importData.author_id !== "string" + ) { + return false; + } + if ( + importData.auto_renew === undefined || + typeof importData.auto_renew !== "number" + ) { + return false; + } + if ( + importData.notification === undefined || + typeof importData.notification !== "number" + ) { + return false; + } + if ( + importData.encryption !== undefined && + typeof importData.encryption !== "number" + ) { + return false; + } + if ( + importData.encryption_password !== undefined && + typeof importData.encryption_password !== "string" + ) { + return false; + } + if ( + importData.prompt !== undefined && + typeof importData.prompt !== "number" + ) { + return false; + } + return true; +}; + +export const isImportJson = (obj: any): obj is ImportJson => { + if (typeof obj !== "object") return false; + const importJson = obj as ImportJson; + if ( + importJson.account_id === undefined || + typeof importJson.account_id !== "number" + ) { + return false; + } + if ( + importJson.user_id === undefined || + typeof importJson.user_id !== "number" + ) { + return false; + } + if ( + importJson.user_role === undefined || + typeof importJson.user_role !== "string" + ) { + return false; + } + if ( + importJson.external_id === undefined || + typeof importJson.external_id !== "string" + ) { + return false; + } + if ( + importJson.delegation_account_id !== undefined && + typeof importJson.delegation_account_id !== "number" + ) { + return false; + } + if ( + importJson.delegation_user_id !== undefined && + typeof importJson.delegation_user_id !== "number" + ) { + return false; + } + if ( + importJson.file_name === undefined || + typeof importJson.file_name !== "string" + ) { + return false; + } + if ( + importJson.data === undefined || + !Array.isArray(importJson.data) || + !importJson.data.every((x) => isImportData(x)) + ) { + return false; + } + return true; +}; diff --git a/dictation_function/src/blobstorage/types/types.ts b/dictation_function/src/blobstorage/types/types.ts new file mode 100644 index 0000000..fec4ed4 --- /dev/null +++ b/dictation_function/src/blobstorage/types/types.ts @@ -0,0 +1,56 @@ +import { IMPORT_USERS_STAGES, USER_ROLES } from "../../constants"; + +export type StageType = + (typeof IMPORT_USERS_STAGES)[keyof typeof IMPORT_USERS_STAGES]; + +export type StageJson = { + filename?: string | undefined; + update: number; + row?: number | undefined; + errors?: ErrorRow[] | undefined; + state: StageType; +}; + +export type ErrorRow = { + name: string; + row: number; + error: string; +}; + +export type ImportJson = { + account_id: number; + user_id: number; + user_role: RoleType; + external_id: string; + delegation_account_id?: number | undefined; + delegation_user_id?: number | undefined; + file_name: string; + date: number; + data: ImportData[]; +}; + +export type ImportData = { + name: string; + email: string; + role: number; + author_id?: string | undefined; + auto_renew: number; + notification: number; + encryption?: number | undefined; + encryption_password?: string | undefined; + prompt?: number | undefined; +}; + +export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES]; + +export type User = { + name: string; + role: RoleType; + email: string; + autoRenew: boolean; + notification: boolean; + authorId?: string | undefined; + encryption?: boolean | undefined; + encryptionPassword?: string | undefined; + prompt?: boolean | undefined; +}; diff --git a/dictation_function/src/common/errors/code.ts b/dictation_function/src/common/errors/code.ts new file mode 100644 index 0000000..25366a9 --- /dev/null +++ b/dictation_function/src/common/errors/code.ts @@ -0,0 +1,79 @@ +/* +エラーコード作成方針 +E+6桁(数字)で構成する。 +- 1~2桁目の値は種類(業務エラー、システムエラー...) +- 3~4桁目の値は原因箇所(トークン、DB、...) +- 5~6桁目の値は任意の重複しない値 +ex) +E00XXXX : システムエラー(通信エラーやDB接続失敗など) +E01XXXX : 業務エラー +EXX00XX : 内部エラー(内部プログラムのエラー) +EXX01XX : トークンエラー(トークン認証関連) +EXX02XX : DBエラー(DB関連) +EXX03XX : ADB2Cエラー(DB関連) +*/ +export const errorCodes = [ + "E009999", // 汎用エラー + "E000101", // トークン形式不正エラー + "E000102", // トークン有効期限切れエラー + "E000103", // トークン非アクティブエラー + "E000104", // トークン署名エラー + "E000105", // トークン発行元エラー + "E000106", // トークンアルゴリズムエラー + "E000107", // トークン不足エラー + "E000108", // トークン権限エラー + "E000301", // ADB2Cへのリクエスト上限超過エラー + "E010001", // パラメータ形式不正エラー + "E010201", // 未認証ユーザエラー + "E010202", // 認証済ユーザエラー + "E010203", // 管理ユーザ権限エラー + "E010204", // ユーザ不在エラー + "E010205", // DBのRoleが想定外の値エラー + "E010206", // DBのTierが想定外の値エラー + "E010207", // ユーザーのRole変更不可エラー + "E010208", // ユーザーの暗号化パスワード不足エラー + "E010209", // ユーザーの同意済み利用規約バージョンが最新でないエラー + "E010301", // メールアドレス登録済みエラー + "E010302", // authorId重複エラー + "E010401", // PONumber重複エラー + "E010501", // アカウント不在エラー + "E010502", // アカウント情報変更不可エラー + "E010503", // 代行操作不許可エラー + "E010601", // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) + "E010602", // タスク変更権限不足エラー + "E010603", // タスク不在エラー + "E010701", // Blobファイル不在エラー + "E010801", // ライセンス不在エラー + "E010802", // ライセンス取り込み済みエラー + "E010803", // ライセンス発行済みエラー + "E010804", // ライセンス数不足エラー + "E010805", // ライセンス有効期限切れエラー + "E010806", // ライセンス割り当て不可エラー + "E010807", // ライセンス割り当て解除不可エラー + "E010808", // ライセンス注文キャンセル不可エラー + "E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) + "E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) + "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) + "E010908", // タイピストグループ不在エラー + "E010909", // タイピストグループ名重複エラー + "E011001", // ワークタイプ重複エラー + "E011002", // ワークタイプ登録上限超過エラー + "E011003", // ワークタイプ不在エラー + "E011004", // ワークタイプ使用中エラー + "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー + "E013002", // ワークフロー不在エラー + "E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった) + "E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった) + "E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた) + "E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた) + "E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた) + "E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている) + "E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) + "E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている) + "E015001", // タイピストグループ削除済みエラー + "E015002", // タイピストグループがワークフローに紐づいているエラー + "E015003", // タイピストグループがルーティングされているエラー + "E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) + "E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) + "E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) +] as const; diff --git a/dictation_function/src/common/errors/index.ts b/dictation_function/src/common/errors/index.ts new file mode 100644 index 0000000..1543e4c --- /dev/null +++ b/dictation_function/src/common/errors/index.ts @@ -0,0 +1,3 @@ +export * from "./code"; +export * from "./types"; +export * from "./utils"; diff --git a/dictation_function/src/common/errors/types.ts b/dictation_function/src/common/errors/types.ts new file mode 100644 index 0000000..8cc801e --- /dev/null +++ b/dictation_function/src/common/errors/types.ts @@ -0,0 +1,9 @@ +import { errorCodes } from "./code"; + +export type ErrorObject = { + message: string; + code: ErrorCodeType; + statusCode?: number; +}; + +export type ErrorCodeType = typeof errorCodes[number]; diff --git a/dictation_function/src/common/errors/utils.ts b/dictation_function/src/common/errors/utils.ts new file mode 100644 index 0000000..3dd2410 --- /dev/null +++ b/dictation_function/src/common/errors/utils.ts @@ -0,0 +1,101 @@ +import { AxiosError } from "axios"; +import { isError } from "lodash"; +import { ErrorResponse } from "../../api"; +import { errorCodes } from "./code"; +import { ErrorCodeType, ErrorObject } from "./types"; + +export const createErrorObject = (error: unknown): ErrorObject => { + // 最低限通常のエラーかを判定 + // Error以外のものがthrowされた場合 + // 基本的にないはずだがプログラム上あるので拾う + if (!isError(error)) { + return { + message: "not error type.", + code: "E009999", + }; + } + + // Axiosエラー 通信してのエラーであるかを判定 + if (!isAxiosError(error)) { + return { + message: "not axios error.", + code: "E009999", + }; + } + + const errorResponse = error.response; + if (!errorResponse) { + return { + message: error.message, + code: "E009999", + statusCode: errorResponse, + }; + } + + const { data } = errorResponse; + + // 想定しているエラーレスポンスの型か判定 + if (!isErrorResponse(data)) { + return { + message: error.message, + code: "E009999", + statusCode: errorResponse.status, + }; + } + + const { message, code } = data; + + // 想定しているエラーコードかを判定 + if (!isErrorCode(code)) { + return { + message, + code: "E009999", + statusCode: errorResponse.status, + }; + } + + return { + message, + code, + statusCode: errorResponse.status, + }; +}; + +const isAxiosError = (e: unknown): e is AxiosError => { + const error = e as AxiosError; + return error?.isAxiosError ?? false; +}; + +const isErrorResponse = (error: unknown): error is ErrorResponse => { + const errorResponse = error as ErrorResponse; + if ( + errorResponse === undefined || + errorResponse.message === undefined || + errorResponse.code === undefined + ) { + return false; + } + + return true; +}; + +const isErrorCode = (errorCode: string): errorCode is ErrorCodeType => + errorCodes.includes(errorCode as ErrorCodeType); + +export const isErrorObject = ( + data: unknown +): data is { error: ErrorObject } => { + if ( + data && + typeof data === "object" && + "error" in data && + typeof (data as { error: ErrorObject }).error === "object" && + typeof (data as { error: ErrorObject }).error.message === "string" && + typeof (data as { error: ErrorObject }).error.code === "string" && + (typeof (data as { error: ErrorObject }).error.statusCode === "number" || + (data as { error: ErrorObject }).error.statusCode === undefined) + ) { + return true; + } + return false; +}; diff --git a/dictation_function/src/common/jwt/index.ts b/dictation_function/src/common/jwt/index.ts new file mode 100644 index 0000000..60cc8f7 --- /dev/null +++ b/dictation_function/src/common/jwt/index.ts @@ -0,0 +1,3 @@ +import { isVerifyError, sign, verify, decode, getJwtKey } from "./jwt"; + +export { isVerifyError, sign, verify, decode, getJwtKey }; diff --git a/dictation_function/src/common/jwt/jwt.spec.ts b/dictation_function/src/common/jwt/jwt.spec.ts new file mode 100644 index 0000000..b58d91c --- /dev/null +++ b/dictation_function/src/common/jwt/jwt.spec.ts @@ -0,0 +1,250 @@ +import { sign, verify, isVerifyError } from "./jwt"; +import base64url from "base64url"; + +test("success sign and verify", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const payload = verify<{ value: "testvalue" }>(token, publicKey); + if (isVerifyError(payload)) { + throw new Error(`${payload.reason} | ${payload.message}`); + } + + expect(payload.value).toBe("testvalue"); +}); + +test("failed sign and verify (jwt expired)", () => { + // 有効期限を0秒にすることで、検証を行った時点で有効期限切れにする + const token = sign({ value: "testvalue" }, 0, privateKey); + const payload = verify<{ value: "testvalue" }>(token, publicKey); + if (!isVerifyError(payload)) { + throw new Error(JSON.stringify(payload)); + } + expect(payload.reason).toBe("ExpiredError"); +}); + +test("failed sign and verify (invalid key pair)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + // 秘密鍵と対ではない公開鍵を使用して検証する + const payload = verify<{ value: "testvalue" }>(token, anotherPublicKey); + if (!isVerifyError(payload)) { + throw new Error(JSON.stringify(payload)); + } + expect(payload.reason).toBe("InvalidToken"); + expect(payload.message).toBe("invalid signature"); +}); + +test("failed sign and verify (invalid public key)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + // 公開鍵の形式になっていない文字列を使用して検証する + const payload = verify<{ value: "testvalue" }>(token, fakePublicKey); + if (!isVerifyError(payload)) { + throw new Error(JSON.stringify(payload)); + } + expect(payload.reason).toBe("InvalidToken"); + expect(payload.message).toBe( + "secretOrPublicKey must be an asymmetric key when using RS256" + ); +}); + +test("failed sign (invalid private key)", () => { + expect(() => { + // 不正な秘密鍵で署名しようとする場合はエラーがthrowされる + sign({ value: "testvalue" }, 5 * 60, fakePrivateKey); + }).toThrowError(); +}); + +test("success rewrite-token verify (as is)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const { header, payload, verifySignature } = splitToken(token); + + { + // 何も操作せずに構築しなおした場合、成功する + const validToken = rebuildToken(header, payload, verifySignature); + + const value = verify<{ value: string }>(validToken, publicKey); + if (isVerifyError(value)) { + throw new Error(`${value.reason} | ${value.message}`); + } + + expect(value.value).toBe("testvalue"); + } +}); + +test("failed rewrite-token verify (override algorithm)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const { payload, verifySignature } = splitToken(token); + + { + // 検証アルゴリズムを「検証なし」に書き換える + const headerObject = { alg: "none" }; + const payloadObject = JSON.parse(payload) as { + value: string; + iat: number; + exp: number; + }; + + // 内容を操作して構築しなおした場合、失敗する + const customToken = rebuildToken( + JSON.stringify(headerObject), + JSON.stringify(payloadObject), + verifySignature + ); + + const value = verify<{ value: string }>(customToken, publicKey); + if (!isVerifyError(value)) { + throw new Error(JSON.stringify(payload)); + } + expect(value.reason).toBe("InvalidToken"); + expect(value.message).toBe("invalid algorithm"); + } +}); + +test("failed rewrite-token verify (override expire)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const { header, payload, verifySignature } = splitToken(token); + + { + // expの値を操作する + const payloadObject = JSON.parse(payload) as { + value: string; + iat: number; + exp: number; + }; + payloadObject.exp = payloadObject.exp + 100000; + + // 内容を操作して構築しなおした場合、失敗する + const customToken = rebuildToken( + header, + JSON.stringify(payloadObject), + verifySignature + ); + + const value = verify<{ value: string }>(customToken, publicKey); + if (!isVerifyError(value)) { + throw new Error(JSON.stringify(payload)); + } + expect(value.reason).toBe("InvalidToken"); + expect(value.message).toBe("invalid signature"); + } +}); + +// JWT改竄テスト用ユーティリティ +const splitToken = ( + token: string +): { header: string; payload: string; verifySignature: string } => { + const splited = token.split("."); + + const header = base64url.decode(splited[0]); + const payload = base64url.decode(splited[1]); + const verifySignature = splited[2]; + return { header, payload, verifySignature }; +}; + +// JWT改竄テスト用ユーティリティ +const rebuildToken = ( + header: string, + payload: string, + verifySignature: string +): string => { + const rebuild_header = base64url.encode(header); + const rebuild_payload = base64url.encode(payload); + return `${rebuild_header}.${rebuild_payload}.${verifySignature}`; +}; + +// テスト用に生成した秘密鍵 +const privateKey = [ + "-----BEGIN RSA PRIVATE KEY-----", + "MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F", + "GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a", + "fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa", + "FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq", + "zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF", + "+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN", + "LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq", + "L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0", + "WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE", + "x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm", + "+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX", + "cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc", + "BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H", + "IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+", + "y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb", + "iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9", + "BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95", + "mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/", + "O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s", + "xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust", + "nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT", + "tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK", + "YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM", + "iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk", + "JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==", + "-----END RSA PRIVATE KEY-----", +].join("\n"); + +// テスト用に生成した公開鍵 +const publicKey = [ + "-----BEGIN PUBLIC KEY-----", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTVLNpW0/FzVCU7qo1DD", + "jOkYWx6s/jE56YOOc3UzaaG/zb1FGyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23", + "tL7p3PS0ZCsLeggcyLctbJAzLy/afF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc5", + "7Tli2x8NiOZr5dafs3vMuIIKNsBaFAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxD", + "alw5DCd0mmdY0vrbRsgkbej0ZzzqzukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn2", + "1AIcn8P3rKZmDyPH+9KNfWC8+ubF+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3a", + "IQIDAQAB", + "-----END PUBLIC KEY-----", +].join("\n"); + +// テスト用に作成した、違う秘密鍵から生成した公開鍵 +const anotherPublicKey = [ + "-----BEGIN PUBLIC KEY-----", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3", + "2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig", + "n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9", + "RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk", + "EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x", + "2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81", + "KQIDAQAB", + "-----END PUBLIC KEY-----", +].join("\n"); + +// 秘密鍵のように見えるが想定する形式と違う +const fakePrivateKey = [ + "-----BAGIN RSA PRIVATE KEY-----", + "MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F", + "GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a", + "fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa", + "FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq", + "zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF", + "+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN", + "LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq", + "L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0", + "WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE", + "x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm", + "+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX", + "cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc", + "BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H", + "IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+", + "y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb", + "iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9", + "BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95", + "mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/", + "O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s", + "xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust", + "nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT", + "tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK", + "YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM", + "iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk", + "JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==", + "-----END RSA PRIVATE KEY-----", +].join("\n"); + +// 公開鍵のように見えるが想定する形式と違う +const fakePublicKey = [ + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3", + "2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig", + "n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9", + "RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk", + "EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x", + "2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81", + "KQIDAQAB", +].join("\n"); diff --git a/dictation_function/src/common/jwt/jwt.ts b/dictation_function/src/common/jwt/jwt.ts new file mode 100644 index 0000000..faf9f85 --- /dev/null +++ b/dictation_function/src/common/jwt/jwt.ts @@ -0,0 +1,130 @@ +import * as jwt from "jsonwebtoken"; +// XXX: decodeがうまく使えないことがあるので応急対応 バージョン9以降だとなる? +import { decode as jwtDecode } from "jsonwebtoken"; + +export type VerifyError = { + reason: "ExpiredError" | "InvalidToken" | "InvalidTimeStamp" | "Unknown"; + message: string; +}; + +export const isVerifyError = (arg: unknown): arg is VerifyError => { + const value = arg as VerifyError; + if (value.message === undefined) { + return false; + } + + if (value.reason === undefined) { + return false; + } + switch (value.reason) { + case "ExpiredError": + case "InvalidTimeStamp": + case "InvalidToken": + case "Unknown": + return true; + default: + return false; + } +}; + +/** + * Payloadと秘密鍵を使用して署名されたJWTを生成します + * @param {T} payload payloadの型 + * @param {number} expirationSeconds トークンの有効期限(秒) + * @param {string} privateKey 署名に使用する秘密鍵 + * @return {string} 署名済みトークン + * @throws {Error} 秘密鍵の形式が間違っている等の理由が格納されたErrorオブジェクト + */ +export const sign = ( + payload: T, + expirationSeconds: number, + privateKey: string +): string => { + try { + const token = jwt.sign(payload, privateKey, { + expiresIn: expirationSeconds, + algorithm: "RS256", + }); + return token; + } catch (e) { + throw e; + } +}; + +/** + * tokenと公開鍵を使用して検証済みJWTのpayloadを取得します + * @param {string} token JWT + * @param {string} publicKey 検証に使用する公開鍵 + * @return {T | VerifyError} Payload または 検証エラーの内容を表すオブジェクト + */ +export const verify = ( + token: string, + publicKey: string +): T | VerifyError => { + try { + const payload = jwt.verify(token, publicKey, { + algorithms: ["RS256"], + }) as T; + return payload; + } catch (e) { + if (e instanceof jwt.TokenExpiredError) { + return { + reason: "ExpiredError", + message: e.message, + }; + } else if (e instanceof jwt.NotBeforeError) { + return { + reason: "InvalidTimeStamp", + message: e.message, + }; + } else if (e instanceof jwt.JsonWebTokenError) { + return { + reason: "InvalidToken", + message: e.message, + }; + } else { + return { + reason: "Unknown", + message: e.message, + }; + } + } +}; + +/** + * tokenから未検証のJWTのpayloadを取得します + * @param {string} token JWT + * @return {T | VerifyError} Payload または デコードエラーの内容を表すオブジェクト + */ +export const decode = (token: string): T | VerifyError => { + try { + const payload = jwtDecode(token, { + json: true, + }) as T; + return payload; + } catch (e) { + if (e instanceof jwt.TokenExpiredError) { + return { + reason: "ExpiredError", + message: e.message, + }; + } else if (e instanceof jwt.NotBeforeError) { + return { + reason: "InvalidTimeStamp", + message: e.message, + }; + } else if (e instanceof jwt.JsonWebTokenError) { + return { + reason: "InvalidToken", + message: e.message, + }; + } else { + return { + reason: "Unknown", + message: e.message, + }; + } + } +}; + +export const getJwtKey = (key: string): string => key.replace(/\\n/g, "\n"); diff --git a/dictation_function/src/common/jwt/types.ts b/dictation_function/src/common/jwt/types.ts new file mode 100644 index 0000000..7ae28d2 --- /dev/null +++ b/dictation_function/src/common/jwt/types.ts @@ -0,0 +1,32 @@ +export type AccessToken = { + /** + * 外部認証サービスの識別子(代行者) + */ + delegateUserId?: string | undefined; + /** + * 外部認証サービスの識別子 + */ + userId: string; + /** + * 半角スペース区切りのRoleを表現する文字列(ex. "author admin") + */ + role: string; + /** + * アカウントの階層情報(1~5までの半角数字) + */ + tier: number; +}; + +// システムの内部で発行し、外部に公開しないトークン +// システム間通信用(例: Azure Functions→AppService)に使用する +export type SystemAccessToken = { + /** + * トークンの発行者名(ログ記録用) + */ + systemName: string; + + /** + * 付加情報を 文字情報として格納できる + */ + context?: string; +}; diff --git a/dictation_function/src/constants/index.ts b/dictation_function/src/constants/index.ts index f1e0b64..88e3e3b 100644 --- a/dictation_function/src/constants/index.ts +++ b/dictation_function/src/constants/index.ts @@ -293,3 +293,45 @@ export const HTTP_STATUS_CODES = { BAD_REQUEST: 400, INTERNAL_SERVER_ERROR: 500, }; + +/** + * ユーザー一括登録用のBlobコンテナ名 + * @const {string} + */ +export const IMPORT_USERS_CONTAINER_NAME = "import-users"; + +/** + * ユーザー一括登録の最大処理時間(分) + * @const {number} + */ +export const IMPORT_USERS_MAX_DURATION_MINUTES = 30; + +/** + * ユーザー一括登録のステージ管理ファイル名 + * @const {string} + */ +export const IMPORT_USERS_STAGE_FILE_NAME = "stage.json"; + +/** + * ユーザー一括登録のステージ管理のステージ + * @const {string} + */ +export const IMPORT_USERS_STAGES = { + CREATED: "created", + PRAPARE: "prepare", + START: "start", + COMPLETE: "complete", + DONE: "done", +} as const; + +/** + * ユーザーのロールと数値のマッピング + * @const {string} + */ +export const RoleNumberMap: Record = { + 1: USER_ROLES.NONE, + 2: USER_ROLES.AUTHOR, + 3: USER_ROLES.TYPIST, +} as const; + +export const SYSTEM_IMPORT_USERS = "import-users"; diff --git a/dictation_function/src/functions/importUsers.ts b/dictation_function/src/functions/importUsers.ts new file mode 100644 index 0000000..6294c8b --- /dev/null +++ b/dictation_function/src/functions/importUsers.ts @@ -0,0 +1,510 @@ +import { app, InvocationContext, Timer } from "@azure/functions"; +import * as dotenv from "dotenv"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { + ADMIN_ROLES, + IMPORT_USERS_MAX_DURATION_MINUTES, + IMPORT_USERS_STAGE_FILE_NAME, + IMPORT_USERS_STAGES, + RoleNumberMap, + SYSTEM_IMPORT_USERS, + TIERS, +} from "../constants"; +import { ErrorRow, ImportData } from "../blobstorage/types/types"; +import { Configuration, UsersApi } from "../api"; +import { createErrorObject } from "../common/errors/utils"; +import { sign, getJwtKey } from "../common/jwt"; +import { AccessToken, SystemAccessToken } from "../common/jwt/types"; +import { isImportJson, isStageJson } from "../blobstorage/types/guards"; + +export async function importUsersProcessing( + context: InvocationContext, + blobstorageService: BlobstorageService, + userApi: UsersApi +): Promise { + context.log(`[IN] importUsersProcessing`); + try { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + + const startUnixTime = getCurrentUnixTime(); + context.log(`importUsersProcessing start: ${startUnixTime}`); + + // ファイルが存在する間ループ + while (true) { + // Blobストレージからファイル名の一覧を取得(stage.json以外) + const bloblist = await blobstorageService.listBlobs(context); + context.log(bloblist); + + // stage.json以外のファイルが存在しない場合は処理中断 + if (bloblist.length === 0) { + break; + } + + // ファイルのうち、日付が最も古いファイルを取得 + let targetFileName = bloblist.sort().at(0); + if (targetFileName === undefined) { + throw new Error("targetFileName is undefined"); + } + let row = 1; + + // stage.jsonを取得(ダウンロード)して読み込む + let stageData = await blobstorageService.downloadFileData( + context, + IMPORT_USERS_STAGE_FILE_NAME + ); + + // stage.jsonが存在しない場合は、新規作成する + if (stageData === undefined) { + stageData = JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.CREATED, + }); + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + stageData + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.CREATED} filename: ${targetFileName}` + ); + } + } + + const stage = JSON.parse(stageData); + + if (!isStageJson(stage)) { + throw new Error("stage.json is invalid"); + } + + // 作業中のstage.jsonが存在する場合は、処理を再開する + if ( + stage.state !== IMPORT_USERS_STAGES.CREATED && + stage.state !== IMPORT_USERS_STAGES.DONE + ) { + // stage.jsonが存在し、内部状態が処理中で、最終更新日時が10分以上前だった場合は処理中断とみなして途中から再開 + const nowUnixTime = getCurrentUnixTime(); + if (nowUnixTime - stage.update > 10 * 60) { + // stage.jsonの内容から処理対象のfilepathを特定する + context.log(stage.filename); + if (stage.filename === undefined) { + context.log("stage.filename is undefined"); + break; + } + targetFileName = stage.filename; + // 処理開始行をstage.jsonを元に復元する + row = stage.row ?? 1; + } else { + // 内部状態が処理中であれば処理中断(処理が終わる前にTimerから再度起動されてしまったケース) + context.log("stage is processing"); + break; + } + } + + { + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.PRAPARE, + filename: targetFileName, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.PRAPARE} filename: ${targetFileName}` + ); + } + } + + // 対象ファイルをダウンロードして読み込む + const importsData = await blobstorageService.downloadFileData( + context, + targetFileName + ); + + // 一括登録ユーザー一覧をメモリ上に展開 + const imports = + importsData === undefined ? undefined : JSON.parse(importsData); + if (!isImportJson(imports)) { + throw new Error(`json: ${targetFileName} is invalid`); + } + + if (imports === undefined) { + break; + } + + // 代行操作トークンを発行する + const accsessToken = await generateDelegationAccessToken( + context, + imports.external_id, + imports.user_role + ); + + // 一括登録ユーザー一覧をループして、一括登録ユーザーを一括登録する + const errors: ErrorRow[] = []; + for (const user of imports.data) { + { + // stage.jsonを更新(ユーザー追加開始) + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.START, + filename: targetFileName, + row: row, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.START} filename: ${targetFileName} row: ${row}` + ); + } + } + + try { + if (!checkUser(context, user, targetFileName, row)) { + throw new Error( + `Invalid user data. filename: ${targetFileName} row: ${row}` + ); + } + + // ユーザーを追加する + await addUser(context, userApi, user, accsessToken); + } catch (e) { + const error = createErrorObject(e); + context.log(error); + // エラーが発生したらエラーコードを控えておく + errors.push({ row: row, error: error.code, name: user.name }); + } + + { + // stage.jsonを更新(ユーザー追加完了) + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.COMPLETE, + filename: targetFileName, + row: row, + errors: errors, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.COMPLETE} filename: ${targetFileName} row: ${row}` + ); + } + } + row++; + + // 500ms待機 + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + // 処理対象のユーザー一覧ファイルを削除する + await blobstorageService.deleteFile(context, targetFileName); + + // システムトークンを発行 + const systemToken = await generateSystemToken(context); + + // 一括登録完了メールを送信する(ODMS Cloudの一括追加完了APIを呼び出す) + await userApi.multipleImportsComplate( + { + accountId: imports.account_id, + filename: imports.file_name, + requestTime: getCurrentUnixTime(), + errors: errors.map((error) => { + return { + name: error.name, + line: error.row, + errorCode: error.error, + }; + }), + }, + { + headers: { authorization: `Bearer ${systemToken}` }, + } + ); + + { + // stage.jsonを更新(処理完了) + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.DONE, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.DONE} filename: ${targetFileName}` + ); + } + } + + // 経過時間を確認して、30分以上経過していたら処理を中断する + { + const currentUnixTime = getCurrentUnixTime(); + // 時間の差分を計算(秒) + const elapsedSec = currentUnixTime - startUnixTime; + + // 30分以上経過していたら処理を中断する + if (elapsedSec > IMPORT_USERS_MAX_DURATION_MINUTES * 60) { + context.log("timeout"); + break; + } + } + } + } catch (e) { + context.log("importUsers failed."); + context.error(e); + throw e; + } finally { + context.log(`[OUT] importUsersProcessing`); + } +} + +export async function importUsers( + myTimer: Timer, + context: InvocationContext +): Promise { + context.log(`[IN] importUsers`); + try { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + + const blobstorageService = new BlobstorageService(); + const userApi = new UsersApi( + new Configuration({ + basePath: process.env.BASE_PATH, + }) + ); + + await importUsersProcessing(context, blobstorageService, userApi); + } catch (e) { + context.log("importUsers failed."); + context.error(e); + throw e; + } finally { + context.log(`[OUT] importUsers`); + } +} + +/** + * ODMS CloudのAPIを呼び出してユーザーを追加する + * @param context + * @param user + * @returns user + */ +export async function addUser( + context: InvocationContext, + userApi: UsersApi, + user: ImportData, + token: string +): Promise { + context.log(`[IN] addUser`); + try { + await userApi.signup( + { + email: user.email, + name: user.name, + role: RoleNumberMap[user.role], + autoRenew: user.auto_renew === 1, + notification: user.notification === 1, + authorId: user.author_id, + encryption: user.encryption === 1, + encryptionPassword: user.encryption_password, + prompt: user.prompt === 1, + }, + { + headers: { authorization: `Bearer ${token}` }, + } + ); + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] addUser`); + } +} +/** + * ユーザーのデータが正しいかどうかをチェック + * @param context + * @param user + * @param fileName + * @param row + * @returns true if user + */ +function checkUser( + context: InvocationContext, + user: ImportData, + fileName: string, + row: number +): boolean { + context.log( + `[IN] checkUser | params: { fileName: ${fileName}, row: ${row} }` + ); + try { + // 名前が255文字以内であること + if (user.name.length > 255) { + context.log(`name is too long. fileName: ${fileName}, row: ${row}`); + return false; + } + const emailPattern = + /^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/; + // メールアドレスが255文字以内であること + if (user.email.length > 255) { + context.log(`email is too long. fileName: ${fileName}, row: ${row}`); + return false; + } + if (!emailPattern.test(user.email)) { + context.log(`Invalid email. fileName: ${fileName}, row: ${row}`); + return false; + } + // ロールが(0/1/2)のいずれかであること + if (![0, 1, 2].includes(user.role)) { + context.log(`Invalid role number. fileName: ${fileName}, row: ${row}`); + return false; + } + // ロールがAuthorの場合 + if (user.role === 1) { + // author_idが必須 + if (user.author_id === undefined) { + context.log( + `author_id is required. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // author_idが16文字以内であること + if (user.author_id.length > 16) { + context.log( + `author_id is too long. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // author_idが半角大文字英数字とハイフンであること + if (!/^[A-Z0-9_]*$/.test(user.author_id)) { + context.log(`author_id is invalid. fileName: ${fileName}, row: ${row}`); + return false; + } + // encryptionが必須 + if (user.encryption === undefined) { + context.log( + `encryption is required. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // encryptionが1の場合 + if (user.encryption === 1) { + // encryption_passwordが必須 + if (user.encryption_password === undefined) { + context.log( + `encryption_password is required. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // 4~16文字の半角英数字と記号のみであること + if (!/^[!-~]{4,16}$/.test(user.encryption_password)) { + context.log( + `encryption_password is invalid. fileName: ${fileName}, row: ${row}` + ); + return false; + } + if (user.prompt === undefined) { + context.log(`prompt is required. fileName: ${fileName}, row: ${row}`); + return false; + } + } + } + + return true; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] checkUser`); + } +} + +/** + * 代行操作用のアクセストークンを生成します + * @param context + * @param externalId + * @returns delegation token + */ +async function generateDelegationAccessToken( + context: InvocationContext, + externalId: string, + role: string +): Promise { + context.log( + `[IN] generateDelegationAccessToken | params: { externalId: ${externalId} }` + ); + try { + // 要求されたトークンの寿命を決定 + const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB); + const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? ""); + + const token = sign( + { + role: `${role} ${ADMIN_ROLES.ADMIN}`, + tier: TIERS.TIER5, + userId: externalId, + delegateUserId: SYSTEM_IMPORT_USERS, + }, + tokenLifetime, + privateKey + ); + + return token; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] generateDelegationAccessToken`); + } +} +/** + * System用のアクセストークンを生成します + * @param context + * @returns system token + */ +async function generateSystemToken( + context: InvocationContext +): Promise { + context.log(`[IN] generateSystemToken`); + try { + // 要求されたトークンの寿命を決定 + const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB); + const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? ""); + + const token = sign( + { + systemName: SYSTEM_IMPORT_USERS, + }, + tokenLifetime, + privateKey + ); + + return token; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] generateSystemToken`); + } +} + +const getCurrentUnixTime = () => Math.floor(new Date().getTime() / 1000); + +// 5分毎に実行 +app.timer("importUsers", { + schedule: "0 */5 * * * *", + handler: importUsers, +}); diff --git a/dictation_function/src/test/importUsers.spec.ts b/dictation_function/src/test/importUsers.spec.ts new file mode 100644 index 0000000..9c07fb7 --- /dev/null +++ b/dictation_function/src/test/importUsers.spec.ts @@ -0,0 +1,90 @@ +import * as dotenv from "dotenv"; +import { InvocationContext } from "@azure/functions"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { importUsersProcessing } from "../functions/importUsers"; +import { + PostMultipleImportsCompleteRequest, + SignupRequest, + UsersApi, +} from "../api/api"; +import { AxiosRequestConfig, AxiosResponse } from "axios"; + +describe("importUsersProcessing", () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + + it("stage.jsonがない状態でユーザー追加できること", async () => { + const context = new InvocationContext(); + + const userApiMock = new UsersApiMock() as UsersApi; + + // // 呼び出し回数でテスト成否を判定 + const spySignup = jest.spyOn(userApiMock, "signup"); + + const blobService = new BlobstorageService(); + + const mockListBlobs = jest + .fn() + .mockReturnValueOnce(["U_20210101_000000.json"]) + .mockReturnValue([]); + const mockDownloadFileData = jest + .fn() + .mockReturnValueOnce(undefined) + .mockReturnValue( + `{"account_id": 1, "user_id": 1, "external_id": "hoge", "user_role": "none", "file_name": "U_20240216_143802_8001_12211.csv", "date": 1111111, "data": [{"name": "name1","email": "email1@example.com","role": 1,"author_id": "AUTHOR_ID1","auto_renew": 1,"notification": 1,"encryption": 1,"encryption_password": "password","prompt": 1},{"name": "name2","email": "email2@example.com","role": 1,"author_id": "AUTHOR_ID2","auto_renew": 1,"notification": 1,"encryption": 1,"encryption_password": "password","prompt": 1}]}` + ); + const mockUpdateFile = jest.fn().mockReturnValue(true); + const mockDeleteFile = jest.fn().mockReturnValue(undefined); + const mockIsFileExists = jest.fn().mockReturnValueOnce(false); + + blobService.listBlobs = mockListBlobs; + blobService.downloadFileData = mockDownloadFileData; + blobService.updateFile = mockUpdateFile; + blobService.deleteFile = mockDeleteFile; + blobService.isFileExists = mockIsFileExists; + + await importUsersProcessing(context, blobService, userApiMock); + expect(spySignup.mock.calls).toHaveLength(2); + }, 30000); + + it("ファイルがない場合はそのまま終了すること", async () => { + const context = new InvocationContext(); + + const userApiMock = new UsersApiMock() as UsersApi; + + // // 呼び出し回数でテスト成否を判定 + const spySignup = jest.spyOn(userApiMock, "signup"); + + const blobService = new BlobstorageService(); + + const mockListBlobs = jest.fn().mockReturnValue([]); + const mockDownloadFileData = jest.fn().mockReturnValue(""); + const mockUpdateFile = jest.fn().mockReturnValue(undefined); + const mockDeleteFile = jest.fn().mockReturnValue(undefined); + const mockIsFileExists = jest.fn().mockReturnValueOnce(false); + + blobService.listBlobs = mockListBlobs; + blobService.downloadFileData = mockDownloadFileData; + blobService.updateFile = mockUpdateFile; + blobService.deleteFile = mockDeleteFile; + blobService.isFileExists = mockIsFileExists; + + await importUsersProcessing(context, blobService, userApiMock); + expect(spySignup.mock.calls).toHaveLength(0); + }, 30000); +}); + +export class UsersApiMock extends UsersApi { + async signup( + ignupRequest: SignupRequest, + options?: AxiosRequestConfig + ): Promise> { + return { data: {}, status: 200, statusText: "", headers: {}, config: {} }; + } + async multipleImportsComplate( + postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, + options?: AxiosRequestConfig + ): Promise> { + return { data: {}, status: 200, statusText: "", headers: {}, config: {} }; + } +} diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 38b804a..c279e1f 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1636,7 +1636,7 @@ } }, "400": { - "description": "パラメータ不正/アカウント不在", + "description": "パラメータ不正", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } @@ -2267,6 +2267,118 @@ "security": [{ "bearer": [] }] } }, + "/users/multiple-imports": { + "post": { + "operationId": "multipleImports", + "summary": "", + "description": "ユーザーを一括登録します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/multiple-imports/complete": { + "post": { + "operationId": "multipleImportsComplate", + "summary": "", + "description": "ユーザー一括登録の完了を通知します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, "/files/audio/upload-finished": { "post": { "operationId": "uploadFinished", @@ -4676,6 +4788,68 @@ "required": ["userId"] }, "PostDeleteUserResponse": { "type": "object", "properties": {} }, + "MultipleImportUser": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "email": { "type": "string", "description": "メールアドレス" }, + "role": { + "type": "number", + "description": "0(none)/1(author)/2(typist)" + }, + "authorId": { "type": "string" }, + "autoRenew": { "type": "number", "description": "0(false)/1(true)" }, + "notification": { + "type": "number", + "description": "0(false)/1(true)" + }, + "encryption": { "type": "number", "description": "0(false)/1(true)" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "number", "description": "0(false)/1(true)" } + }, + "required": ["name", "email", "role", "autoRenew", "notification"] + }, + "PostMultipleImportsRequest": { + "type": "object", + "properties": { + "filename": { "type": "string", "description": "CSVファイル名" }, + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportUser" } + } + }, + "required": ["filename", "users"] + }, + "PostMultipleImportsResponse": { "type": "object", "properties": {} }, + "MultipleImportErrors": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "line": { "type": "number", "description": "エラー発生行数" }, + "errorCode": { "type": "string", "description": "エラーコード" } + }, + "required": ["name", "line", "errorCode"] + }, + "PostMultipleImportsCompleteRequest": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" }, + "filename": { "type": "string", "description": "CSVファイル名" }, + "requestTime": { + "type": "number", + "description": "一括登録受付時刻(UNIXTIME/ミリ秒)" + }, + "errors": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportErrors" } + } + }, + "required": ["accountId", "filename", "requestTime", "errors"] + }, + "PostMultipleImportsCompleteResponse": { + "type": "object", + "properties": {} + }, "AudioOptionItem": { "type": "object", "properties": { From da40e8f09c0f18d869f43e064d1df3f582b21aa9 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 6 Mar 2024 01:58:32 +0000 Subject: [PATCH 039/109] =?UTF-8?q?Merged=20PR=20800:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E4=B8=80=E6=8B=AC=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=EF=BC=86=E3=83=9D=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E7=94=BB=E9=9D=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3753: 画面実装(一括追加ボタン&ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3753) - ユーザー一括登録画面を実装しました。 - 一括登録ポップアップ - テンプレートCSVダウンロード - ファイルインポート - エラー行表示 ## レビューポイント - 行エラーの条件、内容は認識通りでしょうか? - 画面の表示内容は認識通りでしょうか? - CSV変換時にworkerを有効にしているとエラーとなりうまくいかないのでOFFにしてしまいましたが問題ないでしょうか? - @<湯本 開> さん ## UIの変更 - [Task3753](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/Task3753?csf=1&web=1&e=x5M6hr) ## 動作確認状況 - ローカルで確認 - ファイルチェックするところまで --- dictation_client/src/api/api.ts | 286 ++++++++++++++++++ dictation_client/src/assets/images/upload.svg | 1 + dictation_client/src/common/parser.ts | 2 +- .../src/features/user/operations.ts | 72 +++++ .../src/features/user/selectors.ts | 139 +++++++++ dictation_client/src/features/user/state.ts | 9 +- .../src/features/user/userSlice.ts | 26 ++ .../src/pages/UserListPage/importPopup.tsx | 254 ++++++++++++++++ .../src/pages/UserListPage/index.tsx | 22 ++ dictation_client/src/styles/app.module.scss | 50 +++ .../src/styles/app.module.scss.d.ts | 4 +- dictation_client/src/translation/de.json | 55 +++- dictation_client/src/translation/en.json | 47 ++- dictation_client/src/translation/es.json | 47 ++- dictation_client/src/translation/fr.json | 47 ++- 15 files changed, 1026 insertions(+), 35 deletions(-) create mode 100644 dictation_client/src/assets/images/upload.svg create mode 100644 dictation_client/src/pages/UserListPage/importPopup.tsx diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index e166e6f..b68f729 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -1344,6 +1344,92 @@ export interface LicenseOrder { */ 'status': string; } +/** + * + * @export + * @interface MultipleImportErrors + */ +export interface MultipleImportErrors { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportErrors + */ + 'name': string; + /** + * エラー発生行数 + * @type {number} + * @memberof MultipleImportErrors + */ + 'line': number; + /** + * エラーコード + * @type {string} + * @memberof MultipleImportErrors + */ + 'errorCode': string; +} +/** + * + * @export + * @interface MultipleImportUser + */ +export interface MultipleImportUser { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof MultipleImportUser + */ + 'email': string; + /** + * 0(none)/1(author)/2(typist) + * @type {number} + * @memberof MultipleImportUser + */ + 'role': number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'authorId'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'autoRenew': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'notification': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'encryption'?: number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'encryptionPassword'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'prompt'?: number; +} /** * * @export @@ -1525,6 +1611,56 @@ export interface PostDeleteUserRequest { */ 'userId': number; } +/** + * + * @export + * @interface PostMultipleImportsCompleteRequest + */ +export interface PostMultipleImportsCompleteRequest { + /** + * アカウントID + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'accountId': number; + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsCompleteRequest + */ + 'filename': string; + /** + * 一括登録受付時刻(UNIXTIME/ミリ秒) + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'requestTime': number; + /** + * + * @type {Array} + * @memberof PostMultipleImportsCompleteRequest + */ + 'errors': Array; +} +/** + * + * @export + * @interface PostMultipleImportsRequest + */ +export interface PostMultipleImportsRequest { + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsRequest + */ + 'filename': string; + /** + * + * @type {Array} + * @memberof PostMultipleImportsRequest + */ + 'users': Array; +} /** * * @export @@ -7431,6 +7567,86 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports: async (postMultipleImportsRequest: PostMultipleImportsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsRequest' is not null or undefined + assertParamExists('multipleImports', 'postMultipleImportsRequest', postMultipleImportsRequest) + const localVarPath = `/users/multiple-imports`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate: async (postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsCompleteRequest' is not null or undefined + assertParamExists('multipleImportsComplate', 'postMultipleImportsCompleteRequest', postMultipleImportsCompleteRequest) + const localVarPath = `/users/multiple-imports/complete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsCompleteRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -7710,6 +7926,32 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.getUsers']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImports(postMultipleImportsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImports']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImportsComplate(postMultipleImportsCompleteRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImportsComplate']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -7858,6 +8100,26 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath getUsers(options?: any): AxiosPromise { return localVarFp.getUsers(options).then((request) => request(axios, basePath)); }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: any): AxiosPromise { + return localVarFp.multipleImports(postMultipleImportsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: any): AxiosPromise { + return localVarFp.multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -8012,6 +8274,30 @@ export class UsersApi extends BaseAPI { return UsersApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath)); } + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImports(postMultipleImportsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/assets/images/upload.svg b/dictation_client/src/assets/images/upload.svg new file mode 100644 index 0000000..2425a97 --- /dev/null +++ b/dictation_client/src/assets/images/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dictation_client/src/common/parser.ts b/dictation_client/src/common/parser.ts index c2950c3..15fe85b 100644 --- a/dictation_client/src/common/parser.ts +++ b/dictation_client/src/common/parser.ts @@ -40,7 +40,7 @@ export const parseCSV = async (csvString: string): Promise => new Promise((resolve, reject) => { Papa.parse(csvString, { download: false, - worker: true, + worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定 header: true, dynamicTyping: true, complete: (results: ParseResult) => { diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index f9e2663..8b9c782 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -9,6 +9,7 @@ import { UsersApi, LicensesApi, GetAllocatableLicensesResponse, + MultipleImportUser, } from "../../api/api"; import { Configuration } from "../../api/configuration"; import { ErrorObject, createErrorObject } from "../../common/errors"; @@ -498,3 +499,74 @@ export const deleteUserAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const importUsersAsync = createAsyncThunk< + // 正常時の戻り値の型 + { + /* Empty Object */ + }, + // 引数 + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/importUsersAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const { importFileName, importUsers } = state.user.apps; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + if (importFileName === undefined) { + throw new Error("importFileName is undefined"); + } + + // CSVデータをAPIに送信するためのデータに変換 + const users: MultipleImportUser[] = importUsers.map((user) => ({ + name: user.name ?? "", + email: user.email ?? "", + role: user.role ?? 0, + authorId: user.author_id ?? undefined, + autoRenew: user.auto_renew ?? 0, + notification: user.notification ?? 0, + encryption: user.encryption ?? undefined, + encryptionPassword: user.encryption_password ?? undefined, + prompt: user.prompt ?? undefined, + })); + + await usersApi.multipleImports( + { + filename: importFileName, + users, + }, + { headers: { authorization: `Bearer ${accessToken}` } } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("userListPage.message.importSuccess"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index 29c41fb..dc25752 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -382,3 +382,142 @@ const convertValueBasedOnLicenseStatus = ( remaining: undefined, }; }; + +export const selectImportFileName = (state: RootState) => + state.user.apps.importFileName; + +export const selectImportValidationErrors = (state: RootState) => { + const csvUsers = state.user.apps.importUsers; + + let rowNumber = 0; + const invalidInput: number[] = []; + + const duplicatedEmailsMap = new Map(); + const duplicatedAuthorIdsMap = new Map(); + const overMaxRow = csvUsers.length > 100; + + // eslint-disable-next-line no-restricted-syntax + for (const csvUser of csvUsers) { + rowNumber += 1; + + // メールアドレスの重複がある場合、エラーとしてその行番号を追加する + const duplicatedEmailUser = csvUsers.filter( + (x) => x.email === csvUser.email + ); + if (duplicatedEmailUser.length > 1) { + if (csvUser.email !== null && !duplicatedEmailsMap.has(csvUser.email)) { + duplicatedEmailsMap.set(csvUser.email, rowNumber); + } + } + + // AuthorIDの重複がある場合、エラーとしてその行番号を追加する + const duplicatedAuthorIdUser = csvUsers.filter( + (x) => x.author_id === csvUser.author_id + ); + if (duplicatedAuthorIdUser.length > 1) { + if ( + csvUser.author_id !== null && + !duplicatedAuthorIdsMap.has(csvUser.author_id) + ) { + duplicatedAuthorIdsMap.set(csvUser.author_id, rowNumber); + } + } + + // name + if (csvUser.name === null || csvUser.name.length > 225) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // email + const emailPattern = + /^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/; + if ( + csvUser.name === null || + csvUser.name.length > 225 || + !emailPattern.test(csvUser.email ?? "") + ) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // role + if (csvUser.role === null || ![0, 1, 2].includes(csvUser.role)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // role=1(Author) + if (csvUser.role === 1) { + // author_id + if (csvUser.author_id === null || csvUser.author_id.length > 16) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + // 半角英数字と_の組み合わせで16文字まで + const charaTypePattern = /^[A-Z0-9_]{1,16}$/; + const charaType = new RegExp(charaTypePattern).test(csvUser.author_id); + if (!charaType) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // encryption + if (csvUser.encryption === null || ![0, 1].includes(csvUser.encryption)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + if (csvUser.encryption === 1) { + // encryption_password + if (csvUser.encryption === 1) { + const regex = /^[!-~]{4,16}$/; + if (!regex.test(csvUser.encryption_password ?? "")) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + } + + // prompt + if (csvUser.prompt === null || ![0, 1].includes(csvUser.prompt)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + + // auto_renew + if (csvUser.auto_renew === null || ![0, 1].includes(csvUser.auto_renew)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // notification + if ( + csvUser.notification === null || + ![0, 1].includes(csvUser.notification) + ) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + + const duplicatedEmails = Array.from(duplicatedEmailsMap.values()); + const duplicatedAuthorIds = Array.from(duplicatedAuthorIdsMap.values()); + + return { + invalidInput, + duplicatedEmails, + duplicatedAuthorIds, + overMaxRow, + }; +}; diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index f25dd94..5d619e1 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -1,4 +1,9 @@ -import { User, AllocatableLicenseInfo } from "../../api/api"; +import { CSVType } from "common/parser"; +import { + User, + AllocatableLicenseInfo, + MultipleImportUser, +} from "../../api/api"; import { AddUser, UpdateUser, LicenseAllocateUser } from "./types"; export interface UsersState { @@ -19,4 +24,6 @@ export interface Apps { selectedlicenseId: number; hasPasswordMask: boolean; isLoading: boolean; + importFileName: string | undefined; + importUsers: CSVType[]; } diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 42baced..b0fcf5c 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -1,5 +1,6 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { USER_ROLES } from "components/auth/constants"; +import { CSVType } from "common/parser"; import { UsersState } from "./state"; import { addUserAsync, @@ -8,6 +9,7 @@ import { getAllocatableLicensesAsync, deallocateLicenseAsync, deleteUserAsync, + importUsersAsync, } from "./operations"; import { RoleType, UserView } from "./types"; @@ -61,6 +63,8 @@ const initialState: UsersState = { selectedlicenseId: 0, hasPasswordMask: false, isLoading: false, + importFileName: undefined, + importUsers: [], }, }; @@ -242,6 +246,17 @@ export const userSlice = createSlice({ state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser; state.apps.selectedlicenseId = initialState.apps.selectedlicenseId; }, + changeImportFileName: ( + state, + action: PayloadAction<{ fileName: string }> + ) => { + const { fileName } = action.payload; + state.apps.importFileName = fileName; + }, + changeImportCsv: (state, action: PayloadAction<{ users: CSVType[] }>) => { + const { users } = action.payload; + state.apps.importUsers = users; + }, }, extraReducers: (builder) => { builder.addCase(listUsersAsync.pending, (state) => { @@ -300,6 +315,15 @@ export const userSlice = createSlice({ builder.addCase(deleteUserAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(importUsersAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(importUsersAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(importUsersAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); @@ -327,6 +351,8 @@ export const { changeLicenseAllocateUser, changeSelectedlicenseId, cleanupLicenseAllocateInfo, + changeImportFileName, + changeImportCsv, } = userSlice.actions; export default userSlice.reducer; diff --git a/dictation_client/src/pages/UserListPage/importPopup.tsx b/dictation_client/src/pages/UserListPage/importPopup.tsx new file mode 100644 index 0000000..d01d930 --- /dev/null +++ b/dictation_client/src/pages/UserListPage/importPopup.tsx @@ -0,0 +1,254 @@ +import { AppDispatch } from "app/store"; +import React, { useState, useCallback } from "react"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { getTranslationID } from "translation"; +import { useTranslation } from "react-i18next"; +import { + selectIsLoading, + importUsersAsync, + changeImportFileName, + changeImportCsv, + selectImportFileName, + selectImportValidationErrors, +} from "features/user"; +import { parseCSV } from "common/parser"; +import close from "../../assets/images/close.svg"; +import download from "../../assets/images/download.svg"; +import upload from "../../assets/images/upload.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +interface UserAddPopupProps { + isOpen: boolean; + onClose: () => void; +} + +export const ImportPopup: React.FC = (props) => { + const { isOpen, onClose } = props; + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + // AddUserの情報を取得 + + const closePopup = useCallback(() => { + setIsPushImportButton(false); + onClose(); + }, [onClose]); + + const [isPushImportButton, setIsPushImportButton] = useState(false); + const isLoading = useSelector(selectIsLoading); + + const importFileName = useSelector(selectImportFileName); + const { invalidInput, duplicatedEmails, duplicatedAuthorIds, overMaxRow } = + useSelector(selectImportValidationErrors); + + const onDownloadCsv = useCallback(() => { + // csvファイルダウンロード処理 + const filename = `import_users.csv`; + + const importCsvHeader = [ + "name", + "email", + "role", + "author_id", + "auto_renew", + "notification", + "encryption", + "encryption_password", + "prompt", + ].toString(); + + const blob = new Blob([importCsvHeader], { + type: "mime", + }); + const blobURL = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = blobURL; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.parentNode?.removeChild(a); + }, []); + + // ファイルが選択されたときの処理 + const handleFileChange = useCallback( + async (event: React.ChangeEvent) => { + // 選択されたファイルを取得(複数選択されても先頭を取得) + const file = event.target.files?.[0]; + + // ファイルが選択されていれば、storeに保存 + if (file) { + const text = await file.text(); + const users = await parseCSV(text.trimEnd()); + + dispatch(changeImportCsv({ users })); + dispatch(changeImportFileName({ fileName: file.name })); + } + // 同名のファイルを選択した場合、onChangeが発火しないため、valueをクリアする + event.target.value = ""; + }, + [dispatch] + ); + + const onImportUsers = useCallback(async () => { + setIsPushImportButton(true); + if ( + invalidInput.length > 0 || + duplicatedEmails.length > 0 || + duplicatedAuthorIds.length > 0 || + overMaxRow + ) { + return; + } + + await dispatch(importUsersAsync()); + setIsPushImportButton(false); + }, [ + dispatch, + invalidInput, + duplicatedEmails, + duplicatedAuthorIds, + overMaxRow, + ]); + + return ( +
    +
    +

    + {t(getTranslationID("userListPage.label.bulkImport"))} + +

    +
    +
    +
    + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + + + {t(getTranslationID("userListPage.label.downloadCsv"))} + + {t(getTranslationID("userListPage.text.downloadExplain"))} +
    +
    + +
    +
    Input rules
    +
    {t(getTranslationID("userListPage.label.nameLabel"))}
    +
    {t(getTranslationID("userListPage.text.nameRule"))}
    +
    + {t(getTranslationID("userListPage.label.emailAddressLabel"))} +
    +
    {t(getTranslationID("userListPage.text.emailAddressRule"))}
    +
    {t(getTranslationID("userListPage.label.roleLabel"))}
    +
    {t(getTranslationID("userListPage.text.roleRule"))}
    +
    {t(getTranslationID("userListPage.label.authorIdLabel"))}
    +
    {t(getTranslationID("userListPage.text.authorIdRule"))}
    +
    {t(getTranslationID("userListPage.label.autoRenewLabel"))}
    +
    {t(getTranslationID("userListPage.text.autoRenewRule"))}
    +
    + {t(getTranslationID("userListPage.label.notificationLabel"))} +
    +
    {t(getTranslationID("userListPage.text.notificationRule"))}
    +
    {t(getTranslationID("userListPage.label.encryptionLabel"))}
    +
    {t(getTranslationID("userListPage.text.encryptionRule"))}
    +
    + {t( + getTranslationID("userListPage.label.encryptionPasswordLabel") + )} +
    +
    + {t(getTranslationID("userListPage.text.encryptionPasswordRule"))} +
    +
    {t(getTranslationID("userListPage.label.promptLabel"))}
    +
    {t(getTranslationID("userListPage.text.promptRule"))}
    +
    + {isPushImportButton && overMaxRow && ( + + {t(getTranslationID("userListPage.message.overMaxUserError"))} + + )} + {isPushImportButton && invalidInput.length > 0 && ( + <> + + {t( + getTranslationID("userListPage.message.invalidInputError") + )} + + + {invalidInput.map((row) => `L${row}`).join(", ")} + + + )} + {isPushImportButton && duplicatedEmails.length > 0 && ( + <> + + {t( + getTranslationID( + "userListPage.message.duplicateEmailError" + ) + )} + + + {duplicatedEmails.map((row) => `L${row}`).join(", ")} + + + )} + {isPushImportButton && duplicatedAuthorIds.length > 0 && ( + <> + + {t( + getTranslationID( + "userListPage.message.duplicateAuthorIdError" + ) + )} + + + {duplicatedAuthorIds.map((row) => `L${row}`).join(", ")} + + + )} +
    +
    + + Loading +
    +
    +
    +
    +
    + ); +}; diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index 92ecef0..c8a2ba4 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -32,9 +32,11 @@ import personAdd from "../../assets/images/person_add.svg"; import checkFill from "../../assets/images/check_fill.svg"; import checkOutline from "../../assets/images/check_outline.svg"; import progress_activit from "../../assets/images/progress_activit.svg"; +import upload from "../../assets/images/upload.svg"; import { UserAddPopup } from "./popup"; import { UserUpdatePopup } from "./updatePopup"; import { AllocateLicensePopup } from "./allocateLicensePopup"; +import { ImportPopup } from "./importPopup"; const UserListPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -46,6 +48,7 @@ const UserListPage: React.FC = (): JSX.Element => { const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false); const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] = useState(false); + const [isImportPopupOpen, setIsImportPopupOpen] = useState(false); const onOpen = useCallback(() => { setIsPopupOpen(true); @@ -66,6 +69,9 @@ const UserListPage: React.FC = (): JSX.Element => { }, [setIsAllocateLicensePopupOpen, dispatch] ); + const onImportPopupOpen = useCallback(() => { + setIsImportPopupOpen(true); + }, [setIsImportPopupOpen]); const onLicenseDeallocation = useCallback( async (userId: number) => { @@ -134,6 +140,12 @@ const UserListPage: React.FC = (): JSX.Element => { setIsAllocateLicensePopupOpen(false); }} /> + { + setIsImportPopupOpen(false); + }} + />
    { {t(getTranslationID("userListPage.label.addUser"))} +
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + + + {t(getTranslationID("userListPage.label.bulkImport"))} + +
  • diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 8e77dfa..c3a2d1e 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1630,6 +1630,43 @@ _:-ms-lang(x)::-ms-backdrop, margin-bottom: 5rem; } +.formList.userImport .formTitle { + padding: 1rem 4% 0; + line-height: 1.2; +} +.formList.userImport dt:not(.formTitle) { + width: 30%; + padding: 0 4% 0 4%; + font-size: 0.9rem; +} +.formList.userImport dt:not(.formTitle):nth-of-type(odd) { + background: #f0f0f0; +} +.formList.userImport dt:not(.formTitle):nth-of-type(odd) + dd { + background: #f0f0f0; +} +.formList.userImport dd { + width: 58%; + padding: 0.2rem 4% 0.2rem 0; + margin-bottom: 0; + white-space: pre-line; + word-wrap: break-word; + font-size: 0.9rem; + line-height: 1.2; +} +.formList.userImport dd.full { + width: 100%; + padding: 0.2rem 4% 0.2rem 4%; +} +.formList.userImport dd.full .buttonText { + padding: 0 0 0.8rem; +} +.formList.userImport dd .menuLink { + display: inline-block; + margin-bottom: 0.6rem; + padding: 0.5rem 1.5rem 0.5rem 1.3rem; +} + .account .listVertical { margin-bottom: 3rem; } @@ -2284,6 +2321,9 @@ tr.isSelected .menuInTable li a.isDisable { .formList.property dt:not(.formTitle):nth-of-type(odd) + dd { background: #f0f0f0; } +.formList.property dt:has(+ dd.hasInput) { + padding-top: 0.4rem; +} .formList.property dd { width: 58%; padding: 0.2rem 4% 0.2rem 0; @@ -2295,6 +2335,16 @@ tr.isSelected .menuInTable li a.isDisable { .formList.property dd img { height: 1.1rem; } +.formList.property dd .formInput.short { + width: 250px; + padding: 0.3rem 0.3rem 0.1rem; +} +.formList.property dd .formSubmit { + min-width: auto; + padding: 0.2rem 0.5rem; + position: absolute; + right: 0.5rem; +} .formList.property dd.full { width: 100%; padding: 0.2rem 4% 0.2rem 4%; diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index 656d36a..e82c3c7 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -108,11 +108,12 @@ declare const classNames: { readonly clm0: "clm0"; readonly menuInTable: "menuInTable"; readonly isSelected: "isSelected"; + readonly userImport: "userImport"; + readonly menuLink: "menuLink"; readonly odd: "odd"; readonly alignRight: "alignRight"; readonly menuAction: "menuAction"; readonly inTable: "inTable"; - readonly menuLink: "menuLink"; readonly menuIcon: "menuIcon"; readonly colorLink: "colorLink"; readonly isDisable: "isDisable"; @@ -193,6 +194,7 @@ declare const classNames: { readonly hideO10: "hideO10"; readonly op10: "op10"; readonly property: "property"; + readonly hasInput: "hasInput"; readonly formChange: "formChange"; readonly chooseMember: "chooseMember"; readonly holdMember: "holdMember"; diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 9730841..7ed2025 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -64,7 +64,7 @@ "countryExplanation": "Wählen Sie das Land aus, in dem Sie sich befinden. Wenn Ihr Land nicht aufgeführt ist, wählen Sie bitte das nächstgelegene Land aus.", "dealerExplanation": "Bitte wählen Sie den Händler aus, bei dem Sie die Lizenz erwerben möchten.", "adminInfoTitle": "Registrieren Sie die Informationen des primären Administrators", - "passwordTerms": "Bitte legen Sie ein Passwort fest. Das Passwort muss 8–25 Zeichen lang sein und Buchstaben, Zahlen und Symbole enthalten. (Sollte ein kompatibles Symbol auflisten und angeben, ob ein Großbuchstabe erforderlich ist.)" + "passwordTerms": "Bitte legen Sie ein Passwort fest. Das Passwort muss 8–64 Zeichen lang sein und Buchstaben, Zahlen und Symbole enthalten." }, "label": { "company": "Name der Firma", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "(de)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "(de)以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "(de)以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "(de)一度に追加できるユーザーは100件までです。", + "invalidInputError": "(de)以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "Benutzer", @@ -168,7 +173,33 @@ "deleteUser": "Benutzer löschen", "none": "Keiner", "encryptionPassword": "Passwort", - "encryptionPasswordTerm": "Bitte legen Sie Ihr Passwort mit 4 bis 16 alphanumerischen Zeichen und Symbolen fest." + "encryptionPasswordTerm": "Bitte legen Sie Ihr Passwort mit 4 bis 16 alphanumerischen Zeichen und Symbolen fest.", + "bulkImport": "(de)Bulk import", + "downloadCsv": "(de)Download CSV", + "importCsv": "(de)Import CSV", + "inputRules": "(de)Input rules", + "nameLabel": "(de)Name", + "emailAddressLabel": "(de)Email Address", + "roleLabel": "(de)Role", + "authorIdLabel": "(de)Author ID", + "autoRenewLabel": "(de)Auto Renew", + "notificationLabel": "(de)Notification", + "encryptionLabel": "(de)Encryption", + "encryptionPasswordLabel": "(de)Encryption Password", + "promptLabel": "(de)Prompt", + "addUsers": "(de)Add users" + }, + "text": { + "downloadExplain": "(de)Download the csv format and enter it according to the rules below.", + "nameRule": "(de)Maximum 225 characters", + "emailAddressRule": "(de)Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "(de)None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "(de)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "(de)0 or 1", + "notificationRule": "(de)0 or 1", + "encryptionRule": "(de)Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "(de)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "(de)Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -198,16 +229,16 @@ "licenseOrderPage": { "message": { "inputEmptyError": "Pflichtfeld", - "poNumberIncorrectError": "Das Format der Bestellnummer ist ungültig. Für die Bestellnummer können nur alphanumerische Zeichen eingegeben werden.", + "poNumberIncorrectError": "Das Format der PO-Nummer ist ungültig. Für die PO-Nummer können nur alphanumerische Zeichen eingegeben werden.", "newOrderIncorrectError": "Bitte geben Sie für die neue Bestellung eine Zahl größer oder gleich 1 ein.", "confirmOrder": "Möchten Sie eine Bestellung aufgeben?", - "poNumberConflictError": "Die eingegebene Bestellnummer existiert bereits. Bitte geben Sie eine andere Bestellnummer ein.", - "dealerNotFoundError": "(de)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "poNumberConflictError": "Die eingegebene PO-Nummer existiert bereits. Bitte geben Sie eine andere PO-Nummer ein.", + "dealerNotFoundError": "Um eine Lizenz zu bestellen, müssen Sie den Händler angeben, bei dem Sie die Lizenz erwerben möchten. Melden Sie sich bei ODMS Cloud an und richten Sie „Händler“ auf der Registerkarte „Konto“ ein." }, "label": { "title": "Lizenz bestellen", "licenses": "Lizenz-Typ", - "poNumber": "Bestellnummer", + "poNumber": "PO-Nummer", "newOrder": "Anzahl der Lizenzen", "orderButton": "Bestellen", "licenseTypeText": "Ein Jahr" @@ -220,7 +251,9 @@ "taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.", "backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.", "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.", - "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "Das Diktat kann nicht hochgeladen werden, da keine gültige Lizenz zugewiesen ist. Bitte fragen Sie Ihren Administrator.", + "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen." }, "label": { "title": "Diktate", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Transkriptionist ändern", "deleteDictation": "Diktat löschen", "selectedTranscriptionist": "Ausgewählter transkriptionist", - "poolTranscriptionist": "Liste der Transkriptionisten", + "poolTranscriptionist": "Transkriptionsliste", "fileBackup": "Dateisicherung", "downloadForBackup": "Zur Sicherung herunterladen", "applications": "Desktopanwendung", @@ -345,7 +378,7 @@ "orderDate": "Auftragsdatum", "issueDate": "Ausgabetag", "numberOfOrder": "Anzahl der bestellten Lizenzen", - "poNumber": "Bestellnummer", + "poNumber": "PO-Nummer", "status": "Status", "issueRequesting": "Lizenzen auf Bestellung", "issued": "Lizenz ausgestellt", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.", "groupSaveFailedError": "Die Transkriptionistengruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", - "GroupNameAlreadyExistError": "(de)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "Der Name dieser Transkriptionistengruppe ist bereits registriert. Bitte registrieren Sie sich mit einem anderen Namen der Transkriptionistengruppe.", "deleteFailedWorkflowAssigned": "(de)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "(de)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 959ea1d..e3186d5 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -64,7 +64,7 @@ "countryExplanation": "Select the country where you are located. If your country isn't listed, please select the nearest country.", "dealerExplanation": "Please select the dealer you would like to purchase the license from.", "adminInfoTitle": "Register primary administrator's information", - "passwordTerms": "Please set a password. The password must be 8-25 characters must contain letters, numbers, and symbols. (Should list compatible symbol and state if capital letter is needed)." + "passwordTerms": "Please set a password. The password must be 8-64 characters must contain letters, numbers, and symbols." }, "label": { "company": "Company Name", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "一度に追加できるユーザーは100件までです。", + "invalidInputError": "以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "User", @@ -168,7 +173,33 @@ "deleteUser": "Delete User", "none": "None", "encryptionPassword": "Password", - "encryptionPasswordTerm": "Please set your password using 4 to 16 alphanumeric and symbols." + "encryptionPasswordTerm": "Please set your password using 4 to 16 alphanumeric and symbols.", + "bulkImport": "Bulk import", + "downloadCsv": "Download CSV", + "importCsv": "Import CSV", + "inputRules": "Input rules", + "nameLabel": "Name", + "emailAddressLabel": "Email Address", + "roleLabel": "Role", + "authorIdLabel": "Author ID", + "autoRenewLabel": "Auto Renew", + "notificationLabel": "Notification", + "encryptionLabel": "Encryption", + "encryptionPasswordLabel": "Encryption Password", + "promptLabel": "Prompt", + "addUsers": "Add users" + }, + "text": { + "downloadExplain": "Download the csv format and enter it according to the rules below.", + "nameRule": "Maximum 225 characters", + "emailAddressRule": "Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "0 or 1", + "notificationRule": "0 or 1", + "encryptionRule": "Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -202,7 +233,7 @@ "newOrderIncorrectError": "Please enter a number greater than or equal to 1 for the New Order.", "confirmOrder": "Would you like to place an order?", "poNumberConflictError": "PO Number entered already exists. Please enter a different PO Number.", - "dealerNotFoundError": "ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "dealerNotFoundError": "In order to order a license, you need to set up the dealer where you want to purchase it. Sign in to ODMS Cloud and set up \"Dealer\" in the \"Account\" tab." }, "label": { "title": "Order License", @@ -220,7 +251,9 @@ "taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.", "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.", "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.", - "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "Dictation cannot be uploaded because a vaild license is not assigned. Please ask your administrator.", + "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license." }, "label": { "title": "Dictations", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Change Transcriptionist", "deleteDictation": "Delete Dictation", "selectedTranscriptionist": "Selected Transcriptionist", - "poolTranscriptionist": "Transcriptionist List", + "poolTranscriptionist": "Transcription List", "fileBackup": "File Backup", "downloadForBackup": "Download for backup", "applications": "Desktop Application", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.", "groupSaveFailedError": "Transcriptionist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status.", - "GroupNameAlreadyExistError": "このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "This Transcriptionist Group name is already registered. Please register with another Transcriptionist Group name.", "deleteFailedWorkflowAssigned": "TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index f89d531..ea17dd8 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -64,7 +64,7 @@ "countryExplanation": "Seleccione el país donde se encuentra. Si su país no aparece en la lista, seleccione el país más cercano.", "dealerExplanation": "Seleccione el distribuidor al que le gustaría comprar la licencia.", "adminInfoTitle": "Registre la información del administrador principal", - "passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 25 caracteres y debe contener letras, números y símbolos. (Debe enumerar el símbolo compatible e indicar si se necesita una letra mayúscula)." + "passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 64 caracteres y debe contener letras, números y símbolos." }, "label": { "company": "Nombre de empresa", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "(es)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "(es)以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "(es)以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "(es)一度に追加できるユーザーは100件までです。", + "invalidInputError": "(es)以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "Usuario", @@ -168,7 +173,33 @@ "deleteUser": "Borrar usuario", "none": "Ninguno", "encryptionPassword": "Contraseña", - "encryptionPasswordTerm": "Configure su contraseña utilizando de 4 a 16 símbolos alfanuméricos y." + "encryptionPasswordTerm": "Configure su contraseña utilizando de 4 a 16 símbolos alfanuméricos y.", + "bulkImport": "(es)Bulk import", + "downloadCsv": "(es)Download CSV", + "importCsv": "(es)Import CSV", + "inputRules": "(es)Input rules", + "nameLabel": "(es)Name", + "emailAddressLabel": "(es)Email Address", + "roleLabel": "(es)Role", + "authorIdLabel": "(es)Author ID", + "autoRenewLabel": "(es)Auto Renew", + "notificationLabel": "(es)Notification", + "encryptionLabel": "(es)Encryption", + "encryptionPasswordLabel": "(es)Encryption Password", + "promptLabel": "(es)Prompt", + "addUsers": "(es)Add users" + }, + "text": { + "downloadExplain": "(es)Download the csv format and enter it according to the rules below.", + "nameRule": "(es)Maximum 225 characters", + "emailAddressRule": "(es)Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "(es)None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "(es)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "(es)0 or 1", + "notificationRule": "(es)0 or 1", + "encryptionRule": "(es)Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "(es)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "(es)Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -202,7 +233,7 @@ "newOrderIncorrectError": "Ingrese un número mayor o igual a 1 para el Nuevo Pedido.", "confirmOrder": "¿Quieres hacer un pedido?", "poNumberConflictError": "El número de orden de compra ingresado ya existe. Ingrese un número de orden de compra diferente.", - "dealerNotFoundError": "(es)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "dealerNotFoundError": "Para solicitar una licencia, debe configurar el distribuidor donde desea comprarla. Inicie sesión en ODMS Cloud y configure \"Distribuidor\" en la pestaña \"Cuenta\"." }, "label": { "title": "Licencia de pedido", @@ -220,7 +251,9 @@ "taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.", "backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.", "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.", - "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "No se puede cargar el dictado porque no se ha asignado una licencia válida. Consulte a su administrador.", + "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida." }, "label": { "title": "Dictado", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Cambiar transcriptor", "deleteDictation": "Borrar dictado", "selectedTranscriptionist": "Transcriptor seleccionado", - "poolTranscriptionist": "Lista de transcriptores", + "poolTranscriptionist": "Lista de transcriptor", "fileBackup": "Copia de seguridad de archivos", "downloadForBackup": "Descargar para respaldo", "applications": "Aplicación de escritorio", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.", "groupSaveFailedError": "El grupo transcriptor no se pudo salvar. La información mostrada puede estar desactualizada. Así que actualice la pantalla para ver el estado más reciente.", - "GroupNameAlreadyExistError": "(es)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "El nombre de este grupo transcriptor ya está registrado. Regístrese con otro nombre de grupo transcriptor.", "deleteFailedWorkflowAssigned": "(es)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "(es)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index b313ce4..1b473e6 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -64,7 +64,7 @@ "countryExplanation": "Sélectionnez le pays où vous vous trouvez. Si votre pays ne figure pas dans la liste, veuillez sélectionner le pays le plus proche.", "dealerExplanation": "Veuillez sélectionner le revendeur auprès duquel vous souhaitez acheter la licence.", "adminInfoTitle": "Enregistrer les informations de l'administrateur principal", - "passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 25 caractères et doit contenir des lettres, des chiffres et des symboles. (Devrait lister les symboles compatibles et indiquer si une majuscule est nécessaire)." + "passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 64 caractères et doit contenir des lettres, des chiffres et des symboles." }, "label": { "company": "Nom de l'entreprise", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "(fr)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "(fr)以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "(fr)以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "(fr)一度に追加できるユーザーは100件までです。", + "invalidInputError": "(fr)以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "Utilisateur", @@ -168,7 +173,33 @@ "deleteUser": "Supprimer l'utilisateur", "none": "Aucun", "encryptionPassword": "Mot de passe", - "encryptionPasswordTerm": "Veuillez définir votre mot de passe en utilisant 4 à 16 caractères alphanumériques et symboles." + "encryptionPasswordTerm": "Veuillez définir votre mot de passe en utilisant 4 à 16 caractères alphanumériques et symboles.", + "bulkImport": "(fr)Bulk import", + "downloadCsv": "(fr)Download CSV", + "importCsv": "(fr)Import CSV", + "inputRules": "(fr)Input rules", + "nameLabel": "(fr)Name", + "emailAddressLabel": "(fr)Email Address", + "roleLabel": "(fr)Role", + "authorIdLabel": "(fr)Author ID", + "autoRenewLabel": "(fr)Auto Renew", + "notificationLabel": "(fr)Notification", + "encryptionLabel": "(fr)Encryption", + "encryptionPasswordLabel": "(fr)Encryption Password", + "promptLabel": "(fr)Prompt", + "addUsers": "(fr)Add users" + }, + "text": { + "downloadExplain": "(fr)Download the csv format and enter it according to the rules below.", + "nameRule": "(fr)Maximum 225 characters", + "emailAddressRule": "(fr)Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "(fr)None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "(fr)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "(fr)0 or 1", + "notificationRule": "(fr)0 or 1", + "encryptionRule": "(fr)Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "(fr)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "(fr)Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -202,7 +233,7 @@ "newOrderIncorrectError": "Veuillez saisir un nombre supérieur ou égal à 1 pour la nouvelle commande.", "confirmOrder": "Voulez-vous passer commande?", "poNumberConflictError": "Le numéro de bon de commande saisi existe déjà. Veuillez saisir un autre numéro de bon de commande.", - "dealerNotFoundError": "(fr)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "dealerNotFoundError": "Pour commander une licence, vous devez identifier le revendeur où vous souhaitez l'acheter. Connectez-vous à ODMS Cloud et configurez « Revendeur » dans l'onglet « Compte »." }, "label": { "title": "Commander licence", @@ -220,7 +251,9 @@ "taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.", "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.", "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.", - "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "La dictée ne peut pas être téléchargée parce qu'une licence valable n'est pas attribuée. Veuillez vous adresser à votre administrateur.", + "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide." }, "label": { "title": "Dictées", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Changer de transcriptionniste ", "deleteDictation": "Supprimer la dictée", "selectedTranscriptionist": "Transcriptionniste sélectionné", - "poolTranscriptionist": "Liste des transcripteurs", + "poolTranscriptionist": "Liste de transcriptionniste", "fileBackup": "Sauvegarde de fichiers", "downloadForBackup": "Télécharger pour sauvegarde", "applications": "Application de bureau", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.", "groupSaveFailedError": "Le groupe de transcriptionniste n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", - "GroupNameAlreadyExistError": "(fr)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "Ce nom de groupe transcripteur est déjà enregistré. Veuillez vous inscrire avec un autre nom de groupe transcripteur.", "deleteFailedWorkflowAssigned": "(fr)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "(fr)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } From 04c726e96472ba249dfb50a008d15a8384b58afc Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 7 Mar 2024 11:47:53 +0000 Subject: [PATCH 040/109] =?UTF-8?q?Merged=20PR=20808:=20function=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 - 修正箇所がほかの機能に影響していないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば --- .../src/blobstorage/types/guards.ts | 40 ++++++++++++------- .../src/blobstorage/types/types.ts | 2 - dictation_function/src/constants/index.ts | 6 +-- .../src/functions/importUsers.ts | 15 ++++--- .../src/features/users/users.service.ts | 14 ++++++- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/dictation_function/src/blobstorage/types/guards.ts b/dictation_function/src/blobstorage/types/guards.ts index b16a241..c0bbbb0 100644 --- a/dictation_function/src/blobstorage/types/guards.ts +++ b/dictation_function/src/blobstorage/types/guards.ts @@ -1,3 +1,4 @@ +import { InvocationContext } from "@azure/functions"; import { IMPORT_USERS_STAGES } from "../../constants"; import { ErrorRow, ImportData, ImportJson, StageJson } from "./types"; @@ -48,107 +49,116 @@ export const isStageJson = (obj: any): obj is StageJson => { return true; }; -const isImportData = (obj: any): obj is ImportData => { +const isImportData = ( + context: InvocationContext, + obj: any +): obj is ImportData => { if (typeof obj !== "object") return false; const importData = obj as ImportData; if (importData.name === undefined || typeof importData.name !== "string") { + context.log("name is missing or not a string"); return false; } if (importData.email === undefined || typeof importData.email !== "string") { + context.log("email is missing or not a string"); return false; } if (importData.role === undefined || typeof importData.role !== "number") { + context.log("role is missing or not a number"); return false; } if ( importData.author_id !== undefined && typeof importData.author_id !== "string" ) { + context.log("author_id is missing or not a string"); return false; } if ( importData.auto_renew === undefined || typeof importData.auto_renew !== "number" ) { + context.log("auto_renew is missing or not a number"); return false; } if ( importData.notification === undefined || typeof importData.notification !== "number" ) { + context.log("notification is missing or not a number"); return false; } if ( importData.encryption !== undefined && typeof importData.encryption !== "number" ) { + context.log("encryption is missing or not a number"); return false; } if ( importData.encryption_password !== undefined && typeof importData.encryption_password !== "string" ) { + context.log("encryption_password is missing or not a string"); return false; } if ( importData.prompt !== undefined && typeof importData.prompt !== "number" ) { + context.log("prompt is missing or not a number"); return false; } return true; }; -export const isImportJson = (obj: any): obj is ImportJson => { +export const isImportJson = ( + context: InvocationContext, + obj: any +): obj is ImportJson => { if (typeof obj !== "object") return false; const importJson = obj as ImportJson; if ( importJson.account_id === undefined || typeof importJson.account_id !== "number" ) { + context.log("account_id is missing or not a number"); return false; } if ( importJson.user_id === undefined || typeof importJson.user_id !== "number" ) { + context.log("user_id is missing or not a number"); return false; } if ( importJson.user_role === undefined || typeof importJson.user_role !== "string" ) { + context.log("user_role is missing or not a string"); return false; } if ( importJson.external_id === undefined || typeof importJson.external_id !== "string" ) { - return false; - } - if ( - importJson.delegation_account_id !== undefined && - typeof importJson.delegation_account_id !== "number" - ) { - return false; - } - if ( - importJson.delegation_user_id !== undefined && - typeof importJson.delegation_user_id !== "number" - ) { + context.log("external_id is missing or not a string"); return false; } if ( importJson.file_name === undefined || typeof importJson.file_name !== "string" ) { + context.log("file_name is missing or not a string"); return false; } if ( importJson.data === undefined || !Array.isArray(importJson.data) || - !importJson.data.every((x) => isImportData(x)) + !importJson.data.every((x) => isImportData(context, x)) ) { + context.log("data is missing or not an array of ImportData"); return false; } return true; diff --git a/dictation_function/src/blobstorage/types/types.ts b/dictation_function/src/blobstorage/types/types.ts index fec4ed4..97ba857 100644 --- a/dictation_function/src/blobstorage/types/types.ts +++ b/dictation_function/src/blobstorage/types/types.ts @@ -22,8 +22,6 @@ export type ImportJson = { user_id: number; user_role: RoleType; external_id: string; - delegation_account_id?: number | undefined; - delegation_user_id?: number | undefined; file_name: string; date: number; data: ImportData[]; diff --git a/dictation_function/src/constants/index.ts b/dictation_function/src/constants/index.ts index 88e3e3b..f955c2d 100644 --- a/dictation_function/src/constants/index.ts +++ b/dictation_function/src/constants/index.ts @@ -329,9 +329,9 @@ export const IMPORT_USERS_STAGES = { * @const {string} */ export const RoleNumberMap: Record = { - 1: USER_ROLES.NONE, - 2: USER_ROLES.AUTHOR, - 3: USER_ROLES.TYPIST, + 0: USER_ROLES.NONE, + 1: USER_ROLES.AUTHOR, + 2: USER_ROLES.TYPIST, } as const; export const SYSTEM_IMPORT_USERS = "import-users"; diff --git a/dictation_function/src/functions/importUsers.ts b/dictation_function/src/functions/importUsers.ts index 6294c8b..40a281b 100644 --- a/dictation_function/src/functions/importUsers.ts +++ b/dictation_function/src/functions/importUsers.ts @@ -16,6 +16,7 @@ import { createErrorObject } from "../common/errors/utils"; import { sign, getJwtKey } from "../common/jwt"; import { AccessToken, SystemAccessToken } from "../common/jwt/types"; import { isImportJson, isStageJson } from "../blobstorage/types/guards"; +import https from "https"; export async function importUsersProcessing( context: InvocationContext, @@ -128,7 +129,7 @@ export async function importUsersProcessing( // 一括登録ユーザー一覧をメモリ上に展開 const imports = importsData === undefined ? undefined : JSON.parse(importsData); - if (!isImportJson(imports)) { + if (!isImportJson(context, imports)) { throw new Error(`json: ${targetFileName} is invalid`); } @@ -228,6 +229,7 @@ export async function importUsersProcessing( }, { headers: { authorization: `Bearer ${systemToken}` }, + httpsAgent: new https.Agent({ rejectUnauthorized: false }), } ); @@ -317,17 +319,20 @@ export async function addUser( role: RoleNumberMap[user.role], autoRenew: user.auto_renew === 1, notification: user.notification === 1, - authorId: user.author_id, - encryption: user.encryption === 1, - encryptionPassword: user.encryption_password, - prompt: user.prompt === 1, + authorId: user.role === 1 ? user.author_id : undefined, + encryption: user.role === 1 ? user.encryption === 1 : undefined, + encryptionPassword: + user.encryption === 1 ? user.encryption_password : undefined, + prompt: user.role === 1 ? user.prompt === 1 : undefined, }, { headers: { authorization: `Bearer ${token}` }, + httpsAgent: new https.Agent({ rejectUnauthorized: false }), } ); } catch (e) { context.error(e); + context.error(JSON.stringify(e.response?.data)); throw e; } finally { context.log(`[OUT] addUser`); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 2df3703..957dde1 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -1651,7 +1651,19 @@ export class UsersService { external_id: user.external_id, file_name: fileName, date: Math.floor(now.getTime() / 1000), - data: users, + data: users.map((user) => { + return { + name: user.name, + email: user.email, + role: user.role, + author_id: user.authorId, + auto_renew: user.autoRenew, + notification: user.notification, + encryption: user.encryption, + encryption_password: user.encryptionPassword, + prompt: user.prompt, + }; + }), }); // Blobにファイルをアップロード(ユーザー一括登録用) From 146b8a6e4048895990d878319b8495e1ad495705 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 8 Mar 2024 01:09:14 +0000 Subject: [PATCH 041/109] =?UTF-8?q?Merged=20PR=20816:=20function=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 - 修正箇所がほかの機能に影響していないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/features/user/userSlice.ts | 5 +++++ dictation_client/src/pages/UserListPage/importPopup.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index b0fcf5c..98ccfe6 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -257,6 +257,10 @@ export const userSlice = createSlice({ const { users } = action.payload; state.apps.importUsers = users; }, + cleanupImportUsers: (state) => { + state.apps.importFileName = initialState.apps.importFileName; + state.apps.importUsers = initialState.apps.importUsers; + }, }, extraReducers: (builder) => { builder.addCase(listUsersAsync.pending, (state) => { @@ -353,6 +357,7 @@ export const { cleanupLicenseAllocateInfo, changeImportFileName, changeImportCsv, + cleanupImportUsers, } = userSlice.actions; export default userSlice.reducer; diff --git a/dictation_client/src/pages/UserListPage/importPopup.tsx b/dictation_client/src/pages/UserListPage/importPopup.tsx index d01d930..fcc879a 100644 --- a/dictation_client/src/pages/UserListPage/importPopup.tsx +++ b/dictation_client/src/pages/UserListPage/importPopup.tsx @@ -11,6 +11,7 @@ import { changeImportCsv, selectImportFileName, selectImportValidationErrors, + cleanupImportUsers, } from "features/user"; import { parseCSV } from "common/parser"; import close from "../../assets/images/close.svg"; @@ -31,8 +32,9 @@ export const ImportPopup: React.FC = (props) => { const closePopup = useCallback(() => { setIsPushImportButton(false); + dispatch(cleanupImportUsers()); onClose(); - }, [onClose]); + }, [onClose, dispatch]); const [isPushImportButton, setIsPushImportButton] = useState(false); const isLoading = useSelector(selectIsLoading); From 869cbd43e00940612ef970bb5defd7e7e0aaf00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Fri, 8 Mar 2024 05:20:00 +0000 Subject: [PATCH 042/109] =?UTF-8?q?Merged=20PR=20810:=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E9=80=80=E9=81=BF=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=83=96=E3=83=AB=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3848: アカウント退避テーブル作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3848) - アカウントテーブルと同様の構造をしたアカウント退避テーブルを作成 - ※差分: company_nameを削除、active_worktype_idの外部キー制約を削除 ## レビューポイント - 上記要素以外はaccountsテーブルの要素と同等であるか - 張られたインデックスに不足はないか - 想定と違う構造になっていないか ## 動作確認状況 - npm run migrate:up/downを実施 - EXPLAINでindexが機能していそうな事を確認 ``` EXPLAIN SELECT * FROM omds_ccb.accounts_archive where parent_account_id=1; --------------------------------------------------- # id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra '1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_parent_account_id', 'idx_accounts_archive_parent_account_id', '9', 'const', '1', '100.00', NULL ``` ``` EXPLAIN SELECT * FROM omds_ccb.accounts_archive where tier=2; --------------------------------------------------- # id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra '1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_tier', 'idx_accounts_archive_tier', '4', 'const', '2', '100.00', NULL ``` ``` EXPLAIN SELECT * FROM omds_ccb.accounts_archive where parent_account_id=1 AND tier=1; ---------------------------------------------------- # id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra '1', 'SIMPLE', 'accounts_archive', NULL, 'ref', 'idx_accounts_archive_parent_account_id,idx_accounts_archive_tier', 'idx_accounts_archive_parent_account_id', '9', 'const', '1', '50.00', 'Using where' ``` --- .../060-create_accounts_archive.sql | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dictation_server/db/migrations/060-create_accounts_archive.sql diff --git a/dictation_server/db/migrations/060-create_accounts_archive.sql b/dictation_server/db/migrations/060-create_accounts_archive.sql new file mode 100644 index 0000000..e6e8f57 --- /dev/null +++ b/dictation_server/db/migrations/060-create_accounts_archive.sql @@ -0,0 +1,24 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS `accounts_archive` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'ID', + `parent_account_id` BIGINT UNSIGNED COMMENT '親アカウントID', + `tier` INT UNSIGNED NOT NULL COMMENT '商流における階層', + `country` VARCHAR(16) NOT NULL COMMENT '国名(ISO 3166-1 alpha-2)', + `delegation_permission` BOOLEAN NOT NULL DEFAULT 0 COMMENT '上位階層からの代行操作を許可しているか', + `locked` BOOLEAN NOT NULL DEFAULT 0 COMMENT 'アカウントがロック済みであるか', + `verified` BOOLEAN NOT NULL DEFAULT 0 COMMENT 'email認証が完了済みであるか', + `primary_admin_user_id` BIGINT UNSIGNED COMMENT 'プライマリ管理者ユーザーID', + `secondary_admin_user_id` BIGINT UNSIGNED COMMENT 'セカンダリ管理者ユーザーID', + `active_worktype_id` BIGINT UNSIGNED COMMENT 'アカウントで利用するデフォルトのWorkTypeID(Active WorktypeID)の内部ID', + `auto_file_delete` BOOLEAN NOT NULL DEFAULT 0 COMMENT '自動ファイル削除をするかどうか', + `file_retention_days` INT UNSIGNED COMMENT '文字起こし完了してから自動ファイル削除するまでのファイル保持日数', + `deleted_at` TIMESTAMP COMMENT '削除時刻', + `created_by` VARCHAR(255) COMMENT '作成者', + `created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻', + `updated_by` VARCHAR(255) COMMENT '更新者', + `updated_at` TIMESTAMP DEFAULT now() COMMENT '更新時刻', + INDEX `idx_accounts_archive_tier` (`tier`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; + +-- +migrate Down +DROP TABLE `accounts_archive`; \ No newline at end of file From 85edd2296e2cadd43aa0163ffa77b3b2b3d32d51 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Fri, 8 Mar 2024 05:21:22 +0000 Subject: [PATCH 043/109] =?UTF-8?q?Merged=20PR=20820:=20DB=E3=83=9E?= =?UTF-8?q?=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E8=BF=BD=E5=8A=A0=EF=BC=88?= =?UTF-8?q?=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E9=9A=8E=E5=B1=A4?= =?UTF-8?q?=E6=A7=8B=E9=80=A0=E5=A4=89=E6=9B=B4PBI=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3863: DBマイグレーションファイル追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3863) - licenseOrdersテーブルに、from_account_idとstatus検索のマルチカラムインデックスを追加しました。 - 影響範囲(他の機能にも影響があるか) - なし ## レビューポイント - 気になる点あれば ## 動作確認状況 - ローカルでmigrate:up/downをして、想定通りindex作成/削除されることを確認 - 行った修正がデグレを発生させていないことを確認できるか - インデックス追加のみのため無し --- .../db/migrations/061-add_license_orders_index.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 dictation_server/db/migrations/061-add_license_orders_index.sql diff --git a/dictation_server/db/migrations/061-add_license_orders_index.sql b/dictation_server/db/migrations/061-add_license_orders_index.sql new file mode 100644 index 0000000..98004c9 --- /dev/null +++ b/dictation_server/db/migrations/061-add_license_orders_index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `license_orders` ADD INDEX `idx_from_account_id_and_status` (from_account_id,status); + +-- +migrate Down +ALTER TABLE `license_orders` DROP INDEX `idx_from_account_id_and_status`; From 2e6b7c8ab5280647905ff823fafdfb1c3e9aac58 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 8 Mar 2024 05:45:53 +0000 Subject: [PATCH 044/109] =?UTF-8?q?Merged=20PR=20818:=20function=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879) - 行番号をExcelの表記通りとなるように修正 - 途中から始められるようにforループを修正 ## レビューポイント - 共有 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/features/user/selectors.ts | 2 +- dictation_function/src/constants/index.ts | 2 ++ dictation_function/src/functions/importUsers.ts | 16 +++++++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index dc25752..eb9852c 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -389,7 +389,7 @@ export const selectImportFileName = (state: RootState) => export const selectImportValidationErrors = (state: RootState) => { const csvUsers = state.user.apps.importUsers; - let rowNumber = 0; + let rowNumber = 1; const invalidInput: number[] = []; const duplicatedEmailsMap = new Map(); diff --git a/dictation_function/src/constants/index.ts b/dictation_function/src/constants/index.ts index f955c2d..fe21f13 100644 --- a/dictation_function/src/constants/index.ts +++ b/dictation_function/src/constants/index.ts @@ -335,3 +335,5 @@ export const RoleNumberMap: Record = { } as const; export const SYSTEM_IMPORT_USERS = "import-users"; + +export const ROW_START_INDEX = 2; diff --git a/dictation_function/src/functions/importUsers.ts b/dictation_function/src/functions/importUsers.ts index 40a281b..38716b9 100644 --- a/dictation_function/src/functions/importUsers.ts +++ b/dictation_function/src/functions/importUsers.ts @@ -7,6 +7,7 @@ import { IMPORT_USERS_STAGE_FILE_NAME, IMPORT_USERS_STAGES, RoleNumberMap, + ROW_START_INDEX, SYSTEM_IMPORT_USERS, TIERS, } from "../constants"; @@ -47,7 +48,7 @@ export async function importUsersProcessing( if (targetFileName === undefined) { throw new Error("targetFileName is undefined"); } - let row = 1; + let row = ROW_START_INDEX; // stage.jsonを取得(ダウンロード)して読み込む let stageData = await blobstorageService.downloadFileData( @@ -78,24 +79,29 @@ export async function importUsersProcessing( if (!isStageJson(stage)) { throw new Error("stage.json is invalid"); } + context.log(`start stage: ${JSON.stringify(stage)}`); // 作業中のstage.jsonが存在する場合は、処理を再開する if ( stage.state !== IMPORT_USERS_STAGES.CREATED && stage.state !== IMPORT_USERS_STAGES.DONE ) { + context.log( + `stage is pending. filename: ${stage.filename} row: ${stage.row}` + ); + // stage.jsonが存在し、内部状態が処理中で、最終更新日時が10分以上前だった場合は処理中断とみなして途中から再開 const nowUnixTime = getCurrentUnixTime(); if (nowUnixTime - stage.update > 10 * 60) { // stage.jsonの内容から処理対象のfilepathを特定する - context.log(stage.filename); + context.log(`pending filename: ${stage.filename}`); if (stage.filename === undefined) { context.log("stage.filename is undefined"); break; } targetFileName = stage.filename; // 処理開始行をstage.jsonを元に復元する - row = stage.row ?? 1; + row = stage.row ?? ROW_START_INDEX; } else { // 内部状態が処理中であれば処理中断(処理が終わる前にTimerから再度起動されてしまったケース) context.log("stage is processing"); @@ -144,9 +150,9 @@ export async function importUsersProcessing( imports.user_role ); - // 一括登録ユーザー一覧をループして、一括登録ユーザーを一括登録する const errors: ErrorRow[] = []; - for (const user of imports.data) { + // 一括登録ユーザー一覧をループして、一括登録ユーザーを一括登録する(中断された場合は途中から再開するため、sliceで開始行を指定する) + for (const user of imports.data.slice(row - ROW_START_INDEX)) { { // stage.jsonを更新(ユーザー追加開始) const updateSuccess = await blobstorageService.updateFile( From f386a8f7e0a29fec6fd2bca42d95d9680ee9192e Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Mon, 11 Mar 2024 01:29:55 +0000 Subject: [PATCH 045/109] =?UTF-8?q?Merged=20PR=20817:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E8=A6=AA=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=88=E5=A4=89=E6=9B=B4API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3852: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3852) - 親アカウント変更APIのIFを実装し、OpenAPIの生成もしました。 - 影響範囲(他の機能にも影響があるか) - なし ## レビューポイント - controllerのメソッド名にほか良い案ないか? - validationに過不足や間違いないか? - ~~controllerのテストは正常系ひとつだけ追加しているが、他にあったほうがいいものあるか?~~ - ~~個人的には、テスト追加してもnpmライブラリのvalidatorのテストになるだけな気がするため不要では?と思っています。~~ - 「npmライブラリのvalidatorを正しいパラメータで正しく利用しているか」が目的であるとの認識を得たため異常系も追加しました。 ## 動作確認状況 - apigenを実行してOpenAPI生成できることを確認、controllerテスト通ることを確認。 - 行った修正がデグレを発生させていないことを確認できるか - 新規APIのため無し --- dictation_server/src/api/odms/openapi.json | 70 ++++++++++++++++ .../accounts/accounts.controller.spec.ts | 38 +++++++++ .../features/accounts/accounts.controller.ts | 82 +++++++++++++++++++ .../src/features/accounts/types/types.ts | 23 ++++++ 4 files changed, 213 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index c279e1f..c1b24bf 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1664,6 +1664,59 @@ "security": [{ "bearer": [] }] } }, + "/accounts/parent/switch": { + "post": { + "operationId": "switchParent", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SwitchParentRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchParentResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -4541,6 +4594,23 @@ "required": ["accountId", "restricted"] }, "UpdateRestrictionStatusResponse": { "type": "object", "properties": {} }, + "SwitchParentRequest": { + "type": "object", + "properties": { + "to": { + "type": "number", + "description": "切り替え先の親アカウントID" + }, + "children": { + "minItems": 1, + "description": "親を変更したいアカウントIDのリスト", + "type": "array", + "items": { "type": "integer" } + } + }, + "required": ["to", "children"] + }, + "SwitchParentResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { "type": "object", "properties": { "token": { "type": "string" } }, diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index f69ece1..0ccef3d 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -3,6 +3,9 @@ import { AccountsController } from './accounts.controller'; import { AccountsService } from './accounts.service'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from '../auth/auth.service'; +import { SwitchParentRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('AccountsController', () => { let controller: AccountsController; @@ -32,4 +35,39 @@ describe('AccountsController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation switchParentRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = [2]; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('子アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = []; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('子アカウントが重複指定されている場合、リクエストが失敗する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = [2, 2]; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index dceff54..3cb553a 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -77,6 +77,8 @@ import { UpdateFileDeleteSettingResponse, UpdateRestrictionStatusRequest, UpdateRestrictionStatusResponse, + SwitchParentRequest, + SwitchParentResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -2319,4 +2321,84 @@ export class AccountsController { return {}; } + + @ApiResponse({ + status: HttpStatus.OK, + type: SwitchParentResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'switchParent' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2], + }), + ) + @Post('parent/switch') + async switchParent( + @Req() req: Request, + @Body() body: SwitchParentRequest, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO:service層を呼び出す。本実装時に以下は削除する。 + const { to, children } = body; + this.logger.log( + `[${context.getTrackingId()}] to : ${to}, children : ${children.join( + ', ', + )}`, + ); + + return {}; + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 22da103..110d25d 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -369,6 +369,27 @@ export class UpdateRestrictionStatusRequest { restricted: boolean; } +export class SwitchParentRequest { + @ApiProperty({ description: '切り替え先の親アカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + to: number; + + @ApiProperty({ + minItems: 1, + isArray: true, + type: 'integer', + description: '親を変更したいアカウントIDのリスト', + }) + @ArrayMinSize(1) + @IsArray() + @IsInt({ each: true }) + @Min(1, { each: true }) + @IsUnique() + children: number[]; +} + // ============================== // RESPONSE // ============================== @@ -686,6 +707,8 @@ export class UpdateFileDeleteSettingResponse {} export class UpdateRestrictionStatusResponse {} +export class SwitchParentResponse {} + // ============================== // Request/Response外の型 // TODO: Request/Response/その他の型を別ファイルに分ける From ff4cd35ed3ad9240a97b031da0f80d9e58d20888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Mon, 11 Mar 2024 02:08:29 +0000 Subject: [PATCH 046/109] =?UTF-8?q?Merged=20PR=20819:=20[3848]=E3=82=A2?= =?UTF-8?q?=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E5=89=8A=E9=99=A4=E5=87=A6?= =?UTF-8?q?=E7=90=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3847: [3848]アカウント削除処理修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3847) - AccountArchiveエンティティを追加 - アカウント削除時、Accountをアーカイブする処理を追加 - アーカイブ関連テストを追加 ## レビューポイント - 実装の修正内容は問題なさそうか - テストケースの修正内容は問題なさそうか - クエリの変更内容の確認方法は問題なさそうか to 斎藤さん ## レビュー対象外 - テスト用ロガーに以下の比較用前処理を追加するべきだが、別タスクを作って対応予定 - 下記クエリの変更点にて、CommentOut判定に環境変数STAGEを使用している部分にRequestIdが表示されていない部分が存在するが、テスト用環境変数の変更は上記と同じく別タスクを作って対応予定 [タスク 3889: クエリ比較用ログ出力の仕組みを改良](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/3889) ## クエリの変更 - ロガーを有効にした状態でテストを実行し、ログのUUIDと日付を処理して比較できるよう加工した - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/3847?csf=1&web=1&e=xlK011 ## 動作確認状況 - npm run testで確認 - 行った修正がデグレを発生させていないことを確認できるか - アカウント削除テストで発行されるクエリを比較し、AccountArchiveする対象を特定するためのAccountのSELECT、AccountArchiveのINSERTとSELECTのみが追加されている事が確認できたので、デグレはないと判断 --- dictation_server/src/common/test/logger.ts | 65 ++++++++++++++++++ dictation_server/src/common/test/utility.ts | 7 ++ .../accounts/accounts.service.spec.ts | 34 ++++++++++ .../accounts/accounts.repository.service.ts | 19 +++++- .../accounts/entity/account_archive.entity.ts | 66 +++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 dictation_server/src/common/test/logger.ts create mode 100644 dictation_server/src/repositories/accounts/entity/account_archive.entity.ts diff --git a/dictation_server/src/common/test/logger.ts b/dictation_server/src/common/test/logger.ts new file mode 100644 index 0000000..a6f3ba1 --- /dev/null +++ b/dictation_server/src/common/test/logger.ts @@ -0,0 +1,65 @@ +import { Logger, QueryRunner } from 'typeorm'; +import * as fs from 'fs'; +import * as path from 'path'; + +export class FileLogger implements Logger { + private logPath = path.join(__dirname, 'logs'); + + constructor() { + if (!fs.existsSync(this.logPath)) { + fs.mkdirSync(this.logPath, { recursive: true }); + } + } + + private writeToFile(message: string): void { + const logFile = path.join( + this.logPath, + `${new Date().toISOString().split('T')[0]}.log`, + ); + fs.appendFileSync(logFile, `${message}\n`); + } + + logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { + this.writeToFile( + `Query: ${query} -- Parameters: ${JSON.stringify(parameters)}`, + ); + } + + logQueryError( + error: string, + query: string, + parameters?: any[], + queryRunner?: QueryRunner, + ) { + this.writeToFile( + `ERROR: ${error} -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters, + )}`, + ); + } + + logQuerySlow( + time: number, + query: string, + parameters?: any[], + queryRunner?: QueryRunner, + ) { + this.writeToFile( + `SLOW QUERY: ${time}ms -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters, + )}`, + ); + } + + logSchemaBuild(message: string, queryRunner?: QueryRunner) { + this.writeToFile(`Schema Build: ${message}`); + } + + logMigration(message: string, queryRunner?: QueryRunner) { + this.writeToFile(`Migration: ${message}`); + } + + log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner) { + this.writeToFile(`${level.toUpperCase()}: ${message}`); + } +} diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index 6fb99ab..28c8b0c 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -8,6 +8,7 @@ import { USER_ROLES, } from '../../constants'; import { License } from '../../repositories/licenses/entity/license.entity'; +import { AccountArchive } from '../../repositories/accounts/entity/account_archive.entity'; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -398,6 +399,12 @@ export const getUsers = async (dataSource: DataSource): Promise => { * @param dataSource データソース * @returns ユーザー退避テーブルの内容 */ +export const getAccountArchive = async ( + dataSource: DataSource, +): Promise => { + return await dataSource.getRepository(AccountArchive).find(); +}; + export const getUserArchive = async ( dataSource: DataSource, ): Promise => { diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 47a1188..6eb5918 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -39,6 +39,7 @@ import { getUser, getLicenses, getUserArchive, + getAccountArchive, } from '../../common/test/utility'; import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; @@ -7158,6 +7159,11 @@ describe('deleteAccountAndData', () => { ); expect(LicenseAllocationHistoryRecordB.length).not.toBe(0); + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5AccountsA.account.id); + const UserArchive = await getUserArchive(source); expect(UserArchive.length).toBe(2); @@ -7238,6 +7244,12 @@ describe('deleteAccountAndData', () => { expect(accountRecord?.id).not.toBeNull(); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord?.id).not.toBeNull(); + + // アーカイブが作成されていないことを確認 + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(0); + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(0); }); it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行', async () => { if (!source) fail(); @@ -7296,6 +7308,17 @@ describe('deleteAccountAndData', () => { expect(accountRecord).toBe(null); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord).toBe(null); + + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5Accounts.account.id); + + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(2); + const expectUserIds = [tier5Accounts.admin.id, user.id].sort(); + const userArchiveIds = userArchive.map((x) => x.id).sort(); + expect(expectUserIds).toStrictEqual(userArchiveIds); }); it('blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了', async () => { if (!source) fail(); @@ -7355,6 +7378,17 @@ describe('deleteAccountAndData', () => { expect(accountRecord).toBe(null); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord).toBe(null); + + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5Accounts.account.id); + + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(2); + const expectUserIds = [tier5Accounts.admin.id, user.id].sort(); + const userArchiveIds = userArchive.map((x) => x.id).sort(); + expect(expectUserIds).toStrictEqual(userArchiveIds); }); }); describe('getAccountInfoMinimalAccess', () => { diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index a2d4ad5..f15a428 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -64,6 +64,7 @@ import { PartnerInfoFromDb, PartnerLicenseInfoForRepository, } from '../../features/accounts/types/types'; +import { AccountArchive } from './entity/account_archive.entity'; @Injectable() export class AccountsRepositoryService { @@ -1156,6 +1157,23 @@ export class AccountsRepositoryService { accountId: number, ): Promise { return await this.dataSource.transaction(async (entityManager) => { + // 削除対象のアカウントを退避テーブルに退避 + const accountRepo = entityManager.getRepository(Account); + const account = await accountRepo.find({ + where: { + id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + const accountArchiveRepo = entityManager.getRepository(AccountArchive); + await insertEntities( + AccountArchive, + accountArchiveRepo, + account, + this.isCommentOut, + context, + ); + // 削除対象のユーザーを退避テーブルに退避 const users = await this.dataSource.getRepository(User).find({ where: { @@ -1209,7 +1227,6 @@ export class AccountsRepositoryService { ); // アカウントを削除 - const accountRepo = entityManager.getRepository(Account); await deleteEntity( accountRepo, { id: accountId }, diff --git a/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts b/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts new file mode 100644 index 0000000..d78c0ba --- /dev/null +++ b/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts @@ -0,0 +1,66 @@ +import { bigintTransformer } from '../../../common/entity'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'accounts_archive' }) +export class AccountArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + parent_account_id: number | null; + + @Column() + tier: number; + + @Column() + country: string; + + @Column({ default: false }) + delegation_permission: boolean; + + @Column({ default: false }) + locked: boolean; + + @Column({ default: false }) + verified: boolean; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + primary_admin_user_id: number | null; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + secondary_admin_user_id: number | null; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + active_worktype_id: number | null; + + @Column({ default: false }) + auto_file_delete: boolean; + + @Column({ default: 0 }) + file_retention_days: number; + + @Column({ nullable: true, type: 'datetime' }) + deleted_at: Date | null; + + @Column({ nullable: true, type: 'datetime' }) + created_by: string | null; + + @CreateDateColumn({ + type: 'datetime', + }) + created_at: Date; + + @Column({ nullable: true, type: 'datetime' }) + updated_by: string | null; + + @UpdateDateColumn({ + type: 'datetime', + }) + updated_at: Date; +} From ccc03da62dd0e2d369f65f825a83473cc1d0b565 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 11 Mar 2024 02:52:55 +0000 Subject: [PATCH 047/109] =?UTF-8?q?Merged=20PR=20823:=20=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E5=A4=89=E6=95=B0=E3=81=AE=E8=BF=BD=E5=BE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3891: 環境変数の追従](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3891) - `server`に追加した環境変数の追従が`.env.local.example`から漏れていたので追加 ## レビューポイント - 共有 ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - exampleを追加したのみなので動作に影響はなし --- dictation_server/.env.local.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 8c03436..78c2af2 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -20,12 +20,15 @@ STORAGE_TOKEN_EXPIRE_TIME=2 STORAGE_ACCOUNT_NAME_US=saodmsusdev STORAGE_ACCOUNT_NAME_AU=saodmsaudev STORAGE_ACCOUNT_NAME_EU=saodmseudev +STORAGE_ACCOUNT_NAME_IMPORTS=saodmseudev STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX +STORAGE_ACCOUNT_KEY_IMPORTS=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA +STORAGE_ACCOUNT_ENDPOINT_IMPORTS=https://AAAAAAAAAAAAA ACCESS_TOKEN_LIFETIME_WEB=7200 REFRESH_TOKEN_LIFETIME_WEB=86400 REFRESH_TOKEN_LIFETIME_DEFAULT=2592000 From 84fc89071a02b1bc73fae672c372ad82f76c8075 Mon Sep 17 00:00:00 2001 From: masaaki Date: Mon, 11 Mar 2024 06:04:02 +0000 Subject: [PATCH 048/109] =?UTF-8?q?Merged=20PR=20811:=20AzureFunctions?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=91=EF=BC=88DB=E3=81=8B=E3=82=89?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E3=81=AA=E6=83=85=E5=A0=B1=E3=82=92=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=99=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3842: AzureFunctions実装1(DBから必要な情報を取得する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3842) - ライセンス数推移情報CSV出力機能のAzureFuntion用関数を追加しました - DBから必要な情報を取得する処理を実装しました ## レビューポイント - 特にレビューしてほしい箇所 - DBアクセス時の結合・検索条件はラフスケッチの条件に対して過不足ないか - テストケースに不足はないか - 関数名、構造は分かりづらくないか ## UIの変更 - 無し ## クエリの変更 - 新規のため無し ## 動作確認状況 - ローカルで処理が正常終了することを確認 - ユニットテストが通ることを確認 - 行った修正がデグレを発生させていないことを確認できるか - Function全体のunittestを実施し通ることを確認 ## 補足 - 相談、参考資料などがあれば --- .../src/entity/account.entity.ts | 85 +- .../src/entity/license.entity.ts | 129 ++- dictation_function/src/entity/user.entity.ts | 108 ++- .../src/functions/analysisLicenses.ts | 389 +++++++++ .../src/test/analysisLicenses.spec.ts | 737 ++++++++++++++++++ dictation_function/src/test/common/utility.ts | 262 ++++++- 6 files changed, 1694 insertions(+), 16 deletions(-) create mode 100644 dictation_function/src/functions/analysisLicenses.ts create mode 100644 dictation_function/src/test/analysisLicenses.spec.ts diff --git a/dictation_function/src/entity/account.entity.ts b/dictation_function/src/entity/account.entity.ts index 3a1c9af..eed6190 100644 --- a/dictation_function/src/entity/account.entity.ts +++ b/dictation_function/src/entity/account.entity.ts @@ -1,5 +1,6 @@ import { bigintTransformer } from "../common/entity"; -import { User } from "./user.entity"; +import { User, UserArchive } from "./user.entity"; +import { License, LicenseArchive } from "./license.entity"; import { Entity, Column, @@ -75,6 +76,86 @@ export class Account { @JoinColumn({ name: "secondary_admin_user_id" }) secondaryAdminUser: User | null; - @OneToMany(() => User, (user) => user.id) + @OneToMany(() => User, (user) => user.account) + @JoinColumn({ name: "id" }) user: User[] | null; + + @OneToMany(() => UserArchive, (userArchive) => userArchive.account) + @JoinColumn({ name: "id" }) + userArchive: UserArchive[] | null; + + @OneToMany(() => License, (license) => license.account) + licenses: License[] | null; +} + +@Entity({ name: "accounts_archive" }) +export class AccountArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + parent_account_id: number | null; + + @Column() + tier: number; + + @Column() + country: string; + + @Column({ default: false }) + delegation_permission: boolean; + + @Column({ default: false }) + locked: boolean; + + @Column({ default: false }) + verified: boolean; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + primary_admin_user_id: number | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + secondary_admin_user_id: number | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + active_worktype_id: number | null; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; + + @OneToOne(() => UserArchive, (userArchive) => userArchive.id) + @JoinColumn({ name: "primary_admin_user_id" }) + primaryAdminUser: UserArchive | null; + + @OneToOne(() => UserArchive, (userArchive) => userArchive.id) + @JoinColumn({ name: "secondary_admin_user_id" }) + secondaryAdminUser: UserArchive | null; + + @OneToMany(() => UserArchive, (userArchive) => userArchive.account) + @JoinColumn({ name: "id" }) + userArchive: UserArchive[] | null; + + @OneToMany( + () => LicenseArchive, + (licenseArchive) => licenseArchive.accountArchive + ) + licensesArchive: LicenseArchive[] | null; } diff --git a/dictation_function/src/entity/license.entity.ts b/dictation_function/src/entity/license.entity.ts index 56f2f00..c4411f7 100644 --- a/dictation_function/src/entity/license.entity.ts +++ b/dictation_function/src/entity/license.entity.ts @@ -9,8 +9,8 @@ import { ManyToOne, } from "typeorm"; import { bigintTransformer } from "../common/entity"; -import { User } from "./user.entity"; - +import { User, UserArchive } from "./user.entity"; +import { Account, AccountArchive } from "./account.entity"; @Entity({ name: "licenses" }) export class License { @PrimaryGeneratedColumn() @@ -61,6 +61,10 @@ export class License { @OneToOne(() => User, (user) => user.license) @JoinColumn({ name: "allocated_user_id" }) user: User | null; + + @ManyToOne(() => Account, (account) => account.licenses) + @JoinColumn({ name: "account_id" }) + account: Account | null; } @Entity({ name: "license_allocation_history" }) @@ -112,4 +116,125 @@ export class LicenseAllocationHistory { }) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 @JoinColumn({ name: "license_id" }) license: License | null; + + @ManyToOne(() => User, (user) => user.licenseAllocationHistory) // Userエンティティとの関連を設定 + @JoinColumn({ name: "user_id" }) // user_idを外部キーとして使用 + user: User; +} + +@Entity({ name: "licenses_archive" }) +export class LicenseArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true, type: "datetime" }) + expiry_date: Date | null; + + @Column() + account_id: number; + + @Column() + type: string; + + @Column() + status: string; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + allocated_user_id: number | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + order_id: number | null; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + delete_order_id: number | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + updated_at: Date; + + @OneToOne(() => UserArchive, (userArchive) => userArchive.licenseArchive) + @JoinColumn({ name: "allocated_user_id" }) + userArchive: UserArchive | null; + + @ManyToOne( + () => AccountArchive, + (accountArchive) => accountArchive.licensesArchive + ) + @JoinColumn({ name: "account_id" }) + accountArchive: AccountArchive | null; +} + +@Entity({ name: "license_allocation_history_archive" }) +export class LicenseAllocationHistoryArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column() + user_id: number; + + @Column() + license_id: number; + + @Column() + is_allocated: boolean; + + @Column() + account_id: number; + + @Column() + executed_at: Date; + + @Column() + switch_from_type: string; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + updated_at: Date; + + @ManyToOne(() => LicenseArchive, (licensesArchive) => licensesArchive.id, { + createForeignKeyConstraints: false, + }) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 + @JoinColumn({ name: "license_id" }) + license: LicenseArchive | null; + + @ManyToOne( + () => UserArchive, + (userArchive) => userArchive.licenseAllocationHistoryArchive + ) // Userエンティティとの関連を設定 + @JoinColumn({ name: "user_id" }) // user_idを外部キーとして使用 + userArchive: UserArchive; } diff --git a/dictation_function/src/entity/user.entity.ts b/dictation_function/src/entity/user.entity.ts index 10032d4..a63e9c0 100644 --- a/dictation_function/src/entity/user.entity.ts +++ b/dictation_function/src/entity/user.entity.ts @@ -7,9 +7,10 @@ import { OneToOne, JoinColumn, ManyToOne, + OneToMany, } from "typeorm"; -import { License } from "./license.entity"; -import { Account } from "./account.entity"; +import { License, LicenseAllocationHistory, LicenseArchive, LicenseAllocationHistoryArchive } from "./license.entity"; +import { Account, AccountArchive } from "./account.entity"; @Entity({ name: "users" }) export class User { @@ -46,9 +47,6 @@ export class User { @Column({ default: false }) encryption: boolean; - @Column({ nullable: true, type: "varchar" }) - encryption_password: string | null; - @Column({ default: false }) prompt: boolean; @@ -81,4 +79,104 @@ export class User { @OneToOne(() => License, (license) => license.user) license: License | null; + + @OneToMany( + () => LicenseAllocationHistory, + (licenseAllocationHistory) => licenseAllocationHistory.user + ) + licenseAllocationHistory: LicenseAllocationHistory[] | null; +} + + +@Entity({ name: "users_archive" }) +export class UserArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column() + external_id: string; + + @Column() + account_id: number; + + @Column() + role: string; + + @Column({ nullable: true, type: "varchar" }) + author_id: string | null; + + @Column({ nullable: true, type: "varchar" }) + accepted_eula_version: string | null; + + @Column({ nullable: true, type: "varchar" }) + accepted_dpa_version: string | null; + + @Column({ default: false }) + email_verified: boolean; + + @Column({ default: true }) + auto_renew: boolean; + + @Column({ default: true }) + notification: boolean; + + @Column({ default: false }) + encryption: boolean; + + @Column({ default: false }) + prompt: boolean; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; + + @ManyToOne( + () => Account, + (account) => account.userArchive, + { + createForeignKeyConstraints: false, + } + ) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 + @JoinColumn({ name: "account_id" }) + account: Account | null; + + @ManyToOne( + () => AccountArchive, + (accountArchive) => accountArchive.userArchive, + { + createForeignKeyConstraints: false, + } + ) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 + @JoinColumn({ name: "account_id" }) + accountArchive: AccountArchive | null; + + @OneToOne( + () => LicenseArchive, + (licenseArchive) => licenseArchive.userArchive + ) + licenseArchive: LicenseArchive | null; + + @OneToMany( + () => LicenseAllocationHistoryArchive, + (licenseAllocationHistoryArchive) => + licenseAllocationHistoryArchive.userArchive + ) + licenseAllocationHistoryArchive: LicenseAllocationHistoryArchive[] | null; } diff --git a/dictation_function/src/functions/analysisLicenses.ts b/dictation_function/src/functions/analysisLicenses.ts new file mode 100644 index 0000000..3d40276 --- /dev/null +++ b/dictation_function/src/functions/analysisLicenses.ts @@ -0,0 +1,389 @@ +import { app, InvocationContext, Timer } from "@azure/functions"; +import { DataSource, Between } from "typeorm"; +import * as dotenv from "dotenv"; +import { User, UserArchive } from "../entity/user.entity"; +import { Account, AccountArchive } from "../entity/account.entity"; +import { License, LicenseAllocationHistory, LicenseArchive, LicenseAllocationHistoryArchive } from "../entity/license.entity"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { LICENSE_ALLOCATED_STATUS, TIERS, SWITCH_FROM_TYPE } from "../constants"; +import { DateWithDayEndTime } from "../common/types/types"; + +/** + * ライセンス数分析処理のメイン処理:ここから各処理を呼び出す + * @param myTimer + * @param context + */ +export async function analysisLicensesProcessing( + context: InvocationContext, + targetMonthYYYYMM: string, + datasource: DataSource, + blobstorageService: BlobstorageService, +) { + + try { + context.log("[IN]analysisLicensesProcessing"); + + const baseData = await getBaseData(context, targetMonthYYYYMM, datasource); + const baseDataFromDeletedAccounts = await getBaseDataFromDeletedAccounts( + context, + targetMonthYYYYMM, + datasource + ); + // TODO: 後続処理の呼び出しイメージ(別タスクで追加) + // const outputCsvData = await transferData(context, baseData, baseDataFromDeletedAccounts); + // await outputData(context, blobstorageService, outputCsvData); + } catch (e) { + context.log("analysisLicensesProcessing failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]analysisLicensesProcessing"); + } +} + + +/** + * 集計元のデータをDBから取得する処理 + * @param context + * @param targetMonthYYYYMM + * @param datasource + * 内部関数だがテスト用にexportする + */ +export async function getBaseData( + context: InvocationContext, + targetMonthYYYYMM: string, + datasource: DataSource +): Promise { + try { + context.log("[IN]getBaseData"); + + // 第五階層のアカウントとユーザーを取得する + // 第五のアカウントを取得 + const accountRepository = datasource.getRepository(Account); + const accountsAndUsersFromTier5 = await accountRepository.find({ + where: { + tier: TIERS.TIER5, + }, + relations: { + user: true, + userArchive: true + }, + }); + + // 第五階層が保持する有効なライセンスを取得 + const licenseRepository = datasource.getRepository(License); + // 現在時刻を起点とした23:59:59の日付 + const currentDateWithDayEndTime = new DateWithDayEndTime(); + const avairableLicenses = await licenseRepository + .createQueryBuilder("license") + .innerJoin("license.account", "account") + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere( + "(license.expiry_date > :currentDateWithDayEndTime OR license.expiry_date IS NULL)", + { + currentDateWithDayEndTime, + } + ) + .andWhere("license.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に発行したライセンスを取得 + + // timestamp型のDB列をyyyymm形式に変換する場合RDBMS依存の処理が必要になるので、最初の日~最後の日のbetweenで判断する + const year = parseInt(targetMonthYYYYMM.slice(0, 4), 10); + const month = parseInt(targetMonthYYYYMM.slice(4), 10) - 1; // JavaScriptの月は0-indexed + const targetMonthStartDate = new Date(year, month, 1, 0, 0, 0, 0); + const targetMonthEndDate = new Date(year, month + 1, 0, 23, 59, 59, 0); // mysql上ミリ秒999を指定すると四捨五入されて翌日になってしまうのでミリ秒は0を指定 + + const licensesIssuedInTargetMonth = await licenseRepository + .createQueryBuilder("license") + .innerJoin("license.account", "account") + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + created_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に失効したライセンスを取得 + const licensesExpiredInTargetMonth = await licenseRepository + .createQueryBuilder("license") + .innerJoin("license.account", "account") + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + expiry_date: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層がその月におこなったライセンス切り替え情報を取得 + const licenseAllocationHistory = datasource.getRepository( + LicenseAllocationHistory + ); + const switchedlicensesInTargetMonth = await licenseAllocationHistory + .createQueryBuilder("licenseAllocationHistory") + .innerJoinAndSelect("licenseAllocationHistory.user", "user") + .innerJoin("user.account", "account") + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere("licenseAllocationHistory.switch_from_type IN (:...types)", { + types: [SWITCH_FROM_TYPE.CARD, SWITCH_FROM_TYPE.TRIAL], + }) + .andWhere({ + executed_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .getMany(); + + return { + accountsAndUsersFromTier5, + avairableLicenses, + licensesIssuedInTargetMonth, + licensesExpiredInTargetMonth, + switchedlicensesInTargetMonth, + }; + } catch (e) { + context.log("getBaseData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]getBaseData"); + } +} + +/** + * 集計元のデータ(削除されたアカウントの情報)をDBから取得する処理 + * @param context + * @param targetMonthYYYYMM + * @param datasource + * 内部関数だがテスト用にexportする + */ +export async function getBaseDataFromDeletedAccounts( + context: InvocationContext, + targetMonthYYYYMM: string, + datasource: DataSource +): Promise { + try { + context.log("[IN]getBaseDataFromDeletedAccounts"); + + // 第五階層のアカウントとユーザーを取得する + // 第五のアカウントを取得 + const accountArchiveRepository = datasource.getRepository(AccountArchive); + const deletedAccountsAndUsersFromTier5 = await accountArchiveRepository.find({ + where: { + tier: TIERS.TIER5, + }, + relations: { userArchive: true}, + }); + + // 第五階層が保持する有効なライセンスを取得 + const licenseArchiveRepository = datasource.getRepository(LicenseArchive); + // 現在時刻を起点とした23:59:59の日付 + const currentDateWithDayEndTime = new DateWithDayEndTime(); + const deletedAvairableLicenses = await licenseArchiveRepository + .createQueryBuilder("license_archive") + .innerJoin("license_archive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere( + "(license_archive.expiry_date > :currentDateWithDayEndTime OR license_archive.expiry_date IS NULL)", + { + currentDateWithDayEndTime, + } + ) + .andWhere("license_archive.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に発行したライセンスを取得 + + // timestamp型のDB列をyyyymm形式に変換する場合RDBMS依存の処理が必要になるので、最初の日~最後の日のbetweenで判断する + const year = parseInt(targetMonthYYYYMM.slice(0, 4), 10); + const month = parseInt(targetMonthYYYYMM.slice(4), 10) - 1; // JavaScriptの月は0-indexed + const targetMonthStartDate = new Date(year, month, 1, 0, 0, 0, 0); + const targetMonthEndDate = new Date(year, month + 1, 0, 23, 59, 59, 0); // mysql上ミリ秒999を指定すると四捨五入されて翌日になってしまうのでミリ秒は0を指定 + + const deletedLicensesIssuedInTargetMonth = await licenseArchiveRepository + .createQueryBuilder("license_archive") + .innerJoin("license_archive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + created_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license_archive.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に失効したライセンスを取得 + const deletedLicensesExpiredInTargetMonth = await licenseArchiveRepository + .createQueryBuilder("license_archive") + .innerJoin("license_archive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + expiry_date: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license_archive.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層がその月におこなったライセンス切り替え情報を取得 + const licenseAllocationHistoryArchive = datasource.getRepository( + LicenseAllocationHistoryArchive + ); + const deletedSwitchedlicensesInTargetMonth = + await licenseAllocationHistoryArchive + .createQueryBuilder("licenseAllocationHistory_archive") + .innerJoinAndSelect( + "licenseAllocationHistory_archive.userArchive", + "userArchive" + ) + .innerJoin("userArchive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere( + "licenseAllocationHistory_archive.switch_from_type IN (:...types)", + { + types: [SWITCH_FROM_TYPE.CARD, SWITCH_FROM_TYPE.TRIAL], + } + ) + .andWhere({ + executed_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .getMany(); + + return { + deletedAccountsAndUsersFromTier5, + deletedAvairableLicenses, + deletedLicensesIssuedInTargetMonth, + deletedLicensesExpiredInTargetMonth, + deletedSwitchedlicensesInTargetMonth, + }; + } catch (e) { + context.log("getBaseDataFromDeletedAccounts failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]getBaseDataFromDeletedAccounts"); + } +} + + +/** + * ライセンス数分析処理:Azure Functionの関数として呼び出される処理 + * @param myTimer + * @param context + */ +export async function analysisLicenses( + myTimer: Timer, + context: InvocationContext +): Promise { + context.log("[IN]analysisLicenses"); + + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + let datasource: DataSource; + try { + try { + datasource = new DataSource({ + type: "mysql", + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME_CCB, + entities: [ + User, + Account, + License, + LicenseAllocationHistory, + UserArchive, + AccountArchive, + LicenseArchive, + LicenseAllocationHistoryArchive, + ], + }); + await datasource.initialize(); + } catch (e) { + context.log("database initialize failed."); + context.error(e); + throw e; + } + + const blobstorageService = new BlobstorageService(); + + try { + // 現在の日付より、先月の年月をYYYYMM形式で取得 + const currentDate = new Date(); + currentDate.setMonth(currentDate.getMonth() - 1); + const year = currentDate.getFullYear(); + const month = (currentDate.getMonth() + 1).toString().padStart(2, "0"); // 月は0から始まるため+1する + const formattedDate = `${year}${month}`; + + await analysisLicensesProcessing( + context, + formattedDate, + datasource, + blobstorageService + ); + } catch (e) { + context.log("analysisLicensesProcessing failed."); + context.error(e); + throw e; + } + } finally { + context.log("[OUT]analysisLicenses"); + } +} + +app.timer("analysisLicenses", { + schedule: "0 0 0 1 * *", + handler: analysisLicenses, +}); + + +type BaseData = { + // 存在するアカウントの集計元情報 + accountsAndUsersFromTier5: Account[]; + avairableLicenses: License[]; + licensesIssuedInTargetMonth: License[]; + licensesExpiredInTargetMonth: License[]; + switchedlicensesInTargetMonth: LicenseAllocationHistory[]; +}; + +type BaseDataFromDeletedAccounts = { + // 削除されたアカウントの集計元情報 + deletedAccountsAndUsersFromTier5: AccountArchive[]; + deletedAvairableLicenses: LicenseArchive[]; + deletedLicensesIssuedInTargetMonth: LicenseArchive[]; + deletedLicensesExpiredInTargetMonth: LicenseArchive[]; + deletedSwitchedlicensesInTargetMonth: LicenseAllocationHistoryArchive[]; +}; diff --git a/dictation_function/src/test/analysisLicenses.spec.ts b/dictation_function/src/test/analysisLicenses.spec.ts new file mode 100644 index 0000000..cd26bdb --- /dev/null +++ b/dictation_function/src/test/analysisLicenses.spec.ts @@ -0,0 +1,737 @@ +import { DataSource } from "typeorm"; +import { + getBaseData, + getBaseDataFromDeletedAccounts, +} from "../functions/analysisLicenses"; +import { + makeTestAccount, + makeTestUserArchive, + createLicense, + createLicenseAllocationHistory, + makeTestAccountArchive, + createLicenseArchive, + createLicenseAllocationHistoryArchive, +} from "./common/utility"; +import * as dotenv from "dotenv"; +import { + DateWithZeroTime, + ExpirationThresholdDate, +} from "../common/types/types"; +import { InvocationContext } from "@azure/functions"; +import { + LICENSE_ALLOCATED_STATUS, + SWITCH_FROM_TYPE, +} from "../constants"; +describe("analysisLicenses", () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + let source: DataSource | null = null; + + beforeEach(async () => { + source = new DataSource({ + type: "sqlite", + database: ":memory:", + logging: false, + entities: [__dirname + "/../../**/*.entity{.ts,.js}"], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + if (!source) return; + await source.destroy(); + source = null; + }); + + it("getBaseData取得情報の確認", async () => { + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + + // tier4とtier5のアカウント+管理者を作る(tier4は対象外確認用) + // 第五アカウント:2件 + // 第五ユーザー:2件 + const { account: account4, admin: admin4 } = await makeTestAccount( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin1" } + ); + const { account: account5_2, admin: admin5_2 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin2" } + ); + + // 削除ユーザを作成する + const userArchive5 = await makeTestUserArchive(source, { + account_id: account5_1.id, + }); + // 第五階層以外だとヒットしないことの確認 + const userArchive4 = await makeTestUserArchive(source, { + account_id: account4.id, + }); + + // 所有ライセンス + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:2か月前 + // ・期限:null or 14日後 + await createLicense( + source, + 1, + null, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 2, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 3, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 4, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // ※条件的に「所有ライセンス」にもカウントされる(+3) + await createLicense( + source, + 11, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 12, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 13, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 14, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + lastMonth + ); + + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + await createLicense( + source, + 21, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 22, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 23, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 24, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + // 先々月はヒットしないことの確認 + await createLicense( + source, + 25, + last2Month, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + await createLicenseAllocationHistory( + source, + 1, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 2, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // SWITCH_FROM_TYPE.NONEではヒットしないことの確認 + await createLicenseAllocationHistory( + source, + 3, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.NONE + ); + // 先々月の登録ではヒットしないことの確認 + await createLicenseAllocationHistory( + source, + 4, + admin5_1.id, + 1, + true, + account5_1.id, + last2Month, + SWITCH_FROM_TYPE.TRIAL + ); + + const result = await getBaseData(context, lastMonthYYYYMM, source); + expect(result.accountsAndUsersFromTier5).toHaveLength(2); + expect(result.accountsAndUsersFromTier5[0].id).toBe(account5_1.id); + expect(result.accountsAndUsersFromTier5[1].id).toBe(account5_2.id); + + expect(result.accountsAndUsersFromTier5[0].user).toHaveLength(1); + expect(result.accountsAndUsersFromTier5[1].user).toHaveLength(1); + if ( + result.accountsAndUsersFromTier5[0].user && + result.accountsAndUsersFromTier5[1].user + ) { + expect(result.accountsAndUsersFromTier5[0].user[0].id).toBe(admin5_1.id); + expect(result.accountsAndUsersFromTier5[1].user[0].id).toBe(admin5_2.id); + } else { + throw new Error("ユーザー取得できていないので失敗"); + } + + expect(result.accountsAndUsersFromTier5[0].userArchive).toHaveLength(1); + if (result.accountsAndUsersFromTier5[0].userArchive) { + expect(result.accountsAndUsersFromTier5[0].userArchive[0].id).toBe( + userArchive5.id + ); + } else { + throw new Error("ユーザー取得できていないので失敗"); + } + + expect(result.avairableLicenses).toHaveLength(6); + expect(result.avairableLicenses[0].id).toBe(1); + expect(result.avairableLicenses[1].id).toBe(2); + expect(result.avairableLicenses[2].id).toBe(3); + expect(result.avairableLicenses[3].id).toBe(11); + expect(result.avairableLicenses[4].id).toBe(12); + expect(result.avairableLicenses[5].id).toBe(13); + + expect(result.licensesIssuedInTargetMonth).toHaveLength(3); + expect(result.licensesIssuedInTargetMonth[0].id).toBe(11); + expect(result.licensesIssuedInTargetMonth[1].id).toBe(12); + expect(result.licensesIssuedInTargetMonth[2].id).toBe(13); + + expect(result.licensesExpiredInTargetMonth).toHaveLength(3); + expect(result.licensesExpiredInTargetMonth[0].id).toBe(21); + expect(result.licensesExpiredInTargetMonth[1].id).toBe(22); + expect(result.licensesExpiredInTargetMonth[2].id).toBe(23); + + expect(result.switchedlicensesInTargetMonth).toHaveLength(2); + expect(result.switchedlicensesInTargetMonth[0].id).toBe(1); + expect(result.switchedlicensesInTargetMonth[1].id).toBe(2); + + }); + + + it("getBaseDataFromDeletedAccounts取得情報の確認", async () => { + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + + // tier4とtier5のアカウント+管理者を作る(tier4は対象外確認用) + // 第五アカウント:2件 + // 第五ユーザー:2件 + const { account: account4, admin: admin4 } = await makeTestAccountArchive( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1 } = + await makeTestAccountArchive( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin1" } + ); + const { account: account5_2, admin: admin5_2 } = + await makeTestAccountArchive( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin2" } + ); + + // 所有ライセンス + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:2か月前 + // ・期限:null or 14日後 + await createLicenseArchive( + source, + 1, + null, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 2, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 3, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicenseArchive( + source, + 4, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // ※条件的に「所有ライセンス」にもカウントされる(+3) + await createLicenseArchive( + source, + 11, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 12, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 13, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // deleteはヒットしないことの確認 + await createLicenseArchive( + source, + 14, + expiringSoonDate, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + lastMonth + ); + + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + await createLicenseArchive( + source, + 21, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 22, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 23, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicenseArchive( + source, + 24, + lastMonth, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + // 先々月はヒットしないことの確認 + await createLicenseArchive( + source, + 25, + last2Month, + account5_1.id, + "STANDARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + await createLicenseAllocationHistoryArchive( + source, + 1, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 2, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // SWITCH_FROM_TYPE.NONEではヒットしないことの確認 + await createLicenseAllocationHistoryArchive( + source, + 3, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.NONE + ); + // 先々月の登録ではヒットしないことの確認 + await createLicenseAllocationHistoryArchive( + source, + 4, + admin5_1.id, + 1, + true, + account5_1.id, + last2Month, + SWITCH_FROM_TYPE.TRIAL + ); + + const result = await getBaseDataFromDeletedAccounts(context, lastMonthYYYYMM, source); + expect(result.deletedAccountsAndUsersFromTier5).toHaveLength(2); + expect(result.deletedAccountsAndUsersFromTier5[0].id).toBe(account5_1.id); + expect(result.deletedAccountsAndUsersFromTier5[1].id).toBe(account5_2.id); + + expect(result.deletedAccountsAndUsersFromTier5[0].userArchive).toHaveLength( + 1 + ); + expect(result.deletedAccountsAndUsersFromTier5[1].userArchive).toHaveLength( + 1 + ); + if ( + result.deletedAccountsAndUsersFromTier5[0].userArchive && + result.deletedAccountsAndUsersFromTier5[1].userArchive + ) { + expect(result.deletedAccountsAndUsersFromTier5[0].userArchive[0].id).toBe( + admin5_1.id + ); + expect(result.deletedAccountsAndUsersFromTier5[1].userArchive[0].id).toBe( + admin5_2.id + ); + } + + expect(result.deletedAvairableLicenses).toHaveLength(6); + expect(result.deletedAvairableLicenses[0].id).toBe(1); + expect(result.deletedAvairableLicenses[1].id).toBe(2); + expect(result.deletedAvairableLicenses[2].id).toBe(3); + expect(result.deletedAvairableLicenses[3].id).toBe(11); + expect(result.deletedAvairableLicenses[4].id).toBe(12); + expect(result.deletedAvairableLicenses[5].id).toBe(13); + + expect(result.deletedLicensesIssuedInTargetMonth).toHaveLength(3); + expect(result.deletedLicensesIssuedInTargetMonth[0].id).toBe(11); + expect(result.deletedLicensesIssuedInTargetMonth[1].id).toBe(12); + expect(result.deletedLicensesIssuedInTargetMonth[2].id).toBe(13); + + expect(result.deletedLicensesExpiredInTargetMonth).toHaveLength(3); + expect(result.deletedLicensesExpiredInTargetMonth[0].id).toBe(21); + expect(result.deletedLicensesExpiredInTargetMonth[1].id).toBe(22); + expect(result.deletedLicensesExpiredInTargetMonth[2].id).toBe(23); + + expect(result.deletedSwitchedlicensesInTargetMonth).toHaveLength(2); + expect(result.deletedSwitchedlicensesInTargetMonth[0].id).toBe(1); + expect(result.deletedSwitchedlicensesInTargetMonth[1].id).toBe(2); + }); + +}); diff --git a/dictation_function/src/test/common/utility.ts b/dictation_function/src/test/common/utility.ts index f675ed7..a22eed7 100644 --- a/dictation_function/src/test/common/utility.ts +++ b/dictation_function/src/test/common/utility.ts @@ -1,9 +1,14 @@ import { v4 as uuidv4 } from "uuid"; import { DataSource } from "typeorm"; -import { User } from "../../entity/user.entity"; -import { Account } from "../../entity/account.entity"; +import { User, UserArchive } from "../../entity/user.entity"; +import { Account, AccountArchive } from "../../entity/account.entity"; import { ADMIN_ROLES, USER_ROLES } from "../../constants"; -import { License, LicenseAllocationHistory } from "../../entity/license.entity"; +import { + License, + LicenseAllocationHistory, + LicenseArchive, + LicenseAllocationHistoryArchive +} from "../../entity/license.entity"; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -25,8 +30,25 @@ type OverrideUser = Omit< "id" | "account" | "license" | "userGroupMembers" >; +type OverrideAccountArchive = Omit< + AccountArchive, + "id" | "primary_admin_user_id" | "secondary_admin_user_id" | "user" +>; + +// 上書きされたら困る項目を除外したUser型 +type OverrideUserArchive = Omit< + UserArchive, + "id" | "account" | "license" | "userGroupMembers" +>; + type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] }; type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] }; +type AccountArchiveDefault = { + [K in keyof OverrideAccountArchive]?: OverrideAccountArchive[K]; +}; +type UserArchiveDefault = { + [K in keyof OverrideUserArchive]?: OverrideUserArchive[K]; +}; /** * テスト ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する @@ -50,7 +72,6 @@ export const makeTestUser = async ( auto_renew: d?.auto_renew ?? true, notification: d?.notification ?? true, encryption: d?.encryption ?? true, - encryption_password: d?.encryption_password, prompt: d?.prompt ?? true, created_by: d?.created_by ?? "test_runner", created_at: d?.created_at ?? new Date(), @@ -118,7 +139,6 @@ export const makeTestAccount = async ( auto_renew: d?.auto_renew ?? true, notification: d?.notification ?? true, encryption: d?.encryption ?? true, - encryption_password: d?.encryption_password ?? "password", prompt: d?.prompt ?? true, deleted_at: d?.deleted_at ?? "", created_by: d?.created_by ?? "test_runner", @@ -175,7 +195,8 @@ export const createLicense = async ( allocated_user_id: number | null, order_id: number | null, deleted_at: Date | null, - delete_order_id: number | null + delete_order_id: number | null, + created_at?: Date ): Promise => { const { identifiers } = await datasource.getRepository(License).insert({ id: licenseId, @@ -188,13 +209,41 @@ export const createLicense = async ( deleted_at: deleted_at, delete_order_id: delete_order_id, created_by: "test_runner", - created_at: new Date(), + created_at: created_at ? created_at : new Date(), updated_by: "updater", updated_at: new Date(), }); identifiers.pop() as License; }; +export const createLicenseAllocationHistory = async ( + datasource: DataSource, + id: number, + user_id: number, + license_id: number, + is_allocated: boolean, + account_id: number, + executed_at: Date, + switch_from_type: string, +): Promise => { + const { identifiers } = await datasource + .getRepository(LicenseAllocationHistory) + .insert({ + id: id, + user_id: user_id, + license_id: license_id, + is_allocated: is_allocated, + account_id: account_id, + executed_at: executed_at, + switch_from_type: switch_from_type, + created_by: "test_runner", + created_at: new Date(), + updated_by: "updater", + updated_at: new Date(), + }); + identifiers.pop() as LicenseAllocationHistory; +}; + export const selectLicenseByAllocatedUser = async ( datasource: DataSource, userId: number @@ -225,3 +274,202 @@ export const selectLicenseAllocationHistory = async ( }); return { licenseAllocationHistory }; }; + +/** + * テスト ユーティリティ: 指定したプロパティを上書きしたアーカイブユーザーを作成する + * @param dataSource データソース + * @param defaultUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト + * @returns 作成したユーザー + */ +export const makeTestUserArchive = async ( + datasource: DataSource, + defaultUserValue?: UserArchiveDefault +): Promise => { + const d = defaultUserValue; + const { identifiers } = await datasource.getRepository(UserArchive).insert({ + account_id: d?.account_id ?? -1, + external_id: d?.external_id ?? uuidv4(), + role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`, + author_id: d?.author_id, + accepted_eula_version: d?.accepted_eula_version ?? "1.0", + accepted_dpa_version: d?.accepted_dpa_version ?? "1.0", + email_verified: d?.email_verified ?? true, + auto_renew: d?.auto_renew ?? true, + notification: d?.notification ?? true, + encryption: d?.encryption ?? true, + prompt: d?.prompt ?? true, + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + const result = identifiers.pop() as User; + + const userArchive = await datasource.getRepository(UserArchive).findOne({ + where: { + id: result.id, + }, + }); + if (!userArchive) { + throw new Error("Unexpected null"); + } + return userArchive; +}; + + + +/** + * テスト ユーティリティ: 指定したプロパティを上書きしたアカウントとその管理者ユーザーを作成する + * @param dataSource データソース + * @param defaultUserValue Account型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト + * @param defaultAdminUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト(account_id等の所属関係が破壊される上書きは無視する) + * @returns 作成したアカウント + */ +export const makeTestAccountArchive = async ( + datasource: DataSource, + defaultAccountValue?: AccountArchiveDefault, + defaultAdminUserValue?: UserArchiveDefault, + isPrimaryAdminNotExist?: boolean, + isSecondaryAdminNotExist?: boolean +): Promise<{ account: AccountArchive; admin: UserArchive }> => { + let accountId: number; + let userId: number; + { + const d = defaultAccountValue; + const { identifiers } = await datasource + .getRepository(AccountArchive) + .insert({ + tier: d?.tier ?? 1, + parent_account_id: d?.parent_account_id ?? undefined, + country: d?.country ?? "US", + delegation_permission: d?.delegation_permission ?? false, + locked: d?.locked ?? false, + verified: d?.verified ?? true, + deleted_at: d?.deleted_at ?? "", + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + const result = identifiers.pop() as AccountArchive; + accountId = result.id; + } + { + const d = defaultAdminUserValue; + const { identifiers } = await datasource.getRepository(UserArchive).insert({ + external_id: d?.external_id ?? uuidv4(), + account_id: accountId, + role: d?.role ?? "admin none", + author_id: d?.author_id ?? undefined, + accepted_eula_version: d?.accepted_eula_version ?? "1.0", + accepted_dpa_version: d?.accepted_dpa_version ?? "1.0", + email_verified: d?.email_verified ?? true, + auto_renew: d?.auto_renew ?? true, + notification: d?.notification ?? true, + encryption: d?.encryption ?? true, + prompt: d?.prompt ?? true, + deleted_at: d?.deleted_at ?? "", + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + + const result = identifiers.pop() as UserArchive; + userId = result.id; + } + + // Accountの管理者を設定する + let secondaryAdminUserId: number | null = null; + if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) { + secondaryAdminUserId = userId; + } + await datasource.getRepository(AccountArchive).update( + { id: accountId }, + { + primary_admin_user_id: isPrimaryAdminNotExist ? null : userId, + secondary_admin_user_id: secondaryAdminUserId, + } + ); + + const account = await datasource.getRepository(AccountArchive).findOne({ + where: { + id: accountId, + }, + }); + + const admin = await datasource.getRepository(UserArchive).findOne({ + where: { + id: userId, + }, + }); + if (!account || !admin) { + throw new Error("Unexpected null"); + } + + return { + account: account, + admin: admin, + }; +}; + +export const createLicenseArchive = async ( + datasource: DataSource, + licenseId: number, + expiry_date: Date | null, + accountId: number, + type: string, + status: string, + allocated_user_id: number | null, + order_id: number | null, + deleted_at: Date | null, + delete_order_id: number | null, + created_at?: Date +): Promise => { + const { identifiers } = await datasource + .getRepository(LicenseArchive) + .insert({ + id: licenseId, + expiry_date: expiry_date, + account_id: accountId, + type: type, + status: status, + allocated_user_id: allocated_user_id, + order_id: order_id, + deleted_at: deleted_at, + delete_order_id: delete_order_id, + created_by: "test_runner", + created_at: created_at ? created_at : new Date(), + updated_by: "updater", + updated_at: new Date(), + }); + identifiers.pop() as LicenseArchive; +}; + +export const createLicenseAllocationHistoryArchive = async ( + datasource: DataSource, + id: number, + user_id: number, + license_id: number, + is_allocated: boolean, + account_id: number, + executed_at: Date, + switch_from_type: string +): Promise => { + const { identifiers } = await datasource + .getRepository(LicenseAllocationHistoryArchive) + .insert({ + id: id, + user_id: user_id, + license_id: license_id, + is_allocated: is_allocated, + account_id: account_id, + executed_at: executed_at, + switch_from_type: switch_from_type, + created_by: "test_runner", + created_at: new Date(), + updated_by: "updater", + updated_at: new Date(), + }); + identifiers.pop() as LicenseAllocationHistoryArchive; +}; From 43561f237eb46d7b6701d27cc60707f4d8aadeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Tue, 12 Mar 2024 03:57:29 +0000 Subject: [PATCH 049/109] =?UTF-8?q?Merged=20PR=20822:=20=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=83=AA=E6=AF=94=E8=BC=83=E7=94=A8=E3=83=AD=E3=82=B0=E5=87=BA?= =?UTF-8?q?=E5=8A=9B=E3=81=AE=E4=BB=95=E7=B5=84=E3=81=BF=E3=82=92=E6=94=B9?= =?UTF-8?q?=E8=89=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3889: クエリ比較用ログ出力の仕組みを改良](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3889) - SQLクエリを比較可能とするための仕組みを導入 - UUIDや日付等の実行の度に変更される要素を出力段階で置き換えてしまうロガーを追加 - テストで上記ロガーを使うよう修正 - テストで使用する環境変数が必要がないのにlocalを指定するようになっていたため、production想定でテストが実施されるようテスト用環境変数ファイルを変更 ## レビューポイント - この仕組みを使ってデグレを防ぐための「クエリの変更内容を確認する」事が問題なく出来そうか ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - npm run test が正常に実施されることを確認 - 行った修正がデグレを発生させていないことを確認できるか - testファイルにしか参照されない変更なので、デグレは原理的に発生しないはず - testも正常に成功したことを確認したので、デグレは発生していないはず --- dictation_server/.env.test | 2 +- dictation_server/.gitignore | 1 + dictation_server/src/common/test/logger.ts | 94 ++++++++++++++++--- .../accounts/accounts.service.spec.ts | 59 ++++++++++++ .../src/features/auth/auth.service.spec.ts | 9 ++ .../src/features/files/files.service.spec.ts | 13 +++ .../licenses/licenses.service.spec.ts | 13 +++ .../src/features/tasks/tasks.service.spec.ts | 19 ++++ .../templates/templates.service.spec.ts | 5 + .../src/features/terms/terms.service.spec.ts | 3 + .../src/features/users/users.service.spec.ts | 22 ++++- .../workflows/workflows.service.spec.ts | 9 ++ 12 files changed, 232 insertions(+), 17 deletions(-) diff --git a/dictation_server/.env.test b/dictation_server/.env.test index 81bb261..07d483e 100644 --- a/dictation_server/.env.test +++ b/dictation_server/.env.test @@ -1,4 +1,4 @@ -STAGE=local +STAGE=production NO_COLOR=TRUE CORS=TRUE PORT=8081 diff --git a/dictation_server/.gitignore b/dictation_server/.gitignore index 63e9c25..21d6308 100644 --- a/dictation_server/.gitignore +++ b/dictation_server/.gitignore @@ -3,6 +3,7 @@ /dump.rdb /build /openapi/build +/.test # credentials credentials diff --git a/dictation_server/src/common/test/logger.ts b/dictation_server/src/common/test/logger.ts index a6f3ba1..93067ff 100644 --- a/dictation_server/src/common/test/logger.ts +++ b/dictation_server/src/common/test/logger.ts @@ -2,27 +2,91 @@ import { Logger, QueryRunner } from 'typeorm'; import * as fs from 'fs'; import * as path from 'path'; -export class FileLogger implements Logger { - private logPath = path.join(__dirname, 'logs'); +interface IOutput { + initialize(): void; + write(message: string): void; +} - constructor() { +class ConsoleOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + console.log(message); + } +} + +class FileOutput implements IOutput { + private logPath = path.join('/app/dictation_server/.test', 'logs'); + private fileName = new Date().getTime(); + + initialize(): void { if (!fs.existsSync(this.logPath)) { fs.mkdirSync(this.logPath, { recursive: true }); } } - private writeToFile(message: string): void { - const logFile = path.join( - this.logPath, - `${new Date().toISOString().split('T')[0]}.log`, - ); + write(message: string): void { + const logFile = path.join(this.logPath, `${this.fileName}.log`); fs.appendFileSync(logFile, `${message}\n`); } +} + +class NoneOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + // do nothing + } +} + +export class TestLogger implements Logger { + out: IOutput; + + constructor(output: 'none' | 'file' | 'console') { + switch (output) { + case 'none': + this.out = new NoneOutput(); + break; + case 'file': + this.out = new FileOutput(); + break; + case 'console': + this.out = new ConsoleOutput(); + break; + default: + this.out = new NoneOutput(); + break; + } + this.out.initialize(); + } + + private write(message: string): void { + this.out.write(message); + } logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { - this.writeToFile( - `Query: ${query} -- Parameters: ${JSON.stringify(parameters)}`, + const raw = `Query: ${query} -- Parameters: ${JSON.stringify(parameters)}`; + // ex: 2024-03-08T06:38:43.125Z を TIME という文字列に置換 + const dateRemoved = raw.replace( + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, + 'TIME', ); + // ex: /* コメント内容 */ を /* コメント */ という文字列に置換 + const commentRemoved = dateRemoved.replace( + /\/\*.*\*\//g, + '/* RequestID */', + ); + + // UUIDを固定文字列に置換する ex: 88a9c78e-115a-439c-9e23-731d649f0c27 を XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX という文字列に置換 + const uuidRemoved = commentRemoved.replace( + /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, + 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', + ); + this.write(uuidRemoved); } logQueryError( @@ -31,7 +95,7 @@ export class FileLogger implements Logger { parameters?: any[], queryRunner?: QueryRunner, ) { - this.writeToFile( + this.write( `ERROR: ${error} -- Query: ${query} -- Parameters: ${JSON.stringify( parameters, )}`, @@ -44,7 +108,7 @@ export class FileLogger implements Logger { parameters?: any[], queryRunner?: QueryRunner, ) { - this.writeToFile( + this.write( `SLOW QUERY: ${time}ms -- Query: ${query} -- Parameters: ${JSON.stringify( parameters, )}`, @@ -52,14 +116,14 @@ export class FileLogger implements Logger { } logSchemaBuild(message: string, queryRunner?: QueryRunner) { - this.writeToFile(`Schema Build: ${message}`); + this.write(`Schema Build: ${message}`); } logMigration(message: string, queryRunner?: QueryRunner) { - this.writeToFile(`Migration: ${message}`); + this.write(`Migration: ${message}`); } log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner) { - this.writeToFile(`${level.toUpperCase()}: ${message}`); + this.write(`${level.toUpperCase()}: ${message}`); } } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 6eb5918..c20e6e6 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -91,6 +91,7 @@ import { UsersService } from '../users/users.service'; import { truncateAllTable } from '../../common/test/init'; import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; +import { TestLogger } from '../../common/test/logger'; describe('createAccount', () => { let source: DataSource | null = null; @@ -106,6 +107,8 @@ describe('createAccount', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -818,6 +821,8 @@ describe('createPartnerAccount', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1816,6 +1821,8 @@ describe('getLicenseSummary', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2098,6 +2105,8 @@ describe('getPartnerAccount', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2252,6 +2261,8 @@ describe('getOrderHistories', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2396,6 +2407,8 @@ describe('issueLicense', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2724,6 +2737,8 @@ describe('getDealers', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2819,6 +2834,8 @@ describe('createTypistGroup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3131,6 +3148,8 @@ describe('getTypistGroup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3343,6 +3362,8 @@ describe('updateTypistGroup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3771,6 +3792,8 @@ describe('deleteTypistGroup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4084,6 +4107,8 @@ describe('getWorktypes', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4216,6 +4241,8 @@ describe('createWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4381,6 +4408,8 @@ describe('updateWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4672,6 +4701,8 @@ describe('deleteWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4932,6 +4963,8 @@ describe('getOptionItems', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -5083,6 +5116,8 @@ describe('updateOptionItems', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -5421,6 +5456,8 @@ describe('updateActiveWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -5661,6 +5698,8 @@ describe('ライセンス発行キャンセル', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6005,6 +6044,8 @@ describe('パートナー一覧取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6185,6 +6226,8 @@ describe('アカウント情報更新', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6473,6 +6516,8 @@ describe('getAccountInfo', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6548,6 +6593,8 @@ describe('getAuthors', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6712,6 +6759,8 @@ describe('getTypists', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6906,6 +6955,8 @@ describe('deleteAccountAndData', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -7405,6 +7456,8 @@ describe('getAccountInfoMinimalAccess', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -7551,6 +7604,8 @@ describe('getCompanyName', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -7621,6 +7676,8 @@ describe('updateFileDeleteSetting', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -7760,6 +7817,8 @@ describe('updateRestrictionStatus', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts index 85473b3..7843214 100644 --- a/dictation_server/src/features/auth/auth.service.spec.ts +++ b/dictation_server/src/features/auth/auth.service.spec.ts @@ -21,6 +21,7 @@ import { TIERS, USER_ROLES } from '../../constants'; import { decode, isVerifyError } from '../../common/jwt'; import { RefreshToken, AccessToken } from '../../common/token'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('AuthService', () => { it('IDトークンの検証とペイロードの取得に成功する', async () => { @@ -175,6 +176,8 @@ describe('checkIsAcceptedLatestVersion', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -350,6 +353,8 @@ describe('generateDelegationRefreshToken', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -496,6 +501,8 @@ describe('generateDelegationAccessToken', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -607,6 +614,8 @@ describe('updateDelegationAccessToken', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index a849975..c0ce84a 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -44,6 +44,7 @@ import { USER_ROLES, } from '../../constants'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('publishUploadSas', () => { let source: DataSource | null = null; @@ -59,6 +60,8 @@ describe('publishUploadSas', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -254,6 +257,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1323,6 +1328,8 @@ describe('音声ファイルダウンロードURL取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1631,6 +1638,8 @@ describe('テンプレートファイルダウンロードURL取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1894,6 +1903,8 @@ describe('publishTemplateFileUploadSas', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2009,6 +2020,8 @@ describe('templateUploadFinished', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 69b6530..72ac6ea 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -34,6 +34,7 @@ import { overrideSendgridService, } from '../../common/test/overrides'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('ライセンス注文', () => { let source: DataSource | null = null; @@ -49,6 +50,8 @@ describe('ライセンス注文', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -222,6 +225,8 @@ describe('カードライセンス発行', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -312,6 +317,8 @@ describe('カードライセンスを取り込む', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -657,6 +664,8 @@ describe('ライセンス割り当て', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1302,6 +1311,8 @@ describe('ライセンス割り当て解除', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1471,6 +1482,8 @@ describe('ライセンス注文キャンセル', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index ecefac5..304b8ad 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -52,6 +52,7 @@ import { truncateAllTable } from '../../common/test/init'; import { makeDefaultLicensesRepositoryMockValue } from '../accounts/test/accounts.service.mock'; import { DateWithZeroTime } from '../licenses/types/types'; import { createLicense } from '../licenses/test/utility'; +import { TestLogger } from '../../common/test/logger'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { @@ -638,6 +639,8 @@ describe('TasksService', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1033,6 +1036,8 @@ describe('changeCheckoutPermission', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1675,6 +1680,8 @@ describe('checkout', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2446,6 +2453,8 @@ describe('checkin', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2661,6 +2670,8 @@ describe('suspend', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2872,6 +2883,8 @@ describe('cancel', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3500,6 +3513,8 @@ describe('backup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3806,6 +3821,8 @@ describe('getNextTask', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4295,6 +4312,8 @@ describe('deleteTask', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index 46dadf7..37c3813 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -21,6 +21,7 @@ import { getWorkflow, } from '../workflows/test/utility'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; +import { TestLogger } from '../../common/test/logger'; describe('getTemplates', () => { let source: DataSource | null = null; @@ -36,6 +37,8 @@ describe('getTemplates', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -157,6 +160,8 @@ describe('deleteTemplate', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/terms/terms.service.spec.ts b/dictation_server/src/features/terms/terms.service.spec.ts index 3607f41..79552ba 100644 --- a/dictation_server/src/features/terms/terms.service.spec.ts +++ b/dictation_server/src/features/terms/terms.service.spec.ts @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('利用規約取得', () => { let source: DataSource | null = null; @@ -22,6 +23,8 @@ describe('利用規約取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 503329d..5246e06 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -57,8 +57,8 @@ import { import { truncateAllTable } from '../../common/test/init'; import { createTask } from '../files/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; -import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { MultipleImportErrors } from './types/types'; +import { TestLogger } from '../../common/test/logger'; describe('UsersService.confirmUser', () => { let source: DataSource | null = null; @@ -74,6 +74,8 @@ describe('UsersService.confirmUser', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -479,6 +481,8 @@ describe('UsersService.createUser', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1460,6 +1464,8 @@ describe('UsersService.getUsers', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1949,6 +1955,8 @@ describe('UsersService.updateUser', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2545,6 +2553,8 @@ describe('UsersService.updateAcceptedVersion', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2645,6 +2655,8 @@ describe('UsersService.getUserName', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2699,6 +2711,8 @@ describe('UsersService.getRelations', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2905,6 +2919,8 @@ describe('UsersService.deleteUser', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3672,6 +3688,8 @@ describe('UsersService.multipleImports', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3872,6 +3890,8 @@ describe('UsersService.multipleImportsComplate', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/workflows/workflows.service.spec.ts b/dictation_server/src/features/workflows/workflows.service.spec.ts index 5c464aa..633c13b 100644 --- a/dictation_server/src/features/workflows/workflows.service.spec.ts +++ b/dictation_server/src/features/workflows/workflows.service.spec.ts @@ -20,6 +20,7 @@ import { WorkflowsRepositoryService } from '../../repositories/workflows/workflo import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('getWorkflows', () => { let source: DataSource | null = null; @@ -35,6 +36,8 @@ describe('getWorkflows', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('file'), + logging: true, }); return await s.initialize(); })(); @@ -292,6 +295,8 @@ describe('createWorkflows', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1231,6 +1236,8 @@ describe('updateWorkflow', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2433,6 +2440,8 @@ describe('deleteWorkflows', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); From 340aa73bde7f5c23e1fb1f123e1c628ab547c098 Mon Sep 17 00:00:00 2001 From: masaaki Date: Tue, 12 Mar 2024 04:55:04 +0000 Subject: [PATCH 050/109] =?UTF-8?q?Merged=20PR=20827:=20CSV=E5=87=BA?= =?UTF-8?q?=E5=8A=9B=E3=81=8C=E5=A4=B1=E6=95=97=E3=81=97=E3=81=9F=E3=81=A8?= =?UTF-8?q?=E3=81=8D=E3=81=AB=E6=89=8B=E5=8B=95=E3=81=A7=E8=B5=B7=E5=8B=95?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=81=9F=E3=82=81=E3=81=AEFunctions=E3=82=92?= =?UTF-8?q?=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3860: CSV出力が失敗したときに手動で起動するためのFunctionsを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3860) - analysisLicensesについて手動で起動できる処理(analysisLicensesManualRetry)を追加しました - データベース接続の初期化処理について共通化しました ## レビューポイント - 特筆する部分はありません ## UIの変更 - 無し ## クエリの変更 - 無し ## 動作確認状況 - ローカルでpostmanからリクエストを行うことで起動できることを確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - unit testが通ることを確認 - タイマを暫定的に1分にして、ローカル環境で各タイマ処理が正常終了することを確認(DBアクセスが全処理行われることを確認) ## 補足 - 相談、参考資料などがあれば --- .../src/database/initializeDataSource.ts | 41 ++++++++++ .../src/functions/analysisLicenses.ts | 68 ++++++----------- .../functions/analysisLicensesManualRetry.ts | 76 +++++++++++++++++++ .../src/functions/licenseAlert.ts | 21 +---- .../src/functions/licenseAutoAllocation.ts | 20 +---- .../licenseAutoAllocationManualRetry.ts | 28 +------ 6 files changed, 149 insertions(+), 105 deletions(-) create mode 100644 dictation_function/src/database/initializeDataSource.ts create mode 100644 dictation_function/src/functions/analysisLicensesManualRetry.ts diff --git a/dictation_function/src/database/initializeDataSource.ts b/dictation_function/src/database/initializeDataSource.ts new file mode 100644 index 0000000..e573669 --- /dev/null +++ b/dictation_function/src/database/initializeDataSource.ts @@ -0,0 +1,41 @@ +import { User, UserArchive } from "../entity/user.entity"; +import { Account, AccountArchive } from "../entity/account.entity"; +import { + License, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + LicenseArchive, +} from "../entity/license.entity"; +import { InvocationContext, } from "@azure/functions"; +import { DataSource} from "typeorm"; + +export async function initializeDataSource( + context: InvocationContext +): Promise { + try { + const datasource = new DataSource({ + type: "mysql", + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME_CCB, + entities: [ + User, + UserArchive, + Account, + AccountArchive, + License, + LicenseArchive, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + ], + }); + await datasource.initialize(); + return datasource; + } catch (e) { + context.log("Database initialize failed."); + context.error(e); + throw e; + } +} diff --git a/dictation_function/src/functions/analysisLicenses.ts b/dictation_function/src/functions/analysisLicenses.ts index 3d40276..e3d0c83 100644 --- a/dictation_function/src/functions/analysisLicenses.ts +++ b/dictation_function/src/functions/analysisLicenses.ts @@ -1,12 +1,21 @@ import { app, InvocationContext, Timer } from "@azure/functions"; import { DataSource, Between } from "typeorm"; import * as dotenv from "dotenv"; -import { User, UserArchive } from "../entity/user.entity"; import { Account, AccountArchive } from "../entity/account.entity"; -import { License, LicenseAllocationHistory, LicenseArchive, LicenseAllocationHistoryArchive } from "../entity/license.entity"; +import { + License, + LicenseAllocationHistory, + LicenseArchive, + LicenseAllocationHistoryArchive, +} from "../entity/license.entity"; import { BlobstorageService } from "../blobstorage/blobstorage.service"; -import { LICENSE_ALLOCATED_STATUS, TIERS, SWITCH_FROM_TYPE } from "../constants"; +import { + LICENSE_ALLOCATED_STATUS, + TIERS, + SWITCH_FROM_TYPE, +} from "../constants"; import { DateWithDayEndTime } from "../common/types/types"; +import { initializeDataSource } from "../database/initializeDataSource"; /** * ライセンス数分析処理のメイン処理:ここから各処理を呼び出す @@ -17,9 +26,8 @@ export async function analysisLicensesProcessing( context: InvocationContext, targetMonthYYYYMM: string, datasource: DataSource, - blobstorageService: BlobstorageService, + blobstorageService: BlobstorageService ) { - try { context.log("[IN]analysisLicensesProcessing"); @@ -41,7 +49,6 @@ export async function analysisLicensesProcessing( } } - /** * 集計元のデータをDBから取得する処理 * @param context @@ -66,10 +73,10 @@ export async function getBaseData( }, relations: { user: true, - userArchive: true + userArchive: true, }, }); - + // 第五階層が保持する有効なライセンスを取得 const licenseRepository = datasource.getRepository(License); // 現在時刻を起点とした23:59:59の日付 @@ -185,12 +192,13 @@ export async function getBaseDataFromDeletedAccounts( // 第五階層のアカウントとユーザーを取得する // 第五のアカウントを取得 const accountArchiveRepository = datasource.getRepository(AccountArchive); - const deletedAccountsAndUsersFromTier5 = await accountArchiveRepository.find({ - where: { - tier: TIERS.TIER5, - }, - relations: { userArchive: true}, - }); + const deletedAccountsAndUsersFromTier5 = + await accountArchiveRepository.find({ + where: { + tier: TIERS.TIER5, + }, + relations: { userArchive: true }, + }); // 第五階層が保持する有効なライセンスを取得 const licenseArchiveRepository = datasource.getRepository(LicenseArchive); @@ -296,7 +304,6 @@ export async function getBaseDataFromDeletedAccounts( } } - /** * ライセンス数分析処理:Azure Functionの関数として呼び出される処理 * @param myTimer @@ -310,34 +317,8 @@ export async function analysisLicenses( dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; try { - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME_CCB, - entities: [ - User, - Account, - License, - LicenseAllocationHistory, - UserArchive, - AccountArchive, - LicenseArchive, - LicenseAllocationHistoryArchive, - ], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } - + const datasource = await initializeDataSource(context); const blobstorageService = new BlobstorageService(); try { @@ -365,11 +346,10 @@ export async function analysisLicenses( } app.timer("analysisLicenses", { - schedule: "0 0 0 1 * *", + schedule: "0 0 0 1 * *", handler: analysisLicenses, }); - type BaseData = { // 存在するアカウントの集計元情報 accountsAndUsersFromTier5: Account[]; diff --git a/dictation_function/src/functions/analysisLicensesManualRetry.ts b/dictation_function/src/functions/analysisLicensesManualRetry.ts new file mode 100644 index 0000000..d21c61b --- /dev/null +++ b/dictation_function/src/functions/analysisLicensesManualRetry.ts @@ -0,0 +1,76 @@ +import { + HttpRequest, + HttpResponseInit, + InvocationContext, + app, + HttpMethod, +} from "@azure/functions"; +import { analysisLicensesProcessing } from "./analysisLicenses"; +import * as dotenv from "dotenv"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { initializeDataSource } from "../database/initializeDataSource"; +import { HTTP_METHODS, HTTP_STATUS_CODES } from "../constants"; + +export async function analysisLicensesManualRetry( + req: HttpRequest, + context: InvocationContext +): Promise { + context.log(req); + try { + if (req.method === HTTP_METHODS.POST) { + context.log("[IN]analysisLicensesManualRetry"); + + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + const datasource = await initializeDataSource(context); + const blobstorageService = new BlobstorageService(); + + try { + // 現在の日付より、先月の年月をYYYYMM形式で取得 + const currentDate = new Date(); + currentDate.setMonth(currentDate.getMonth() - 1); + const year = currentDate.getFullYear(); + const month = (currentDate.getMonth() + 1).toString().padStart(2, "0"); // 月は0から始まるため+1する + const formattedDate = `${year}${month}`; + + await analysisLicensesProcessing( + context, + formattedDate, + datasource, + blobstorageService + ); + + return { + status: HTTP_STATUS_CODES.OK, + body: "analysisLicensesProcessing has been triggered.", + }; + } catch (e) { + context.log("analysisLicensesProcessing failed."); + context.error(e); + throw e; + } + } else { + context.log(`Please use the POST method. method = [${req.method}]`); + return { + status: HTTP_STATUS_CODES.BAD_REQUEST, + body: `Please use the POST method. method = [${req.method}]`, + }; + } + } catch (e) { + context.log("analysisLicensesManualRetry failed."); + context.error(e); + return { + status: HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, + body: "analysisLicensesManualRetry failed.", + }; + } finally { + context.log("[OUT]analysisLicensesManualRetry"); + } +} + +// httpトリガは定時処理licenseAutoAllocationの異常時の手動再実行用 +app.http("analysisLicensesManualRetry", { + methods: [HTTP_METHODS.POST as HttpMethod], + authLevel: "function", + handler: analysisLicensesManualRetry, +}); diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts index a11af0d..da9f1d7 100644 --- a/dictation_function/src/functions/licenseAlert.ts +++ b/dictation_function/src/functions/licenseAlert.ts @@ -1,6 +1,5 @@ import { app, InvocationContext, Timer } from "@azure/functions"; import { Between, DataSource, In, IsNull, MoreThan, Not } from "typeorm"; -import { User } from "../entity/user.entity"; import { Account } from "../entity/account.entity"; import { ADB2C_SIGN_IN_TYPE, @@ -29,6 +28,7 @@ import { SEND_COMPLETE_PREFIX, DONE, } from "../common/cache/constants"; +import { initializeDataSource } from "../database/initializeDataSource"; export async function licenseAlertProcessing( context: InvocationContext, @@ -99,24 +99,7 @@ export async function licenseAlert( dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME_CCB, - entities: [User, Account, License], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } - + const datasource = await initializeDataSource(context); let redisClient: RedisClient; try { // redis接続 diff --git a/dictation_function/src/functions/licenseAutoAllocation.ts b/dictation_function/src/functions/licenseAutoAllocation.ts index a1ece5d..9313a6a 100644 --- a/dictation_function/src/functions/licenseAutoAllocation.ts +++ b/dictation_function/src/functions/licenseAutoAllocation.ts @@ -16,6 +16,7 @@ import { DateWithZeroTime, NewAllocatedLicenseExpirationDate, } from "../common/types/types"; +import { initializeDataSource } from "../database/initializeDataSource"; export async function licenseAutoAllocationProcessing( context: InvocationContext, @@ -77,24 +78,7 @@ export async function licenseAutoAllocation( context.log("[IN]licenseAutoAllocation"); dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME_CCB, - entities: [User, Account, License, LicenseAllocationHistory], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } - + const datasource = await initializeDataSource(context); await licenseAutoAllocationProcessing(context, datasource); } catch (e) { context.log("licenseAutoAllocation failed."); diff --git a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts index c34de46..f01542e 100644 --- a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts +++ b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts @@ -7,11 +7,8 @@ import { } from "@azure/functions"; import { licenseAutoAllocationProcessing } from "./licenseAutoAllocation"; import * as dotenv from "dotenv"; -import { DataSource } from "typeorm"; -import { User } from "../entity/user.entity"; -import { Account } from "../entity/account.entity"; -import { License, LicenseAllocationHistory } from "../entity/license.entity"; import { HTTP_METHODS, HTTP_STATUS_CODES } from "../constants"; +import { initializeDataSource } from "../database/initializeDataSource"; export async function licenseAutoAllocationManualRetry( req: HttpRequest, @@ -40,24 +37,7 @@ export async function licenseAutoAllocationManualRetry( context.log("[IN]licenseAutoAllocationManualRetry"); dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME_CCB, - entities: [User, Account, License, LicenseAllocationHistory], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } - + const datasource = await initializeDataSource(context); await licenseAutoAllocationProcessing(context, datasource, dateToTrigger); context.log("Automatic license allocation has been triggered."); return { @@ -65,10 +45,10 @@ export async function licenseAutoAllocationManualRetry( body: "Automatic license allocation has been triggered.", }; } else { - context.log("Please use the POST method."); + context.log(`Please use the POST method. Requested method = [${req.method}]`); return { status: HTTP_STATUS_CODES.BAD_REQUEST, - body: "Please use the POST method.", + body: `Please use the POST method. method = [${req.method}]`, }; } } catch (e) { From 415fd2eb58bd42999d2bb00c222e72e346a842f4 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Tue, 12 Mar 2024 05:43:49 +0000 Subject: [PATCH 051/109] =?UTF-8?q?Merged=20PR=20825:=20AzureFunctions?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=92=EF=BC=88=E5=8F=96=E5=BE=97=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=83=87=E3=83=BC=E3=82=BF=E3=82=92CSV=E7=94=A8?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=8F=9B=E3=81=99=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3844: AzureFunctions実装2(取得したデータをCSV用に変換する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3844) transferData()の実装 ccbの最新よりマージした状態での引数に変更 各ライセンス数をCSV配列に出力できるところまでを実装 (実際にローカルにCSV出力して中身を確認済み) ![image.png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/825/attachments/image.png) ※UIの変更ではない為、ここにそのまま張り付けさせていただきます。 ## レビューポイント - 関数化の範囲は適切か。 テストコードは最低限の記述になるが、問題ないか (33行*7項目の突合せをコード上に実装するのは時間的余裕がないためやってない) 詳細な動作確認は、別タスク https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%2029-2?workitem=3861 で行います。 ## 補足 - 相談、参考資料などがあれば --- dictation_function/src/constants/index.ts | 53 + .../src/functions/analysisLicenses.ts | 1410 ++++++++++++++++- .../src/test/analysisLicenses.spec.ts | 545 ++++++- dictation_function/src/test/common/utility.ts | 8 +- 4 files changed, 1981 insertions(+), 35 deletions(-) diff --git a/dictation_function/src/constants/index.ts b/dictation_function/src/constants/index.ts index fe21f13..0c31242 100644 --- a/dictation_function/src/constants/index.ts +++ b/dictation_function/src/constants/index.ts @@ -337,3 +337,56 @@ export const RoleNumberMap: Record = { export const SYSTEM_IMPORT_USERS = "import-users"; export const ROW_START_INDEX = 2; + +/** + * ライセンス数推移出力機能のCSVヘッダ + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_HEADER = { + ACCOUNT: "アカウント", + TARGET_YEAE_AND_MONTH: "対象年月", + CATEGORY_1: "カテゴリー1", + CATEGORY_2: "カテゴリー2", + LICENSE_TYPE: "ライセンス種別", + ROLE: "役割", + COUNT: "数量", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(カテゴリー1) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_CATEGORY_1 = { + VALID_LICENSES: "有効ライセンス数", + NEW_ISSUE_LICENSES: "新規発行ライセンス数", + INVALID_LICENSES: "失効ライセンス数", + SWICHED_LICENSES: "有効ライセンス切り替え", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(カテゴリー2) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_CATEGORY_2 = { + OWNER_LICENSES: "所有ライセンス数", + IN_USE_LICENSES: "使用中ライセンス数", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(ライセンス種別) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_LICENSE_TYPE = { + TRIAL: "Trial", + STANDARD: "Standard", + CARD: "Card", + SWITCH_FROM_TRIAL: "トライアルから切り替え", + SWITCH_FROM_CARD: "カードから切り替え", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(役割) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_ROLE = { + AUTHOR: "Author", + TYPIST: "Typist", + NONE: "None", + UNALLOCATED: "Unallocated", +}; diff --git a/dictation_function/src/functions/analysisLicenses.ts b/dictation_function/src/functions/analysisLicenses.ts index e3d0c83..515b3aa 100644 --- a/dictation_function/src/functions/analysisLicenses.ts +++ b/dictation_function/src/functions/analysisLicenses.ts @@ -13,7 +13,19 @@ import { LICENSE_ALLOCATED_STATUS, TIERS, SWITCH_FROM_TYPE, + LICENSE_COUNT_ANALYSIS_CATEGORY_1, + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE, + LICENSE_COUNT_ANALYSIS_ROLE, + LICENSE_COUNT_ANALYSIS_CATEGORY_2, + BLOB_STORAGE_REGION_AU, + USER_ROLES, + BLOB_STORAGE_REGION_US, + BLOB_STORAGE_REGION_EU, + LICENSE_TYPE, + LICENSE_COUNT_ANALYSIS_HEADER, } from "../constants"; +import * as fs from "fs"; +import * as path from "path"; import { DateWithDayEndTime } from "../common/types/types"; import { initializeDataSource } from "../database/initializeDataSource"; @@ -37,8 +49,13 @@ export async function analysisLicensesProcessing( targetMonthYYYYMM, datasource ); + const outputCsvData = await transferData( + context, + baseData, + baseDataFromDeletedAccounts, + targetMonthYYYYMM + ); // TODO: 後続処理の呼び出しイメージ(別タスクで追加) - // const outputCsvData = await transferData(context, baseData, baseDataFromDeletedAccounts); // await outputData(context, blobstorageService, outputCsvData); } catch (e) { context.log("analysisLicensesProcessing failed."); @@ -367,3 +384,1394 @@ type BaseDataFromDeletedAccounts = { deletedLicensesExpiredInTargetMonth: LicenseArchive[]; deletedSwitchedlicensesInTargetMonth: LicenseAllocationHistoryArchive[]; }; + +type outputDataAnalysisLicensesCSV = { + outputDataUS: string[]; + outputDataEU: string[]; + outputDataAU: string[]; +}; + +/** + * アカウントと紐づくユーザー、ライセンスからCSV出力用の配列を作成する + * @param context + * @param baseData + * @param baseDataFromDeletedAccounts + * @param targetMonthYYYYMM + * @returns outputDataAnalysisLicensesCSV + */ +export async function transferData( + context: InvocationContext, + baseData: BaseData, + baseDataFromDeletedAccounts: BaseDataFromDeletedAccounts, + targetMonthYYYYMM: string +): Promise { + context.log("[IN]transferData"); + class userIdAndRoles { + id: number; + role: string; + } + const accountsAndUsersFromTier5 = baseData.accountsAndUsersFromTier5; + const validLicenses = baseData.avairableLicenses; + const currentMonthIssuedLicenses = baseData.licensesIssuedInTargetMonth; + const invalidLicenses = baseData.licensesExpiredInTargetMonth; + const switchedLicenses = baseData.switchedlicensesInTargetMonth; + const deletedAccountsAndUsersFromTier5 = + baseDataFromDeletedAccounts.deletedAccountsAndUsersFromTier5; + const deletedValidLicenses = + baseDataFromDeletedAccounts.deletedAvairableLicenses; + const deletedCurrentMonthIssuedLicenses = + baseDataFromDeletedAccounts.deletedLicensesIssuedInTargetMonth; + const deletedInvalidLicenses = + baseDataFromDeletedAccounts.deletedLicensesExpiredInTargetMonth; + const deletedSwitchedLicenses = + baseDataFromDeletedAccounts.deletedSwitchedlicensesInTargetMonth; + + // 出力データ格納配列 + let outputDataUS: string[] = []; + let outputDataEU: string[] = []; + let outputDataAU: string[] = []; + // 出力データのヘッダーを作成 + const header = [ + '"' + LICENSE_COUNT_ANALYSIS_HEADER.ACCOUNT + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.TARGET_YEAE_AND_MONTH + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_1 + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_2 + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.LICENSE_TYPE + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.ROLE + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.COUNT + '"\r\n', + ] as string[]; + // ヘッダーを出力データに追加 + outputDataUS.push(...header); + outputDataEU.push(...header); + outputDataAU.push(...header); + + // ユーザーIDとロールを格納する配列(型が違う為新たに作成する) + let tier5userIdAndRoles: userIdAndRoles[] = []; + try { + // 第五階層のアカウントごとにループ + for (const account of accountsAndUsersFromTier5) { + // account.userとaccount.userArchiveが存在しない場合次のアカウントに進む + if (!account.user && !account.userArchive) { + console.log( + "account.user and account.userArchive is not exist.accountId:" + + account.id + ); + continue; + } + + // ユーザーとユーザーアーカイブからユーザーIDとロールを取得する + if (account.user) { + tier5userIdAndRoles = account.user.map((user) => { + return { id: user.id, role: user.role }; + }); + } + if (account.userArchive) { + tier5userIdAndRoles = tier5userIdAndRoles.concat( + account.userArchive.map((userArchive) => { + return { id: userArchive.id, role: userArchive.role }; + }) + ); + } + // アカウントに紐づくライセンスを取得 + const accountLicenses = validLicenses.filter( + (license) => license.account_id === account.id + ); + // 抽出したライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const trialLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const normalLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const cardLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated + const usedTrialLicenses = trialLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedNormalLicenses = normalLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedCardLicenses = cardLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + // どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。 + // (allcated_user_idからユーザーを特定) + // (Author・Typist・None) + const usedTrialLicensesAuthor = usedTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const usedTrialLicensesTypist = usedTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const usedTrialLicensesNone = usedTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const usedNormalLicensesAuthor = usedNormalLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const usedNormalLicensesTypist = usedNormalLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const usedNormalLicensesNone = usedNormalLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const usedCardLicensesAuthor = usedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const usedCardLicensesTypist = usedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const usedCardLicensesNone = usedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + // 使用中のライセンスの数をカウント + const trialLicensesCount = trialLicenses.length; + const normalLicensesCount = normalLicenses.length; + const cardLicensesCount = cardLicenses.length; + const usedTrialLicensesAuthorCount = usedTrialLicensesAuthor.length; + const usedTrialLicensesTypistCount = usedTrialLicensesTypist.length; + const usedTrialLicensesNoneCount = usedTrialLicensesNone.length; + const usedNormalLicensesAuthorCount = usedNormalLicensesAuthor.length; + const usedNormalLicensesTypistCount = usedNormalLicensesTypist.length; + const usedNormalLicensesNoneCount = usedNormalLicensesNone.length; + const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length; + const usedCardLicensesTypistCount = usedCardLicensesTypist.length; + const usedCardLicensesNoneCount = usedCardLicensesNone.length; + + // アカウントに紐づく当月発行ライセンスを取得 + const accountCurrentMonthIssuedLicenses = + currentMonthIssuedLicenses.filter( + (license) => license.account_id === account.id + ); + // 当月発行ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const currentMonthIssuedTrialLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const currentMonthIssuedNormalLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const currentMonthIssuedCardLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 当月発行ライセンスの数をカウント + const currentMonthIssuedTrialLicensesCount = + currentMonthIssuedTrialLicenses.length; + const currentMonthIssuedNormalLicensesCount = + currentMonthIssuedNormalLicenses.length; + const currentMonthIssuedCardLicensesCount = + currentMonthIssuedCardLicenses.length; + + // アカウントに紐づく失効ライセンスを取得 + const accountInvalidLicenses = invalidLicenses.filter( + (license) => license.account_id === account.id + ); + // 失効ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const invalidTrialLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const invalidNormalLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const invalidCardLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // どのロールのユーザーに割り当てたまま失効したライセンスかを判別し、ロールごとに分ける。 + //(allcated_user_idからユーザーを特定、値がない場合は未割当) + // (Author・Typist・None・未割当) + const invalidTrialLicensesAuthor = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const invalidTrialLicensesTypist = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const invalidTrialLicensesNone = invalidTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const invalidNormalLicensesAuthor = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const invalidNormalLicensesTypist = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const invalidNormalLicensesNone = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const invalidCardLicensesAuthor = invalidCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const invalidCardLicensesTypist = invalidCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const invalidCardLicensesNone = invalidCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const invalidTrialLicensesUnallocated = invalidTrialLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidNormalLicensesUnallocated = invalidNormalLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidCardLicensesUnallocated = invalidCardLicenses.filter( + (license) => !license.allocated_user_id + ); + // 失効ライセンスの数をカウント + const invalidTrialLicensesAuthorCount = invalidTrialLicensesAuthor.length; + const invalidTrialLicensesTypistCount = invalidTrialLicensesTypist.length; + const invalidTrialLicensesNoneCount = invalidTrialLicensesNone.length; + const invalidTrialLicensesUnallocatedCount = + invalidTrialLicensesUnallocated.length; + const invalidNormalLicensesAuthorCount = + invalidNormalLicensesAuthor.length; + const invalidNormalLicensesTypistCount = + invalidNormalLicensesTypist.length; + const invalidNormalLicensesNoneCount = invalidNormalLicensesNone.length; + const invalidNormalLicensesUnallocatedCount = + invalidNormalLicensesUnallocated.length; + const invalidCardLicensesAuthorCount = invalidCardLicensesAuthor.length; + const invalidCardLicensesTypistCount = invalidCardLicensesTypist.length; + const invalidCardLicensesNoneCount = invalidCardLicensesNone.length; + const invalidCardLicensesUnallocatedCount = + invalidCardLicensesUnallocated.length; + + // アカウントに紐づく切り替えライセンスを取得 + const accountSwitchedLicenses = switchedLicenses.filter( + (license) => license.account_id === account.id + ); + // どの種別のライセンスから切り替えられたかで分ける(switch_from_typeカラムで判別)(トライアル・カード) + const switchedTrialLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.TRIAL + ); + const switchedCardLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.CARD + ); + // どのロールのユーザーに対して切り替えが行われたかで分ける。 + //(user_idからユーザーを特定) + //(Typist・Author・None) + const switchedTypistLicensesTypist = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.user_id && user.role === USER_ROLES.TYPIST + ) + ); + const switchedTypistLicensesAuthor = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.user_id && user.role === USER_ROLES.AUTHOR + ) + ); + const switchedTypistLicensesNone = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.user_id && user.role === USER_ROLES.NONE + ) + ); + const switchedCardLicensesTypist = switchedCardLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.user_id && user.role === USER_ROLES.TYPIST + ) + ); + const switchedCardLicensesAuthor = switchedCardLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.user_id && user.role === USER_ROLES.AUTHOR + ) + ); + const switchedCardLicensesNone = switchedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => user.id === license.user_id && user.role === USER_ROLES.NONE + ) + ); + // 切り替えライセンスの数をカウント + const switchedTypistLicensesTypistCount = + switchedTypistLicensesTypist.length; + const switchedTypistLicensesAuthorCount = + switchedTypistLicensesAuthor.length; + const switchedTypistLicensesNoneCount = switchedTypistLicensesNone.length; + const switchedCardLicensesTypistCount = switchedCardLicensesTypist.length; + const switchedCardLicensesAuthorCount = switchedCardLicensesAuthor.length; + const switchedCardLicensesNoneCount = switchedCardLicensesNone.length; + + // 国に対応したリージョンに応じた配列に格納する + if (BLOB_STORAGE_REGION_US.includes(account.country)) { + outputDataUS = outputDataUS.concat( + await createOutputData( + context, + account.company_name, + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_EU.includes(account.country)) { + outputDataEU = outputDataEU.concat( + await createOutputData( + context, + account.company_name, + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_AU.includes(account.country)) { + outputDataAU = outputDataAU.concat( + await createOutputData( + context, + account.company_name, + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else { + throw new Error("invalid country"); + } + } + // 削除版 + tier5userIdAndRoles = []; + // 第五階層のアカウントごとにループ + for (const account of deletedAccountsAndUsersFromTier5) { + // account.userArchiveが存在しない場合次のアカウントに進む + if (!account.userArchive) { + continue; + } + // アカウントに紐づくユーザーを取得 + if (account.userArchive) { + tier5userIdAndRoles = account.userArchive.map((userArchive) => { + return { id: userArchive.id, role: userArchive.role }; + }); + } + // アカウントに紐づくライセンスを取得 + const accountLicenses = deletedValidLicenses.filter( + (license) => license.account_id === account.id + ); + // 抽出したライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const trialLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const normalLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const cardLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated + const usedTrialLicenses = trialLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedNormalLicenses = normalLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedCardLicenses = cardLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + // どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。 + // (allcated_user_idからユーザーを特定) + // (Author・Typist・None) + const usedTrialLicensesAuthor = usedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const usedTrialLicensesTypist = usedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const usedTrialLicensesNone = usedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const usedNormalLicensesAuthor = usedNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const usedNormalLicensesTypist = usedNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const usedNormalLicensesNone = usedNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const usedCardLicensesAuthor = usedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const usedCardLicensesTypist = usedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const usedCardLicensesNone = usedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + // 使用中のライセンスの数をカウント + const trialLicensesCount = trialLicenses.length; + const normalLicensesCount = normalLicenses.length; + const cardLicensesCount = cardLicenses.length; + const usedTrialLicensesAuthorCount = usedTrialLicensesAuthor.length; + const usedTrialLicensesTypistCount = usedTrialLicensesTypist.length; + const usedTrialLicensesNoneCount = usedTrialLicensesNone.length; + const usedNormalLicensesAuthorCount = usedNormalLicensesAuthor.length; + const usedNormalLicensesTypistCount = usedNormalLicensesTypist.length; + const usedNormalLicensesNoneCount = usedNormalLicensesNone.length; + const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length; + const usedCardLicensesTypistCount = usedCardLicensesTypist.length; + const usedCardLicensesNoneCount = usedCardLicensesNone.length; + + // アカウントに紐づく当月発行ライセンスを取得 + const accountCurrentMonthIssuedLicenses = + deletedCurrentMonthIssuedLicenses.filter( + (license) => license.account_id === account.id + ); + // 当月発行ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const currentMonthIssuedTrialLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const currentMonthIssuedNormalLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const currentMonthIssuedCardLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 当月発行ライセンスの数をカウント + const currentMonthIssuedTrialLicensesCount = + currentMonthIssuedTrialLicenses.length; + const currentMonthIssuedNormalLicensesCount = + currentMonthIssuedNormalLicenses.length; + const currentMonthIssuedCardLicensesCount = + currentMonthIssuedCardLicenses.length; + + // アカウントに紐づく失効ライセンスを取得 + const accountInvalidLicenses = deletedInvalidLicenses.filter( + (license) => license.account_id === account.id + ); + // 失効ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const invalidTrialLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const invalidNormalLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const invalidCardLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // どのロールのユーザーに割り当てたまま失効したライセンスかを判別し、ロールごとに分ける。 + //(allcated_user_idからユーザーを特定、値がない場合は未割当) + // (Author・Typist・None・未割当) + const invalidTrialLicensesAuthor = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const invalidTrialLicensesTypist = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const invalidTrialLicensesNone = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const invalidNormalLicensesAuthor = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const invalidNormalLicensesTypist = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const invalidNormalLicensesNone = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const invalidCardLicensesAuthor = invalidCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const invalidCardLicensesTypist = invalidCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const invalidCardLicensesNone = invalidCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const invalidTrialLicensesUnallocated = invalidTrialLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidNormalLicensesUnallocated = invalidNormalLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidCardLicensesUnallocated = invalidCardLicenses.filter( + (license) => !license.allocated_user_id + ); + // 失効ライセンスの数をカウント + const invalidTrialLicensesAuthorCount = invalidTrialLicensesAuthor.length; + const invalidTrialLicensesTypistCount = invalidTrialLicensesTypist.length; + const invalidTrialLicensesNoneCount = invalidTrialLicensesNone.length; + const invalidTrialLicensesUnallocatedCount = + invalidTrialLicensesUnallocated.length; + const invalidNormalLicensesAuthorCount = + invalidNormalLicensesAuthor.length; + const invalidNormalLicensesTypistCount = + invalidNormalLicensesTypist.length; + const invalidNormalLicensesNoneCount = invalidNormalLicensesNone.length; + const invalidNormalLicensesUnallocatedCount = + invalidNormalLicensesUnallocated.length; + const invalidCardLicensesAuthorCount = invalidCardLicensesAuthor.length; + const invalidCardLicensesTypistCount = invalidCardLicensesTypist.length; + const invalidCardLicensesNoneCount = invalidCardLicensesNone.length; + const invalidCardLicensesUnallocatedCount = + invalidCardLicensesUnallocated.length; + + // アカウントに紐づく切り替えライセンスを取得 + const accountSwitchedLicenses = deletedSwitchedLicenses.filter( + (license) => license.account_id === account.id + ); + // どの種別のライセンスから切り替えられたかで分ける(switch_from_typeカラムで判別)(トライアル・カード) + const switchedTrialLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.TRIAL + ); + const switchedCardLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.CARD + ); + // どのロールのユーザーに対して切り替えが行われたかで分ける。 + //(user_idからユーザーを特定) + //(Typist・Author・None) + const switchedTypistLicensesTypist = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find((user) => user.id === license.user_id) + ?.role === USER_ROLES.TYPIST + ); + const switchedTypistLicensesAuthor = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find((user) => user.id === license.user_id) + ?.role === USER_ROLES.AUTHOR + ); + const switchedTypistLicensesNone = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find((user) => user.id === license.user_id) + ?.role === USER_ROLES.NONE + ); + const switchedCardLicensesTypist = switchedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find((user) => user.id === license.user_id) + ?.role === USER_ROLES.TYPIST + ); + const switchedCardLicensesAuthor = switchedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find((user) => user.id === license.user_id) + ?.role === USER_ROLES.AUTHOR + ); + const switchedCardLicensesNone = switchedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find((user) => user.id === license.user_id) + ?.role === USER_ROLES.NONE + ); + // 切り替えライセンスの数をカウント + const switchedTypistLicensesTypistCount = + switchedTypistLicensesTypist.length; + const switchedTypistLicensesAuthorCount = + switchedTypistLicensesAuthor.length; + const switchedTypistLicensesNoneCount = switchedTypistLicensesNone.length; + const switchedCardLicensesTypistCount = switchedCardLicensesTypist.length; + const switchedCardLicensesAuthorCount = switchedCardLicensesAuthor.length; + const switchedCardLicensesNoneCount = switchedCardLicensesNone.length; + + // 国に対応したリージョンに応じた配列に格納する + if (BLOB_STORAGE_REGION_US.includes(account.country)) { + outputDataUS = outputDataUS.concat( + await createOutputData( + context, + account.id.toString(), + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_EU.includes(account.country)) { + outputDataEU = outputDataEU.concat( + await createOutputData( + context, + account.id.toString(), + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_AU.includes(account.country)) { + outputDataAU = outputDataAU.concat( + await createOutputData( + context, + account.id.toString(), + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } + } + // outputDataUSをローカルにCSV出力(テスト用、別タスクで消す) + // outputDataUSの配列をCSV形式に変換 + /*let csvContentUS = ""; + for (let i = 0; i < outputDataUS.length; i++) { + //カンマ区切りの文字列を作成 + + csvContentUS += outputDataUS[i]; + } + // CSVファイルを出力 + const filePathUS = path.join(__dirname, "outputDataUS.csv"); + fs.writeFileSync(filePathUS, csvContentUS); + */ + return { + outputDataUS: outputDataUS, + outputDataEU: outputDataEU, + outputDataAU: outputDataAU, + }; + } catch (e) { + context.log("transferData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]transferData"); + } +} + +/** + * 出力データ作成 + * @param context + * @param company_name + * @param targetMonthYYYYMM + * @param trialLicensesCount + * @param normalLicensesCount + * @param cardLicensesCount + * @param usedTrialLicensesAuthorCount + * @param usedTrialLicensesTypistCount + * @param usedTrialLicensesNoneCount + * @param usedNormalLicensesAuthorCount + * @param usedNormalLicensesTypistCount + * @param usedNormalLicensesNoneCount + * @param usedCardLicensesAuthorCount + * @param usedCardLicensesTypistCount + * @param usedCardLicensesNoneCount + * @param currentMonthIssuedTrialLicensesCount + * @param currentMonthIssuedNormalLicensesCount + * @param currentMonthIssuedCardLicensesCount + * @param invalidTrialLicensesAuthorCount + * @param invalidTrialLicensesTypistCount + * @param invalidTrialLicensesNoneCount + * @param invalidTrialLicensesUnallocatedCount + * @param invalidNormalLicensesAuthorCount + * @param invalidNormalLicensesTypistCount + * @param invalidNormalLicensesNoneCount + * @param invalidNormalLicensesUnallocatedCount + * @param invalidCardLicensesAuthorCount + * @param invalidCardLicensesTypistCount + * @param invalidCardLicensesNoneCount + * @param invalidCardLicensesUnallocatedCount + * @param switchedTypistLicensesAuthorCount + * @param switchedTypistLicensesTypistCount + * @param switchedTypistLicensesNoneCount + * @param switchedCardLicensesAuthorCount + * @param switchedCardLicensesTypistCount + * @param switchedCardLicensesNoneCount + * @returns string[] + */ +async function createOutputData( + context: InvocationContext, + company_name: string, + targetMonthYYYYMM: string, + trialLicensesCount: number, + normalLicensesCount: number, + cardLicensesCount: number, + usedTrialLicensesAuthorCount: number, + usedTrialLicensesTypistCount: number, + usedTrialLicensesNoneCount: number, + usedNormalLicensesAuthorCount: number, + usedNormalLicensesTypistCount: number, + usedNormalLicensesNoneCount: number, + usedCardLicensesAuthorCount: number, + usedCardLicensesTypistCount: number, + usedCardLicensesNoneCount: number, + currentMonthIssuedTrialLicensesCount: number, + currentMonthIssuedNormalLicensesCount: number, + currentMonthIssuedCardLicensesCount: number, + invalidTrialLicensesAuthorCount: number, + invalidTrialLicensesTypistCount: number, + invalidTrialLicensesNoneCount: number, + invalidTrialLicensesUnallocatedCount: number, + invalidNormalLicensesAuthorCount: number, + invalidNormalLicensesTypistCount: number, + invalidNormalLicensesNoneCount: number, + invalidNormalLicensesUnallocatedCount: number, + invalidCardLicensesAuthorCount: number, + invalidCardLicensesTypistCount: number, + invalidCardLicensesNoneCount: number, + invalidCardLicensesUnallocatedCount: number, + switchedTypistLicensesAuthorCount: number, + switchedTypistLicensesTypistCount: number, + switchedTypistLicensesNoneCount: number, + switchedCardLicensesAuthorCount: number, + switchedCardLicensesTypistCount: number, + switchedCardLicensesNoneCount: number +): Promise { + context.log("[IN]createOutputData"); + const resultOutputData: string[] = []; + try { + // アカウントが保持するトライアルライセンス[] + resultOutputData.push( + // 会社名(ダブルクォーテーションで囲む) + '"' + company_name + '",', + // 対象年月(先月)YYYYMM + // 2024年3月に実行した場合:202402 + '"' + targetMonthYYYYMM + '",', + // カテゴリー1 + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + // カテゴリー2 + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', + // ライセンス種別 + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + // 役割 + '"' + "" + '",', + // 数量 + '"' + trialLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持する通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + "" + '",', + '"' + normalLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持するカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + "" + '",', + '"' + cardLicensesCount.toString() + '"\r\n' + ); + // Authorが使用中のトライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + usedTrialLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistが使用中のトライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + usedTrialLicensesTypistCount.toString() + '"\r\n' + ); + // Noneが使用中のトライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + usedTrialLicensesNoneCount.toString() + '"\r\n' + ); + // Authorが使用中の通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + usedNormalLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistが使用中の通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + usedNormalLicensesTypistCount.toString() + '"\r\n' + ); + // Noneが使用中の通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + usedNormalLicensesNoneCount.toString() + '"\r\n' + ); + // Authorが使用中のカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + usedCardLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistが使用中のカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + usedCardLicensesTypistCount.toString() + '"\r\n' + ); + // Noneが使用中のカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + usedCardLicensesNoneCount.toString() + '"\r\n' + ); + // アカウントが保持する当月発行トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + "" + '",', + '"' + currentMonthIssuedTrialLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持する当月発行通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + "" + '",', + '"' + currentMonthIssuedNormalLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持する当月発行カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + "" + '",', + '"' + currentMonthIssuedCardLicensesCount.toString() + '"\r\n' + ); + // Authorに割り当てられたままの失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + invalidTrialLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistに割り当てられたままの失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + invalidTrialLicensesTypistCount.toString() + '"\r\n' + ); + // Noneに割り当てられたままの失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + invalidTrialLicensesNoneCount.toString() + '"\r\n' + ); + // 未割当の失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.UNALLOCATED + '",', + '"' + invalidTrialLicensesUnallocatedCount.toString() + '"\r\n' + ); + // Authorに割り当てられたままの失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + invalidNormalLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistに割り当てられたままの失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + invalidNormalLicensesTypistCount.toString() + '"\r\n' + ); + // Noneに割り当てられたままの失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + invalidNormalLicensesNoneCount.toString() + '"\r\n' + ); + // 未割当の失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.UNALLOCATED + '",', + '"' + invalidNormalLicensesUnallocatedCount.toString() + '"\r\n' + ); + // Authorに割り当てられたままの失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + invalidCardLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistに割り当てられたままの失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + invalidCardLicensesTypistCount.toString() + '"\r\n' + ); + // Noneに割り当てられたままの失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + invalidCardLicensesNoneCount.toString() + '"\r\n' + ); + // 未割当の失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.UNALLOCATED + '",', + '"' + invalidCardLicensesUnallocatedCount.toString() + '"\r\n' + ); + // Authorにトライアルライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + switchedTypistLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistにトライアルライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + switchedTypistLicensesTypistCount.toString() + '"\r\n' + ); + // Noneにトライアルライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + switchedTypistLicensesNoneCount.toString() + '"\r\n' + ); + // Authorにカードライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + switchedCardLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistにカードライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + switchedCardLicensesTypistCount.toString() + '"\r\n' + ); + // Noneにカードライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + switchedCardLicensesNoneCount.toString() + '"\r\n' + ); + return resultOutputData; + } catch (e) { + context.log("createOutputData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]createOutputData"); + } +} diff --git a/dictation_function/src/test/analysisLicenses.spec.ts b/dictation_function/src/test/analysisLicenses.spec.ts index cd26bdb..4545ca7 100644 --- a/dictation_function/src/test/analysisLicenses.spec.ts +++ b/dictation_function/src/test/analysisLicenses.spec.ts @@ -2,6 +2,7 @@ import { DataSource } from "typeorm"; import { getBaseData, getBaseDataFromDeletedAccounts, + transferData, } from "../functions/analysisLicenses"; import { makeTestAccount, @@ -11,6 +12,7 @@ import { makeTestAccountArchive, createLicenseArchive, createLicenseAllocationHistoryArchive, + makeTestUser, } from "./common/utility"; import * as dotenv from "dotenv"; import { @@ -20,6 +22,9 @@ import { import { InvocationContext } from "@azure/functions"; import { LICENSE_ALLOCATED_STATUS, + LICENSE_COUNT_ANALYSIS_CATEGORY_1, + LICENSE_COUNT_ANALYSIS_CATEGORY_2, + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE, SWITCH_FROM_TYPE, } from "../constants"; describe("analysisLicenses", () => { @@ -112,7 +117,7 @@ describe("analysisLicenses", () => { 1, null, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, null, null, @@ -125,7 +130,7 @@ describe("analysisLicenses", () => { 2, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.REUSABLE, null, null, @@ -138,7 +143,7 @@ describe("analysisLicenses", () => { 3, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -152,7 +157,7 @@ describe("analysisLicenses", () => { 4, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.DELETED, null, null, @@ -173,7 +178,7 @@ describe("analysisLicenses", () => { 11, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, null, null, @@ -186,7 +191,7 @@ describe("analysisLicenses", () => { 12, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.REUSABLE, null, null, @@ -199,7 +204,7 @@ describe("analysisLicenses", () => { 13, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -213,7 +218,7 @@ describe("analysisLicenses", () => { 14, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.DELETED, null, null, @@ -233,7 +238,7 @@ describe("analysisLicenses", () => { 21, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, null, null, @@ -246,7 +251,7 @@ describe("analysisLicenses", () => { 22, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.REUSABLE, null, null, @@ -259,7 +264,7 @@ describe("analysisLicenses", () => { 23, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -273,7 +278,7 @@ describe("analysisLicenses", () => { 24, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.DELETED, null, null, @@ -287,7 +292,7 @@ describe("analysisLicenses", () => { 25, last2Month, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -391,10 +396,8 @@ describe("analysisLicenses", () => { expect(result.switchedlicensesInTargetMonth).toHaveLength(2); expect(result.switchedlicensesInTargetMonth[0].id).toBe(1); expect(result.switchedlicensesInTargetMonth[1].id).toBe(2); - }); - it("getBaseDataFromDeletedAccounts取得情報の確認", async () => { if (!source) fail(); const context = new InvocationContext(); @@ -456,7 +459,7 @@ describe("analysisLicenses", () => { 1, null, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, null, null, @@ -469,7 +472,7 @@ describe("analysisLicenses", () => { 2, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.REUSABLE, null, null, @@ -482,7 +485,7 @@ describe("analysisLicenses", () => { 3, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -496,7 +499,7 @@ describe("analysisLicenses", () => { 4, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.DELETED, null, null, @@ -517,7 +520,7 @@ describe("analysisLicenses", () => { 11, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, null, null, @@ -530,7 +533,7 @@ describe("analysisLicenses", () => { 12, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.REUSABLE, null, null, @@ -543,7 +546,7 @@ describe("analysisLicenses", () => { 13, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -557,7 +560,7 @@ describe("analysisLicenses", () => { 14, expiringSoonDate, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.DELETED, null, null, @@ -577,7 +580,7 @@ describe("analysisLicenses", () => { 21, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, null, null, @@ -590,7 +593,7 @@ describe("analysisLicenses", () => { 22, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.REUSABLE, null, null, @@ -603,7 +606,7 @@ describe("analysisLicenses", () => { 23, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -617,7 +620,7 @@ describe("analysisLicenses", () => { 24, lastMonth, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.DELETED, null, null, @@ -631,7 +634,7 @@ describe("analysisLicenses", () => { 25, last2Month, account5_1.id, - "STANDARD", + "NORMAL", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -688,7 +691,11 @@ describe("analysisLicenses", () => { SWITCH_FROM_TYPE.TRIAL ); - const result = await getBaseDataFromDeletedAccounts(context, lastMonthYYYYMM, source); + const result = await getBaseDataFromDeletedAccounts( + context, + lastMonthYYYYMM, + source + ); expect(result.deletedAccountsAndUsersFromTier5).toHaveLength(2); expect(result.deletedAccountsAndUsersFromTier5[0].id).toBe(account5_1.id); expect(result.deletedAccountsAndUsersFromTier5[1].id).toBe(account5_2.id); @@ -734,4 +741,484 @@ describe("analysisLicenses", () => { expect(result.deletedSwitchedlicensesInTargetMonth[1].id).toBe(2); }); + it("transferDataの確認", async () => { + // getBaseData取得情報の確認とgetBaseDataFromDeletedAccounts取得情報の確認 + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + const last2MonthYYYYMM = `${last2Month.getFullYear()}${( + last2Month.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // tier4とtier5のアカウント+管理者を作る + const { account: account4, admin: admin4 } = await makeTestAccount( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1_1 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { + external_id: "external_id_tier5admin1", + role: "author", + } + ); + // 第五アカウントに紐づくユーザーを作成する + const user5_1_2 = await makeTestUser(source, { + account_id: account5_1.id, + role: "typist", + }); + const user5_1_3 = await makeTestUser(source, { + account_id: account5_1.id, + role: "typist", + }); + const user5_1_4 = await makeTestUser(source, { + account_id: account5_1.id, + role: "typist", + }); + const user5_1_5 = await makeTestUser(source, { + account_id: account5_1.id, + role: "none", + }); + const user5_1_6 = await makeTestUser(source, { + account_id: account5_1.id, + role: "author", + }); + const user5_1_7 = await makeTestUser(source, { + account_id: account5_1.id, + role: "typist", + }); + const user5_1_8 = await makeTestUser(source, { + account_id: account5_1.id, + role: "none", + }); + const user5_1_9 = await makeTestUser(source, { + account_id: account5_1.id, + role: "none", + }); + const user5_1_10 = await makeTestUser(source, { + account_id: account5_1.id, + role: "typist", + }); + + // 削除ユーザを作成する + const userArchive5_1_4 = await makeTestUserArchive(source, { + account_id: account5_1.id, + }); + // 第五階層以外だとヒットしないことの確認 + const userArchive4 = await makeTestUserArchive(source, { + account_id: account4.id, + }); + + // 所有ライセンス + // trialLicensesCount 3件 + // normalLicensesCount 5件 + // cardLicensesCount 4件 + // usedTrialLicensesAuthorCount 1件 + await createLicense( + source, + 1, + null, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + admin5_1_1.id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesTypistCount 2件 + await createLicense( + source, + 2, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_2.id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 3, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_3.id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesNoneCount 0件 + // usedNormalLicensesAuthorCount 0件 + // usedNormalLicensesTypistCount 1件 + await createLicense( + source, + 4, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_4.id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesNoneCount 1件 + await createLicense( + source, + 5, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_5.id, + null, + null, + null, + last2Month + ); + // usedCardLicensesAuthorCount 1件 + await createLicense( + source, + 6, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_6.id, + null, + null, + null, + last2Month + ); + // usedCardLicensesTypistCount 1件 + await createLicense( + source, + 7, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_7.id, + null, + null, + null, + last2Month + ); + // usedCardLicensesNoneCount 1件 + await createLicense( + source, + 8, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_8.id, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 100, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // currentMonthIssuedTrialLicensesCount 0件 + // currentMonthIssuedNormalLicensesCount 3件 + await createLicense( + source, + 11, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 12, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 13, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedCardLicensesCount 1件 + await createLicense( + source, + 14, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 101, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + lastMonth + ); + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + // invalidTrialLicensesAuthorCount 0件 + // invalidTrialLicensesTypistCount 0件 + // invalidTrialLicensesNoneCount 1件 + await createLicense( + source, + 22, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_9.id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesUnallocatedCount 0件 + // invalidNormalLicensesAuthorCount 0件 + // invalidNormalLicensesTypistCount 1件 + await createLicense( + source, + 23, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + user5_1_10.id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesNoneCount 0件 + // invalidNormalLicensesUnallocatedCount 1件 + await createLicense( + source, + 24, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // invalidCardLicensesAuthorCount 0件 + // invalidCardLicensesTypistCount 0件 + // invalidCardLicensesNoneCount 0件 + // invalidCardLicensesUnallocatedCount 0件 + // deleteはヒットしないことの確認 + await createLicense( + source, + 102, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + // 先々月はヒットしないことの確認 + await createLicense( + source, + 103, + last2Month, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + // switchedTypistLicensesAuthorCount 1件 + await createLicenseAllocationHistory( + source, + 1, + admin5_1_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesTypistCount 0件 + // switchedTypistLicensesNoneCount 0件 + // switchedCardLicensesAuthorCount 1件 + await createLicenseAllocationHistory( + source, + 2, + admin5_1_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + // switchedCardLicensesTypistCount 0件 + // switchedCardLicensesNoneCount 0件 + // SWITCH_FROM_TYPE.NONEではヒットしないことの確認 + await createLicenseAllocationHistory( + source, + 3, + admin5_1_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.NONE + ); + // 先々月の登録ではヒットしないことの確認 + await createLicenseAllocationHistory( + source, + 4, + admin5_1_1.id, + 1, + true, + account5_1.id, + last2Month, + SWITCH_FROM_TYPE.TRIAL + ); + + const result = await getBaseData(context, lastMonthYYYYMM, source); + const result_D = await getBaseDataFromDeletedAccounts( + context, + lastMonthYYYYMM, + source + ); + const transferDataResult = await transferData( + context, + result, + result_D, + lastMonthYYYYMM + ); + // ヘッダー行 + // "アカウント", "対象年月", "カテゴリー1", "カテゴリー2", "ライセンス種別", "役割", "数量" + expect(transferDataResult.outputDataUS[0]).toEqual('"アカウント",'); + expect(transferDataResult.outputDataUS[1]).toEqual('"対象年月",'); + expect(transferDataResult.outputDataUS[2]).toEqual(`"カテゴリー1",`); + expect(transferDataResult.outputDataUS[3]).toEqual(`"カテゴリー2",`); + expect(transferDataResult.outputDataUS[4]).toEqual(`"ライセンス種別",`); + expect(transferDataResult.outputDataUS[5]).toEqual(`"役割",`); + expect(transferDataResult.outputDataUS[6]).toEqual(`"数量"` + "\r\n"); + // データ行 + // "アカウント", "対象年月", "カテゴリー1", "カテゴリー2", "ライセンス種別", "役割", "数量" + // 1行目だけ確認する + expect(transferDataResult.outputDataUS[7]).toEqual( + `"${account5_1.company_name}",` + ); + expect(transferDataResult.outputDataUS[8]).toEqual(`"${lastMonthYYYYMM}",`); + expect(transferDataResult.outputDataUS[9]).toEqual( + `"${LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES}",` + ); + expect(transferDataResult.outputDataUS[10]).toEqual( + `"${LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES}",` + ); + expect(transferDataResult.outputDataUS[11]).toEqual( + `"${LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL}",` + ); + expect(transferDataResult.outputDataUS[12]).toEqual(`"",`); + expect(transferDataResult.outputDataUS[13]).toEqual(`"3"` + "\r\n"); + }); }); diff --git a/dictation_function/src/test/common/utility.ts b/dictation_function/src/test/common/utility.ts index a22eed7..c167750 100644 --- a/dictation_function/src/test/common/utility.ts +++ b/dictation_function/src/test/common/utility.ts @@ -7,7 +7,7 @@ import { License, LicenseAllocationHistory, LicenseArchive, - LicenseAllocationHistoryArchive + LicenseAllocationHistoryArchive, } from "../../entity/license.entity"; type InitialTestDBState = { @@ -131,7 +131,7 @@ export const makeTestAccount = async ( const { identifiers } = await datasource.getRepository(User).insert({ external_id: d?.external_id ?? uuidv4(), account_id: accountId, - role: d?.role ?? "admin none", + role: d?.role ?? "none", author_id: d?.author_id ?? undefined, accepted_eula_version: d?.accepted_eula_version ?? "1.0", accepted_dpa_version: d?.accepted_dpa_version ?? "1.0", @@ -224,7 +224,7 @@ export const createLicenseAllocationHistory = async ( is_allocated: boolean, account_id: number, executed_at: Date, - switch_from_type: string, + switch_from_type: string ): Promise => { const { identifiers } = await datasource .getRepository(LicenseAllocationHistory) @@ -316,8 +316,6 @@ export const makeTestUserArchive = async ( return userArchive; }; - - /** * テスト ユーティリティ: 指定したプロパティを上書きしたアカウントとその管理者ユーザーを作成する * @param dataSource データソース From 9f5ccabb0c2fc7c8f09431cd5d19acd34990d1a5 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 13 Mar 2024 02:01:28 +0000 Subject: [PATCH 052/109] =?UTF-8?q?Merged=20PR=20830:=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E5=89=8A=E9=99=A4=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3896: アカウント削除修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3896) - アカウント削除を実行すると失敗するので、対応を実施しました。 - `AccountsRepositoryModule`のインポートに`AccountArchive`を追加しました。 ## レビューポイント - 対応内容は適切でしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - npm run test - 削除実行 - 行った修正がデグレを発生させていないことを確認できるか - リポジトリのインポートへの追加のみなので対象の処理ができることとtestが通ることで確認 --- .../src/repositories/accounts/accounts.repository.module.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dictation_server/src/repositories/accounts/accounts.repository.module.ts b/dictation_server/src/repositories/accounts/accounts.repository.module.ts index 999d78a..1ce9003 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.module.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.module.ts @@ -3,9 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Account } from './entity/account.entity'; import { AccountsRepositoryService } from './accounts.repository.service'; import { License, LicenseOrder } from '../licenses/entity/license.entity'; +import { AccountArchive } from './entity/account_archive.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Account, License, LicenseOrder])], + imports: [ + TypeOrmModule.forFeature([Account, AccountArchive, License, LicenseOrder]), + ], providers: [AccountsRepositoryService], exports: [AccountsRepositoryService], }) From 83e297cc9b651797b1bfe1ba9b703069453fa3ab Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Wed, 13 Mar 2024 07:41:25 +0000 Subject: [PATCH 053/109] =?UTF-8?q?Merged=20PR=20821:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E3=83=91=E3=83=BC=E3=83=88=E3=83=8A?= =?UTF-8?q?=E3=83=BC=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9=E4=B8=80?= =?UTF-8?q?=E8=A6=A7=E7=94=BB=E9=9D=A2=EF=BC=86=E9=9A=8E=E5=B1=A4=E6=A7=8B?= =?UTF-8?q?=E9=80=A0=E5=A4=89=E6=9B=B4=E3=83=9D=E3=83=83=E3=83=97=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3854: 画面実装(パートナーライセンス一覧画面&階層構造変更ポップアップ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3854) - パートナーライセンス一覧に「Change Owner」ボタンを配置し、表示制御およびクリック時にポップアップ表示する処理の実装 - アカウント階層構造変更ポップアップの処理全体実装 - サーバー側のエラーコード定義 ## レビューポイント - 「一括」を表現するためのドロップダウンの構築や処理周りで改善点ないか(to:斎藤くん) - コンポーネントでの状態管理でお作法に違反しているところないか(to:斎藤くん) - 修正箇所がほかの機能に影響していないか - パートナーライセンス一覧の画面表示に何らか悪影響ないか?(to:ガンさん) ## 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/Task3854?csf=1&web=1&e=jBGQrR ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認しました - 第一階層でログインしてかつ第三または第四視点での一覧を確認しているときにChangeOwnerボタンが表示される - ボタン押下すると、仕様通りにポップアップの表示が行われる - ポップアップにて入力項目に入力できる&バリデーション効いている - ポップアップにて実行ボタン押下するとAPI実行できる&処理結果に応じて仕様通りの挙動をすること - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - パートナーライセンス画面に新規ボタンを配置した&新規ポップアップの実装のみのため、 ポップアップでの処理が正常終了/失敗/何もせず閉じた場合に元の画面の表示が今まで通り動くことを確認済み。 --- dictation_client/src/api/api.ts | 94 ++++++++ .../src/assets/images/change_circle.svg | 18 ++ .../src/assets/images/shuffle.svg | 1 + dictation_client/src/common/errors/code.ts | 4 + .../license/partnerLicense/operations.ts | 85 ++++++++ .../partnerLicense/partnerLicenseSlice.ts | 15 +- .../pages/LicensePage/changeOwnerPopup.tsx | 203 ++++++++++++++++++ .../src/pages/LicensePage/partnerLicense.tsx | 41 ++++ dictation_client/src/styles/app.module.scss | 55 +++++ .../src/styles/app.module.scss.d.ts | 7 + dictation_client/src/translation/de.json | 17 +- dictation_client/src/translation/en.json | 17 +- dictation_client/src/translation/es.json | 17 +- dictation_client/src/translation/fr.json | 17 +- dictation_server/src/common/error/code.ts | 4 + dictation_server/src/common/error/message.ts | 4 + 16 files changed, 594 insertions(+), 5 deletions(-) create mode 100644 dictation_client/src/assets/images/change_circle.svg create mode 100644 dictation_client/src/assets/images/shuffle.svg create mode 100644 dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index b68f729..28ca9ca 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -1840,6 +1840,25 @@ export interface SignupRequest { */ 'prompt'?: boolean; } +/** + * + * @export + * @interface SwitchParentRequest + */ +export interface SwitchParentRequest { + /** + * 切り替え先の親アカウントID + * @type {number} + * @memberof SwitchParentRequest + */ + 'to': number; + /** + * 親を変更したいアカウントIDのリスト + * @type {Array} + * @memberof SwitchParentRequest + */ + 'children': Array; +} /** * * @export @@ -3474,6 +3493,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + switchParent: async (switchParentRequest: SwitchParentRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'switchParentRequest' is not null or undefined + assertParamExists('switchParent', 'switchParentRequest', switchParentRequest) + const localVarPath = `/accounts/parent/switch`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(switchParentRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -4043,6 +4102,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.issueLicense']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async switchParent(switchParentRequest: SwitchParentRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.switchParent(switchParentRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.switchParent']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -4369,6 +4441,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: any): AxiosPromise { return localVarFp.issueLicense(issueLicenseRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + switchParent(switchParentRequest: SwitchParentRequest, options?: any): AxiosPromise { + return localVarFp.switchParent(switchParentRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4725,6 +4807,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).issueLicense(issueLicenseRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public switchParent(switchParentRequest: SwitchParentRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).switchParent(switchParentRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/assets/images/change_circle.svg b/dictation_client/src/assets/images/change_circle.svg new file mode 100644 index 0000000..bf74c41 --- /dev/null +++ b/dictation_client/src/assets/images/change_circle.svg @@ -0,0 +1,18 @@ + + + + + + diff --git a/dictation_client/src/assets/images/shuffle.svg b/dictation_client/src/assets/images/shuffle.svg new file mode 100644 index 0000000..ac127a9 --- /dev/null +++ b/dictation_client/src/assets/images/shuffle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index ce0c7b6..9ba15db 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -77,4 +77,8 @@ export const errorCodes = [ "E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) "E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) "E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) + "E017001", // 親アカウント変更不可エラー(指定したアカウントが存在しない) + "E017002", // 親アカウント変更不可エラー(階層関係が不正) + "E017003", // 親アカウント変更不可エラー(リージョンが同一でない) + "E017004", // 親アカウント変更不可エラー(国が同一でない) ] as const; diff --git a/dictation_client/src/features/license/partnerLicense/operations.ts b/dictation_client/src/features/license/partnerLicense/operations.ts index fbff656..8ceb0a8 100644 --- a/dictation_client/src/features/license/partnerLicense/operations.ts +++ b/dictation_client/src/features/license/partnerLicense/operations.ts @@ -105,3 +105,88 @@ export const getPartnerLicenseAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const switchParentAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + // パラメータ + to: number; + children: number[]; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/switchParentAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + + const { to, children } = args; + + try { + await accountsApi.switchParent( + { + to, + children, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // TODO:エラー処理 + if (error.code === "E017001") { + errorMessage = getTranslationID( + "changeOwnerPopup.message.accountNotFoundError" + ); + } + + if (error.code === "E017002") { + errorMessage = getTranslationID( + "changeOwnerPopup.message.hierarchyMismatchError" + ); + } + + if (error.code === "E017003") { + errorMessage = getTranslationID( + "changeOwnerPopup.message.regionMismatchError" + ); + } + + if (error.code === "E017004") { + errorMessage = getTranslationID( + "changeOwnerPopup.message.countryMismatchError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts index c6c8b92..b3c894b 100644 --- a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts +++ b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts @@ -1,7 +1,11 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { PartnerLicenseInfo } from "api"; import { PartnerLicensesState, HierarchicalElement } from "./state"; -import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations"; +import { + getMyAccountAsync, + getPartnerLicenseAsync, + switchParentAsync, +} from "./operations"; import { ACCOUNTS_VIEW_LIMIT } from "./constants"; const initialState: PartnerLicensesState = { @@ -109,6 +113,15 @@ export const partnerLicenseSlice = createSlice({ builder.addCase(getPartnerLicenseAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(switchParentAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(switchParentAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(switchParentAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); export const { diff --git a/dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx b/dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx new file mode 100644 index 0000000..04084f1 --- /dev/null +++ b/dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx @@ -0,0 +1,203 @@ +import React, { useState, useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectChildrenPartnerLicenses, + selectIsLoading, + selectOwnPartnerLicense, +} from "features/license/partnerLicense/selectors"; +import { + getMyAccountAsync, + switchParentAsync, +} from "features/license/partnerLicense/operations"; +import { useTranslation } from "react-i18next"; +import { getTranslationID } from "translation"; +import { AppDispatch } from "app/store"; +import { clearHierarchicalElement } from "features/license/partnerLicense"; +import styles from "../../styles/app.module.scss"; +import close from "../../assets/images/close.svg"; +import shuffle from "../../assets/images/shuffle.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +interface ChangeOwnerPopupProps { + onClose: () => void; +} + +const ChangeOwnerPopup: React.FC = (props) => { + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + + const [selectedChildId, setSelectedChildId] = useState(null); + const [selectedChildName, setSelectedChildName] = useState(""); + const [destinationParentId, setDestinationParentId] = useState(""); + const [error, setError] = useState(""); + + const originParentLicenseInfo = useSelector(selectOwnPartnerLicense); + const childrenLicenseInfos = useSelector(selectChildrenPartnerLicenses); + const isLoading = useSelector(selectIsLoading); + + const { onClose } = props; + const closePopup = useCallback(() => { + if (isLoading) return; + onClose(); + }, [isLoading, onClose]); + + const bulkDisplayName = "-- Bulk --"; + const bulkValue = "bulk"; + + const onBulkChange = useCallback( + (e: React.ChangeEvent) => { + const { value } = e.target; + const childId = value === bulkValue ? null : Number(value); + setSelectedChildId(childId); + + // 一括追加のときは子アカウント名を表示しない + let childName = ""; + if (childId) { + const child = childrenLicenseInfos.find((c) => c.accountId === childId); + // childがundefinedになることはないが、コード解析対応のためのチェック + if (child) { + childName = child.companyName; + } + } + setSelectedChildName(childName); + }, + [childrenLicenseInfos] + ); + + const onSaveClick = useCallback(async () => { + const destinationParentIdNum = Number(destinationParentId); + if ( + Number.isNaN(destinationParentIdNum) || // 数値でない場合 + destinationParentIdNum <= 0 || // IDにならない数値の場合 + destinationParentId.length > 7 // 8桁以上の場合(本システムの特徴として8桁以上になることはあり得ない) + ) { + setError(t(getTranslationID("changeOwnerPopup.label.invalidInputError"))); + return; + } + setError(""); + if ( + // eslint-disable-next-line no-alert + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const children = selectedChildId + ? [selectedChildId] + : childrenLicenseInfos.map((child) => child.accountId); + const { meta } = await dispatch( + switchParentAsync({ to: Number(destinationParentId), children }) + ); + if (meta.requestStatus === "fulfilled") { + dispatch(getMyAccountAsync()); + dispatch(clearHierarchicalElement()); + closePopup(); + } + }, [ + childrenLicenseInfos, + closePopup, + destinationParentId, + dispatch, + selectedChildId, + t, + ]); + + return ( +
    +
    +

    + {t(getTranslationID("changeOwnerPopup.label.title"))} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */} + close +

    +
    +
    +
    +
    + {t(getTranslationID("changeOwnerPopup.label.upperLayerId"))} +
    +
    +

    + + + {originParentLicenseInfo.companyName} + +

    +

    +

    + setDestinationParentId(e.target.value)} + /> + {error} +

    +
    +
    + +
    +
    + {t(getTranslationID("changeOwnerPopup.label.lowerLayerId"))} +
    +
    + + {selectedChildName} +
    +
    + {/* 処理中や子アカウントが1件も存在しない場合、Saveボタンを押せないようにする */} + 0 + ? styles.isActive + : "" + }`} + onClick={onSaveClick} + disabled={isLoading || childrenLicenseInfos.length <= 0} + /> +
    + + Loading +
    + +
    +
    + ); +}; + +export default ChangeOwnerPopup; diff --git a/dictation_client/src/pages/LicensePage/partnerLicense.tsx b/dictation_client/src/pages/LicensePage/partnerLicense.tsx index acc9801..03cf8aa 100644 --- a/dictation_client/src/pages/LicensePage/partnerLicense.tsx +++ b/dictation_client/src/pages/LicensePage/partnerLicense.tsx @@ -12,6 +12,7 @@ import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup"; import postAdd from "../../assets/images/post_add.svg"; import history from "../../assets/images/history.svg"; import returnLabel from "../../assets/images/undo.svg"; +import changeOwnerIcon from "../../assets/images/change_circle.svg"; import { isApproveTier } from "../../features/auth/utils"; import { TIERS } from "../../components/auth/constants"; import { @@ -37,6 +38,7 @@ import { LicenseOrderPopup } from "./licenseOrderPopup"; import { LicenseOrderHistory } from "./licenseOrderHistory"; import { LicenseSummary } from "./licenseSummary"; import progress_activit from "../../assets/images/progress_activit.svg"; +import ChangeOwnerPopup from "./changeOwnerPopup"; const PartnerLicense: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -49,6 +51,7 @@ const PartnerLicense: React.FC = (): JSX.Element => { const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] = useState(false); const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false); + const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false); // 階層表示用 const tierNames: { [key: number]: string } = { @@ -148,6 +151,11 @@ const PartnerLicense: React.FC = (): JSX.Element => { [dispatch, setIslicenseOrderHistoryOpen] ); + // changeOwnerボタン押下時 + const onClickChangeOwner = useCallback(() => { + setIsChangeOwnerPopupOpen(true); + }, [setIsChangeOwnerPopupOpen]); + // マウント時のみ実行 useEffect(() => { dispatch(getMyAccountAsync()); @@ -245,6 +253,13 @@ const PartnerLicense: React.FC = (): JSX.Element => { }} /> )} + {isChangeOwnerPopupOpen && ( + { + setIsChangeOwnerPopupOpen(false); + }} + /> + )} {!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
    @@ -329,6 +344,26 @@ const PartnerLicense: React.FC = (): JSX.Element => { )} +
  • + {isVisibleChangeOwner(ownPartnerLicenseInfo.tier) && ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + + + {t( + getTranslationID( + "partnerLicense.label.changeOwnerButton" + ) + )} + + )} +
    • {hierarchicalElements.map((value) => ( @@ -545,4 +580,10 @@ const PartnerLicense: React.FC = (): JSX.Element => { ); }; +const isVisibleChangeOwner = (partnerTier: number) => + // 自身が第一階層または第二階層で、表示しているパートナーが第三階層または第四階層の場合のみ表示 + isApproveTier([TIERS.TIER1, TIERS.TIER2]) && + (partnerTier.toString() === TIERS.TIER3 || + partnerTier.toString() === TIERS.TIER4); + export default PartnerLicense; diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index c3a2d1e..36371a3 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1999,6 +1999,61 @@ tr.isSelected .menuInTable li a.isDisable { text-align: right; } +.formList dd.ownerChange { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin-bottom: 0; +} +.formList dd.ownerChange p.Owner, +.formList dd.ownerChange p.newOwner { + width: 150px; +} +.formList dd.ownerChange .arrowR { + width: 8%; + height: 20px; + margin-top: 10px; + margin-right: 2%; + background: #e6e6e6; + position: relative; +} +.formList dd.ownerChange .arrowR::after { + content: ""; + border-top: 20px transparent solid; + border-bottom: 20px transparent solid; + border-left: 20px #e6e6e6 solid; + position: absolute; + top: 50%; + right: -15px; + transform: translateY(-50%); +} +.formList dd.ownerChange + .full { + width: 66%; + margin-left: 30%; + margin-bottom: -10px; + text-align: center; +} +.formList dd.ownerChange + .full .transOwner { + width: 100px; +} +.formList dd.lowerTrans { + margin-bottom: 1.5rem; + position: relative; + text-align: center; +} +.formList dd.lowerTrans select, +.formList dd.lowerTrans span { + margin: 0 auto; +} +.formList dd .txName { + display: block; + width: 150px; + padding: 0.2rem 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .dictation .menuAction { margin-top: -1rem; height: 34px; diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index e82c3c7..7f380fa 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -129,6 +129,13 @@ declare const classNames: { readonly cardHistory: "cardHistory"; readonly partner: "partner"; readonly isOpen: "isOpen"; + readonly ownerChange: "ownerChange"; + readonly Owner: "Owner"; + readonly newOwner: "newOwner"; + readonly arrowR: "arrowR"; + readonly transOwner: "transOwner"; + readonly lowerTrans: "lowerTrans"; + readonly txName: "txName"; readonly alignLeft: "alignLeft"; readonly displayOptions: "displayOptions"; readonly tableFilter: "tableFilter"; diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 9a7af9d..6620775 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -368,7 +368,8 @@ "shortage": "Lizenzmangel", "issueRequesting": "Lizenzen auf Bestellung", "viewDetails": "Details anzeigen", - "accounts": "konten" + "accounts": "konten", + "changeOwnerButton": "(de)Change Owner" } }, "orderHistoriesPage": { @@ -619,5 +620,19 @@ "saveButton": "(de)Save Settings", "daysValidationError": "(de)Daysには1~999の数字を入力してください。" } + }, + "changeOwnerPopup": { + "message": { + "accountNotFoundError": "(de)変更先のアカウントIDは存在しません。", + "hierarchyMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + }, + "label": { + "invalidInputError": "(de)変更先アカウントIDには1~9999999の数字を入力してください。", + "title": "(de)Change Owner", + "upperLayerId": "(de)Upper Layer ID", + "lowerLayerId": "(de)Lower Layer ID" + } } } \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 7b91390..3151765 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -368,7 +368,8 @@ "shortage": "License Shortage", "issueRequesting": "Licenses on Order", "viewDetails": "View Details", - "accounts": "accounts" + "accounts": "accounts", + "changeOwnerButton": "Change Owner" } }, "orderHistoriesPage": { @@ -619,5 +620,19 @@ "saveButton": "Save Settings", "daysValidationError": "Daysには1~999の数字を入力してください。" } + }, + "changeOwnerPopup": { + "message": { + "accountNotFoundError": "変更先のアカウントIDは存在しません。", + "hierarchyMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + }, + "label": { + "invalidInputError": "変更先アカウントIDには1~9999999の数字を入力してください。", + "title": "Change Owner", + "upperLayerId": "Upper Layer ID", + "lowerLayerId": "Lower Layer ID" + } } } \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 4dd9b29..a1bc703 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -368,7 +368,8 @@ "shortage": "Escasez de licencias", "issueRequesting": "Licencias en Pedido", "viewDetails": "Ver detalles", - "accounts": "cuentas" + "accounts": "cuentas", + "changeOwnerButton": "(es)Change Owner" } }, "orderHistoriesPage": { @@ -619,5 +620,19 @@ "saveButton": "(es)Save Settings", "daysValidationError": "(es)Daysには1~999の数字を入力してください。" } + }, + "changeOwnerPopup": { + "message": { + "accountNotFoundError": "(es)変更先のアカウントIDは存在しません。", + "hierarchyMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + }, + "label": { + "invalidInputError": "(es)変更先アカウントIDには1~9999999の数字を入力してください。", + "title": "(es)Change Owner", + "upperLayerId": "(es)Upper Layer ID", + "lowerLayerId": "(es)Lower Layer ID" + } } } \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index af62dc2..d8b0555 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -368,7 +368,8 @@ "shortage": "Pénurie de licences", "issueRequesting": "Licences en commande", "viewDetails": "Voir les détails", - "accounts": "comptes" + "accounts": "comptes", + "changeOwnerButton": "(fr)Change Owner" } }, "orderHistoriesPage": { @@ -619,5 +620,19 @@ "saveButton": "(fr)Save Settings", "daysValidationError": "(fr)Daysには1~999の数字を入力してください。" } + }, + "changeOwnerPopup": { + "message": { + "accountNotFoundError": "(fr)変更先のアカウントIDは存在しません。", + "hierarchyMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + }, + "label": { + "invalidInputError": "(fr)変更先アカウントIDには1~9999999の数字を入力してください。", + "title": "(fr)Change Owner", + "upperLayerId": "(fr)Upper Layer ID", + "lowerLayerId": "(fr)Lower Layer ID" + } } } \ No newline at end of file diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index a789ab7..84f6dd6 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -82,4 +82,8 @@ export const ErrorCodes = [ 'E016001', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) 'E016002', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) 'E016003', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) + 'E017001', // 親アカウント変更不可エラー(指定したアカウントが存在しない) + 'E017002', // 親アカウント変更不可エラー(階層関係が不正) + 'E017003', // 親アカウント変更不可エラー(リージョンが同一でない) + 'E017004', // 親アカウント変更不可エラー(国が同一でない) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 793cdf5..2e22585 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -72,4 +72,8 @@ export const errors: Errors = { E016002: 'Template file delete failed Error: workflow assigned', E016003: 'Template file delete failed Error: not finished task has template file', + E017001: 'Parent account switch failed Error: account not found', + E017002: 'Parent account switch failed Error: hierarchy mismatch', + E017003: 'Parent account switch failed Error: region mismatch', + E017004: 'Parent account switch failed Error: country mismatch', }; From 2b68a9f054d1e2431818402e583b39a5719f32bd Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Wed, 13 Mar 2024 07:54:10 +0000 Subject: [PATCH 054/109] =?UTF-8?q?Merged=20PR=20824:=20AzureFunctions?= =?UTF-8?q?=E5=AE=9F=E8=A3=853=EF=BC=88CSV=E3=82=92=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=82=B8=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=AB=E9=85=8D=E7=BD=AE=E3=81=99=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3846: AzureFunctions実装3(CSVをストレージアカウントに配置する)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3846) outputDataを追加 →outputAnalysisLicensesDataに変更(アラートルールなどでログを見るので、何の処理か理解できるように) blobstorageService.tsに以下を追加 - uploadFileAnalysisLicensesCSV (ライセンスCSVを配置する) - createContainerAnalysisを追加 (コンテナーを作成する) 環境変数の追加 ## レビューポイント - 今回追加されたJP-EASTのストレージアカウントのコンテナーが、第一階層のアカウントのものであるかどうかはソース上は特に意識していないが問題ないでしょうか。 ## 動作確認状況 - ローカルで確認(モックでソース上処理が通ることのみ確認のみ) 詳細なテストは別タスクで行う。 ## 補足 - 相談、参考資料などがあれば --- .../src/blobstorage/blobstorage.service.ts | 115 +++++++++++++++--- dictation_function/src/constants/index.ts | 12 ++ .../src/functions/analysisLicenses.ts | 100 ++++++++++++--- .../functions/analysisLicensesManualRetry.ts | 2 +- .../src/test/analysisLicenses.spec.ts | 92 ++++++++++++++ .../blobstorage/blobstorage.service.ts | 2 - 6 files changed, 286 insertions(+), 37 deletions(-) diff --git a/dictation_function/src/blobstorage/blobstorage.service.ts b/dictation_function/src/blobstorage/blobstorage.service.ts index d9b4472..2adfee4 100644 --- a/dictation_function/src/blobstorage/blobstorage.service.ts +++ b/dictation_function/src/blobstorage/blobstorage.service.ts @@ -2,29 +2,52 @@ import { BlobServiceClient, StorageSharedKeyCredential, } from "@azure/storage-blob"; -import { IMPORT_USERS_CONTAINER_NAME } from "../constants"; +import { + IMPORT_USERS_CONTAINER_NAME, + LICENSE_COUNT_ANALYSIS_CONTAINER_NAME, +} from "../constants"; import { InvocationContext } from "@azure/functions"; export class BlobstorageService { private readonly blobServiceClient: BlobServiceClient; private readonly sharedKeyCredential: StorageSharedKeyCredential; - constructor() { - if ( - !process.env.STORAGE_ACCOUNT_NAME_IMPORT || - !process.env.STORAGE_ACCOUNT_KEY_IMPORT || - !process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT - ) { - throw new Error("Storage account information is missing"); - } - this.sharedKeyCredential = new StorageSharedKeyCredential( - process.env.STORAGE_ACCOUNT_NAME_IMPORT, - process.env.STORAGE_ACCOUNT_KEY_IMPORT - ); - this.blobServiceClient = new BlobServiceClient( - process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT, - this.sharedKeyCredential - ); + constructor(useAnalysisBlob: boolean = false) { + if (!useAnalysisBlob) { + if ( + !process.env.STORAGE_ACCOUNT_NAME_IMPORT || + !process.env.STORAGE_ACCOUNT_KEY_IMPORT || + !process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT + ) { + throw new Error("Storage account information is missing"); + } + + this.sharedKeyCredential = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_IMPORT, + process.env.STORAGE_ACCOUNT_KEY_IMPORT + ); + this.blobServiceClient = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT, + this.sharedKeyCredential + ); + } else { + if ( + !process.env.STORAGE_ACCOUNT_NAME_ANALYSIS || + !process.env.STORAGE_ACCOUNT_KEY_ANALYSIS || + !process.env.STORAGE_ACCOUNT_ENDPOINT_ANALYSIS + ) { + throw new Error("Storage account information for analysis is missing"); + } + + this.sharedKeyCredential = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_ANALYSIS, + process.env.STORAGE_ACCOUNT_KEY_ANALYSIS + ); + this.blobServiceClient = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_ANALYSIS, + this.sharedKeyCredential + ); + } } /** * Lists blobs @@ -181,4 +204,62 @@ export class BlobstorageService { context.log(`[OUT] ${this.isFileExists.name}`); } } + + /** + * uplocad file analysis licenses csv + * @param context + * @param filename + * @param data + * @returns boolean + */ + public async uploadFileAnalysisLicensesCSV( + context: InvocationContext, + filename: string, + data: string + ): Promise { + context.log( + `[IN] ${this.uploadFileAnalysisLicensesCSV.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + LICENSE_COUNT_ANALYSIS_CONTAINER_NAME + ); + const { response } = await containerClient.uploadBlockBlob( + filename, + data, + data.length + ); + if (response.errorCode) { + context.log(`update failed. response errorCode: ${response.errorCode}`); + return false; + } + return true; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] ${this.uploadFileAnalysisLicensesCSV.name}`); + } + } + /** + * Create container analysis + * @param context + * @returns container + */ + async createContainerAnalysis(context: InvocationContext): Promise { + context.log(`[IN] ${this.createContainerAnalysis.name}`); + + try { + // コンテナ作成 + const containerClient = this.blobServiceClient.getContainerClient( + LICENSE_COUNT_ANALYSIS_CONTAINER_NAME + ); + await containerClient.create(); + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] ${this.createContainerAnalysis.name}`); + } + } } diff --git a/dictation_function/src/constants/index.ts b/dictation_function/src/constants/index.ts index 0c31242..6f94278 100644 --- a/dictation_function/src/constants/index.ts +++ b/dictation_function/src/constants/index.ts @@ -390,3 +390,15 @@ export const LICENSE_COUNT_ANALYSIS_ROLE = { NONE: "None", UNALLOCATED: "Unallocated", }; + +/** + * ライセンス数推移出力機能のファイルの先頭文字列 + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_FRONT_STRING = "LicenseAggregated"; + +/** + * ライセンス数推移CSV用のコンテナー名 + * @const {string} + */ +export const LICENSE_COUNT_ANALYSIS_CONTAINER_NAME = "analysis-licenses"; diff --git a/dictation_function/src/functions/analysisLicenses.ts b/dictation_function/src/functions/analysisLicenses.ts index 515b3aa..b0caa71 100644 --- a/dictation_function/src/functions/analysisLicenses.ts +++ b/dictation_function/src/functions/analysisLicenses.ts @@ -23,9 +23,8 @@ import { BLOB_STORAGE_REGION_EU, LICENSE_TYPE, LICENSE_COUNT_ANALYSIS_HEADER, + LICENSE_COUNT_ANALYSIS_FRONT_STRING, } from "../constants"; -import * as fs from "fs"; -import * as path from "path"; import { DateWithDayEndTime } from "../common/types/types"; import { initializeDataSource } from "../database/initializeDataSource"; @@ -55,8 +54,11 @@ export async function analysisLicensesProcessing( baseDataFromDeletedAccounts, targetMonthYYYYMM ); - // TODO: 後続処理の呼び出しイメージ(別タスクで追加) - // await outputData(context, blobstorageService, outputCsvData); + await outputAnalysisLicensesData( + context, + blobstorageService, + outputCsvData + ); } catch (e) { context.log("analysisLicensesProcessing failed."); context.error(e); @@ -336,7 +338,7 @@ export async function analysisLicenses( dotenv.config({ path: ".env.local", override: true }); try { const datasource = await initializeDataSource(context); - const blobstorageService = new BlobstorageService(); + const blobstorageService = new BlobstorageService(true); try { // 現在の日付より、先月の年月をYYYYMM形式で取得 @@ -1321,18 +1323,6 @@ export async function transferData( ); } } - // outputDataUSをローカルにCSV出力(テスト用、別タスクで消す) - // outputDataUSの配列をCSV形式に変換 - /*let csvContentUS = ""; - for (let i = 0; i < outputDataUS.length; i++) { - //カンマ区切りの文字列を作成 - - csvContentUS += outputDataUS[i]; - } - // CSVファイルを出力 - const filePathUS = path.join(__dirname, "outputDataUS.csv"); - fs.writeFileSync(filePathUS, csvContentUS); - */ return { outputDataUS: outputDataUS, outputDataEU: outputDataEU, @@ -1775,3 +1765,79 @@ async function createOutputData( context.log("[OUT]createOutputData"); } } + +/** + * 出力データを第一階層のストレージアカウントにCSVファイルとして出力する + * @param context + * @param blobstorageService: BlobstorageService, + * @param outputCsvData: outputDataAnalysisLicensesCSV + */ +export async function outputAnalysisLicensesData( + context: InvocationContext, + blobstorageService: BlobstorageService, + outputCsvData: outputDataAnalysisLicensesCSV +): Promise { + context.log("[IN]outputAnalysisLicensesData"); + try { + let csvContentUS = ""; + let csvContentEU = ""; + let csvContentAU = ""; + // 出力日時を取得 + const outputDateTime = new Date().toISOString(); + // YYYYMMDDHH24MISS形式に変換 + const outputDateTimeYYYYMMDDHH24MISS = outputDateTime.replace(/[-T:]/g, ""); + // 出力ファイル名を作成 + const outputFileNameUS = + LICENSE_COUNT_ANALYSIS_FRONT_STRING + + `_US_${outputDateTimeYYYYMMDDHH24MISS}.csv`; + const outputFileNameEU = + LICENSE_COUNT_ANALYSIS_FRONT_STRING + + `_EU_${outputDateTimeYYYYMMDDHH24MISS}.csv`; + const outputFileNameAU = + LICENSE_COUNT_ANALYSIS_FRONT_STRING + + `_AU_${outputDateTimeYYYYMMDDHH24MISS}.csv`; + + // outputDataUSの配列をCSV形式に変換 + for (let i = 0; i < outputCsvData.outputDataUS.length; i++) { + //カンマ区切りの文字列を作成 + csvContentUS += outputCsvData.outputDataUS[i]; + } + // outputDataEUの配列をCSV形式に変換 + for (let i = 0; i < outputCsvData.outputDataEU.length; i++) { + //カンマ区切りの文字列を作成 + csvContentEU += outputCsvData.outputDataEU[i]; + } + // outputDataAUの配列をCSV形式に変換 + for (let i = 0; i < outputCsvData.outputDataAU.length; i++) { + //カンマ区切りの文字列を作成 + csvContentAU += outputCsvData.outputDataAU[i]; + } + await blobstorageService.createContainerAnalysis(context); + // 出力ファイル名を指定して出力 + const resultUS = await blobstorageService.uploadFileAnalysisLicensesCSV( + context, + outputFileNameUS, + outputCsvData.outputDataUS.join("\r\n") + ); + context.log("resultUS: " + resultUS); + const resultEU = await blobstorageService.uploadFileAnalysisLicensesCSV( + context, + outputFileNameEU, + outputCsvData.outputDataEU.join("\r\n") + ); + const resultAU = await blobstorageService.uploadFileAnalysisLicensesCSV( + context, + outputFileNameAU, + outputCsvData.outputDataAU.join("\r\n") + ); + // 出力結果を返却 + // 3つのリージョンの出力が全て成功した場合にtrueを返却 + return resultUS && resultEU && resultAU; + } catch (e) { + context.log("outputAnalysisLicensesData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]outputAnalysisLicensesData"); + } +} diff --git a/dictation_function/src/functions/analysisLicensesManualRetry.ts b/dictation_function/src/functions/analysisLicensesManualRetry.ts index d21c61b..8b9dfaf 100644 --- a/dictation_function/src/functions/analysisLicensesManualRetry.ts +++ b/dictation_function/src/functions/analysisLicensesManualRetry.ts @@ -23,7 +23,7 @@ export async function analysisLicensesManualRetry( dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); const datasource = await initializeDataSource(context); - const blobstorageService = new BlobstorageService(); + const blobstorageService = new BlobstorageService(true); try { // 現在の日付より、先月の年月をYYYYMM形式で取得 diff --git a/dictation_function/src/test/analysisLicenses.spec.ts b/dictation_function/src/test/analysisLicenses.spec.ts index 4545ca7..0330d68 100644 --- a/dictation_function/src/test/analysisLicenses.spec.ts +++ b/dictation_function/src/test/analysisLicenses.spec.ts @@ -2,6 +2,7 @@ import { DataSource } from "typeorm"; import { getBaseData, getBaseDataFromDeletedAccounts, + outputAnalysisLicensesData, transferData, } from "../functions/analysisLicenses"; import { @@ -27,6 +28,7 @@ import { LICENSE_COUNT_ANALYSIS_LICENSE_TYPE, SWITCH_FROM_TYPE, } from "../constants"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; describe("analysisLicenses", () => { dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); @@ -1221,4 +1223,94 @@ describe("analysisLicenses", () => { expect(transferDataResult.outputDataUS[12]).toEqual(`"",`); expect(transferDataResult.outputDataUS[13]).toEqual(`"3"` + "\r\n"); }); + + it("outputDataの確認(Mock)", async () => { + const blobService = new BlobstorageService(); + + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + const last2MonthYYYYMM = `${last2Month.getFullYear()}${( + last2Month.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // tier4とtier5のアカウント+管理者を作る + const { account: account4, admin: admin4 } = await makeTestAccount( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1_1 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { + external_id: "external_id_tier5admin1", + role: "author", + } + ); + + // 所有ライセンス + // usedTrialLicensesAuthorCount 1件 + await createLicense( + source, + 1, + null, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + admin5_1_1.id, + null, + null, + null, + last2Month + ); + + const result = await getBaseData(context, lastMonthYYYYMM, source); + const result_D = await getBaseDataFromDeletedAccounts( + context, + lastMonthYYYYMM, + source + ); + const transferDataResult = await transferData( + context, + result, + result_D, + lastMonthYYYYMM + ); + const mockUploadFileAnalysisLicensesCSV = jest.fn().mockReturnValue(true); + const mockCreateContainerAnalysis = jest.fn().mockReturnValue(true); + blobService.uploadFileAnalysisLicensesCSV = + mockUploadFileAnalysisLicensesCSV; + blobService.createContainerAnalysis = mockCreateContainerAnalysis; + + const resultOutputData = await outputAnalysisLicensesData( + context, + blobService, + transferDataResult + ); + expect(resultOutputData).toEqual(true); + }); }); diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index baaf57e..fcd8301 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -45,7 +45,6 @@ export class BlobstorageService { this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_IMPORTS'), this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_IMPORTS'), ); - this.blobServiceClientUS = new BlobServiceClient( this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_US'), this.sharedKeyCredentialUS, @@ -104,7 +103,6 @@ export class BlobstorageService { ); } } - /** * 指定されたコンテナを削除します。(コンテナが存在しない場合、何もせず終了します) * @param context From 9256566f8937ebeef5556190f13f15fc89943bd1 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Thu, 14 Mar 2024 15:03:17 +0000 Subject: [PATCH 055/109] =?UTF-8?q?Merged=20PR=20839:=20DEV=E5=8B=95?= =?UTF-8?q?=E4=BD=9C=E7=A2=BA=E8=AA=8D=E3=81=AE=E3=83=90=E3=82=B0=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3918: DEV動作確認のバグ対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3918) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など ## 補足 - 相談、参考資料などがあれば --- dictation_function/src/blobstorage/blobstorage.service.ts | 4 ++++ dictation_function/src/functions/analysisLicenses.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dictation_function/src/blobstorage/blobstorage.service.ts b/dictation_function/src/blobstorage/blobstorage.service.ts index 2adfee4..8d6e4b7 100644 --- a/dictation_function/src/blobstorage/blobstorage.service.ts +++ b/dictation_function/src/blobstorage/blobstorage.service.ts @@ -254,6 +254,10 @@ export class BlobstorageService { const containerClient = this.blobServiceClient.getContainerClient( LICENSE_COUNT_ANALYSIS_CONTAINER_NAME ); + // コンテナが存在しない場合のみ作成 + if (await containerClient.exists()) { + return; + } await containerClient.create(); } catch (e) { context.error(e); diff --git a/dictation_function/src/functions/analysisLicenses.ts b/dictation_function/src/functions/analysisLicenses.ts index b0caa71..ac3403d 100644 --- a/dictation_function/src/functions/analysisLicenses.ts +++ b/dictation_function/src/functions/analysisLicenses.ts @@ -1817,18 +1817,18 @@ export async function outputAnalysisLicensesData( const resultUS = await blobstorageService.uploadFileAnalysisLicensesCSV( context, outputFileNameUS, - outputCsvData.outputDataUS.join("\r\n") + csvContentUS ); context.log("resultUS: " + resultUS); const resultEU = await blobstorageService.uploadFileAnalysisLicensesCSV( context, outputFileNameEU, - outputCsvData.outputDataEU.join("\r\n") + csvContentEU ); const resultAU = await blobstorageService.uploadFileAnalysisLicensesCSV( context, outputFileNameAU, - outputCsvData.outputDataAU.join("\r\n") + csvContentAU ); // 出力結果を返却 // 3つのリージョンの出力が全て成功した場合にtrueを返却 From 5f4a05044b2534a89e34d5478aff676a3714e887 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Fri, 15 Mar 2024 00:53:58 +0000 Subject: [PATCH 056/109] =?UTF-8?q?Merged=20PR=20845:=20=E3=82=AF=E3=83=A9?= =?UTF-8?q?=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E3=83=A1=E3=83=83=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E5=A4=89=E6=9B=B4=EF=BC=88=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E9=9A=8E=E5=B1=A4=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3917: クライアントメッセージ変更](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3917) - メッセージレビューで変更あったため修正して自動生成しました。 ## レビューポイント - 情報共有 --- dictation_client/src/translation/de.json | 11 ++++++----- dictation_client/src/translation/en.json | 11 ++++++----- dictation_client/src/translation/es.json | 11 ++++++----- dictation_client/src/translation/fr.json | 11 ++++++----- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 6620775..f72b729 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -252,8 +252,9 @@ "backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.", "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.", "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", - "licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist.Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.", - "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen." + "licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.", + "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.", + "fileAlreadyDeletedError": "(de)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" }, "label": { "title": "Diktate", @@ -624,9 +625,9 @@ "changeOwnerPopup": { "message": { "accountNotFoundError": "(de)変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + "hierarchyMismatchError": "(de)パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "(de)パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "(de)パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" }, "label": { "invalidInputError": "(de)変更先アカウントIDには1~9999999の数字を入力してください。", diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 3151765..cc746f0 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -252,8 +252,9 @@ "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.", "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.", "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", - "licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned.Please ask your administrator to assign a valid license.", - "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license." + "licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.", + "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.", + "fileAlreadyDeletedError": "既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" }, "label": { "title": "Dictations", @@ -624,9 +625,9 @@ "changeOwnerPopup": { "message": { "accountNotFoundError": "変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + "hierarchyMismatchError": "パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" }, "label": { "invalidInputError": "変更先アカウントIDには1~9999999の数字を入力してください。", diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index a1bc703..94ec1e8 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -252,8 +252,9 @@ "backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.", "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.", "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", - "licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida.Solicite a su administrador que le asigne una licencia válida.", - "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida." + "licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.", + "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.", + "fileAlreadyDeletedError": "(es)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" }, "label": { "title": "Dictado", @@ -624,9 +625,9 @@ "changeOwnerPopup": { "message": { "accountNotFoundError": "(es)変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + "hierarchyMismatchError": "(es)パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "(es)パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "(es)パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" }, "label": { "invalidInputError": "(es)変更先アカウントIDには1~9999999の数字を入力してください。", diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index d8b0555..4f6cf56 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -252,8 +252,9 @@ "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.", "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.", "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", - "licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée.Veuillez demander à votre administrateur d'attribuer une licence valide.", - "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide." + "licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.", + "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.", + "fileAlreadyDeletedError": "(fr)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" }, "label": { "title": "Dictées", @@ -624,9 +625,9 @@ "changeOwnerPopup": { "message": { "accountNotFoundError": "(fr)変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。" + "hierarchyMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", + "regionMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", + "countryMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" }, "label": { "invalidInputError": "(fr)変更先アカウントIDには1~9999999の数字を入力してください。", From 66c643677d4985b7836baab08ecbc7494d6ff2c5 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 15 Mar 2024 06:53:41 +0000 Subject: [PATCH 057/109] =?UTF-8?q?Merged=20PR=20829:=20=E9=9F=B3=E5=A3=B0?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=83=90=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E3=83=9D=E3=83=83=E3=83=97=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3882: 音声ファイルバックアップポップアップ修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3882) - 音声ファイルバックアップ中にファイル削除された場合の処理を追加しました。 - ダウンロードで対象ファイルが削除されていた場合に特別なエラーとなるようにしています。 - タスクバックアップで対象ファイルが削除されていた場合でも成功となるようにしています。 ## レビューポイント - 対応するエラーは適切でしょうか? ## UIの変更 - [Task3882](https://ndstokyo.sharepoint.com/:i:/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/Task3882/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%89%8A%E9%99%A4%E6%B8%88%E3%81%BF.png?csf=1&web=1&e=1BKVh8) ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - ダウンロード実行中にファイル削除 - backupAPIからタスク不在エラーを返却して成功するか確認 --- .../src/features/dictation/operations.ts | 35 ++++++++++++++++--- dictation_client/src/translation/de.json | 2 +- dictation_client/src/translation/en.json | 2 +- dictation_client/src/translation/es.json | 2 +- dictation_client/src/translation/fr.json | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 2cad273..522b6d9 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -565,10 +565,21 @@ export const backupTasksAsync = createAsyncThunk< a.click(); a.parentNode?.removeChild(a); - // eslint-disable-next-line no-await-in-loop - await tasksApi.backup(task.audioFileId, { - headers: { authorization: `Bearer ${accessToken}` }, - }); + // バックアップ済みに更新 + try { + // eslint-disable-next-line no-await-in-loop + await tasksApi.backup(task.audioFileId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + if (error.code === "E010603") { + // タスクが削除済みの場合は成功扱いとする + } else { + throw e; + } + } } } @@ -580,8 +591,22 @@ export const backupTasksAsync = createAsyncThunk< ); return {}; } catch (e) { - // e ⇒ errorObjectに変換" + // e ⇒ errorObjectに変換 const error = createErrorObject(e); + if (error.code === "E010603") { + // 存在しない音声ファイルをダウンロードしようとした場合 + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID( + "dictationPage.message.fileAlreadyDeletedError" + ), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } + thunkApi.dispatch( openSnackbar({ level: "error", diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index f72b729..5f65907 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -636,4 +636,4 @@ "lowerLayerId": "(de)Lower Layer ID" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index cc746f0..6d1eba5 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -636,4 +636,4 @@ "lowerLayerId": "Lower Layer ID" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 94ec1e8..d0c183f 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -636,4 +636,4 @@ "lowerLayerId": "(es)Lower Layer ID" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 4f6cf56..0e801cf 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -636,4 +636,4 @@ "lowerLayerId": "(fr)Lower Layer ID" } } -} \ No newline at end of file +} From f80912c617bd10b1652ec73c9ac304359044b739 Mon Sep 17 00:00:00 2001 From: masaaki Date: Fri, 15 Mar 2024 07:41:56 +0000 Subject: [PATCH 058/109] =?UTF-8?q?Merged=20PR=20834:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3904: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3904) - 「プロダクト バックログ項目 1242: パートナーを削除したい」のAPI IFを作成しました - 影響範囲(他の機能にも影響があるか) - 新規IFのため影響はなし ## レビューポイント - controllerの試験実装が初なので、テストケース過不足ないか確認いただきたいです。 ## UIの変更 - 無し ## クエリの変更 - 無し ## 動作確認状況 - ユニットテストが通ることを確認、ローカル環境でpostmanで呼び出せることを確認、SWAGGER UI上で追加されていることを確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - ユニットテストが通ることを確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/api/odms/openapi.json | 66 +++++++++++++++ .../accounts/accounts.controller.spec.ts | 61 +++++++++++++- .../features/accounts/accounts.controller.ts | 80 +++++++++++++++++++ .../src/features/accounts/types/types.ts | 10 +++ 4 files changed, 216 insertions(+), 1 deletion(-) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index c1b24bf..6940f83 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1717,6 +1717,61 @@ "security": [{ "bearer": [] }] } }, + "/accounts/partner/delete": { + "post": { + "operationId": "deletePartnerAccount", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletePartnerAccountRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletePartnerAccountResponse" + } + } + } + }, + "400": { + "description": "実施者との親子関係不正や下位アカウント存在など削除実施条件に合致しない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -4611,6 +4666,17 @@ "required": ["to", "children"] }, "SwitchParentResponse": { "type": "object", "properties": {} }, + "DeletePartnerAccountRequest": { + "type": "object", + "properties": { + "targetAccountId": { + "type": "number", + "description": "削除対象のアカウントID" + } + }, + "required": ["targetAccountId"] + }, + "DeletePartnerAccountResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { "type": "object", "properties": { "token": { "type": "string" } }, diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index 0ccef3d..3b89aef 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -3,7 +3,10 @@ import { AccountsController } from './accounts.controller'; import { AccountsService } from './accounts.service'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from '../auth/auth.service'; -import { SwitchParentRequest } from './types/types'; +import { + SwitchParentRequest, + DeletePartnerAccountRequest, +} from './types/types'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @@ -70,4 +73,60 @@ describe('AccountsController', () => { expect(errors.length).toBe(1); }); }); + + describe('valdation deletePartnerAccount', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new DeletePartnerAccountRequest(); + request.targetAccountId = 1; + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('削除対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = new DeletePartnerAccountRequest(); + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('削除対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = new DeletePartnerAccountRequest(); + request.targetAccountId = 0; + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('削除対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + class DeletePartnerAccountRequestString { + targetAccountId: string; + } + const request = new DeletePartnerAccountRequestString(); + request.targetAccountId = 'a'; + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 3cb553a..29f63b5 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -79,6 +79,8 @@ import { UpdateRestrictionStatusResponse, SwitchParentRequest, SwitchParentResponse, + DeletePartnerAccountRequest, + DeletePartnerAccountResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -2401,4 +2403,82 @@ export class AccountsController { return {}; } + + @Post('partner/delete') + @ApiResponse({ + status: HttpStatus.OK, + type: DeletePartnerAccountResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + '実施者との親子関係不正や下位アカウント存在など削除実施条件に合致しない', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'deletePartnerAccount' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3], + }), + ) + async deletePartnerAccount( + @Req() req: Request, + @Body() body: DeletePartnerAccountRequest, + ): Promise { + const { targetAccountId } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO:service層を呼び出す。本実装時に以下は削除する。 + // await this.accountService.deletePartnerAccount(context, userId, targetAccountId); + + return {}; + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 110d25d..2ba21c6 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -390,6 +390,14 @@ export class SwitchParentRequest { children: number[]; } +export class DeletePartnerAccountRequest { + @ApiProperty({ description: '削除対象のアカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + targetAccountId: number; +} + // ============================== // RESPONSE // ============================== @@ -709,6 +717,8 @@ export class UpdateRestrictionStatusResponse {} export class SwitchParentResponse {} +export class DeletePartnerAccountResponse {} + // ============================== // Request/Response外の型 // TODO: Request/Response/その他の型を別ファイルに分ける From cab7a75ec1e4a6977b7193b4b141f5e6751adaa2 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Fri, 15 Mar 2024 12:19:16 +0000 Subject: [PATCH 059/109] =?UTF-8?q?Merged=20PR=20849:=20=E6=9C=89=E5=8A=B9?= =?UTF-8?q?=E3=81=AA=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E5=86=85?= =?UTF-8?q?=E3=81=AE=E5=89=8A=E9=99=A4=E3=81=95=E3=82=8C=E3=81=9F=E3=83=A6?= =?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AE=E5=89=B2=E3=82=8A=E5=BD=93?= =?UTF-8?q?=E3=81=A6=E5=B1=A5=E6=AD=B4=E3=81=8C=E9=9B=86=E8=A8=88=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3929: 有効なアカウント内の削除されたユーザーの割り当て履歴が集計されない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3929) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など ## 補足 - 相談、参考資料などがあれば --- .../verification/verification.service.ts | 36 +- dictation_client/src/translation/de.json | 6 +- dictation_client/src/translation/en.json | 6 +- dictation_client/src/translation/es.json | 6 +- dictation_client/src/translation/fr.json | 6 +- .../src/functions/analysisLicenses.ts | 106 +- .../src/test/analysisLicenses.spec.ts | 1880 +++++++++++++++-- 7 files changed, 1814 insertions(+), 232 deletions(-) diff --git a/data_migration_tools/server/src/features/verification/verification.service.ts b/data_migration_tools/server/src/features/verification/verification.service.ts index 10733f5..97194b6 100644 --- a/data_migration_tools/server/src/features/verification/verification.service.ts +++ b/data_migration_tools/server/src/features/verification/verification.service.ts @@ -382,18 +382,26 @@ function compareCardLicenses( } */ - const formattedActivated = getFormattedDate( - filterdCardLicenses[0].activated_at, - `yyyy/MM/dd hh:mm:ss` + const formattedFileActivated = getFormattedDate( + cardlicensesInputFile.activated_at + ? new Date(cardlicensesInputFile.activated_at) + : null, + `yyyy/MM/dd hh:mm:ss`, + true ); - if (cardlicensesInputFile.activated_at !== formattedActivated) { + const formattedDbActivated = getFormattedDate( + filterdCardLicenses[0].activated_at, + `yyyy/MM/dd hh:mm:ss`, + true + ); + if (formattedFileActivated !== formattedDbActivated) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "cardLicenses", columnName: "activated_at", - fileData: cardlicensesInputFile.activated_at, - databaseData: formattedActivated, + fileData: formattedFileActivated, + databaseData: formattedDbActivated, reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); @@ -574,7 +582,13 @@ function compareLicenses( // expiry_dateについて、時はゼロパディングした値で比較する(×01~09 ○1~9) if ( !licensesFromDatabase[i] || - licensesFromFile[i].expired_date !== + getFormattedDate( + licensesFromFile[i].expired_date + ? new Date(licensesFromFile[i].expired_date) + : null, + `yyyy/MM/dd hh:mm:ss`, + true + ) !== getFormattedDate( licensesFromDatabase[i].expiry_date, `yyyy/MM/dd hh:mm:ss`, @@ -586,7 +600,13 @@ function compareLicenses( inputRow: licensesFromFile[i].row, diffTargetTable: "licenses", columnName: "expired_date", - fileData: licensesFromFile[i].expired_date, + fileData: getFormattedDate( + licensesFromFile[i].expired_date + ? new Date(licensesFromFile[i].expired_date) + : null, + `yyyy/MM/dd hh:mm:ss`, + true + ), databaseData: licensesFromDatabase[i] ? getFormattedDate( licensesFromDatabase[i].expiry_date, diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 5f65907..a96efdc 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -154,7 +154,7 @@ "status": "Status", "expiration": "Verfallsdatum", "remaining": "Verbleibender Zeitraum", - "autoRenew": "Automatische Erneuerung", + "autoRenew": "Automatisch zuweisen", "licenseAlert": "Lizenzalarm", "notification": "Benachrichtigung", "users": "Benutzer", @@ -436,7 +436,7 @@ "templateOptional": "Vorlage (Optional)", "editRule": "Regel bearbeiten", "selected": "Ausgewählter transkriptionist", - "pool": "Liste der Transkriptionisten", + "pool": "Transkriptionsliste", "selectAuthor": "Autoren-ID auswählen", "selectWorktypeId": "Aufgabentypkennung auswählen", "selectTemplate": "Vorlage auswählen" @@ -456,7 +456,7 @@ "addTypistGroup": "Transkriptionist Gruppe hinzufügen", "transcriptionist": "Transkriptionist", "selected": "Ausgewählter transkriptionist", - "pool": "Liste der Transkriptionisten", + "pool": "Transkriptionsliste", "add": "Hinzufügen", "remove": "Entfernen", "editTypistGroup": "Transkriptionistengruppe bearbeiten" diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 6d1eba5..57615cc 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -154,7 +154,7 @@ "status": "Status", "expiration": "Expiration Date", "remaining": "Remaining Period", - "autoRenew": "Auto Renew", + "autoRenew": "Auto Assign", "licenseAlert": "License Alert", "notification": "Notification", "users": "Users", @@ -436,7 +436,7 @@ "templateOptional": "Template (Optional)", "editRule": "Edit Rule", "selected": "Selected Transcriptionist", - "pool": "Transcriptionist List", + "pool": "Transcription List", "selectAuthor": "Select Author ID", "selectWorktypeId": "Select Worktype ID", "selectTemplate": "Select Template" @@ -456,7 +456,7 @@ "addTypistGroup": "Add Transcriptionist Group", "transcriptionist": "Transcriptionist", "selected": "Selected Transcriptionist", - "pool": "Transcriptionist List", + "pool": "Transcription List", "add": "Add", "remove": "Remove", "editTypistGroup": "Edit Transcriptionist Group" diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index d0c183f..47200a8 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -154,7 +154,7 @@ "status": "Estado", "expiration": "Fecha de caducidad", "remaining": "Período restante", - "autoRenew": "Renovación Automática", + "autoRenew": "Asignación automática", "licenseAlert": "Alerta de licencia", "notification": "Notificación", "users": "Usuarios", @@ -436,7 +436,7 @@ "templateOptional": "Plantilla (Opcional)", "editRule": "Editar regla", "selected": "Transcriptor seleccionado", - "pool": "Lista de transcriptores", + "pool": "Lista de transcriptor", "selectAuthor": "Seleccionar ID de autor", "selectWorktypeId": "Seleccionar ID de tipo de trabajo", "selectTemplate": "Seleccionar Plantilla" @@ -456,7 +456,7 @@ "addTypistGroup": "Agregar grupo transcriptor", "transcriptionist": "Transcriptor", "selected": "Transcriptor seleccionado", - "pool": "Lista de transcriptores", + "pool": "Lista de transcriptor", "add": "Añadir", "remove": "Eliminar", "editTypistGroup": "Editar grupo transcriptor" diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 0e801cf..4560e2a 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -154,7 +154,7 @@ "status": "État", "expiration": "Date d'expiration", "remaining": "Période restante", - "autoRenew": "Renouvellement automatique", + "autoRenew": "Assignation automatique", "licenseAlert": "Alerte de licence", "notification": "Notification", "users": "Utilisateurs", @@ -436,7 +436,7 @@ "templateOptional": "Masque (Facultatif)", "editRule": "Modifier la règle", "selected": "Transcriptionniste sélectionné", - "pool": "Liste des transcripteurs", + "pool": "Liste de transcriptionniste", "selectAuthor": "Sélectionner le Identifiant Auteur", "selectWorktypeId": "Sélectionner le Identifiant du Type de travail", "selectTemplate": "Sélectionner le Masque" @@ -456,7 +456,7 @@ "addTypistGroup": "Ajouter un groupe de transcripteurs", "transcriptionist": "Transcriptionniste", "selected": "Transcriptionniste sélectionné", - "pool": "Liste des transcripteurs", + "pool": "Liste de transcriptionniste", "add": "Ajouter", "remove": "Supprimer", "editTypistGroup": "Modifier le groupe de transcripteurs" diff --git a/dictation_function/src/functions/analysisLicenses.ts b/dictation_function/src/functions/analysisLicenses.ts index ac3403d..22f4709 100644 --- a/dictation_function/src/functions/analysisLicenses.ts +++ b/dictation_function/src/functions/analysisLicenses.ts @@ -27,6 +27,7 @@ import { } from "../constants"; import { DateWithDayEndTime } from "../common/types/types"; import { initializeDataSource } from "../database/initializeDataSource"; +import { bigintTransformer } from "../common/entity"; /** * ライセンス数分析処理のメイン処理:ここから各処理を呼び出す @@ -166,8 +167,11 @@ export async function getBaseData( ); const switchedlicensesInTargetMonth = await licenseAllocationHistory .createQueryBuilder("licenseAllocationHistory") - .innerJoinAndSelect("licenseAllocationHistory.user", "user") - .innerJoin("user.account", "account") + .innerJoin( + "accounts", + "account", + "licenseAllocationHistory.account_id = account.id" + ) .where("account.tier = :tier", { tier: TIERS.TIER5 }) .andWhere("licenseAllocationHistory.switch_from_type IN (:...types)", { types: [SWITCH_FROM_TYPE.CARD, SWITCH_FROM_TYPE.TRIAL], @@ -408,10 +412,6 @@ export async function transferData( targetMonthYYYYMM: string ): Promise { context.log("[IN]transferData"); - class userIdAndRoles { - id: number; - role: string; - } const accountsAndUsersFromTier5 = baseData.accountsAndUsersFromTier5; const validLicenses = baseData.avairableLicenses; const currentMonthIssuedLicenses = baseData.licensesIssuedInTargetMonth; @@ -448,7 +448,7 @@ export async function transferData( outputDataAU.push(...header); // ユーザーIDとロールを格納する配列(型が違う為新たに作成する) - let tier5userIdAndRoles: userIdAndRoles[] = []; + let tier5userIdAndRoles: { id: number; role: string }[] = []; try { // 第五階層のアカウントごとにループ for (const account of accountsAndUsersFromTier5) { @@ -464,13 +464,20 @@ export async function transferData( // ユーザーとユーザーアーカイブからユーザーIDとロールを取得する if (account.user) { tier5userIdAndRoles = account.user.map((user) => { - return { id: user.id, role: user.role }; + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + return { id: bigintTransformer.from(user.id), role: user.role }; }); } if (account.userArchive) { tier5userIdAndRoles = tier5userIdAndRoles.concat( account.userArchive.map((userArchive) => { - return { id: userArchive.id, role: userArchive.role }; + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + return { + id: bigintTransformer.from(userArchive.id), + role: userArchive.role, + }; }) ); } @@ -735,40 +742,59 @@ export async function transferData( (license) => tier5userIdAndRoles.find( (user) => - user.id === license.user_id && user.role === USER_ROLES.TYPIST + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.TYPIST ) ); const switchedTypistLicensesAuthor = switchedTrialLicenses.filter( (license) => tier5userIdAndRoles.find( (user) => - user.id === license.user_id && user.role === USER_ROLES.AUTHOR + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.AUTHOR ) ); const switchedTypistLicensesNone = switchedTrialLicenses.filter( (license) => tier5userIdAndRoles.find( (user) => - user.id === license.user_id && user.role === USER_ROLES.NONE + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.NONE ) ); const switchedCardLicensesTypist = switchedCardLicenses.filter( (license) => tier5userIdAndRoles.find( (user) => - user.id === license.user_id && user.role === USER_ROLES.TYPIST + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.TYPIST ) ); const switchedCardLicensesAuthor = switchedCardLicenses.filter( (license) => tier5userIdAndRoles.find( (user) => - user.id === license.user_id && user.role === USER_ROLES.AUTHOR + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.AUTHOR ) ); const switchedCardLicensesNone = switchedCardLicenses.filter((license) => tier5userIdAndRoles.find( - (user) => user.id === license.user_id && user.role === USER_ROLES.NONE + (user) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.NONE ) ); // 切り替えライセンスの数をカウント @@ -920,7 +946,12 @@ export async function transferData( // アカウントに紐づくユーザーを取得 if (account.userArchive) { tier5userIdAndRoles = account.userArchive.map((userArchive) => { - return { id: userArchive.id, role: userArchive.role }; + return { + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + id: bigintTransformer.from(userArchive.id), + role: userArchive.role, + }; }); } // アカウントに紐づくライセンスを取得 @@ -1159,33 +1190,51 @@ export async function transferData( //(Typist・Author・None) const switchedTypistLicensesTypist = switchedTrialLicenses.filter( (license) => - tier5userIdAndRoles?.find((user) => user.id === license.user_id) - ?.role === USER_ROLES.TYPIST + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.TYPIST ); const switchedTypistLicensesAuthor = switchedTrialLicenses.filter( (license) => - tier5userIdAndRoles?.find((user) => user.id === license.user_id) - ?.role === USER_ROLES.AUTHOR + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.AUTHOR ); const switchedTypistLicensesNone = switchedTrialLicenses.filter( (license) => - tier5userIdAndRoles?.find((user) => user.id === license.user_id) - ?.role === USER_ROLES.NONE + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.NONE ); const switchedCardLicensesTypist = switchedCardLicenses.filter( (license) => - tier5userIdAndRoles?.find((user) => user.id === license.user_id) - ?.role === USER_ROLES.TYPIST + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.TYPIST ); const switchedCardLicensesAuthor = switchedCardLicenses.filter( (license) => - tier5userIdAndRoles?.find((user) => user.id === license.user_id) - ?.role === USER_ROLES.AUTHOR + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.AUTHOR ); const switchedCardLicensesNone = switchedCardLicenses.filter( (license) => - tier5userIdAndRoles?.find((user) => user.id === license.user_id) - ?.role === USER_ROLES.NONE + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.NONE ); // 切り替えライセンスの数をカウント const switchedTypistLicensesTypistCount = @@ -1819,7 +1868,6 @@ export async function outputAnalysisLicensesData( outputFileNameUS, csvContentUS ); - context.log("resultUS: " + resultUS); const resultEU = await blobstorageService.uploadFileAnalysisLicensesCSV( context, outputFileNameEU, diff --git a/dictation_function/src/test/analysisLicenses.spec.ts b/dictation_function/src/test/analysisLicenses.spec.ts index 0330d68..e2cfb0b 100644 --- a/dictation_function/src/test/analysisLicenses.spec.ts +++ b/dictation_function/src/test/analysisLicenses.spec.ts @@ -26,9 +26,11 @@ import { LICENSE_COUNT_ANALYSIS_CATEGORY_1, LICENSE_COUNT_ANALYSIS_CATEGORY_2, LICENSE_COUNT_ANALYSIS_LICENSE_TYPE, + LICENSE_COUNT_ANALYSIS_ROLE, SWITCH_FROM_TYPE, } from "../constants"; import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { User, UserArchive } from "../entity/user.entity"; describe("analysisLicenses", () => { dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); @@ -790,65 +792,54 @@ describe("analysisLicenses", () => { } ); // 第五アカウントに紐づくユーザーを作成する - const user5_1_2 = await makeTestUser(source, { - account_id: account5_1.id, - role: "typist", - }); - const user5_1_3 = await makeTestUser(source, { - account_id: account5_1.id, - role: "typist", - }); - const user5_1_4 = await makeTestUser(source, { - account_id: account5_1.id, - role: "typist", - }); - const user5_1_5 = await makeTestUser(source, { - account_id: account5_1.id, - role: "none", - }); - const user5_1_6 = await makeTestUser(source, { - account_id: account5_1.id, - role: "author", - }); - const user5_1_7 = await makeTestUser(source, { - account_id: account5_1.id, - role: "typist", - }); - const user5_1_8 = await makeTestUser(source, { - account_id: account5_1.id, - role: "none", - }); - const user5_1_9 = await makeTestUser(source, { - account_id: account5_1.id, - role: "none", - }); - const user5_1_10 = await makeTestUser(source, { - account_id: account5_1.id, - role: "typist", - }); + // 各ロール33人作成 + // users[0]~users[32] // author + // users[33]~users[65] // typist + // users[66]~users[98] // none + // author + const numberOfUsers = 33; + let users: User[] = []; - // 削除ユーザを作成する - const userArchive5_1_4 = await makeTestUserArchive(source, { - account_id: account5_1.id, - }); - // 第五階層以外だとヒットしないことの確認 - const userArchive4 = await makeTestUserArchive(source, { - account_id: account4.id, - }); + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUser(source, { + account_id: account5_1.id, + role: "author", + }); + if (user) { + users.push(user); + } + } + // typist + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUser(source, { + account_id: account5_1.id, + role: "typist", + }); + if (user) { + users.push(user); + } + } + // none + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUser(source, { + account_id: account5_1.id, + role: "none", + }); + if (user) { + users.push(user); + } + } // 所有ライセンス - // trialLicensesCount 3件 - // normalLicensesCount 5件 - // cardLicensesCount 4件 // usedTrialLicensesAuthorCount 1件 await createLicense( source, 1, - null, + expiringSoonDate, account5_1.id, "TRIAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - admin5_1_1.id, + users[0].id, null, null, null, @@ -862,7 +853,7 @@ describe("analysisLicenses", () => { account5_1.id, "TRIAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_2.id, + users[33].id, null, null, null, @@ -875,65 +866,208 @@ describe("analysisLicenses", () => { account5_1.id, "TRIAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_3.id, + users[34].id, null, null, null, last2Month ); - // usedTrialLicensesNoneCount 0件 - // usedNormalLicensesAuthorCount 0件 - // usedNormalLicensesTypistCount 1件 + // usedTrialLicensesNoneCount 3件 await createLicense( source, 4, expiringSoonDate, account5_1.id, - "NORMAL", + "TRIAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_4.id, + users[66].id, null, null, null, last2Month ); - // usedNormalLicensesNoneCount 1件 await createLicense( source, 5, expiringSoonDate, account5_1.id, - "NORMAL", + "TRIAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_5.id, + users[67].id, null, null, null, last2Month ); - // usedCardLicensesAuthorCount 1件 await createLicense( source, 6, expiringSoonDate, account5_1.id, - "CARD", + "TRIAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_6.id, + users[68].id, null, null, null, last2Month ); - // usedCardLicensesTypistCount 1件 + // usedNormalLicensesAuthorCount 4件 await createLicense( source, 7, expiringSoonDate, account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[1].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 8, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[2].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 9, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[3].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 10, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[4].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesTypistCount 1件 + await createLicense( + source, + 11, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[35].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesNoneCount 2件 + await createLicense( + source, + 12, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[69].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 13, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[70].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesAuthorCount 2件 + await createLicense( + source, + 14, + expiringSoonDate, + account5_1.id, "CARD", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_7.id, + users[5].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 15, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[6].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesTypistCount 3件 + await createLicense( + source, + 16, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[36].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 17, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[37].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 18, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[38].id, null, null, null, @@ -942,26 +1076,12 @@ describe("analysisLicenses", () => { // usedCardLicensesNoneCount 1件 await createLicense( source, - 8, + 19, expiringSoonDate, account5_1.id, "CARD", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_8.id, - null, - null, - null, - last2Month - ); - // deleteはヒットしないことの確認 - await createLicense( - source, - 100, - expiringSoonDate, - account5_1.id, - "NORMAL", - LICENSE_ALLOCATED_STATUS.DELETED, - null, + users[71].id, null, null, null, @@ -973,11 +1093,50 @@ describe("analysisLicenses", () => { // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED // ・作成日:今日から1か月前 // ・期限:14日後 - // currentMonthIssuedTrialLicensesCount 0件 - // currentMonthIssuedNormalLicensesCount 3件 + // currentMonthIssuedTrialLicensesCount 3件 await createLicense( source, - 11, + 21, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 22, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 23, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedNormalLicensesCount 2件 + await createLicense( + source, + 24, expiringSoonDate, account5_1.id, "NORMAL", @@ -990,20 +1149,7 @@ describe("analysisLicenses", () => { ); await createLicense( source, - 12, - expiringSoonDate, - account5_1.id, - "NORMAL", - LICENSE_ALLOCATED_STATUS.UNALLOCATED, - null, - null, - null, - null, - lastMonth - ); - await createLicense( - source, - 13, + 25, expiringSoonDate, account5_1.id, "NORMAL", @@ -1017,7 +1163,7 @@ describe("analysisLicenses", () => { // currentMonthIssuedCardLicensesCount 1件 await createLicense( source, - 14, + 26, expiringSoonDate, account5_1.id, "CARD", @@ -1028,63 +1174,232 @@ describe("analysisLicenses", () => { null, lastMonth ); - // deleteはヒットしないことの確認 - await createLicense( - source, - 101, - expiringSoonDate, - account5_1.id, - "NORMAL", - LICENSE_ALLOCATED_STATUS.DELETED, - null, - null, - null, - null, - lastMonth - ); + // その月に失効したライセンスを作成 // 条件: // ・第五アカウント // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED // ・作成日:今日から2か月前 // ・期限:先月 - // invalidTrialLicensesAuthorCount 0件 - // invalidTrialLicensesTypistCount 0件 - // invalidTrialLicensesNoneCount 1件 + // invalidTrialLicensesAuthorCount 5件 await createLicense( source, - 22, + 27, lastMonth, account5_1.id, "TRIAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_9.id, + users[7].id, null, null, null, last2Month ); - // invalidTrialLicensesUnallocatedCount 0件 - // invalidNormalLicensesAuthorCount 0件 - // invalidNormalLicensesTypistCount 1件 await createLicense( source, - 23, + 28, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[8].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 29, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[9].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 30, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[10].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 31, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[11].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesTypistCount 3件 + await createLicense( + source, + 32, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[40].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 33, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[41].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 34, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[42].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesNoneCount 2件 + await createLicense( + source, + 35, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[74].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 36, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[75].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesUnallocatedCount 1件 + await createLicense( + source, + 37, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesAuthorCount 2件 + await createLicense( + source, + 38, lastMonth, account5_1.id, "NORMAL", LICENSE_ALLOCATED_STATUS.ALLOCATED, - user5_1_10.id, + users[12].id, null, null, null, last2Month ); - // invalidNormalLicensesNoneCount 0件 - // invalidNormalLicensesUnallocatedCount 1件 await createLicense( source, - 24, + 39, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[13].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesTypistCount 1件 + await createLicense( + source, + 40, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[43].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesNoneCount 2件 + await createLicense( + source, + 41, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[76].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 42, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[77].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesUnallocatedCount 2件 + await createLicense( + source, + 43, lastMonth, account5_1.id, "NORMAL", @@ -1095,32 +1410,159 @@ describe("analysisLicenses", () => { null, last2Month ); - - // invalidCardLicensesAuthorCount 0件 - // invalidCardLicensesTypistCount 0件 - // invalidCardLicensesNoneCount 0件 - // invalidCardLicensesUnallocatedCount 0件 - // deleteはヒットしないことの確認 await createLicense( source, - 102, + 44, lastMonth, account5_1.id, "NORMAL", - LICENSE_ALLOCATED_STATUS.DELETED, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, null, null, last2Month ); - // 先々月はヒットしないことの確認 + // invalidCardLicensesAuthorCount 1件 await createLicense( source, - 103, - last2Month, + 45, + lastMonth, account5_1.id, - "NORMAL", + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[14].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesTypistCount 2件 + await createLicense( + source, + 46, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[44].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 47, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[45].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesNoneCount 3件 + await createLicense( + source, + 48, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[78].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 49, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[79].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 50, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[80].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesUnallocatedCount 5件 + await createLicense( + source, + 51, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 52, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 53, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 54, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 55, + lastMonth, + account5_1.id, + "CARD", LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, null, @@ -1138,52 +1580,1011 @@ describe("analysisLicenses", () => { await createLicenseAllocationHistory( source, 1, - admin5_1_1.id, + users[1].id, 1, true, account5_1.id, lastMonth, SWITCH_FROM_TYPE.TRIAL ); - // switchedTypistLicensesTypistCount 0件 - // switchedTypistLicensesNoneCount 0件 - // switchedCardLicensesAuthorCount 1件 + // switchedTypistLicensesTypistCount 2件 await createLicenseAllocationHistory( source, 2, - admin5_1_1.id, - 1, + users[34].id, + 2, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + await createLicenseAllocationHistory( + source, + 3, + users[35].id, + 3, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesNoneCount 1件 + await createLicenseAllocationHistory( + source, + 4, + users[66].id, + 4, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedNormalLicensesAuthorCount 1件 + await createLicenseAllocationHistory( + source, + 6, + users[5].id, + 6, true, account5_1.id, lastMonth, SWITCH_FROM_TYPE.CARD ); - // switchedCardLicensesTypistCount 0件 - // switchedCardLicensesNoneCount 0件 - // SWITCH_FROM_TYPE.NONEではヒットしないことの確認 + // switchedNormalLicensesTypistCount 2件 await createLicenseAllocationHistory( source, - 3, - admin5_1_1.id, - 1, + 7, + users[36].id, + 7, true, account5_1.id, lastMonth, - SWITCH_FROM_TYPE.NONE + SWITCH_FROM_TYPE.CARD ); - // 先々月の登録ではヒットしないことの確認 await createLicenseAllocationHistory( source, - 4, - admin5_1_1.id, - 1, + 8, + users[37].id, + 8, true, account5_1.id, - last2Month, - SWITCH_FROM_TYPE.TRIAL + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 5, + users[67].id, + 5, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 9, + users[69].id, + 9, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 10, + users[70].id, + 10, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD ); const result = await getBaseData(context, lastMonthYYYYMM, source); + + // 削除されたアカウントとユーザーの情報を作成する + const { account: account5_1_D } = await makeTestAccountArchive( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { + external_id: "external_id_tier5admin1", + role: "author", + } + ); + + // 第五アカウントに紐づくユーザーを作成する + // 各ロール33人作成 + // users[0]~users[32] // author + // users[33]~users[65] // typist + // users[66]~users[98] // none + // author + let deleteUsers: UserArchive[] = []; + + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUserArchive(source, { + account_id: account5_1_D.id, + role: "author", + }); + if (user) { + deleteUsers.push(user); + } + } + // typist + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUserArchive(source, { + account_id: account5_1_D.id, + role: "typist", + }); + if (user) { + deleteUsers.push(user); + } + } + // none + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUserArchive(source, { + account_id: account5_1_D.id, + role: "none", + }); + if (user) { + deleteUsers.push(user); + } + } + // 所有ライセンス + // usedTrialLicensesAuthorCount 1件 + await createLicenseArchive( + source, + 1, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[0].id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesTypistCount 2件 + await createLicenseArchive( + source, + 2, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[33].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 3, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[34].id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesNoneCount 3件 + await createLicenseArchive( + source, + 4, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[66].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 5, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[67].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 6, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[68].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesAuthorCount 4件 + await createLicenseArchive( + source, + 7, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[1].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 8, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[2].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 9, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[3].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 10, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[4].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesTypistCount 1件 + await createLicenseArchive( + source, + 11, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[35].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesNoneCount 2件 + await createLicenseArchive( + source, + 12, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[69].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 13, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[70].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesAuthorCount 2件 + await createLicenseArchive( + source, + 14, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[5].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 15, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[6].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesTypistCount 3件 + await createLicenseArchive( + source, + 16, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[36].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 17, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[37].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 18, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[38].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesNoneCount 1件 + await createLicenseArchive( + source, + 19, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[71].id, + null, + null, + null, + last2Month + ); + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // currentMonthIssuedTrialLicensesCount 3件 + await createLicenseArchive( + source, + 21, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 22, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 23, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedNormalLicensesCount 2件 + await createLicenseArchive( + source, + 24, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 25, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedCardLicensesCount 1件 + await createLicenseArchive( + source, + 26, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + // invalidTrialLicensesAuthorCount 5件 + await createLicenseArchive( + source, + 27, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[7].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 28, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[8].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 29, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[9].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 30, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[10].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 31, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[11].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesTypistCount 3件 + await createLicenseArchive( + source, + 32, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[40].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 33, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[41].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 34, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[42].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesNoneCount 2件 + await createLicenseArchive( + source, + 35, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[74].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 36, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[75].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesUnallocatedCount 1件 + await createLicenseArchive( + source, + 37, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesAuthorCount 2件 + await createLicenseArchive( + source, + 38, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[12].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 39, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[13].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesTypistCount 1件 + await createLicenseArchive( + source, + 40, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[43].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesNoneCount 2件 + await createLicenseArchive( + source, + 41, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[76].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 42, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[77].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesUnallocatedCount 2件 + await createLicenseArchive( + source, + 43, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 44, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // invalidCardLicensesAuthorCount 1件 + await createLicenseArchive( + source, + 45, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[14].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesTypistCount 2件 + await createLicenseArchive( + source, + 46, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[44].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 47, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[45].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesNoneCount 3件 + await createLicenseArchive( + source, + 48, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[78].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 49, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[79].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 50, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[80].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesUnallocatedCount 5件 + await createLicenseArchive( + source, + 51, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 52, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 53, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 54, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 55, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + // switchedTypistLicensesAuthorCount 1件 + await createLicenseAllocationHistoryArchive( + source, + 1, + users[1].id, + 1, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesTypistCount 2件 + await createLicenseAllocationHistoryArchive( + source, + 2, + users[34].id, + 2, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + await createLicenseAllocationHistoryArchive( + source, + 3, + users[35].id, + 3, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesNoneCount 1件 + await createLicenseAllocationHistoryArchive( + source, + 4, + users[66].id, + 4, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedNormalLicensesAuthorCount 1件 + await createLicenseAllocationHistoryArchive( + source, + 6, + users[5].id, + 6, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + // switchedNormalLicensesTypistCount 2件 + await createLicenseAllocationHistoryArchive( + source, + 7, + users[36].id, + 7, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 8, + users[37].id, + 8, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 5, + users[67].id, + 5, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 9, + users[69].id, + 9, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 10, + users[70].id, + 10, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); const result_D = await getBaseDataFromDeletedAccounts( context, lastMonthYYYYMM, @@ -1195,33 +2596,147 @@ describe("analysisLicenses", () => { result_D, lastMonthYYYYMM ); - // ヘッダー行 - // "アカウント", "対象年月", "カテゴリー1", "カテゴリー2", "ライセンス種別", "役割", "数量" - expect(transferDataResult.outputDataUS[0]).toEqual('"アカウント",'); - expect(transferDataResult.outputDataUS[1]).toEqual('"対象年月",'); - expect(transferDataResult.outputDataUS[2]).toEqual(`"カテゴリー1",`); - expect(transferDataResult.outputDataUS[3]).toEqual(`"カテゴリー2",`); - expect(transferDataResult.outputDataUS[4]).toEqual(`"ライセンス種別",`); - expect(transferDataResult.outputDataUS[5]).toEqual(`"役割",`); - expect(transferDataResult.outputDataUS[6]).toEqual(`"数量"` + "\r\n"); - // データ行 - // "アカウント", "対象年月", "カテゴリー1", "カテゴリー2", "ライセンス種別", "役割", "数量" - // 1行目だけ確認する - expect(transferDataResult.outputDataUS[7]).toEqual( - `"${account5_1.company_name}",` + let csvContentUS = ""; + for (let i = 0; i < transferDataResult.outputDataUS.length; i++) { + //カンマ区切りの文字列を作成 + csvContentUS += transferDataResult.outputDataUS[i]; + } + expect(csvContentUS).toBe( + '"アカウント","対象年月","カテゴリー1","カテゴリー2","ライセンス種別","役割","数量"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","所有ライセンス数","Trial","","9"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","所有ライセンス数","Standard","","9"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","所有ライセンス数","Card","","7"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Trial","Author","1"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Trial","None","3"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Standard","Author","4"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Standard","None","2"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Card","Author","2"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Card","Typist","3"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Card","None","1"' + + "\r\n" + + '"test inc.","202402","新規発行ライセンス数","","Trial","","3"' + + "\r\n" + + '"test inc.","202402","新規発行ライセンス数","","Standard","","2"' + + "\r\n" + + '"test inc.","202402","新規発行ライセンス数","","Card","","1"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Trial","Author","5"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Trial","Typist","3"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Trial","None","2"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Trial","Unallocated","1"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Standard","Author","2"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Standard","Typist","1"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Standard","None","2"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Standard","Unallocated","2"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Card","Author","1"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Card","Typist","2"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Card","None","3"' + + "\r\n" + + '"test inc.","202402","失効ライセンス数","","Card","Unallocated","5"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","","トライアルから切り替え","Author","1"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","","トライアルから切り替え","Typist","2"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","","トライアルから切り替え","None","1"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","","カードから切り替え","Author","1"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","","カードから切り替え","Typist","2"' + + "\r\n" + + '"test inc.","202402","有効ライセンス数","","カードから切り替え","None","3"' + + "\r\n" + + '"1","202402","有効ライセンス数","所有ライセンス数","Trial","","9"' + + "\r\n" + + '"1","202402","有効ライセンス数","所有ライセンス数","Standard","","9"' + + "\r\n" + + '"1","202402","有効ライセンス数","所有ライセンス数","Card","","7"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Trial","Author","1"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Trial","None","3"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Standard","Author","4"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Standard","None","2"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Card","Author","2"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Card","Typist","3"' + + "\r\n" + + '"1","202402","有効ライセンス数","使用中ライセンス数","Card","None","1"' + + "\r\n" + + '"1","202402","新規発行ライセンス数","","Trial","","3"' + + "\r\n" + + '"1","202402","新規発行ライセンス数","","Standard","","2"' + + "\r\n" + + '"1","202402","新規発行ライセンス数","","Card","","1"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Trial","Author","5"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Trial","Typist","3"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Trial","None","2"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Trial","Unallocated","1"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Standard","Author","2"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Standard","Typist","1"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Standard","None","2"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Standard","Unallocated","2"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Card","Author","1"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Card","Typist","2"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Card","None","3"' + + "\r\n" + + '"1","202402","失効ライセンス数","","Card","Unallocated","5"' + + "\r\n" + + '"1","202402","有効ライセンス数","","トライアルから切り替え","Author","1"' + + "\r\n" + + '"1","202402","有効ライセンス数","","トライアルから切り替え","Typist","2"' + + "\r\n" + + '"1","202402","有効ライセンス数","","トライアルから切り替え","None","1"' + + "\r\n" + + '"1","202402","有効ライセンス数","","カードから切り替え","Author","1"' + + "\r\n" + + '"1","202402","有効ライセンス数","","カードから切り替え","Typist","2"' + + "\r\n" + + '"1","202402","有効ライセンス数","","カードから切り替え","None","3"' + + "\r\n" ); - expect(transferDataResult.outputDataUS[8]).toEqual(`"${lastMonthYYYYMM}",`); - expect(transferDataResult.outputDataUS[9]).toEqual( - `"${LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES}",` - ); - expect(transferDataResult.outputDataUS[10]).toEqual( - `"${LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES}",` - ); - expect(transferDataResult.outputDataUS[11]).toEqual( - `"${LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL}",` - ); - expect(transferDataResult.outputDataUS[12]).toEqual(`"",`); - expect(transferDataResult.outputDataUS[13]).toEqual(`"3"` + "\r\n"); }); it("outputDataの確認(Mock)", async () => { @@ -1231,7 +2746,6 @@ describe("analysisLicenses", () => { const context = new InvocationContext(); const currentDate = new DateWithZeroTime(); - const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); // 現在の日付を取得 const nowDate = new Date(); From 75f0a49fc12b6525dbc377f2316cbcee64d68ac3 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Mon, 18 Mar 2024 05:47:24 +0000 Subject: [PATCH 060/109] =?UTF-8?q?Merged=20PR=20831:=20=E8=A6=AA=E3=82=A2?= =?UTF-8?q?=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E5=A4=89=E6=9B=B4API?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3853: 親アカウント変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3853) - 親アカウント切り替えAPIを実装しました。 ## レビューポイント - Service層の関数の分け方に改善点ないか? - テストケースで他にあったほうがいいものや観点などあるか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで全テスト通ることを確認 - 行った修正がデグレを発生させていないことを確認できるか - 新規APIの実装のため既存実装に変更なし --- dictation_server/src/app.module.ts | 4 +- .../features/accounts/accounts.controller.ts | 7 +- .../accounts/accounts.service.spec.ts | 382 ++++++++++++++++++ .../src/features/accounts/accounts.service.ts | 200 +++++++++ .../src/features/accounts/test/utility.ts | 12 + .../accounts/accounts.repository.service.ts | 71 ++++ .../src/repositories/accounts/errors/types.ts | 28 ++ 7 files changed, 695 insertions(+), 9 deletions(-) diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 7525fb7..f35fe54 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -167,9 +167,7 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; }) export class AppModule { configure(consumer: MiddlewareConsumer) { - consumer - .apply(LoggerMiddleware) - .forRoutes(''); + consumer.apply(LoggerMiddleware).forRoutes(''); // stage=localの場合はmiddlewareを適用しない // ローカル環境ではサーバーから静的ファイルも返すため、APIリクエスト以外のリクエストにもmiddlewareが適用されてしまう if (process.env.STAGE !== 'local') { diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 29f63b5..0e56a73 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2393,13 +2393,8 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO:service層を呼び出す。本実装時に以下は削除する。 const { to, children } = body; - this.logger.log( - `[${context.getTrackingId()}] to : ${to}, children : ${children.join( - ', ', - )}`, - ); + await this.accountService.switchParent(context, to, children); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index c20e6e6..7b43645 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -19,6 +19,7 @@ import { createLicenseSetExpiryDateAndStatus, createOptionItems, createWorktype, + getLicenseOrders, getOptionItems, getSortCriteria, getTypistGroup, @@ -92,6 +93,7 @@ import { truncateAllTable } from '../../common/test/init'; import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; import { TestLogger } from '../../common/test/logger'; +import { Account } from '../../repositories/accounts/entity/account.entity'; describe('createAccount', () => { let source: DataSource | null = null; @@ -7930,3 +7932,383 @@ describe('updateRestrictionStatus', () => { } }); }); + +describe('switchParent', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('第三階層<->第四階層間の階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 3, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 4, + country: `NZ`, // 同じリージョンで切り替えできることの確認 + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + + it('第四階層<->第五階層間の階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 5, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 5, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + + it('切り替え先親アカウントが存在しない場合は400エラー(親アカウント不在エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child = new Account(); + child.id = 1; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + try { + // 切り替え先アカウントを作成せずに実行する + await service.switchParent(context, 10, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017001')); + } else { + fail(); + } + } + }); + + it('切り替え先親アカウントが第三・第四以外の階層の場合は400エラー(階層関係不適切エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child = new Account(); + child.id = 1; + child.tier = 4; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + // 親アカウントの階層が第五階層の場合に失敗する + const parent = new Account(); + parent.id = 10; + try { + parent.tier = 5; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + + try { + // 親アカウントの階層が第二階層の場合に失敗する + parent.tier = 2; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + + try { + // 親アカウントの階層が第一階層の場合に失敗する + parent.tier = 1; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + }); + + it('第五階層の子アカウントに対して、第三階層の切り替え先親アカウントを指定した場合は400エラー(階層関係不適切エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 5; + const child2 = new Account(); + child1.id = 1; + child1.tier = 4; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 3; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + }); + + it('第三<->第四の切り替えで、親子でリージョンが異なる場合は400エラー(リージョン関係不一致エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 4; + child1.country = 'AU'; + + const child2 = new Account(); + child2.id = 2; + child2.tier = 4; + child2.country = 'US'; // このアカウントだけリージョンが異なるようにしておく + + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 3; + parent.country = 'AU'; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017003')); + } else { + fail(); + } + } + }); + + it('第四<->第五の切り替えで、親子で国が異なる場合は400エラー(国関係不一致エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 5; + child1.country = 'AU'; + + const child2 = new Account(); + child2.id = 2; + child2.tier = 5; + child2.country = 'NZ'; // このアカウントだけ国が異なるようにしておく + + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 4; + parent.country = 'AU'; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017004')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 1cd288f..d6af114 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -15,6 +15,9 @@ import { OPTION_ITEM_VALUE_TYPE, MANUAL_RECOVERY_REQUIRED, LICENSE_ISSUE_STATUS, + BLOB_STORAGE_REGION_AU, + BLOB_STORAGE_REGION_EU, + BLOB_STORAGE_REGION_US, } from '../../constants'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { @@ -47,7 +50,10 @@ import { LicensesRepositoryService } from '../../repositories/licenses/licenses. import { AccountNotFoundError, AdminUserNotFoundError, + CountryMismatchError, DealerAccountNotFoundError, + HierarchyMismatchError, + RegionMismatchError, } from '../../repositories/accounts/errors/types'; import { Context } from '../../common/log'; import { @@ -2633,4 +2639,198 @@ export class AccountsService { ); } } + + /** + * 指定した子アカウントの親アカウントを、指定した親アカウントに変更する + * @param context + * @param newParent + * @param children + * @returns parent + */ + async switchParent( + context: Context, + newParent: number, + children: number[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.switchParent.name + } | params: { ` + + `newParent: ${newParent}, ` + + `children: ${children.join(', ')}};`, + ); + + try { + // 切り替え対象の情報取得 + const childrenAccounts = await this.accountRepository.findAccountsById( + context, + children, + ); + if (childrenAccounts.length !== children.length) { + // 指定された子アカウントが一つでも存在しない場合は通常運用ではありえないので汎用エラー + throw new Error('Some children accounts are not found'); + } + + const parentAccount = await this.accountRepository.findAccountById( + context, + newParent, + ); + if (!parentAccount) { + // 指定された親アカウントが存在しない場合は通常運用で起こりうるため、BAD_REQUEST + throw new AccountNotFoundError( + `Parent account is not found. accountId=${newParent}`, + ); + } + + // 切り替え可否チェック(階層関係) + if ( + !this.isValidHierarchyRelation( + parentAccount.tier, + childrenAccounts.map((x) => x.tier), + ) + ) { + throw new HierarchyMismatchError( + `Invalid hierarchy relation. parentAccount.tier=${parentAccount.tier}`, + ); + } + // 切り替え可否チェック(リージョン・国関係) + const { success, errorType } = this.isValidLocationRelation( + parentAccount, + childrenAccounts, + ); + if (!success) { + throw errorType; + } + + // 切り替え処理実施 + await this.accountRepository.switchParentAccount( + context, + newParent, + children, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E017001'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E017002'), + HttpStatus.BAD_REQUEST, + ); + case RegionMismatchError: + throw new HttpException( + makeErrorResponse('E017003'), + HttpStatus.BAD_REQUEST, + ); + case CountryMismatchError: + throw new HttpException( + makeErrorResponse('E017004'), + HttpStatus.BAD_REQUEST, + ); + } + } + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.switchParent.name}`, + ); + } + } + + /** + * 切り替え対象の親アカウントと子アカウントの階層関係が正しいかどうかをチェックする + * @param parentTier + * @param childrenTiers + * @returns true if valid hierarchy relation + */ + private isValidHierarchyRelation( + parentTier: number, + childrenTiers: number[], + ): boolean { + // 全ての子アカウントの階層が、親アカウントの階層の一つ下であるかつ、第三<->第四または第四<->第五の切り替えの場合のみ判定OK。 + if ( + parentTier === TIERS.TIER3 && + childrenTiers.every((child) => child === TIERS.TIER4) + ) { + return true; + } else if ( + parentTier === TIERS.TIER4 && + childrenTiers.every((child) => child === TIERS.TIER5) + ) { + return true; + } + + return false; + } + + /** + * 切り替え対象の親アカウントと子アカウントのリージョン・国関係が正しいかどうかをチェックする。 + * @param parent + * @param children + * @returns valid location relation + */ + private isValidLocationRelation( + parent: Account, + children: Account[], + ): { + success: boolean; + errorType: null | RegionMismatchError | CountryMismatchError; + } { + // 第三<->第四の切り替えはリージョンの一致を確認し、第四<->第五の切り替えは国の一致を確認する。 + if (parent.tier === TIERS.TIER3) { + if ( + !children.every( + (child) => + this.getRegion(child.country) === this.getRegion(parent.country), + ) + ) { + return { + success: false, + errorType: new RegionMismatchError('Invalid region relation'), + }; + } + + return { success: true, errorType: null }; + } else if (parent.tier === TIERS.TIER4) { + if (!children.every((child) => child.country === parent.country)) { + return { + success: false, + errorType: new CountryMismatchError('Invalid country relation'), + }; + } + + return { success: true, errorType: null }; + } else { + // 親アカウントの階層が想定外の場合、本関数の使い方が間違っているので例外を投げる + throw new Error('Not implemented'); + } + } + + /** + * 国の所属する地域を取得する。 + * @param country + * @returns region + */ + private getRegion(country: string): string { + // OMDS様より、地域はBlobStorageのリージョンで判定するでOKとのこと。 + if (BLOB_STORAGE_REGION_AU.includes(country)) { + return 'AU'; + } else if (BLOB_STORAGE_REGION_EU.includes(country)) { + return 'EU'; + } else if (BLOB_STORAGE_REGION_US.includes(country)) { + return 'US'; + } else { + // ここに到達する場合は、国が想定外の値であるため、例外を投げる + throw new Error(`Invalid country. country=${country}`); + } + } } diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index ff0e436..046c2d2 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -248,3 +248,15 @@ export const createAudioFile = async ( const audioFile = audioFileIdentifiers.pop() as AudioFile; return { audioFileId: audioFile.id }; }; + +// ライセンス注文を取得する +export const getLicenseOrders = async ( + datasource: DataSource, + accountId: number, +): Promise => { + return await datasource.getRepository(LicenseOrder).find({ + where: { + from_account_id: accountId, + }, + }); +}; diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index f15a428..c063325 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -280,6 +280,23 @@ export class AccountsRepositoryService { return account; } + /** + * 指定されたアカウントIDのアカウント一覧を取得する + * @param context + * @param ids + * @returns accounts by id + */ + async findAccountsById(context: Context, ids: number[]): Promise { + const accounts = await this.dataSource.getRepository(Account).find({ + where: { + id: In(ids), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + return accounts; + } + /** * OMDSTokyoのアカウント情報を取得する * @param id @@ -1476,4 +1493,58 @@ export class AccountsRepositoryService { ); }); } + + /** + * アカウント階層構造変更とそれに伴う処理を行う。 + * @param context + * @param newParent + * @param children + * @returns parent account + */ + async switchParentAccount( + context: Context, + newParent: number, + children: number[], + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + const childrenAccounts = await accountRepo.find({ + where: { + id: In(children), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + await updateEntity( + accountRepo, + { id: In(childrenAccounts.map((child) => child.id)) }, + { + parent_account_id: newParent, + delegation_permission: false, + }, + this.isCommentOut, + context, + ); + + const licenseOrderRepo = entityManager.getRepository(LicenseOrder); + const cancelTargets = await licenseOrderRepo.find({ + where: { + from_account_id: In(children), + status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + await updateEntity( + licenseOrderRepo, + { id: In(cancelTargets.map((cancelTarget) => cancelTarget.id)) }, + { + status: LICENSE_ISSUE_STATUS.CANCELED, + canceled_at: new Date(), + }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/repositories/accounts/errors/types.ts b/dictation_server/src/repositories/accounts/errors/types.ts index 826700c..4cb5779 100644 --- a/dictation_server/src/repositories/accounts/errors/types.ts +++ b/dictation_server/src/repositories/accounts/errors/types.ts @@ -26,3 +26,31 @@ export class AccountLockedError extends Error { this.name = 'AccountLockedError'; } } + +/** + * 階層構造関係不適切エラー + */ +export class HierarchyMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'HierarchyMismatchError'; + } +} +/** + * 所属リージョン不一致エラー + */ +export class RegionMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'RegionMismatchError'; + } +} +/** + * 所属国不一致エラー + */ +export class CountryMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'CountryMismatchError'; + } +} From ac3d523c0e261c004ad3e1c094a04f08732d8e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Tue, 19 Mar 2024 07:36:03 +0000 Subject: [PATCH 061/109] =?UTF-8?q?Merged=20PR=20826:=20Azure=20Functions?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E9=9F=B3=E5=A3=B0=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E5=89=8A=E9=99=A4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3880: Azure Functions実装(音声ファイル削除)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3880) - 自動音声ファイル削除を実装 - 上記のテストを実装 - テストにMySQLを使用する仕組みを導入 ## レビューポイント - テストケースは十分か - テスト内容は妥当か - developにデプロイする前の動作確認・ユニットテストとして十分か ## クエリの変更 - 新規処理のため、既存からの変更はなし ## 動作確認状況 - DBが空の状態でローカル環境で実行し、0件削除のログが出ることを確認 - 削除対象が正しいか等はdevelopでチェック予定 - 行った修正がデグレを発生させていないことを確認できるか - 既存処理の変更はなし --- azure-pipelines-staging.yml | 20 +- dictation_function/.devcontainer/Dockerfile | 17 +- .../.devcontainer/docker-compose.yml | 9 + .../.devcontainer/pipeline-docker-compose.yml | 31 + dictation_function/.env.test | 35 + dictation_function/.gitignore | 2 + dictation_function/package.json | 2 +- .../blobstorage/audioBlobStorage.service.ts | 146 ++ dictation_function/src/constants/index.ts | 6 + .../src/database/initializeDataSource.ts | 10 +- .../src/entity/account.entity.ts | 7 + .../src/entity/audio_file.entity.ts | 40 + .../src/entity/audio_option_item.entity.ts | 13 + dictation_function/src/entity/task.entity.ts | 59 + .../src/functions/deleteAudioFiles.ts | 187 +++ .../src/test/analysisLicenses.spec.ts | 2 +- dictation_function/src/test/common/init.ts | 21 + dictation_function/src/test/common/logger.ts | 129 ++ dictation_function/src/test/common/utility.ts | 191 ++- .../src/test/deleteAudioFiles.spec.ts | 1496 +++++++++++++++++ .../src/test/importUsers.spec.ts | 2 +- .../src/test/licenseAlert.spec.ts | 4 +- .../src/test/licenseAutoAllocation.spec.ts | 2 +- 23 files changed, 2403 insertions(+), 28 deletions(-) create mode 100644 dictation_function/.devcontainer/pipeline-docker-compose.yml create mode 100644 dictation_function/.env.test create mode 100644 dictation_function/src/blobstorage/audioBlobStorage.service.ts create mode 100644 dictation_function/src/entity/audio_file.entity.ts create mode 100644 dictation_function/src/entity/audio_option_item.entity.ts create mode 100644 dictation_function/src/entity/task.entity.ts create mode 100644 dictation_function/src/functions/deleteAudioFiles.ts create mode 100644 dictation_function/src/test/common/init.ts create mode 100644 dictation_function/src/test/common/logger.ts create mode 100644 dictation_function/src/test/deleteAudioFiles.spec.ts diff --git a/azure-pipelines-staging.yml b/azure-pipelines-staging.yml index 65c19c4..16550b1 100644 --- a/azure-pipelines-staging.yml +++ b/azure-pipelines-staging.yml @@ -196,22 +196,12 @@ jobs: displayName: Bash Script (Test) inputs: targetType: inline + workingDirectory: dictation_function/.devcontainer script: | - cd dictation_function - npm run test - env: - TENANT_NAME: xxxxxxxxxxxx - SIGNIN_FLOW_NAME: xxxxxxxxxxxx - ADB2C_TENANT_ID: $(adb2c-tenant-id) - ADB2C_CLIENT_ID: $(adb2c-client-id) - ADB2C_CLIENT_SECRET: $(adb2c-client-secret) - ADB2C_ORIGIN: xxxxxx - SENDGRID_API_KEY: $(sendgrid-api-key) - MAIL_FROM: xxxxxx - APP_DOMAIN: xxxxxxxxx - REDIS_HOST: xxxxxxxxxxxx - REDIS_PORT: 0 - REDIS_PASSWORD: xxxxxxxxxxxx + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_function sudo npm ci + docker-compose exec -T dictation_function sudo npm run test - task: Docker@0 displayName: build inputs: diff --git a/dictation_function/.devcontainer/Dockerfile b/dictation_function/.devcontainer/Dockerfile index c33b122..97e310d 100644 --- a/dictation_function/.devcontainer/Dockerfile +++ b/dictation_function/.devcontainer/Dockerfile @@ -6,7 +6,7 @@ FROM node:18.17.1-buster RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \ echo "Asia/Tokyo" > /etc/timezone - + # Options for setup script ARG INSTALL_ZSH="true" ARG UPGRADE_PACKAGES="false" @@ -24,6 +24,21 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$ && apt-get install default-jre -y \ && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts +# COPY --from=golang:1.18-buster /usr/local/go/ /usr/local/go/ +ENV GO111MODULE=auto +COPY library-scripts/go-debian.sh /tmp/library-scripts/ +RUN bash /tmp/library-scripts/go-debian.sh "1.18" "/usr/local/go" "${GOPATH}" "${USERNAME}" "false" \ + && apt-get clean -y && rm -rf /tmp/library-scripts +ENV PATH="/usr/local/go/bin:${PATH}" +RUN mkdir -p /tmp/gotools \ + && cd /tmp/gotools \ + && export GOPATH=/tmp/gotools \ + && export GOCACHE=/tmp/gotools/cache \ + # sql-migrate + && go install github.com/rubenv/sql-migrate/sql-migrate@v1.1.2 \ + && mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \ + && rm -rf /tmp/gotools + # Update NPM RUN npm install -g npm diff --git a/dictation_function/.devcontainer/docker-compose.yml b/dictation_function/.devcontainer/docker-compose.yml index ebcd52d..78d2750 100644 --- a/dictation_function/.devcontainer/docker-compose.yml +++ b/dictation_function/.devcontainer/docker-compose.yml @@ -16,6 +16,15 @@ services: - CHOKIDAR_USEPOLLING=true networks: - external + test_mysql_db: + image: mysql:8.0-bullseye + environment: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: odms + MYSQL_USER: user + MYSQL_PASSWORD: password + networks: + - external networks: external: name: omds_network diff --git a/dictation_function/.devcontainer/pipeline-docker-compose.yml b/dictation_function/.devcontainer/pipeline-docker-compose.yml new file mode 100644 index 0000000..16a94b6 --- /dev/null +++ b/dictation_function/.devcontainer/pipeline-docker-compose.yml @@ -0,0 +1,31 @@ +version: "3" + +services: + dictation_function: + build: . + working_dir: /app/dictation_function + volumes: + - ../../:/app + - node_modules:/app/dictation_function/node_modules + environment: + - CHOKIDAR_USEPOLLING=true + depends_on: + - test_mysql_db + networks: + - dictation_function_network + test_mysql_db: + image: mysql:8.0-bullseye + environment: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: odms + MYSQL_USER: user + MYSQL_PASSWORD: password + networks: + - dictation_function_network +networks: + dictation_function_network: + name: test_dictation_function_network + +# Data Volume として永続化する +volumes: + node_modules: diff --git a/dictation_function/.env.test b/dictation_function/.env.test new file mode 100644 index 0000000..5026a47 --- /dev/null +++ b/dictation_function/.env.test @@ -0,0 +1,35 @@ +STAGE=local +TENANT_NAME=tenantoname +SIGNIN_FLOW_NAME=b2c_1_signin_dev +ADB2C_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx +ADB2C_ORIGIN=https://xxxxxxx.b2clogin.com/xxxxxxxx.onmicrosoft.com/b2c_1_signin_dev/ +KEY_VAULT_NAME=xxxxxxxxxxxxxxx +SENDGRID_API_KEY=SG.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +MAIL_FROM=noreply@se0223.com +APP_DOMAIN=http://localhost:8081/ +REDIS_HOST=redis-cache +REDIS_PORT=6379 +REDIS_PASSWORD=omdsredispass +ADB2C_CACHE_TTL=86400 +STORAGE_ACCOUNT_NAME_US=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_AU=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_EU=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_IMPORT=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_ANALYSIS=saxxxxxxxxx +STORAGE_ACCOUNT_KEY_US=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_AU=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_EU=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_IMPORT=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_ANALYSIS=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_ENDPOINT_US=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_AU=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_EU=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_IMPORT=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_ANALYSIS=https://saxxxxxxxxx.blob.core.windows.net/ +BASE_PATH=http://localhost:8081 +ACCESS_TOKEN_LIFETIME_WEB=7200000 +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n" +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n" \ No newline at end of file diff --git a/dictation_function/.gitignore b/dictation_function/.gitignore index b351586..27683c5 100644 --- a/dictation_function/.gitignore +++ b/dictation_function/.gitignore @@ -11,6 +11,8 @@ Publish *.Cache project.lock.json +.test/ + /packages /TestResults diff --git a/dictation_function/package.json b/dictation_function/package.json index fce3022..0be5119 100644 --- a/dictation_function/package.json +++ b/dictation_function/package.json @@ -9,7 +9,7 @@ "clean": "rimraf dist", "prestart": "npm run clean && npm run build", "start": "func start", - "test": "jest", + "test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test && jest -w 1", "codegen": "sh codegen.sh" }, "dependencies": { diff --git a/dictation_function/src/blobstorage/audioBlobStorage.service.ts b/dictation_function/src/blobstorage/audioBlobStorage.service.ts new file mode 100644 index 0000000..4acf23a --- /dev/null +++ b/dictation_function/src/blobstorage/audioBlobStorage.service.ts @@ -0,0 +1,146 @@ +import { + BlobServiceClient, + ContainerClient, + StorageSharedKeyCredential, +} from "@azure/storage-blob"; +import { + BLOB_STORAGE_REGION_AU, + BLOB_STORAGE_REGION_EU, + BLOB_STORAGE_REGION_US, +} from "../constants"; +import { InvocationContext } from "@azure/functions"; + +export class AudioBlobStorageService { + private readonly sharedKeyCredentialUS: StorageSharedKeyCredential; + private readonly sharedKeyCredentialEU: StorageSharedKeyCredential; + private readonly sharedKeyCredentialAU: StorageSharedKeyCredential; + + private readonly blobServiceClientUS: BlobServiceClient; + private readonly blobServiceClientEU: BlobServiceClient; + private readonly blobServiceClientAU: BlobServiceClient; + + constructor() { + if ( + !process.env.STORAGE_ACCOUNT_ENDPOINT_US || + !process.env.STORAGE_ACCOUNT_ENDPOINT_AU || + !process.env.STORAGE_ACCOUNT_ENDPOINT_EU || + !process.env.STORAGE_ACCOUNT_NAME_US || + !process.env.STORAGE_ACCOUNT_KEY_US || + !process.env.STORAGE_ACCOUNT_NAME_AU || + !process.env.STORAGE_ACCOUNT_KEY_AU || + !process.env.STORAGE_ACCOUNT_NAME_EU || + !process.env.STORAGE_ACCOUNT_KEY_EU + ) { + throw new Error("Storage account information is missing"); + } + + // リージョンごとのSharedKeyCredentialを生成 + this.sharedKeyCredentialUS = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_US, + process.env.STORAGE_ACCOUNT_KEY_US + ); + this.sharedKeyCredentialAU = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_AU, + process.env.STORAGE_ACCOUNT_KEY_AU + ); + this.sharedKeyCredentialEU = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_EU, + process.env.STORAGE_ACCOUNT_KEY_EU + ); + + // Audioファイルの保存先のリージョンごとにBlobServiceClientを生成 + this.blobServiceClientUS = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_US, + this.sharedKeyCredentialUS + ); + this.blobServiceClientAU = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_AU, + this.sharedKeyCredentialAU + ); + this.blobServiceClientEU = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_EU, + this.sharedKeyCredentialEU + ); + } + + /** + * 指定されたファイルを削除します。 + * @param context + * @param accountId + * @param country + * @param fileName + * @returns file + */ + async deleteFile( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise { + context.log( + `[IN] ${this.deleteFile.name} | params: { ` + + `accountId: ${accountId} ` + + `country: ${country} ` + + `fileName: ${fileName} };` + ); + + try { + // 国に応じたリージョンでコンテナ名を指定してClientを取得 + const containerClient = this.getContainerClient( + context, + accountId, + country + ); + // コンテナ内のBlobパス名を指定してClientを取得 + const blobClient = containerClient.getBlobClient(fileName); + + const { succeeded, errorCode, date } = await blobClient.deleteIfExists(); + context.log( + `succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}` + ); + + // 失敗時、Blobが存在しない場合以外はエラーとして例外をスローする + // Blob不在の場合のエラーコードは「BlobNotFound」以下を参照 + // https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes + if (!succeeded && errorCode !== "BlobNotFound") { + throw new Error( + `delete blob failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}` + ); + } + } catch (e) { + context.error(`error=${e}`); + throw e; + } finally { + context.log(`[OUT] ${this.deleteFile.name}`); + } + } + + /** + * 指定してアカウントIDと国に応じたリージョンのコンテナクライアントを取得します。 + * @param context + * @param accountId + * @param country + * @returns + */ + private getContainerClient( + context: InvocationContext, + accountId: number, + country: string + ): ContainerClient { + context.log( + `[IN] ${this.getContainerClient.name} | params: { ` + + `accountId: ${accountId}; country: ${country} };` + ); + + const containerName = `account-${accountId}`; + if (BLOB_STORAGE_REGION_US.includes(country)) { + return this.blobServiceClientUS.getContainerClient(containerName); + } else if (BLOB_STORAGE_REGION_AU.includes(country)) { + return this.blobServiceClientAU.getContainerClient(containerName); + } else if (BLOB_STORAGE_REGION_EU.includes(country)) { + return this.blobServiceClientEU.getContainerClient(containerName); + } else { + throw new Error("invalid country"); + } + } +} diff --git a/dictation_function/src/constants/index.ts b/dictation_function/src/constants/index.ts index 6f94278..ca6be1e 100644 --- a/dictation_function/src/constants/index.ts +++ b/dictation_function/src/constants/index.ts @@ -338,6 +338,12 @@ export const SYSTEM_IMPORT_USERS = "import-users"; export const ROW_START_INDEX = 2; +/** + * ファイル保持日数の初期値 + * @const {number} + */ +export const FILE_RETENTION_DAYS_DEFAULT = 30; + /** * ライセンス数推移出力機能のCSVヘッダ * @const {string[]} diff --git a/dictation_function/src/database/initializeDataSource.ts b/dictation_function/src/database/initializeDataSource.ts index e573669..f323d54 100644 --- a/dictation_function/src/database/initializeDataSource.ts +++ b/dictation_function/src/database/initializeDataSource.ts @@ -6,8 +6,11 @@ import { LicenseAllocationHistoryArchive, LicenseArchive, } from "../entity/license.entity"; -import { InvocationContext, } from "@azure/functions"; -import { DataSource} from "typeorm"; +import { InvocationContext } from "@azure/functions"; +import { DataSource } from "typeorm"; +import { Task } from "../entity/task.entity"; +import { AudioFile } from "../entity/audio_file.entity"; +import { AudioOptionItem } from "../entity/audio_option_item.entity"; export async function initializeDataSource( context: InvocationContext @@ -25,6 +28,9 @@ export async function initializeDataSource( UserArchive, Account, AccountArchive, + Task, + AudioFile, + AudioOptionItem, License, LicenseArchive, LicenseAllocationHistory, diff --git a/dictation_function/src/entity/account.entity.ts b/dictation_function/src/entity/account.entity.ts index eed6190..043be02 100644 --- a/dictation_function/src/entity/account.entity.ts +++ b/dictation_function/src/entity/account.entity.ts @@ -11,6 +11,7 @@ import { JoinColumn, OneToMany, } from "typeorm"; +import { FILE_RETENTION_DAYS_DEFAULT } from "../constants"; @Entity({ name: "accounts" }) export class Account { @@ -47,6 +48,12 @@ export class Account { @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) active_worktype_id: number | null; + @Column({ default: false }) + auto_file_delete: boolean; + + @Column({ default: FILE_RETENTION_DAYS_DEFAULT }) + file_retention_days: number; + @Column({ nullable: true, type: "datetime" }) deleted_at: Date | null; diff --git a/dictation_function/src/entity/audio_file.entity.ts b/dictation_function/src/entity/audio_file.entity.ts new file mode 100644 index 0000000..4de4b82 --- /dev/null +++ b/dictation_function/src/entity/audio_file.entity.ts @@ -0,0 +1,40 @@ +import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm"; + +@Entity({ name: "audio_files" }) +export class AudioFile { + @PrimaryGeneratedColumn() + id: number; + + @Column() + account_id: number; + @Column() + owner_user_id: number; + @Column() + url: string; + @Column() + file_name: string; + @Column() + author_id: string; + @Column() + work_type_id: string; + @Column() + started_at: Date; + @Column({ type: "time" }) + duration: string; + @Column() + finished_at: Date; + @Column() + uploaded_at: Date; + @Column() + file_size: number; + @Column() + priority: string; + @Column() + audio_format: string; + @Column({ nullable: true, type: "varchar" }) + comment: string | null; + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + @Column() + is_encrypted: boolean; +} diff --git a/dictation_function/src/entity/audio_option_item.entity.ts b/dictation_function/src/entity/audio_option_item.entity.ts new file mode 100644 index 0000000..a3fe3cc --- /dev/null +++ b/dictation_function/src/entity/audio_option_item.entity.ts @@ -0,0 +1,13 @@ +import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; + +@Entity({ name: "audio_option_items" }) +export class AudioOptionItem { + @PrimaryGeneratedColumn() + id: number; + @Column() + audio_file_id: number; + @Column() + label: string; + @Column() + value: string; +} diff --git a/dictation_function/src/entity/task.entity.ts b/dictation_function/src/entity/task.entity.ts new file mode 100644 index 0000000..ceb96fc --- /dev/null +++ b/dictation_function/src/entity/task.entity.ts @@ -0,0 +1,59 @@ +import { AudioOptionItem } from "./audio_option_item.entity"; +import { AudioFile } from "./audio_file.entity"; +import { + Entity, + Column, + PrimaryGeneratedColumn, + OneToOne, + JoinColumn, + OneToMany, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; +import { bigintTransformer } from "../common/entity"; +import { Account } from "./account.entity"; + +@Entity({ name: "tasks" }) +export class Task { + @PrimaryGeneratedColumn() + id: number; + @Column() + job_number: string; + @Column() + account_id: number; + @Column({ nullable: true, type: "boolean" }) + is_job_number_enabled: boolean | null; + @Column() + audio_file_id: number; + @Column() + status: string; + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + typist_user_id: number | null; + @Column() + priority: string; + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + template_file_id: number | null; + @Column({ nullable: true, type: "datetime" }) + started_at: Date | null; + @Column({ nullable: true, type: "datetime" }) + finished_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; +} diff --git a/dictation_function/src/functions/deleteAudioFiles.ts b/dictation_function/src/functions/deleteAudioFiles.ts new file mode 100644 index 0000000..b710969 --- /dev/null +++ b/dictation_function/src/functions/deleteAudioFiles.ts @@ -0,0 +1,187 @@ +import { app, InvocationContext, Timer } from "@azure/functions"; +import { DataSource, In } from "typeorm"; +import { Task } from "../entity/task.entity"; +import { AudioFile } from "../entity/audio_file.entity"; +import { AudioOptionItem } from "../entity/audio_option_item.entity"; +import { MANUAL_RECOVERY_REQUIRED } from "../constants"; +import * as dotenv from "dotenv"; +import { initializeDataSource } from "../database/initializeDataSource"; +import { AudioBlobStorageService } from "../blobstorage/audioBlobStorage.service"; + +app.timer("deleteAudioFiles", { + schedule: "0 3 * * *", // 毎日UTC 3:00に実行 + handler: deleteAudioFiles, +}); + +// 削除対象となる音声ファイルとタスクを特定する為の情報 +type TargetTaskInfo = { + /** + * タスクID + * column: tasks.id + */ + id: number; + + /** + * ファイルID + * column: audio_files.id + */ + audio_file_id: number; + + /** + * ファイル名 + * column: audio_files.file_name + */ + file_name: string; + + /** + * アカウントID + * column: accounts.id + */ + account_id: number; + + /** + * アカウントの所属する地域 + * column: accounts.country + */ + country: string; +}; + +export async function deleteAudioFilesProcessing( + context: InvocationContext, + dataSource: DataSource, + blobStorageService: AudioBlobStorageService, + now: Date +): Promise { + context.log(`[IN] deleteAudioFilesProcessing. now=${now}`); + try { + // 削除対象のタスクとファイルを取得 + const targets = await getProcessTargets(dataSource, now); + context.log(`delete targets: ${JSON.stringify(targets)}`); + + // DBからレコードを削除 + await deleteRecords(dataSource, targets); + + // タスクに紐づくファイルを削除 + for (const target of targets) { + try { + const { account_id, country, file_name } = target; + await blobStorageService.deleteFile( + context, + account_id, + country, + file_name + ); + context.log(`file delete success. target=${JSON.stringify(target)}`); + } catch (e) { + context.log( + `${MANUAL_RECOVERY_REQUIRED} file delete failed. target=${JSON.stringify( + target + )}` + ); + } + } + } catch (error) { + // DB関連で例外が発生した場合、アラートを出す為のログを出力する + context.error( + `${MANUAL_RECOVERY_REQUIRED} Failed to execute auto file deletion function. error=${error}` + ); + throw error; + } finally { + context.log(`[OUT] deleteAudioFilesProcessing`); + } +} + +async function deleteAudioFiles( + myTimer: Timer, + context: InvocationContext +): Promise { + context.log("[IN]deleteAudioFiles"); + + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + const dataSource = await initializeDataSource(context); + try { + await deleteAudioFilesProcessing( + context, + dataSource, + new AudioBlobStorageService(), + new Date() + ); + } catch (e) { + context.log("deleteAudioFilesProcessing failed."); + context.error(e); + throw e; + } finally { + await dataSource.destroy(); + context.log("[OUT]deleteAudioFiles"); + } +} + +/** + * タスクに紐づくファイルを削除する + * @param dataSource + * @param targets + * @returns Promise + */ +// テスト容易性を高めて開発効率を上げるため、本来はexport不要だがexportを行う 2024/03/13 +export async function deleteRecords( + dataSource: DataSource, + targets: TargetTaskInfo[] +): Promise { + const taskIds = targets.map((target) => target.id); + const audioFileIds = targets.map((target) => target.audio_file_id); + + // taskIdsに紐づくタスクを削除 + await dataSource.transaction(async (manager) => { + const taskRepository = manager.getRepository(Task); + const audioFileRepository = manager.getRepository(AudioFile); + const audioOptionItem = manager.getRepository(AudioOptionItem); + + // tasksテーブルから削除 + await taskRepository.delete({ + id: In(taskIds), + }); + // audio_filesテーブルから削除 + await audioFileRepository.delete({ + id: In(audioFileIds), + }); + // audio_option_itemsテーブルから削除 + await audioOptionItem.delete({ + audio_file_id: In(audioFileIds), + }); + }); +} + +/* + * ファイル削除対象のタスクを取得する + * @param dataSource + * @param now + * @returns Promise<{ account_id: number; audio_file_id: number; file_name: string; }[]> + */ +// テスト容易性を高めて開発効率を上げるため、本来はexport不要だがexportを行う 2024/03/13 +export async function getProcessTargets( + dataSource: DataSource, + now: Date +): Promise { + return await dataSource + .createQueryBuilder(Task, "tasks") + // SELECTでエイリアスを指定して、結果オブジェクトのプロパティとTargetTaskInfoのプロパティを一致させる + .select("tasks.id", "id") + .addSelect("tasks.audio_file_id", "audio_file_id") + .addSelect("audio_files.file_name", "file_name") + .addSelect("accounts.id", "account_id") + .addSelect("accounts.country", "country") + .where("tasks.finished_at IS NOT NULL") + .innerJoin("accounts", "accounts", "accounts.id = tasks.account_id") + .innerJoin( + "audio_files", + "audio_files", + "audio_files.id = tasks.audio_file_id" + ) + .andWhere("accounts.auto_file_delete = true") + .andWhere( + "DATE_ADD(tasks.finished_at, INTERVAL accounts.file_retention_days DAY) < :now", + { now } + ) + .getRawMany(); +} diff --git a/dictation_function/src/test/analysisLicenses.spec.ts b/dictation_function/src/test/analysisLicenses.spec.ts index e2cfb0b..75e694c 100644 --- a/dictation_function/src/test/analysisLicenses.spec.ts +++ b/dictation_function/src/test/analysisLicenses.spec.ts @@ -33,7 +33,7 @@ import { BlobstorageService } from "../blobstorage/blobstorage.service"; import { User, UserArchive } from "../entity/user.entity"; describe("analysisLicenses", () => { dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); + dotenv.config({ path: ".env.test", override: true }); let source: DataSource | null = null; beforeEach(async () => { diff --git a/dictation_function/src/test/common/init.ts b/dictation_function/src/test/common/init.ts new file mode 100644 index 0000000..bfe1512 --- /dev/null +++ b/dictation_function/src/test/common/init.ts @@ -0,0 +1,21 @@ +import { DataSource } from 'typeorm'; + +export const truncateAllTable = async (source: DataSource) => { + const entities = source.entityMetadatas; + const queryRunner = source.createQueryRunner(); + + try { + await queryRunner.startTransaction(); + await queryRunner.query('SET FOREIGN_KEY_CHECKS=0'); + for (const entity of entities) { + await queryRunner.query(`TRUNCATE TABLE \`${entity.tableName}\``); + } + await queryRunner.query('SET FOREIGN_KEY_CHECKS=1'); + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); + } +}; diff --git a/dictation_function/src/test/common/logger.ts b/dictation_function/src/test/common/logger.ts new file mode 100644 index 0000000..19f5124 --- /dev/null +++ b/dictation_function/src/test/common/logger.ts @@ -0,0 +1,129 @@ +import { Logger, QueryRunner } from "typeorm"; +import * as fs from "fs"; +import * as path from "path"; + +interface IOutput { + initialize(): void; + write(message: string): void; +} + +class ConsoleOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + console.log(message); + } +} + +class FileOutput implements IOutput { + private logPath = path.join("/app/dictation_function/.test", "logs"); + private fileName = new Date().getTime(); + + initialize(): void { + if (!fs.existsSync(this.logPath)) { + fs.mkdirSync(this.logPath, { recursive: true }); + } + } + + write(message: string): void { + const logFile = path.join(this.logPath, `${this.fileName}.log`); + fs.appendFileSync(logFile, `${message}\n`); + } +} + +class NoneOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + // do nothing + } +} + +export class TestLogger implements Logger { + out: IOutput; + + constructor(output: "none" | "file" | "console") { + switch (output) { + case "none": + this.out = new NoneOutput(); + break; + case "file": + this.out = new FileOutput(); + break; + case "console": + this.out = new ConsoleOutput(); + break; + default: + this.out = new NoneOutput(); + break; + } + this.out.initialize(); + } + + private write(message: string): void { + this.out.write(message); + } + + logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { + const raw = `Query: ${query} -- Parameters: ${JSON.stringify(parameters)}`; + // ex: 2024-03-08T06:38:43.125Z を TIME という文字列に置換 + const dateRemoved = raw.replace( + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, + "TIME" + ); + // ex: /* コメント内容 */ を /* コメント */ という文字列に置換 + const commentRemoved = dateRemoved.replace( + /\/\*.*\*\//g, + "/* RequestID */" + ); + + // UUIDを固定文字列に置換する ex: 88a9c78e-115a-439c-9e23-731d649f0c27 を XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX という文字列に置換 + const uuidRemoved = commentRemoved.replace( + /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + ); + this.write(uuidRemoved); + } + + logQueryError( + error: string, + query: string, + parameters?: any[], + queryRunner?: QueryRunner + ) { + this.write( + `ERROR: ${error} -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters + )}` + ); + } + + logQuerySlow( + time: number, + query: string, + parameters?: any[], + queryRunner?: QueryRunner + ) { + this.write( + `SLOW QUERY: ${time}ms -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters + )}` + ); + } + + logSchemaBuild(message: string, queryRunner?: QueryRunner) { + this.write(`Schema Build: ${message}`); + } + + logMigration(message: string, queryRunner?: QueryRunner) { + this.write(`Migration: ${message}`); + } + + log(level: "log" | "info" | "warn", message: any, queryRunner?: QueryRunner) { + this.write(`${level.toUpperCase()}: ${message}`); + } +} diff --git a/dictation_function/src/test/common/utility.ts b/dictation_function/src/test/common/utility.ts index c167750..d9c6a07 100644 --- a/dictation_function/src/test/common/utility.ts +++ b/dictation_function/src/test/common/utility.ts @@ -1,14 +1,19 @@ import { v4 as uuidv4 } from "uuid"; -import { DataSource } from "typeorm"; +import { DataSource, In } from "typeorm"; import { User, UserArchive } from "../../entity/user.entity"; import { Account, AccountArchive } from "../../entity/account.entity"; -import { ADMIN_ROLES, USER_ROLES } from "../../constants"; +import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from "../../constants"; import { License, LicenseAllocationHistory, LicenseArchive, LicenseAllocationHistoryArchive, } from "../../entity/license.entity"; +import { bigintTransformer } from "../../common/entity"; +import { Task } from "../../entity/task.entity"; +import { AudioFile } from "../../entity/audio_file.entity"; +import { AudioOptionItem } from "../../entity/audio_option_item.entity"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -41,6 +46,8 @@ type OverrideUserArchive = Omit< "id" | "account" | "license" | "userGroupMembers" >; +type OverrideTask = Omit; + type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] }; type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] }; type AccountArchiveDefault = { @@ -49,6 +56,7 @@ type AccountArchiveDefault = { type UserArchiveDefault = { [K in keyof OverrideUserArchive]?: OverrideUserArchive[K]; }; +type TaskDefault = { [K in keyof OverrideTask]?: OverrideTask[K] }; /** * テスト ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する @@ -117,6 +125,8 @@ export const makeTestAccount = async ( locked: d?.locked ?? false, company_name: d?.company_name ?? "test inc.", verified: d?.verified ?? true, + auto_file_delete: d?.auto_file_delete ?? false, + file_retention_days: d?.file_retention_days ?? 30, deleted_at: d?.deleted_at ?? "", created_by: d?.created_by ?? "test_runner", created_at: d?.created_at ?? new Date(), @@ -124,7 +134,7 @@ export const makeTestAccount = async ( updated_at: d?.updated_at ?? new Date(), }); const result = identifiers.pop() as Account; - accountId = result.id; + accountId = bigintTransformer.from(result.id); } { const d = defaultAdminUserValue; @@ -148,7 +158,7 @@ export const makeTestAccount = async ( }); const result = identifiers.pop() as User; - userId = result.id; + userId = bigintTransformer.from(result.id); } // Accountの管理者を設定する @@ -471,3 +481,176 @@ export const createLicenseAllocationHistoryArchive = async ( }); identifiers.pop() as LicenseAllocationHistoryArchive; }; + +export const makeTestTask = async ( + datasource: DataSource, + accountId: number, + ownerUserId: number, + identifierName: string, + defaultTaskValue?: TaskDefault +): Promise<{ task: Task; file: AudioFile; options: AudioOptionItem[] }> => { + const d = defaultTaskValue; + + // AudioFileを作成 + const { identifiers: identifiers1 } = await datasource + .getRepository(AudioFile) + .insert({ + account_id: accountId, + owner_user_id: ownerUserId, + url: `https://example.com/${identifierName}`, + file_name: `test${identifierName}.wav`, + author_id: "test_author", + work_type_id: "test_work_type", + started_at: new Date(), + duration: "00:00:00", + finished_at: new Date(), + uploaded_at: new Date(), + file_size: 1024, + priority: "01", + audio_format: "wav", + comment: `test_comment_${identifierName}`, + deleted_at: new Date(), + is_encrypted: false, + }); + + const result = identifiers1.pop() as AudioFile; + const audioFileId = bigintTransformer.from(result.id); + + // AudioFileを取得 + const file = await datasource.getRepository(AudioFile).findOne({ + where: { + id: audioFileId, + }, + }); + if (!file) { + throw new Error("Unexpected null"); + } + + // Taskを作成 + const { identifiers: identifiers2 } = await datasource + .getRepository(Task) + .insert({ + job_number: d?.job_number ?? "0001", + account_id: accountId, + is_job_number_enabled: d?.is_job_number_enabled ?? true, + audio_file_id: file.id, + status: d?.status ?? "Uploaded", + typist_user_id: d?.typist_user_id, + priority: d?.priority ?? "01", + template_file_id: d?.template_file_id, + started_at: d?.started_at ?? new Date(), + finished_at: d?.finished_at ?? new Date(), + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + const result2 = identifiers2.pop() as Task; + const taskId = bigintTransformer.from(result2.id); + + const task = await datasource.getRepository(Task).findOne({ + where: { + id: taskId, + }, + }); + if (!task) { + throw new Error("Unexpected null"); + } + + // AudioOptionItemを作成 + const item01 = await datasource.getRepository(AudioOptionItem).insert({ + audio_file_id: audioFileId, + label: `test_option_label_${identifierName}_01`, + value: `test_option_value_${identifierName}_01`, + }); + const item02 = await datasource.getRepository(AudioOptionItem).insert({ + audio_file_id: audioFileId, + label: `test_option_label_${identifierName}_02`, + value: `test_option_value_${identifierName}_02`, + }); + const optionItemResult01 = item01.identifiers.pop() as AudioOptionItem; + const optionItemResult02 = item02.identifiers.pop() as AudioOptionItem; + const optionItemID01 = bigintTransformer.from(optionItemResult01.id); + const optionItemID02 = bigintTransformer.from(optionItemResult02.id); + + const optionItems = await datasource.getRepository(AudioOptionItem).find({ + where: { + id: In([optionItemID01, optionItemID02]), + }, + }); + + return { task, file, options: optionItems }; +}; + +export const makeManyTestTasks = async ( + datasource: DataSource, + inputFiles: QueryDeepPartialEntity[], + task_finished_at: Date +): Promise => { + const fileRepository = datasource.getRepository(AudioFile); + const result = await fileRepository.insert(inputFiles); + const audioFileIds = result.identifiers.map((id) => id.id); + const files = await fileRepository.find({ + where: { + id: In(audioFileIds), + }, + }); + + const tasks = files.map((file, index): QueryDeepPartialEntity => { + return { + job_number: `0001_${index}`, + account_id: file.account_id, + is_job_number_enabled: true, + audio_file_id: file.id, + status: TASK_STATUS.FINISHED, + typist_user_id: null, + priority: "01", + template_file_id: null, + started_at: new Date(), + finished_at: task_finished_at, + created_by: "test_runner", + created_at: new Date(), + updated_by: "updater", + updated_at: new Date(), + }; + }); + + const taskRepository = datasource.getRepository(Task); + const x = await taskRepository.insert(tasks); + + const partialOptions = files.flatMap( + (file, index): QueryDeepPartialEntity[] => { + return [ + { + audio_file_id: file.id, + label: `test_option_label_${index}_01`, + value: `test_option_value_${index}_01`, + }, + { + audio_file_id: file.id, + label: `test_option_label_${index}_02`, + value: `test_option_value_${index}_02`, + }, + ]; + } + ); + + const optionRepository = datasource.getRepository(AudioOptionItem); + await optionRepository.insert(partialOptions); +}; + +export const getTasks = async (datasource: DataSource): Promise => { + return await datasource.getRepository(Task).find(); +}; + +export const getAudioFiles = async ( + datasource: DataSource +): Promise => { + return await datasource.getRepository(AudioFile).find(); +}; + +export const getAudioOptionItems = async ( + datasource: DataSource +): Promise => { + return await datasource.getRepository(AudioOptionItem).find(); +}; diff --git a/dictation_function/src/test/deleteAudioFiles.spec.ts b/dictation_function/src/test/deleteAudioFiles.spec.ts new file mode 100644 index 0000000..f165e4b --- /dev/null +++ b/dictation_function/src/test/deleteAudioFiles.spec.ts @@ -0,0 +1,1496 @@ +import * as dotenv from "dotenv"; +import { InvocationContext } from "@azure/functions"; +import { DataSource } from "typeorm"; +import { truncateAllTable } from "./common/init"; +import { + deleteAudioFilesProcessing, + deleteRecords, + getProcessTargets, +} from "../functions/deleteAudioFiles"; +import { + getAudioFiles, + getAudioOptionItems, + getTasks, + makeManyTestTasks, + makeTestAccount, + makeTestTask, +} from "./common/utility"; +import { TASK_STATUS } from "../constants"; +import { TestLogger } from "./common/logger"; +import { AudioFile } from "../entity/audio_file.entity"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; +import { AudioBlobStorageService } from "../blobstorage/audioBlobStorage.service"; +import { User, UserArchive } from "../entity/user.entity"; +import { Account, AccountArchive } from "../entity/account.entity"; +import { Task } from "../entity/task.entity"; +import { + License, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + LicenseArchive, +} from "../entity/license.entity"; +import { AudioOptionItem } from "../entity/audio_option_item.entity"; + +describe("getProcessTargets | 削除対象を特定するQueryが正常に動作するか確認する", () => { + let source: DataSource | null = null; + beforeAll(async () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.test", override: true }); + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: "mysql", + host: "test_mysql_db", + port: 3306, + username: "user", + password: "password", + database: "odms", + entities: [__dirname + "/../../**/*.entity{.ts,.js}"], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger("none"), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it("ファイル削除対象のタスクが存在しない場合、空の配列が取得できる", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account, admin } = await makeTestAccount(source, { + file_retention_days: 2, + auto_file_delete: true, + }); + + { + // ファイル削除対象のタスクが存在しない場合、空の配列が取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([]); + } + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスクが存在しない場合、空の配列が取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([]); + } + }); + + it("ファイル削除対象のタスク情報のみを取得できる(対象となる期限切れのタスク情報のみが取れる)", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account, admin } = await makeTestAccount(source, { + file_retention_days: 2, + auto_file_delete: true, + }); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + // 2日と1秒前(削除対象)のタスクを作成 + const { task, file } = await makeTestTask( + source, + account.id, + admin.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + { + // ファイル削除対象のタスク情報1件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task2, file: file2 } = await makeTestTask( + source, + account.id, + admin.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task3, file: file3 } = await makeTestTask( + source, + account.id, + admin.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account.id, + country: account.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + file_name: file3.file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account.id, + country: account.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + file_name: file3.file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + }); + + it("ファイル削除対象のタスク情報のみを取得できる(auto_file_delete=falseのアカウントの情報のタスク情報は取れない)", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // auto_file_deleteがtrueののアカウントを作成 + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + // auto_file_deleteがfalseののアカウントを作成 + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: false, + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 2日と1秒前(削除対象)のタスクを作成 + const { task, file } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + await makeTestTask(source, account02.id, admin02.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + { + // ファイル削除対象のタスク情報1件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + { + // ファイル削除対象のタスク情報1件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task2, file: file2 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + await makeTestTask(source, account02.id, admin02.id, "case04", { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task3, file: file3 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + await makeTestTask(source, account02.id, admin02.id, "case06", { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + file_name: file3.file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + file_name: file.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + file_name: file2.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + file_name: file3.file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + }); + + it("ファイル削除対象のタスク情報のみを取得できる(auto_file_delete=trueのアカウントの情報のタスク情報は全て取れる)", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // auto_file_deleteがtrueののアカウントを作成 + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + // auto_file_deleteがfalseののアカウントを作成 + const { account: account03, admin: admin03 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: false, + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 2日と1秒前(削除対象)のタスクを作成 + const { task: task01, file: file01 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + const { task: task02, file: file02 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + file_name: file01.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + file_name: file02.file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task03, file: file03 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + const { task: task04, file: file04 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case04", { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報4件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + file_name: file01.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + file_name: file02.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + file_name: file03.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + file_name: file04.file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報4件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + file_name: file01.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + file_name: file02.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + file_name: file03.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + file_name: file04.file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task05, file: file05 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + const { task: task06, file: file06 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case06", { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報6件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + file_name: file01.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + file_name: file02.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + file_name: file03.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + file_name: file04.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task05.id, + audio_file_id: file05.id, + file_name: file05.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task06.id, + audio_file_id: file06.id, + file_name: file06.file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報6件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + file_name: file01.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + file_name: file02.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + file_name: file03.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + file_name: file04.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task05.id, + audio_file_id: file05.id, + file_name: file05.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task06.id, + audio_file_id: file06.id, + file_name: file06.file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + }); +}); + +describe("deleteRecords | 削除対象タスク等を削除できる", () => { + let source: DataSource | null = null; + beforeAll(async () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: "mysql", + host: "test_mysql_db", + port: 3306, + username: "user", + password: "password", + database: "odms", + entities: [ + User, + UserArchive, + Account, + AccountArchive, + Task, + AudioFile, + AudioOptionItem, + License, + LicenseArchive, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + ], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger("none"), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it("競合により存在しないタスクを削除しようとしても例外は発生せず、成功扱いとなる", async () => { + if (!source) fail(); + + await deleteRecords(source, [ + { + id: 1, + audio_file_id: 1, + file_name: "test", + account_id: 1, + country: "US", + }, + ]); + }); + + it("対象としたタスクやAudioFile等が削除される", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // auto_file_deleteがtrueののアカウントを作成 + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + // auto_file_deleteがfalseののアカウントを作成 + const { account: account03, admin: admin03 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: false, + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 2日と1秒前(削除対象)のタスクを作成 + const { task: task01, file: file01 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + const { task: task02, file: file02 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task03, file: file03 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + const { task: task04, file: file04 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case04", { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task05, file: file05 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + const { task: task06, file: file06 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case06", { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + }); + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報6件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + file_name: file01.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + file_name: file02.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + file_name: file03.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + file_name: file04.file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task05.id, + audio_file_id: file05.id, + file_name: file05.file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task06.id, + audio_file_id: file06.id, + file_name: file06.file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(20); + const files = await getAudioFiles(source); + expect(files.length).toEqual(20); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(40); + } + + // 削除対象のタスク情報を削除 + await deleteRecords(source, result); + + // 削除後のタスク情報を取得 + const result2 = await getProcessTargets(source, now); + // 削除対象のタスク情報が削除されているので取得が0件になる + expect(result2).toEqual([]); + + { + // DB全体のレコードを確認 + // 削除対象外のタスク情報のみが残っている + const tasks = await getTasks(source); + expect(tasks.length).toEqual(14); + const files = await getAudioFiles(source); + expect(files.length).toEqual(14); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(28); + } + } + }); + + it("対象としたタスクやAudioFile等が大量に存在しても削除される", async () => { + if (!source) fail(); + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // "大量"の数を定義 + const count = 10000; + + // auto_file_deleteがtrueののアカウントを作成 + const { account, admin } = await makeTestAccount(source, { + file_retention_days: 30, + auto_file_delete: true, + }); + + // ファイルを10000件作成 + const createdFiles = [...Array(count).keys()].map( + (index): QueryDeepPartialEntity => { + return { + account_id: account.id, + owner_user_id: admin.id, + url: `https://example.com/${index}`, + file_name: `test${index}.wav`, + author_id: "test_author", + work_type_id: "test_work_type", + started_at: new Date(), + duration: "00:00:00", + finished_at: new Date(), + uploaded_at: new Date(), + file_size: 1024, + priority: "01", + audio_format: "wav", + comment: `test_comment_${index}`, + deleted_at: new Date(), + is_encrypted: false, + }; + } + ); + + // ファイルを元に、10年前に完了した扱いのタスクを作成 + const finished_at = new Date("2014-02-26T23:59:59Z"); + await makeManyTestTasks(source, createdFiles, finished_at); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(count); + + const files = await getAudioFiles(source); + expect(files.length).toEqual(count); + + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(2 * count); + } + + const result = await getProcessTargets(source, now); + expect(result.length).toEqual(count); + + await deleteRecords(source, result); + + { + // 削除後のタスク情報を取得 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(0); + + const files = await getAudioFiles(source); + expect(files.length).toEqual(0); + + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(0); + } + }, 100000); +}); + +describe("deleteAudioFilesProcessing", () => { + let source: DataSource | null = null; + beforeAll(async () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.test", override: true }); + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: "mysql", + host: "test_mysql_db", + port: 3306, + username: "user", + password: "password", + database: "odms", + entities: [__dirname + "/../../**/*.entity{.ts,.js}"], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger("none"), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it("BlobとDBの削除が正常に行われる", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + country: "JP", + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file1 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file2 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file3 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case05", + { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + const args: { accountId: number; fileName: string; country: string }[] = []; + const blobstorage = new AudioBlobStorageService(); + Object.defineProperty(blobstorage, blobstorage.deleteFile.name, { + value: async ( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise => { + args.push({ accountId, country, fileName }); + }, + writable: true, + }); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(5); + const files = await getAudioFiles(source); + expect(files.length).toEqual(5); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(10); + } + + const context = new InvocationContext(); + await deleteAudioFilesProcessing(context, source, blobstorage, now); + + // 想定通りの呼び出しが行われているか + { + const { accountId, country, fileName } = args[0]; + expect(fileName).toEqual(file1.file_name); + expect(accountId).toEqual(account01.id); + expect(country).toEqual(account01.country); + } + { + const { accountId, country, fileName } = args[1]; + expect(fileName).toEqual(file2.file_name); + expect(accountId).toEqual(account01.id); + expect(country).toEqual(account01.country); + } + { + const { accountId, country, fileName } = args[2]; + expect(fileName).toEqual(file3.file_name); + expect(accountId).toEqual(account02.id); + expect(country).toEqual(account02.country); + } + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(2); + const files = await getAudioFiles(source); + expect(files.length).toEqual(2); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(4); + } + }); +}); diff --git a/dictation_function/src/test/importUsers.spec.ts b/dictation_function/src/test/importUsers.spec.ts index 9c07fb7..d35bb73 100644 --- a/dictation_function/src/test/importUsers.spec.ts +++ b/dictation_function/src/test/importUsers.spec.ts @@ -11,7 +11,7 @@ import { AxiosRequestConfig, AxiosResponse } from "axios"; describe("importUsersProcessing", () => { dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); + dotenv.config({ path: ".env.test", override: true }); it("stage.jsonがない状態でユーザー追加できること", async () => { const context = new InvocationContext(); diff --git a/dictation_function/src/test/licenseAlert.spec.ts b/dictation_function/src/test/licenseAlert.spec.ts index c3891a0..58548db 100644 --- a/dictation_function/src/test/licenseAlert.spec.ts +++ b/dictation_function/src/test/licenseAlert.spec.ts @@ -18,7 +18,7 @@ import { promisify } from "util"; describe("licenseAlert", () => { dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); + dotenv.config({ path: ".env.test", override: true }); let source: DataSource | null = null; const redisClient = createClient(); @@ -80,7 +80,7 @@ describe("licenseAlert", () => { // redisからキャッシュが削除されていることを確認 const getAsync = promisify(redisClient.keys).bind(redisClient); const keys = await getAsync(`*`); - expect(keys).toHaveLength(0); + expect(keys).toHaveLength(0); }); it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => { diff --git a/dictation_function/src/test/licenseAutoAllocation.spec.ts b/dictation_function/src/test/licenseAutoAllocation.spec.ts index d0bf9ef..5f1261b 100644 --- a/dictation_function/src/test/licenseAutoAllocation.spec.ts +++ b/dictation_function/src/test/licenseAutoAllocation.spec.ts @@ -18,7 +18,7 @@ import { InvocationContext } from "@azure/functions"; describe("licenseAutoAllocation", () => { dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); + dotenv.config({ path: ".env.test", override: true }); let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ From 6e93a5be79edcd5174e12a1862e6bb95dbda6d4b Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 22 Mar 2024 06:12:47 +0000 Subject: [PATCH 062/109] =?UTF-8?q?Merged=20PR=20846:=20=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=8A=E3=83=BC=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=88=E5=89=8A=E9=99=A4API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3834: パートナーアカウント削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3834) - パートナーアカウント削除APIとUTを実装しました。 ## レビューポイント - 削除対象データは適切でしょうか? - テストケースに不足はないでしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - テストとローカルで実行確認 - 行った修正がデグレを発生させていないことを確認できるか - 既存処理への変更なし --- dictation_server/src/common/error/code.ts | 1 + dictation_server/src/common/error/message.ts | 1 + .../features/accounts/accounts.controller.ts | 7 +- .../accounts/accounts.service.spec.ts | 1137 ++++++++++++++++- .../src/features/accounts/accounts.service.ts | 148 +++ .../src/gateways/sendgrid/sendgrid.service.ts | 60 + .../accounts/accounts.repository.service.ts | 221 ++++ .../src/repositories/accounts/errors/types.ts | 7 + .../src/templates/template_U_123.html | 65 + .../src/templates/template_U_123.txt | 38 + 10 files changed, 1682 insertions(+), 3 deletions(-) create mode 100644 dictation_server/src/templates/template_U_123.html create mode 100644 dictation_server/src/templates/template_U_123.txt diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 84f6dd6..6cdd041 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -86,4 +86,5 @@ export const ErrorCodes = [ 'E017002', // 親アカウント変更不可エラー(階層関係が不正) 'E017003', // 親アカウント変更不可エラー(リージョンが同一でない) 'E017004', // 親アカウント変更不可エラー(国が同一でない) + 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 2e22585..6d3855d 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -76,4 +76,5 @@ export const errors: Errors = { E017002: 'Parent account switch failed Error: hierarchy mismatch', E017003: 'Parent account switch failed Error: region mismatch', E017004: 'Parent account switch failed Error: country mismatch', + E018001: 'Partner account delete failed Error: not satisfied conditions', }; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 0e56a73..0f47c2e 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2471,8 +2471,11 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO:service層を呼び出す。本実装時に以下は削除する。 - // await this.accountService.deletePartnerAccount(context, userId, targetAccountId); + await this.accountService.deletePartnerAccount( + context, + userId, + targetAccountId, + ); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 7b43645..7cd6f4a 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -49,6 +49,7 @@ import { LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, + MANUAL_RECOVERY_REQUIRED, OPTION_ITEM_VALUE_TYPE, STORAGE_SIZE_PER_LICENSE, TASK_STATUS, @@ -90,10 +91,19 @@ import { } from '../workflows/test/utility'; import { UsersService } from '../users/users.service'; import { truncateAllTable } from '../../common/test/init'; -import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; +import { + createTask, + getCheckoutPermissions, + getTasks, +} from '../tasks/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; import { TestLogger } from '../../common/test/logger'; import { Account } from '../../repositories/accounts/entity/account.entity'; +import { + createTemplateFile, + getTemplateFiles, +} from '../templates/test/utility'; +import { createUserGroup } from '../users/test/utility'; describe('createAccount', () => { let source: DataSource | null = null; @@ -8312,3 +8322,1128 @@ describe('switchParent', () => { } }); }); + +describe('deletePartnerAccount', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('パートナーアカウント情報が削除されること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + } + }); + it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + const { account: tier3Parent } = await makeTestAccount(source, { tier: 3 }); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Parent.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(4); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + try { + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E018001')); + } else { + fail(); + } + } + }); + it('パートナーアカウントが親が子アカウントを持つ場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + // 第5階層のアカウント作成 + await makeTestAccount(source, { + parent_account_id: tier4Account.id, + tier: 5, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(4); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + try { + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E018001')); + } else { + fail(); + } + } + }); + it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: () => { + throw new Error('deleteUsers failed'); + }, + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( + true, + ); + } + }); + it('Blobコンテナの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: () => { + throw new Error('deleteContainer failed'); + }, + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( + true, + ); + } + }); + it('メール送信失敗時でも処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + overrideSendgridService(service, { + sendMail: async () => { + throw new Error('sendMail failed'); + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index d6af114..b4a943d 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -54,6 +54,7 @@ import { DealerAccountNotFoundError, HierarchyMismatchError, RegionMismatchError, + PartnerAccountDeletionError, } from '../../repositories/accounts/errors/types'; import { Context } from '../../common/log'; import { @@ -2833,4 +2834,151 @@ export class AccountsService { throw new Error(`Invalid country. country=${country}`); } } + /** + * 指定したアカウントIDのパートナーアカウントを削除する + * @param context + * @param externalId + * @param targetAccountId 削除対象パートナーのアカウントID + * @returns partner account + */ + async deletePartnerAccount( + context: Context, + externalId: string, + targetAccountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deletePartnerAccount.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId},};`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: parentAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (parentAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + + // 削除対象のパートナーアカウントを取得する + const targetAccount = await this.accountRepository.findAccountById( + context, + targetAccountId, + ); + + if (targetAccount === null) { + throw new AccountNotFoundError( + `Account not found. targetAccountId: ${targetAccountId}`, + ); + } + + // メール送信に必要な情報を取得する + if (!targetAccount.primary_admin_user_id) { + throw new Error( + `primary_admin_user_id not found. accountId: ${targetAccountId}`, + ); + } + const primaryAdminUser = await this.usersRepository.findUserById( + context, + targetAccount.primary_admin_user_id, + ); + const adb2cAdmin = await this.adB2cService.getUser( + context, + primaryAdminUser.external_id, + ); + const { + displayName: targetPrimaryAdminName, + emailAddress: targetPrimaryAdminEmail, + } = getUserNameAndMailAddress(adb2cAdmin); + + if (!targetPrimaryAdminEmail) { + throw new Error( + `adb2c user mail not found. externalId: ${primaryAdminUser.external_id}`, + ); + } + + // アカウント削除処理(DB) + const targetUsers = await this.accountRepository.deletePartnerAccount( + context, + parentAccount.id, + targetAccountId, + ); + + // アカウント削除処理(Azure AD B2C) + try { + // 削除対象アカウント内のADB2Cユーザーをすべて削除する + await this.adB2cService.deleteUsers( + targetUsers.map((x) => x.external_id), + context, + ); + this.logger.log( + `[${context.getTrackingId()}] delete ADB2C users: ${targetAccountId}, users_id: ${targetUsers.map( + (x) => x.external_id, + )}`, + ); + } catch (e) { + // ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 + this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete ADB2C users: ${targetAccountId}, users_id: ${targetUsers.map( + (x) => x.external_id, + )}`, + ); + } + + // アカウント削除処理(Blob Storage) + await this.deleteBlobContainer( + targetAccountId, + targetAccount.country, + context, + ); + + // メール送信処理 + try { + const { companyName: parentCompanyName, adminEmails: parentEmails } = + await this.getAccountInformation(context, parentAccount.id); + + await this.sendgridService.sendMailWithU123( + context, + targetAccount.company_name, + targetPrimaryAdminName, + targetPrimaryAdminEmail, + parentCompanyName, + parentEmails, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case PartnerAccountDeletionError: + throw new HttpException( + makeErrorResponse('E018001'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deletePartnerAccount.name}`, + ); + } + } } diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index b21e914..2309c23 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -87,6 +87,8 @@ export class SendGridService { private readonly templateU122Text: string; private readonly templateU122NoParentHtml: string; private readonly templateU122NoParentText: string; + private readonly templateU123Html: string; + private readonly templateU123Text: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); @@ -328,6 +330,14 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_122_no_parent.txt`), 'utf-8', ); + this.templateU123Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_123.html`), + 'utf-8', + ); + this.templateU123Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_123.txt`), + 'utf-8', + ); } } @@ -1428,6 +1438,56 @@ export class SendGridService { } } + /** + * U-123のテンプレートを使用したメールを送信する + * @param context + * @param partnerAccountName + * @param partnerPrimaryName + * @param partnerPrimaryMail + * @param dealerAccountName + * @param dealerEmails + * @returns mail with u123 + */ + async sendMailWithU123( + context: Context, + partnerAccountName: string, + partnerPrimaryName: string, + partnerPrimaryMail: string, + dealerAccountName: string, + dealerEmails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU123.name}`, + ); + try { + const subject = 'パートナーアカウント情報消去完了通知 [U-123]'; + + const html = this.templateU123Html + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName); + const text = this.templateU123Text + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName); + + // メールを送信する + await this.sendMail( + context, + [partnerPrimaryMail], + dealerEmails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU123.name}`, + ); + } + } + /** * メールを送信する * @param context diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index c063325..ab49646 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -35,6 +35,7 @@ import { AccountNotFoundError, AdminUserNotFoundError, DealerAccountNotFoundError, + PartnerAccountDeletionError, } from './errors/types'; import { AlreadyLicenseAllocatedError, @@ -1547,4 +1548,224 @@ export class AccountsRepositoryService { ); }); } + + /** + * 指定したパートナーアカウントを削除する + * @param context + * @param parentAccountId + * @param targetAccountId + * @returns partner account + */ + async deletePartnerAccount( + context: Context, + parentAccountId: number, + targetAccountId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + // 削除対象のユーザーを取得 + const userRepo = entityManager.getRepository(User); + const users = await userRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const accountRepo = entityManager.getRepository(Account); + + // 対象アカウントが存在するかチェック + const targetAccount = await accountRepo.findOne({ + where: { id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!targetAccount) { + throw new AccountNotFoundError( + `Account is not found. id: ${targetAccountId}`, + ); + } + + // 実行者のアカウントが対象アカウントの親アカウントでない場合はエラー + if (targetAccount.parent_account_id !== parentAccountId) { + throw new PartnerAccountDeletionError( + `Target account is not child account. parentAccountId: ${parentAccountId}, targetAccountId: ${targetAccountId}`, + ); + } + + // 対象アカウントに子アカウントが存在する場合はエラー + const childrenAccounts = await accountRepo.find({ + where: { parent_account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // 子アカウントが存在する場合はエラー + if (childrenAccounts.length > 0) { + throw new PartnerAccountDeletionError( + `Target account has children account. targetAccountId: ${targetAccountId}`, + ); + } + + // ユーザテーブルのレコードを削除する + await deleteEntity( + userRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ソート条件のテーブルのレコードを削除する + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + await deleteEntity( + sortCriteriaRepo, + { user_id: In(users.map((user) => user.id)) }, + this.isCommentOut, + context, + ); + + // アカウントを削除 + await deleteEntity( + accountRepo, + { id: targetAccountId }, + this.isCommentOut, + context, + ); + // ライセンス系(card_license_issue以外)のテーブルのレコードを削除する + const orderRepo = entityManager.getRepository(LicenseOrder); + await deleteEntity( + orderRepo, + { from_account_id: targetAccountId }, + this.isCommentOut, + context, + ); + const licenseRepo = entityManager.getRepository(License); + const targetLicenses = await licenseRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + const cardLicenseRepo = entityManager.getRepository(CardLicense); + await deleteEntity( + cardLicenseRepo, + { license_id: In(targetLicenses.map((license) => license.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + licenseRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + const licenseAllocationHistoryRepo = entityManager.getRepository( + LicenseAllocationHistory, + ); + await deleteEntity( + licenseAllocationHistoryRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ユーザーグループ系のテーブルのレコードを削除する + const userGroupRepo = entityManager.getRepository(UserGroup); + const targetUserGroup = await userGroupRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const userGroupMemberRepo = entityManager.getRepository(UserGroupMember); + await deleteEntity( + userGroupMemberRepo, + { + user_group_id: In(targetUserGroup.map((userGroup) => userGroup.id)), + }, + this.isCommentOut, + context, + ); + await deleteEntity( + userGroupRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ワークタイプ系のテーブルのレコードを削除する + const worktypeRepo = entityManager.getRepository(Worktype); + const taggerWorktypes = await worktypeRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const optionItemRepo = entityManager.getRepository(OptionItem); + await deleteEntity( + optionItemRepo, + { worktype_id: In(taggerWorktypes.map((worktype) => worktype.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + worktypeRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // テンプレートファイルテーブルのレコードを削除する + const templateFileRepo = entityManager.getRepository(TemplateFile); + await deleteEntity( + templateFileRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // オーディオファイル系のテーブルのレコードを削除する + const audioFileRepo = entityManager.getRepository(AudioFile); + const targetaudioFiles = await audioFileRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const audioOptionItemsRepo = entityManager.getRepository(AudioOptionItem); + await deleteEntity( + audioOptionItemsRepo, + { + audio_file_id: In(targetaudioFiles.map((audioFile) => audioFile.id)), + }, + this.isCommentOut, + context, + ); + await deleteEntity( + audioFileRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // タスク系のテーブルのレコードを削除する + const taskRepo = entityManager.getRepository(Task); + const targetTasks = await taskRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + await deleteEntity( + checkoutPermissionRepo, + { task_id: In(targetTasks.map((task) => task.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + taskRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + return users; + }); + } } diff --git a/dictation_server/src/repositories/accounts/errors/types.ts b/dictation_server/src/repositories/accounts/errors/types.ts index 4cb5779..4b59a1b 100644 --- a/dictation_server/src/repositories/accounts/errors/types.ts +++ b/dictation_server/src/repositories/accounts/errors/types.ts @@ -27,6 +27,13 @@ export class AccountLockedError extends Error { } } +// パートナーアカウント削除不可エラー +export class PartnerAccountDeletionError extends Error { + constructor(message: string) { + super(message); + this.name = 'PartnerAccountDeletionError'; + } +} /** * 階層構造関係不適切エラー */ diff --git a/dictation_server/src/templates/template_U_123.html b/dictation_server/src/templates/template_U_123.html new file mode 100644 index 0000000..2e46fc4 --- /dev/null +++ b/dictation_server/src/templates/template_U_123.html @@ -0,0 +1,65 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
      +

      <English>

      +

      Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

      +

      ODMS Cloudをご利用いただきありがとうございます。

      +

      + お客様のアカウント情報は$DEALER_NAME$によりODMS + Cloudから削除されました。 +

      +

      + 再度ODMS + Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM + Degital Solutionsに問い合わせください。 +

      +

      + If you received this e-mail in error, please delete this e-mail from + your system.
      + This is an automatically generated e-mail, please do not reply. +

      +
      +
      +

      <Deutsch>

      +

      Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

      +

      ODMS Cloudをご利用いただきありがとうございます。

      +

      + お客様のアカウント情報は$DEALER_NAME$によりODMS + Cloudから削除されました。 +

      +

      + 再度ODMS + Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM + Degital Solutionsに問い合わせください。 +

      +

      + If you received this e-mail in error, please delete this e-mail from + your system.
      + This is an automatically generated e-mail, please do not reply. +

      +
      +
      +

      <Français>

      +

      Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

      +

      ODMS Cloudをご利用いただきありがとうございます。

      +

      + お客様のアカウント情報は$DEALER_NAME$によりODMS + Cloudから削除されました。 +

      +

      + 再度ODMS + Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM + Degital Solutionsに問い合わせください。 +

      +

      + If you received this e-mail in error, please delete this e-mail from + your system.
      + This is an automatically generated e-mail, please do not reply. +

      +
      + + diff --git a/dictation_server/src/templates/template_U_123.txt b/dictation_server/src/templates/template_U_123.txt new file mode 100644 index 0000000..3a98e16 --- /dev/null +++ b/dictation_server/src/templates/template_U_123.txt @@ -0,0 +1,38 @@ + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +ODMS Cloudをご利用いただきありがとうございます。 + +お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 + +再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +ODMS Cloudをご利用いただきありがとうございます。 + +お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 + +再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. + + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +ODMS Cloudをご利用いただきありがとうございます。 + +お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 + +再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail, please do not reply. \ No newline at end of file From eb6b413adb74712bb9ae413a854e09c85f90c2bf Mon Sep 17 00:00:00 2001 From: masaaki Date: Fri, 22 Mar 2024 07:50:58 +0000 Subject: [PATCH 063/109] =?UTF-8?q?Merged=20PR=20848:=20=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=8A=E3=83=BC=E4=B8=80=E8=A6=A7=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3833: パートナー一覧画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3833) - パートナー一覧画面のパートナーにカーソルを合わせた際に、Delete Partnerリンクが表示される。 押下した場合は、本当に削除するのかを確認するメッセージが表示され、どのPartnerを削除するかメッセージ欄に表示されること。 ## レビューポイント - 特筆する点はなし ## UIの変更 https://ndstokyo.sharepoint.com/sites/Piranha/Shared%20Documents/Forms/AllItems.aspx?csf=1&web=1&e=hzPw9b&cid=e8e0702d%2D3730%2D4295%2Dbb9d%2D40e6b1998906&FolderCTID=0x012000C0DCEE65AC2177479C3C761CD137C9C9&id=%2Fsites%2FPiranha%2FShared%20Documents%2FGeneral%2FOMDS%2F%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%2FTask3833&viewid=786a81cf%2Dd15f%2D4dc2%2D9e55%2Dc7a729fbc72f ## クエリの変更 なし ## 動作確認状況 - ローカルで確認(APIからの返却値を直接指定する方式で確認) - 行った修正がデグレを発生させていないことを確認できるか 新規機能追加のみなので問題なし ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/api/api.ts | 88 +++++++++++++++++++ dictation_client/src/common/errors/code.ts | 1 + .../src/features/partner/operations.ts | 60 +++++++++++++ .../src/features/partner/partnerSlice.ts | 15 +++- .../src/pages/PartnerPage/index.tsx | 35 +++++++- dictation_client/src/translation/de.json | 4 +- dictation_client/src/translation/en.json | 4 +- dictation_client/src/translation/es.json | 4 +- dictation_client/src/translation/fr.json | 4 +- 9 files changed, 207 insertions(+), 8 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 28ca9ca..c363b52 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -693,6 +693,19 @@ export interface DeleteAccountRequest { */ 'accountId': number; } +/** + * + * @export + * @interface DeletePartnerAccountRequest + */ +export interface DeletePartnerAccountRequest { + /** + * 削除対象のアカウントID + * @type {number} + * @memberof DeletePartnerAccountRequest + */ + 'targetAccountId': number; +} /** * * @export @@ -2857,6 +2870,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deletePartnerAccount: async (deletePartnerAccountRequest: DeletePartnerAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deletePartnerAccountRequest' is not null or undefined + assertParamExists('deletePartnerAccount', 'deletePartnerAccountRequest', deletePartnerAccountRequest) + const localVarPath = `/accounts/partner/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deletePartnerAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します * @summary @@ -3886,6 +3939,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.deleteAccountAndData']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deletePartnerAccount(deletePartnerAccountRequest: DeletePartnerAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deletePartnerAccount(deletePartnerAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deletePartnerAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します * @summary @@ -4276,6 +4342,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise { return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deletePartnerAccount(deletePartnerAccountRequest: DeletePartnerAccountRequest, options?: any): AxiosPromise { + return localVarFp.deletePartnerAccount(deletePartnerAccountRequest, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します * @summary @@ -4608,6 +4684,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deletePartnerAccount(deletePartnerAccountRequest: DeletePartnerAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deletePartnerAccount(deletePartnerAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 9ba15db..ed1570d 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -81,4 +81,5 @@ export const errorCodes = [ "E017002", // 親アカウント変更不可エラー(階層関係が不正) "E017003", // 親アカウント変更不可エラー(リージョンが同一でない) "E017004", // 親アカウント変更不可エラー(国が同一でない) + "E018001", // パートナーアカウント削除エラー(削除条件を満たしていない) ] as const; diff --git a/dictation_client/src/features/partner/operations.ts b/dictation_client/src/features/partner/operations.ts index d59efbe..2dac369 100644 --- a/dictation_client/src/features/partner/operations.ts +++ b/dictation_client/src/features/partner/operations.ts @@ -8,6 +8,7 @@ import { AccountsApi, CreatePartnerAccountRequest, GetPartnersResponse, + DeletePartnerAccountRequest, } from "../../api/api"; import { Configuration } from "../../api/configuration"; @@ -116,3 +117,62 @@ export const getPartnerInfoAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +// パートナーアカウント削除 +export const deletePartnerAccountAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + // パラメータ + accountId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/deletePartnerAccountAsync", async (args, thunkApi) => { + const { accountId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + const deletePartnerAccountRequest: DeletePartnerAccountRequest = { + targetAccountId: accountId, + }; + await accountApi.deletePartnerAccount(deletePartnerAccountRequest, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + if (error.code === "E018001") { + errorMessage = getTranslationID( + "partnerPage.message.partnerDeleteFailedError" + ); + } + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/partner/partnerSlice.ts b/dictation_client/src/features/partner/partnerSlice.ts index 0eb0ce4..cf09f4a 100644 --- a/dictation_client/src/features/partner/partnerSlice.ts +++ b/dictation_client/src/features/partner/partnerSlice.ts @@ -1,6 +1,10 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { PartnerState } from "./state"; -import { createPartnerAccountAsync, getPartnerInfoAsync } from "./operations"; +import { + createPartnerAccountAsync, + getPartnerInfoAsync, + deletePartnerAccountAsync, +} from "./operations"; import { LIMIT_PARTNER_VIEW_NUM } from "./constants"; const initialState: PartnerState = { @@ -97,6 +101,15 @@ export const partnerSlice = createSlice({ builder.addCase(getPartnerInfoAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(deletePartnerAccountAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deletePartnerAccountAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deletePartnerAccountAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); export const { diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx index 835cfa3..d1006e3 100644 --- a/dictation_client/src/pages/PartnerPage/index.tsx +++ b/dictation_client/src/pages/PartnerPage/index.tsx @@ -16,6 +16,7 @@ import { selectTotalPage, getPartnerInfoAsync, selectPartnersInfo, + deletePartnerAccountAsync, } from "features/partner/index"; import { changeDelegateAccount, @@ -109,6 +110,31 @@ const PartnerPage: React.FC = (): JSX.Element => { [dispatch, navigate, t] ); + // delete account押下時処理 + const onDeleteAccount = useCallback( + async (accountId: number, companyName: string) => { + // ダイアログ確認 + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm( + `${t( + getTranslationID("partnerPage.message.partnerDeleteConfirm") + )} ${companyName}` + ) + ) { + return; + } + + const { meta } = await dispatch(deletePartnerAccountAsync({ accountId })); + if (meta.requestStatus === "fulfilled") { + dispatch( + getPartnerInfoAsync({ limit: LIMIT_PARTNER_VIEW_NUM, offset }) + ); + } + }, + [dispatch, t, offset] + ); + // HTML return ( <> @@ -185,10 +211,14 @@ const PartnerPage: React.FC = (): JSX.Element => {
    )} {displayColumn.FileName && ( - + )} {displayColumn.FileLength && ( diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index e163161..f6ba44f 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -254,7 +254,9 @@ "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.", "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.", - "fileAlreadyDeletedError": "(de)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "(de)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "(de)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "(de)このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Diktate", @@ -300,7 +302,9 @@ "fileBackup": "Dateisicherung", "downloadForBackup": "Zur Sicherung herunterladen", "applications": "Desktopanwendung", - "cancelDictation": "Transkription abbrechen" + "cancelDictation": "Transkription abbrechen", + "rawFileName": "(de)Raw File Name", + "fileNameSave": "(de)Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index df0214b..cee7426 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -254,7 +254,9 @@ "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.", "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.", - "fileAlreadyDeletedError": "既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Dictations", @@ -300,7 +302,9 @@ "fileBackup": "File Backup", "downloadForBackup": "Download for backup", "applications": "Desktop Application", - "cancelDictation": "Cancel Transcription" + "cancelDictation": "Cancel Transcription", + "rawFileName": "Raw File Name", + "fileNameSave": "Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index e526a1d..c6bfe33 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -254,7 +254,9 @@ "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.", "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.", - "fileAlreadyDeletedError": "(es)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "(es)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "(es)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "(es)このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Dictado", @@ -300,7 +302,9 @@ "fileBackup": "Copia de seguridad de archivos", "downloadForBackup": "Descargar para respaldo", "applications": "Aplicación de escritorio", - "cancelDictation": "Cancelar transcripción" + "cancelDictation": "Cancelar transcripción", + "rawFileName": "(es)Raw File Name", + "fileNameSave": "(es)Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 393e5d9..071295b 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -254,7 +254,9 @@ "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.", "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.", - "fileAlreadyDeletedError": "(fr)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "(fr)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "(fr)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "(fr)このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Dictées", @@ -300,7 +302,9 @@ "fileBackup": "Sauvegarde de fichiers", "downloadForBackup": "Télécharger pour sauvegarde", "applications": "Application de bureau", - "cancelDictation": "Annuler la transcription" + "cancelDictation": "Annuler la transcription", + "rawFileName": "(fr)Raw File Name", + "fileNameSave": "(fr)Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 4a1e484..0727f5b 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -853,7 +853,7 @@ export class TasksRepositoryService { audioFile.account_id = account_id; audioFile.owner_user_id = owner_user_id; audioFile.url = url; - audioFile.file_name = file_name; + audioFile.file_name = file_name.replace('.zip', ''); audioFile.raw_file_name = file_name; audioFile.author_id = author_id; audioFile.work_type_id = work_type_id; From c469f943f18b3c81a9625813dec3516310f2054d Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 17 Apr 2024 00:18:48 +0000 Subject: [PATCH 078/109] =?UTF-8?q?Merged=20PR=20872:=20=E7=94=9F=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E5=90=8D=E3=81=AE=E5=88=9D=E6=9C=9F?= =?UTF-8?q?=E5=80=A4=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B=E3=83=9E?= =?UTF-8?q?=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4125: 生ファイル名の初期値を追加するマイグレーションファイル作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4125) - 生ファイル名と表示ファイル名をそれぞれ修正して初期値を入れるスクリプトを追加しました。 - 生ファイル名:これまでfile_nameカラムに入っていた値 - file_nameの値から.zipを除いたもの ## レビューポイント - この値の操作で問題はないか ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - migrate up/downで想定通りになることを確認 --- .../migrations/064-update_raw_file_name_from_file_name.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 dictation_server/db/migrations/064-update_raw_file_name_from_file_name.sql diff --git a/dictation_server/db/migrations/064-update_raw_file_name_from_file_name.sql b/dictation_server/db/migrations/064-update_raw_file_name_from_file_name.sql new file mode 100644 index 0000000..e08aff8 --- /dev/null +++ b/dictation_server/db/migrations/064-update_raw_file_name_from_file_name.sql @@ -0,0 +1,7 @@ +-- +migrate Up +UPDATE `audio_files` SET `raw_file_name` = `file_name`; +UPDATE `audio_files` SET `file_name` = TRIM(TRAILING '.zip' FROM `raw_file_name`); + +-- +migrate Down +UPDATE `audio_files` SET `file_name` = `raw_file_name`; +UPDATE `audio_files` SET `raw_file_name` = ''; \ No newline at end of file From 69241ed36c9038825dc1df9119b36c5431b1a7b8 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 17 Apr 2024 00:23:08 +0000 Subject: [PATCH 079/109] =?UTF-8?q?Merged=20PR=20871:=20Function=EF=BC=88?= =?UTF-8?q?=E9=9F=B3=E5=A3=B0=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E8=87=AA?= =?UTF-8?q?=E5=8B=95=E5=89=8A=E9=99=A4=EF=BC=89=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4060: Function(音声ファイル自動削除)修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4060) - 自動ファイル削除で生ファイル名を利用するように修正しUTを追従しました。 ## レビューポイント - 生ファイル名を使う方法は適切でしょうか? - テストの修正は適切でしょうか? ## UIの変更 - なし ## クエリの変更 - select対象をraw_file_nameに修正 ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - テストが通ることを確認 --- .../src/entity/audio_file.entity.ts | 2 + .../src/functions/deleteAudioFiles.ts | 10 +- dictation_function/src/test/common/utility.ts | 1 + .../src/test/deleteAudioFiles.spec.ts | 113 +++++++++--------- 4 files changed, 65 insertions(+), 61 deletions(-) diff --git a/dictation_function/src/entity/audio_file.entity.ts b/dictation_function/src/entity/audio_file.entity.ts index 4de4b82..00f3ea2 100644 --- a/dictation_function/src/entity/audio_file.entity.ts +++ b/dictation_function/src/entity/audio_file.entity.ts @@ -14,6 +14,8 @@ export class AudioFile { @Column() file_name: string; @Column() + raw_file_name: string; + @Column() author_id: string; @Column() work_type_id: string; diff --git a/dictation_function/src/functions/deleteAudioFiles.ts b/dictation_function/src/functions/deleteAudioFiles.ts index b710969..a8382ae 100644 --- a/dictation_function/src/functions/deleteAudioFiles.ts +++ b/dictation_function/src/functions/deleteAudioFiles.ts @@ -29,9 +29,9 @@ type TargetTaskInfo = { /** * ファイル名 - * column: audio_files.file_name + * column: audio_files.raw_file_name */ - file_name: string; + raw_file_name: string; /** * アカウントID @@ -64,12 +64,12 @@ export async function deleteAudioFilesProcessing( // タスクに紐づくファイルを削除 for (const target of targets) { try { - const { account_id, country, file_name } = target; + const { account_id, country, raw_file_name } = target; await blobStorageService.deleteFile( context, account_id, country, - file_name + raw_file_name ); context.log(`file delete success. target=${JSON.stringify(target)}`); } catch (e) { @@ -168,7 +168,7 @@ export async function getProcessTargets( // SELECTでエイリアスを指定して、結果オブジェクトのプロパティとTargetTaskInfoのプロパティを一致させる .select("tasks.id", "id") .addSelect("tasks.audio_file_id", "audio_file_id") - .addSelect("audio_files.file_name", "file_name") + .addSelect("audio_files.raw_file_name", "raw_file_name") .addSelect("accounts.id", "account_id") .addSelect("accounts.country", "country") .where("tasks.finished_at IS NOT NULL") diff --git a/dictation_function/src/test/common/utility.ts b/dictation_function/src/test/common/utility.ts index d9c6a07..1e288e9 100644 --- a/dictation_function/src/test/common/utility.ts +++ b/dictation_function/src/test/common/utility.ts @@ -499,6 +499,7 @@ export const makeTestTask = async ( owner_user_id: ownerUserId, url: `https://example.com/${identifierName}`, file_name: `test${identifierName}.wav`, + raw_file_name: `test${identifierName}.wav`, author_id: "test_author", work_type_id: "test_work_type", started_at: new Date(), diff --git a/dictation_function/src/test/deleteAudioFiles.spec.ts b/dictation_function/src/test/deleteAudioFiles.spec.ts index a6f2ad7..0b3f18a 100644 --- a/dictation_function/src/test/deleteAudioFiles.spec.ts +++ b/dictation_function/src/test/deleteAudioFiles.spec.ts @@ -150,7 +150,7 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account.id, country: account.country, }, @@ -178,14 +178,14 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account.id, country: account.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account.id, country: account.country, }, @@ -207,14 +207,14 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account.id, country: account.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account.id, country: account.country, }, @@ -242,21 +242,21 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account.id, country: account.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account.id, country: account.country, }, { id: task3.id, audio_file_id: file3.id, - file_name: file3.file_name, + raw_file_name: file3.raw_file_name, account_id: account.id, country: account.country, }, @@ -278,21 +278,21 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account.id, country: account.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account.id, country: account.country, }, { id: task3.id, audio_file_id: file3.id, - file_name: file3.file_name, + raw_file_name: file3.raw_file_name, account_id: account.id, country: account.country, }, @@ -374,7 +374,7 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account01.id, country: account01.country, }, @@ -389,7 +389,7 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account01.id, country: account01.country, }, @@ -422,14 +422,14 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account01.id, country: account01.country, }, @@ -456,14 +456,14 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account01.id, country: account01.country, }, @@ -496,21 +496,21 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task3.id, audio_file_id: file3.id, - file_name: file3.file_name, + raw_file_name: file3.raw_file_name, account_id: account01.id, country: account01.country, }, @@ -537,21 +537,21 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task.id, audio_file_id: file.id, - file_name: file.file_name, + raw_file_name: file.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task2.id, audio_file_id: file2.id, - file_name: file2.file_name, + raw_file_name: file2.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task3.id, audio_file_id: file3.id, - file_name: file3.file_name, + raw_file_name: file3.raw_file_name, account_id: account01.id, country: account01.country, }, @@ -660,14 +660,14 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task01.id, audio_file_id: file01.id, - file_name: file01.file_name, + raw_file_name: file01.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task02.id, audio_file_id: file02.id, - file_name: file02.file_name, + raw_file_name: file02.raw_file_name, account_id: account02.id, country: account02.country, }, @@ -711,28 +711,28 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task01.id, audio_file_id: file01.id, - file_name: file01.file_name, + raw_file_name: file01.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task02.id, audio_file_id: file02.id, - file_name: file02.file_name, + raw_file_name: file02.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task03.id, audio_file_id: file03.id, - file_name: file03.file_name, + raw_file_name: file03.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task04.id, audio_file_id: file04.id, - file_name: file04.file_name, + raw_file_name: file04.raw_file_name, account_id: account02.id, country: account02.country, }, @@ -764,28 +764,28 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task01.id, audio_file_id: file01.id, - file_name: file01.file_name, + raw_file_name: file01.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task02.id, audio_file_id: file02.id, - file_name: file02.file_name, + raw_file_name: file02.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task03.id, audio_file_id: file03.id, - file_name: file03.file_name, + raw_file_name: file03.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task04.id, audio_file_id: file04.id, - file_name: file04.file_name, + raw_file_name: file04.raw_file_name, account_id: account02.id, country: account02.country, }, @@ -829,42 +829,42 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task01.id, audio_file_id: file01.id, - file_name: file01.file_name, + raw_file_name: file01.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task02.id, audio_file_id: file02.id, - file_name: file02.file_name, + raw_file_name: file02.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task03.id, audio_file_id: file03.id, - file_name: file03.file_name, + raw_file_name: file03.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task04.id, audio_file_id: file04.id, - file_name: file04.file_name, + raw_file_name: file04.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task05.id, audio_file_id: file05.id, - file_name: file05.file_name, + raw_file_name: file05.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task06.id, audio_file_id: file06.id, - file_name: file06.file_name, + raw_file_name: file06.raw_file_name, account_id: account02.id, country: account02.country, }, @@ -891,42 +891,42 @@ describe("getProcessTargets | 削除対象を特定するQueryが正常に動作 { id: task01.id, audio_file_id: file01.id, - file_name: file01.file_name, + raw_file_name: file01.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task02.id, audio_file_id: file02.id, - file_name: file02.file_name, + raw_file_name: file02.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task03.id, audio_file_id: file03.id, - file_name: file03.file_name, + raw_file_name: file03.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task04.id, audio_file_id: file04.id, - file_name: file04.file_name, + raw_file_name: file04.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task05.id, audio_file_id: file05.id, - file_name: file05.file_name, + raw_file_name: file05.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task06.id, audio_file_id: file06.id, - file_name: file06.file_name, + raw_file_name: file06.raw_file_name, account_id: account02.id, country: account02.country, }, @@ -989,7 +989,7 @@ describe("deleteRecords | 削除対象タスク等を削除できる", () => { { id: 1, audio_file_id: 1, - file_name: "test", + raw_file_name: "test", account_id: 1, country: "US", }, @@ -1184,42 +1184,42 @@ describe("deleteRecords | 削除対象タスク等を削除できる", () => { { id: task01.id, audio_file_id: file01.id, - file_name: file01.file_name, + raw_file_name: file01.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task02.id, audio_file_id: file02.id, - file_name: file02.file_name, + raw_file_name: file02.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task03.id, audio_file_id: file03.id, - file_name: file03.file_name, + raw_file_name: file03.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task04.id, audio_file_id: file04.id, - file_name: file04.file_name, + raw_file_name: file04.raw_file_name, account_id: account02.id, country: account02.country, }, { id: task05.id, audio_file_id: file05.id, - file_name: file05.file_name, + raw_file_name: file05.raw_file_name, account_id: account01.id, country: account01.country, }, { id: task06.id, audio_file_id: file06.id, - file_name: file06.file_name, + raw_file_name: file06.raw_file_name, account_id: account02.id, country: account02.country, }, @@ -1278,6 +1278,7 @@ describe("deleteRecords | 削除対象タスク等を削除できる", () => { owner_user_id: admin.id, url: `https://example.com/${index}`, file_name: `test${index}.wav`, + raw_file_name: `test${index}.wav`, author_id: "test_author", work_type_id: "test_work_type", started_at: new Date(), @@ -1467,19 +1468,19 @@ describe("deleteAudioFilesProcessing", () => { // 想定通りの呼び出しが行われているか { const { accountId, country, fileName } = args[0]; - expect(fileName).toEqual(file1.file_name); + expect(fileName).toEqual(file1.raw_file_name); expect(accountId).toEqual(account01.id); expect(country).toEqual(account01.country); } { const { accountId, country, fileName } = args[1]; - expect(fileName).toEqual(file2.file_name); + expect(fileName).toEqual(file2.raw_file_name); expect(accountId).toEqual(account01.id); expect(country).toEqual(account01.country); } { const { accountId, country, fileName } = args[2]; - expect(fileName).toEqual(file3.file_name); + expect(fileName).toEqual(file3.raw_file_name); expect(accountId).toEqual(account02.id); expect(country).toEqual(account02.country); } @@ -1556,7 +1557,7 @@ describe("deleteAudioFilesProcessing", () => { .find((log) => log.includes(MANUAL_RECOVERY_REQUIRED)); expect(log).toBeDefined(); expect(log).toEqual( - `[MANUAL_RECOVERY_REQUIRED] file delete failed. target={"id":"1","audio_file_id":"1","account_id":"1","country":"US","file_name":"testcase03.wav"}` + `[MANUAL_RECOVERY_REQUIRED] file delete failed. target={"id":"1","audio_file_id":"1","account_id":"1","country":"US","raw_file_name":"testcase03.wav"}` ); }); From 23862ad3acaed57ee003c8f4c5c95c1b188ace54 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 17 Apr 2024 01:49:34 +0000 Subject: [PATCH 080/109] =?UTF-8?q?Merged=20PR=20874:=20=E4=B8=8D=E5=85=B7?= =?UTF-8?q?=E5=90=88=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4137: 不具合修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4137) - ファイル名変更時にダイアログを追加 ## レビューポイント - 共有 ## UIの変更 - ダイアログを追加 ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - ローカル確認 --- .../src/pages/DictationPage/filePropertyPopup.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx index ab234b2..4f3e9bb 100644 --- a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx +++ b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx @@ -47,6 +47,15 @@ export const FilePropertyPopup: React.FC = (props) => { if (fileName.length === 0) { return; } + + // ダイアログ確認 + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + const { meta } = await dispatch( renameFileAsync({ audioFileId: selectedFileTask?.audioFileId ?? 0, @@ -59,7 +68,7 @@ export const FilePropertyPopup: React.FC = (props) => { if (meta.requestStatus === "fulfilled") { onClose(true); } - }, [dispatch, onClose, fileName, selectedFileTask]); + }, [t, dispatch, onClose, fileName, selectedFileTask]); return (
    From 9ee29e91baeecbcdfd34bc05d9c2ca77ec90ab7a Mon Sep 17 00:00:00 2001 From: makabe Date: Fri, 19 Apr 2024 09:10:48 +0900 Subject: [PATCH 081/109] =?UTF-8?q?=E3=83=9E=E3=82=A4=E3=82=B0=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=B3=E3=83=9E=E3=83=B3?= =?UTF-8?q?=E3=83=89=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictation_server/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dictation_server/package.json b/dictation_server/package.json index a8d50bd..7ca9f5b 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -24,9 +24,9 @@ "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\"", - "migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=ccb", - "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=ccb", - "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "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", + "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=local", "migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test" }, "dependencies": { From 7bfd424a646839c01117a98bdf2d246f17736fa3 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 19 Apr 2024 02:14:05 +0000 Subject: [PATCH 082/109] =?UTF-8?q?Merged=20PR=20875:=20=E3=83=A1=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E5=A4=9A=E8=A8=80=E8=AA=9E=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3859: メールの多言語対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3859) - メールの文面を各言語版に置き換えました。 - アカウント情報消去完了通知 [U-111]  - ユーザー一括登録 受付通知 [U-120]  - ユーザー一括登録 完了通知 [U-121]  - ユーザー一括登録 失敗通知 [U-122]  - パートナーアカウント情報消去完了通知 [U-123]  ## レビューポイント - 対応メールは適切でしょうか? - メール文面は適切でしょうか? - 反映内容は適切でしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - ローカル確認、マージ後にdevelop動作確認 --- .../accounts/accounts.service.spec.ts | 6 +- .../src/gateways/sendgrid/sendgrid.service.ts | 8 +- .../src/templates/template_U_111.html | 135 +++++----- .../src/templates/template_U_111.txt | 8 +- .../src/templates/template_U_120.html | 156 ++++++------ .../src/templates/template_U_120.txt | 63 ++--- .../templates/template_U_120_no_parent.html | 135 +++++----- .../templates/template_U_120_no_parent.txt | 57 ++--- .../src/templates/template_U_121.html | 141 +++++----- .../src/templates/template_U_121.txt | 50 ++-- .../templates/template_U_121_no_parent.html | 120 +++++---- .../templates/template_U_121_no_parent.txt | 44 ++-- .../src/templates/template_U_122.html | 240 ++++++++++-------- .../src/templates/template_U_122.txt | 80 +++--- .../templates/template_U_122_no_parent.html | 219 +++++++++------- .../templates/template_U_122_no_parent.txt | 74 +++--- .../src/templates/template_U_123.html | 125 +++++---- .../src/templates/template_U_123.txt | 32 +-- 18 files changed, 885 insertions(+), 808 deletions(-) diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index cd50a2f..190d524 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -8544,7 +8544,7 @@ describe('deletePartnerAccount', () => { expect(templateFileRecord.length).toBe(0); // パートナーアカウント削除完了通知が送信されていること - expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + expect(_subject).toBe('Partner Account Deleted Notification [U-123]'); } }); it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => { @@ -9052,7 +9052,7 @@ describe('deletePartnerAccount', () => { expect(templateFileRecord.length).toBe(0); // パートナーアカウント削除完了通知が送信されていること - expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + expect(_subject).toBe('Partner Account Deleted Notification [U-123]'); // 手動復旧が必要なエラーログが出力されていること expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( @@ -9258,7 +9258,7 @@ describe('deletePartnerAccount', () => { expect(templateFileRecord.length).toBe(0); // パートナーアカウント削除完了通知が送信されていること - expect(_subject).toBe('パートナーアカウント情報消去完了通知 [U-123]'); + expect(_subject).toBe('Partner Account Deleted Notification [U-123]'); // 手動復旧が必要なエラーログが出力されていること expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index f5c219b..b399326 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -1252,7 +1252,7 @@ export class SendGridService { `[IN] [${context.getTrackingId()}] ${this.sendMailWithU120.name}`, ); try { - const subject = 'ユーザー一括登録 受付通知 [U-120]'; + const subject = 'User Bulk Registration Received Notification [U-120]'; let html: string; let text: string; @@ -1319,7 +1319,7 @@ export class SendGridService { `[IN] [${context.getTrackingId()}] ${this.sendMailWithU121.name}`, ); try { - const subject = 'ユーザー一括登録 完了通知 [U-121]'; + const subject = 'User Bulk Registration Completed Notification [U-121]'; let html: string; let text: string; @@ -1400,7 +1400,7 @@ export class SendGridService { ? 'エラーはありません' : otherErrors.map((x) => `L${x}`).join(', '); - const subject = 'ユーザー一括登録 失敗通知 [U-122]'; + const subject = 'User Bulk Registration Failed Notification [U-122]'; let html: string; let text: string; @@ -1470,7 +1470,7 @@ export class SendGridService { `[IN] [${context.getTrackingId()}] ${this.sendMailWithU123.name}`, ); try { - const subject = 'パートナーアカウント情報消去完了通知 [U-123]'; + const subject = 'Partner Account Deleted Notification [U-123]'; const html = this.templateU123Html .replaceAll(CUSTOMER_NAME, partnerAccountName) diff --git a/dictation_server/src/templates/template_U_111.html b/dictation_server/src/templates/template_U_111.html index ae7fb1d..b074cfd 100644 --- a/dictation_server/src/templates/template_U_111.html +++ b/dictation_server/src/templates/template_U_111.html @@ -1,69 +1,68 @@ - - Account Deleted Notification [U-111] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    -

    - Thank you for using ODMS Cloud. Your account, including all information - has been deleted from the ODMS Cloud. -

    -

    - If you wish to use ODMS Cloud again, you will need to register your - account information again and order annual licenses from an OM Digital - Solutions authorized dealer.
    - URL: $TOP_URL$ -

    -

    - If you have received this e-mail in error, please delete this e-mail - from your system.
    - This is an automatically generated e-mail and this mailbox is not - monitored. Please do not reply. -

    -
    -
    -

    <Deutsch>

    -

    Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    -

    - Vielen Dank, dass Sie ODMS Cloud nutzen. Ihr Konto, einschließlich aller - Informationen, wurde aus der ODMS Cloud gelöscht. -

    -

    - Wenn Sie ODMS Cloud erneut nutzen möchten, müssen Sie Ihre - Kontoinformationen erneut registrieren und Jahreslizenzen bei einem - autorisierten OM Digital Solutions-Händler bestellen.
    - URL: $TOP_URL$ -

    -

    - Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese - E-Mail bitte aus Ihrem System.
    - Dies ist eine automatisch generierte - E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie - nicht. -

    -

    -
    -
    -

    <Français>

    -

    Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    -

    - Merci d'utiliser ODMS Cloud. Votre compte, y compris toutes les - informations, a été supprimé du cloud ODMS. -

    -

    - Si vous souhaitez utiliser à nouveau ODMS Cloud, vous devrez à nouveau - enregistrer les informations de votre compte et commander des licences - annuelles auprès d'un concessionnaire agréé OM Digital Solutions.
    - URL: $TOP_URL$ -

    -

    - Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail - de votre système.
    - Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres - n'est pas surveillée. Merci de ne pas répondre. -

    -
    - - + + + Account Deleted Notification [U-111] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +

    +

    + Thank you for using the ODMS Cloud. Your account, including all information has been deleted from the ODMS Cloud. +

    +

    + If you wish to use ODMS Cloud again, you will need to register your account information again and order annual + licenses from an OM SYSTEM authorized dealer.
    + URL: $TOP_URL$ +

    +

    + If you have received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +

    +

    + Vielen Dank, dass Sie ODMS Cloud nutzen. Ihr Konto, einschließlich aller Informationen, wurde aus der ODMS Cloud + gelöscht. +

    +

    + Wenn Sie ODMS Cloud erneut nutzen möchten, müssen Sie Ihre Kontoinformationen erneut registrieren und + Jahreslizenzen + bei einem autorisierten OM SYSTEM-Händler bestellen.
    + URL: $TOP_URL$ +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +

    +

    + Merci d'utiliser ODMS Cloud. Votre compte, y compris toutes les informations, a été supprimé du cloud ODMS. +

    +

    + Si vous souhaitez utiliser à nouveau ODMS Cloud, vous devrez à nouveau enregistrer les informations de votre + compte et + commander des licences annuelles auprès d'un concessionnaire agréé OM SYSTEM.
    + URL: $TOP_URL$ +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_111.txt b/dictation_server/src/templates/template_U_111.txt index f91b490..8dcb096 100644 --- a/dictation_server/src/templates/template_U_111.txt +++ b/dictation_server/src/templates/template_U_111.txt @@ -2,9 +2,9 @@ Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ -Thank you for using ODMS Cloud. Your account, including all information has been deleted from the ODMS Cloud. +Thank you for using the ODMS Cloud. Your account, including all information has been deleted from the ODMS Cloud. -If you wish to use ODMS Cloud again, you will need to register your account information again and order annual licenses from an OM Digital Solutions authorized dealer. +If you wish to use ODMS Cloud again, you will need to register your account information again and order annual licenses from an OM SYSTEM authorized dealer. URL: $TOP_URL$ If you have received this e-mail in error, please delete this e-mail from your system. @@ -16,7 +16,7 @@ Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ Vielen Dank, dass Sie ODMS Cloud nutzen. Ihr Konto, einschließlich aller Informationen, wurde aus der ODMS Cloud gelöscht. -Wenn Sie ODMS Cloud erneut nutzen möchten, müssen Sie Ihre Kontoinformationen erneut registrieren und Jahreslizenzen bei einem autorisierten OM Digital Solutions-Händler bestellen. +Wenn Sie ODMS Cloud erneut nutzen möchten, müssen Sie Ihre Kontoinformationen erneut registrieren und Jahreslizenzen bei einem autorisierten OM SYSTEM-Händler bestellen. URL: $TOP_URL$ Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. @@ -28,7 +28,7 @@ Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ Merci d'utiliser ODMS Cloud. Votre compte, y compris toutes les informations, a été supprimé du cloud ODMS. -Si vous souhaitez utiliser à nouveau ODMS Cloud, vous devrez à nouveau enregistrer les informations de votre compte et commander des licences annuelles auprès d'un concessionnaire agréé OM Digital Solutions. +Si vous souhaitez utiliser à nouveau ODMS Cloud, vous devrez à nouveau enregistrer les informations de votre compte et commander des licences annuelles auprès d'un concessionnaire agréé OM SYSTEM. URL: $TOP_URL$ Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. diff --git a/dictation_server/src/templates/template_U_120.html b/dictation_server/src/templates/template_U_120.html index b4b4e75..3f5c3ad 100644 --- a/dictation_server/src/templates/template_U_120.html +++ b/dictation_server/src/templates/template_U_120.html @@ -1,80 +1,80 @@ - - ユーザー一括登録 受付通知 [U-120] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録を受け付けました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ・登録完了には時間がかかる場合がありますので少々お待ちください。
    - ・登録完了通知は別途お送りします。
    - ・CSVファイルの内容に間違いがある場合は登録を完了できません。 -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Deutsch>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録を受け付けました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ・登録完了には時間がかかる場合がありますので少々お待ちください。
    - ・登録完了通知は別途お送りします。
    - ・CSVファイルの内容に間違いがある場合は登録を完了できません。 -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Français>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録を受け付けました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ・登録完了には時間がかかる場合がありますので少々お待ちください。
    - ・登録完了通知は別途お送りします。
    - ・CSVファイルの内容に間違いがある場合は登録を完了できません。 -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    - - + + User Bulk Registration Received Notification [U-120] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

    + We have received your bulk user registration request.
    + - Date and time: $REQUEST_TIME$
    + - SCV file name: $FILE_NAME$ +

    +

    + ・Please wait until the registration is complete. This may take a few minutes to process.
    + ・Notification will be sent separately upon completion.
    + ・Registration cannot be completed if there are invalid values in the CSV file. +

    +

    + If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. +

    +

    + If you received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

    + Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten.
    + - Datum und Uhrzeit: $REQUEST_TIME$
    + - SCV-Dateiname: $FILE_NAME$ +

    +

    + ・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern.
    + ・Eine Benachrichtigung wird nach Abschluss separat verschickt.
    + ・Die Registrierung kann nicht abgeschlossen werden, wenn die CSV-Datei ungültige Werte enthält. +

    +

    + Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +

    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

    + Nous avons reçu votre demande d'enregistrement groupé d'utilisateur.
    + - Date et heure : $REQUEST_TIME$
    + - Nom du fichier SCV : $FILE_NAME$ +

    +

    + ・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes.
    + ・Une notification sera envoyée séparément une fois terminée.
    + ・L'inscription ne peut pas être complétée s'il y a des valeurs invalides dans le fichier CSV. +

    +

    + Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_120.txt b/dictation_server/src/templates/template_U_120.txt index e8c68ec..e33e7e0 100644 --- a/dictation_server/src/templates/template_U_120.txt +++ b/dictation_server/src/templates/template_U_120.txt @@ -2,56 +2,49 @@ Dear $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +We have received your bulk user registration request. + - Date and time: $REQUEST_TIME$ + - SCV file name: $FILE_NAME$ -CSVファイルによるユーザー一括登録を受け付けました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・Please wait until the registration is complete. This may take a few minutes to process. +・Notification will be sent separately upon completion. +・Registration cannot be completed if there are invalid values in the CSV file. -・登録完了には時間がかかる場合がありますので少々お待ちください。 -・登録完了通知は別途お送りします。 -・CSVファイルの内容に間違いがある場合は登録を完了できません。 - -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 +If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. - +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. -Dear $CUSTOMER_NAME$, +Sehr geehrte(r) $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. + - Datum und Uhrzeit: $REQUEST_TIME$ + - SCV-Dateiname: $FILE_NAME$ -CSVファイルによるユーザー一括登録を受け付けました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern. +・Eine Benachrichtigung wird nach Abschluss separat verschickt. +・Die Registrierung kann nicht abgeschlossen werden, wenn die CSV-Datei ungültige Werte enthält. -・登録完了には時間がかかる場合がありますので少々お待ちください。 -・登録完了通知は別途お送りします。 -・CSVファイルの内容に間違いがある場合は登録を完了できません。 +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. -Dear $CUSTOMER_NAME$, +Chère/Cher $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier SCV : $FILE_NAME$ -CSVファイルによるユーザー一括登録を受け付けました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes. +・Une notification sera envoyée séparément une fois terminée. +・L'inscription ne peut pas être complétée s'il y a des valeurs invalides dans le fichier CSV. -・登録完了には時間がかかる場合がありますので少々お待ちください。 -・登録完了通知は別途お送りします。 -・CSVファイルの内容に間違いがある場合は登録を完了できません。 +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. \ No newline at end of file +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_120_no_parent.html b/dictation_server/src/templates/template_U_120_no_parent.html index 692411d..730449c 100644 --- a/dictation_server/src/templates/template_U_120_no_parent.html +++ b/dictation_server/src/templates/template_U_120_no_parent.html @@ -1,68 +1,71 @@ - - ユーザー一括登録 受付通知 [U-120] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録を受け付けました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ・登録完了には時間がかかる場合がありますので少々お待ちください。
    - ・登録完了通知は別途お送りします。
    - ・CSVファイルの内容に間違いがある場合は登録を完了できません。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Deutsch>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録を受け付けました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ・登録完了には時間がかかる場合がありますので少々お待ちください。
    - ・登録完了通知は別途お送りします。
    - ・CSVファイルの内容に間違いがある場合は登録を完了できません。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Français>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録を受け付けました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ・登録完了には時間がかかる場合がありますので少々お待ちください。
    - ・登録完了通知は別途お送りします。
    - ・CSVファイルの内容に間違いがある場合は登録を完了できません。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    - - + + User Bulk Registration Received Notification [U-120] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

    + We have received your bulk user registration request.
    + - Date and time: $REQUEST_TIME$
    + - SCV file name: $FILE_NAME$ +

    +

    + ・Please wait until the registration is complete. This may take a few minutes to process.
    + ・Notification will be sent separately upon completion.
    + ・Registration cannot be completed if there are invalid values in the CSV file. +

    +

    + If you received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

    + Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten.
    + - Datum und Uhrzeit: $REQUEST_TIME$
    + - SCV-Dateiname: $FILE_NAME$ +

    +

    + ・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern.
    + ・Eine Benachrichtigung wird nach Abschluss separat verschickt.
    + ・Die Registrierung kann nicht abgeschlossen werden, wenn die CSV-Datei ungültige Werte enthält. +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +

    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

    + Nous avons reçu votre demande d'enregistrement groupé d'utilisateur.
    + - Date et heure : $REQUEST_TIME$
    + - Nom du fichier SCV : $FILE_NAME$ +

    +

    + ・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes.
    + ・Une notification sera envoyée séparément une fois terminée.
    + ・L'inscription ne peut pas être complétée s'il y a des valeurs invalides dans le fichier CSV. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_120_no_parent.txt b/dictation_server/src/templates/template_U_120_no_parent.txt index fca8bcf..d5d85fa 100644 --- a/dictation_server/src/templates/template_U_120_no_parent.txt +++ b/dictation_server/src/templates/template_U_120_no_parent.txt @@ -2,50 +2,43 @@ Dear $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +We have received your bulk user registration request. + - Date and time: $REQUEST_TIME$ + - SCV file name: $FILE_NAME$ -CSVファイルによるユーザー一括登録を受け付けました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ - -・登録完了には時間がかかる場合がありますので少々お待ちください。 -・登録完了通知は別途お送りします。 -・CSVファイルの内容に間違いがある場合は登録を完了できません。 +・Please wait until the registration is complete. This may take a few minutes to process. +・Notification will be sent separately upon completion. +・Registration cannot be completed if there are invalid values in the CSV file. If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. - +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. -Dear $CUSTOMER_NAME$, +Sehr geehrte(r) $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. + - Datum und Uhrzeit: $REQUEST_TIME$ + - SCV-Dateiname: $FILE_NAME$ -CSVファイルによるユーザー一括登録を受け付けました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern. +・Eine Benachrichtigung wird nach Abschluss separat verschickt. +・Die Registrierung kann nicht abgeschlossen werden, wenn die CSV-Datei ungültige Werte enthält. -・登録完了には時間がかかる場合がありますので少々お待ちください。 -・登録完了通知は別途お送りします。 -・CSVファイルの内容に間違いがある場合は登録を完了できません。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. -Dear $CUSTOMER_NAME$, +Chère/Cher $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier SCV : $FILE_NAME$ -CSVファイルによるユーザー一括登録を受け付けました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes. +・Une notification sera envoyée séparément une fois terminée. +・L'inscription ne peut pas être complétée s'il y a des valeurs invalides dans le fichier CSV. -・登録完了には時間がかかる場合がありますので少々お待ちください。 -・登録完了通知は別途お送りします。 -・CSVファイルの内容に間違いがある場合は登録を完了できません。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. \ No newline at end of file +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121.html b/dictation_server/src/templates/template_U_121.html index 957b7ae..4598b6b 100644 --- a/dictation_server/src/templates/template_U_121.html +++ b/dictation_server/src/templates/template_U_121.html @@ -1,65 +1,80 @@ - - Storage Usage Exceeded Notification [U-119] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録が完了しました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Deutsch>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録が完了しました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Français>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録が完了しました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    - - + + User Bulk Registration Completed Notification [U-121] + + + +
    +

    <English>

    +

    + Bulk user registration using the CSV file has been completed.
    + - Date and time: $REQUEST_TIME$
    + - SCV file name: $FILE_NAME$ +

    +

    + ・User Registration Notification [U-114] will be sent to the registered users.
    + ・Registration will not be completed unless the user verifies their email address.
    + ・You can check the verification status of each user from the [User] tab in the ODMS Cloud. +

    +

    + If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. +

    +

    + If you received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

    + Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen.
    + - Datum und Uhrzeit: $REQUEST_TIME$
    + - SCV-Dateiname: $FILE_NAME$ +

    +

    + ・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet.
    + ・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt.
    + ・Sie können den Verifizierungsstatus jedes Benutzers auf der Registerkarte [Benutzer] in der ODMS Cloud + überprüfen. +

    +

    + Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

    + L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé.
    + - Date et heure : $REQUEST_TIME$
    + - Nom du fichier SCV : $FILE_NAME$ +

    +

    + ・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés.
    + ・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail.
    + ・Vous pouvez vérifier le statut de vérification de chaque utilisateur à partir de l'onglet [Utilisateur] dans le + cloud + ODMS. +

    +

    + Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121.txt b/dictation_server/src/templates/template_U_121.txt index f1995de..63e9176 100644 --- a/dictation_server/src/templates/template_U_121.txt +++ b/dictation_server/src/templates/template_U_121.txt @@ -2,43 +2,49 @@ Dear $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Bulk user registration using the CSV file has been completed. + - Date and time: $REQUEST_TIME$ + - SCV file name: $FILE_NAME$ -CSVファイルによるユーザー一括登録が完了しました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・User Registration Notification [U-114] will be sent to the registered users. +・Registration will not be completed unless the user verifies their email address. +・You can check the verification status of each user from the [User] tab in the ODMS Cloud. -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 +If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. -Dear $CUSTOMER_NAME$, +Sehr geehrte(r) $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen. + - Datum und Uhrzeit: $REQUEST_TIME$ + - SCV-Dateiname: $FILE_NAME$ -CSVファイルによるユーザー一括登録が完了しました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet. +・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt. +・Sie können den Verifizierungsstatus jedes Benutzers auf der Registerkarte [Benutzer] in der ODMS Cloud überprüfen. -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. -Dear $CUSTOMER_NAME$, +Chère/Cher $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier SCV : $FILE_NAME$ -CSVファイルによるユーザー一括登録が完了しました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés. +・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail. +・Vous pouvez vérifier le statut de vérification de chaque utilisateur à partir de l'onglet [Utilisateur] dans le cloud ODMS. -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. \ No newline at end of file +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121_no_parent.html b/dictation_server/src/templates/template_U_121_no_parent.html index 0ae8b99..25393c9 100644 --- a/dictation_server/src/templates/template_U_121_no_parent.html +++ b/dictation_server/src/templates/template_U_121_no_parent.html @@ -1,53 +1,71 @@ - - Storage Usage Exceeded Notification [U-119] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録が完了しました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Deutsch>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録が完了しました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Français>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - CSVファイルによるユーザー一括登録が完了しました。
    - - リクエスト日時:$REQUEST_TIME$
    - - SCVファイル名:$FILE_NAME$ -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    - - + + User Bulk Registration Completed Notification [U-121] + + + +
    +

    <English>

    +

    + Bulk user registration using the CSV file has been completed.
    + - Date and time: $REQUEST_TIME$
    + - SCV file name: $FILE_NAME$ +

    +

    + ・User Registration Notification [U-114] will be sent to the registered users.
    + ・Registration will not be completed unless the user verifies their email address.
    + ・You can check the verification status of each user from the [User] tab in the ODMS Cloud. +

    +

    + If you received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

    + Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen.
    + - Datum und Uhrzeit: $REQUEST_TIME$
    + - SCV-Dateiname: $FILE_NAME$ +

    +

    + ・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet.
    + ・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt.
    + ・Sie können den Verifizierungsstatus jedes Benutzers auf der Registerkarte [Benutzer] in der ODMS Cloud + überprüfen. +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

    + L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé.
    + - Date et heure : $REQUEST_TIME$
    + - Nom du fichier SCV : $FILE_NAME$ +

    +

    + ・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés.
    + ・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail.
    + ・Vous pouvez vérifier le statut de vérification de chaque utilisateur à partir de l'onglet [Utilisateur] dans le + cloud + ODMS. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121_no_parent.txt b/dictation_server/src/templates/template_U_121_no_parent.txt index cc81ddf..c1f7987 100644 --- a/dictation_server/src/templates/template_U_121_no_parent.txt +++ b/dictation_server/src/templates/template_U_121_no_parent.txt @@ -2,37 +2,43 @@ Dear $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Bulk user registration using the CSV file has been completed. + - Date and time: $REQUEST_TIME$ + - SCV file name: $FILE_NAME$ -CSVファイルによるユーザー一括登録が完了しました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・User Registration Notification [U-114] will be sent to the registered users. +・Registration will not be completed unless the user verifies their email address. +・You can check the verification status of each user from the [User] tab in the ODMS Cloud. If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. -Dear $CUSTOMER_NAME$, +Sehr geehrte(r) $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen. + - Datum und Uhrzeit: $REQUEST_TIME$ + - SCV-Dateiname: $FILE_NAME$ -CSVファイルによるユーザー一括登録が完了しました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet. +・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt. +・Sie können den Verifizierungsstatus jedes Benutzers auf der Registerkarte [Benutzer] in der ODMS Cloud überprüfen. -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. -Dear $CUSTOMER_NAME$, +Chère/Cher $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier SCV : $FILE_NAME$ -CSVファイルによるユーザー一括登録が完了しました。 - - リクエスト日時:$REQUEST_TIME$ - - SCVファイル名:$FILE_NAME$ +・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés. +・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail. +・Vous pouvez vérifier le statut de vérification de chaque utilisateur à partir de l'onglet [Utilisateur] dans le cloud ODMS. -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. \ No newline at end of file +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122.html b/dictation_server/src/templates/template_U_122.html index 7a5de32..f33c4c3 100644 --- a/dictation_server/src/templates/template_U_122.html +++ b/dictation_server/src/templates/template_U_122.html @@ -1,107 +1,137 @@ - - Storage Usage Exceeded Notification [U-119] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    CSVファイルによるユーザー一括登録に失敗しました。

    -

    - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail - addressと重複しています。
    - $EMAIL_DUPLICATION$ -

    -

    - 2. - 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    - $AUTHOR_ID_DUPLICATION$ -

    -

    - *既に登録済みのE-mail address, Author IDを再登録することはできません。 -

    -

    - 3. - 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    - $UNEXPECTED_ERROR$ -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system. This is an automatically generated e-mail, please do not - reply. -

    -
    -
    -

    <Deutsch>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    CSVファイルによるユーザー一括登録に失敗しました。

    -

    - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail - addressと重複しています。
    - $EMAIL_DUPLICATION$ -

    -

    - 2. - 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    - $AUTHOR_ID_DUPLICATION$ -

    -

    - *既に登録済みのE-mail address, Author IDを再登録することはできません。 -

    -

    - 3. - 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    - $UNEXPECTED_ERROR$ -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system. This is an automatically generated e-mail, please do not - reply. -

    -
    -
    -

    <Français>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    CSVファイルによるユーザー一括登録に失敗しました。

    -

    - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail - addressと重複しています。
    - $EMAIL_DUPLICATION$ -

    -

    - 2. - 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    - $AUTHOR_ID_DUPLICATION$ -

    -

    - *既に登録済みのE-mail address, Author IDを再登録することはできません。 -

    -

    - 3. - 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    - $UNEXPECTED_ERROR$ -

    -

    - ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ - にお問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system. This is an automatically generated e-mail, please do not - reply. -

    -
    - - + + User Bulk Registration Failed Notification [U-122] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

    + Bulk user registration using the CSV file has failed. The cause and location of the error is shown in 1, 2, and 3 + below. ( L = Line ) +

    +

    + 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in + another + line.
    + $EMAIL_DUPLICATION$ +

    +

    + 2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line.
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + * E-mail address and Author ID that have already been registered cannot be registered again.
    + * Rows without errors have been successfully registered. Therefore, if you use the same CSV file and register the + user + that has been successfully registered, a duplicate error will occur. Please create a CSV file containing only the + lines where the error occurred, or manually register them one by one. +

    +

    + 3. An unexpected error occurred during user registration on the following line. If it does not succeed after + trying + again, please contact your dealer.
    +    $UNEXPECTED_ERROR$ +

    +

    + If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. +

    +

    + If you received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

    + Die Massenregistrierung von Benutzern mithilfe der CSV-Datei ist fehlgeschlagen. Die Ursache und der Ort des + Fehlers + werden in 1, 2 und 3 unten angezeigt. (L = Linie) +

    +

    + 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in + einer + anderen Zeile.
    + $EMAIL_DUPLICATION$ +

    +

    + 2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen + Zeile.
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden.
    + * Zeilen ohne Fehler wurden erfolgreich registriert. Wenn Sie daher dieselbe CSV-Datei verwenden und den + erfolgreich + registrierten Benutzer registrieren, tritt ein doppelter Fehler auf. Bitte erstellen Sie eine CSV-Datei, die nur + die + Zeilen enthält, in denen der Fehler aufgetreten ist, oder registrieren Sie sie einzeln manuell. +

    +

    + 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch + nach + einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler.
    + $UNEXPECTED_ERROR$ +

    +

    + Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

    + L'enregistrement groupé des utilisateurs à l'aide du fichier CSV a échoué. La cause et l'emplacement de l'erreur + sont + indiqués aux points 1, 2 et 3 ci-dessous. ( L = Ligne ) +

    +

    + 1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une + autre ligne.
    + $EMAIL_DUPLICATION$ +

    +

    + 2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans + une + autre ligne.
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau.
    + * Les lignes sans erreurs ont été enregistrées avec succès. Par conséquent, si vous utilisez le même fichier CSV + et + enregistrez l'utilisateur qui a été enregistré avec succès, une erreur en double se produira. Veuillez créer un + fichier CSV contenant uniquement les lignes où l'erreur s'est produite, ou enregistrez-les manuellement une par + une. +

    +

    + 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela + ne + fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur.
    + $UNEXPECTED_ERROR$ +

    +

    + Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122.txt b/dictation_server/src/templates/template_U_122.txt index f4f2710..6729c16 100644 --- a/dictation_server/src/templates/template_U_122.txt +++ b/dictation_server/src/templates/template_U_122.txt @@ -2,73 +2,67 @@ Dear $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Bulk user registration using the CSV file has failed. The cause and location of the error is shown in 1, 2, and 3 below. ( L = Line ) -CSVファイルによるユーザー一括登録に失敗しました。 + 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in another line. + $EMAIL_DUPLICATION$ - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 - $EMAIL_DUPLICATION$ + 2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line. + $AUTHOR_ID_DUPLICATION$ - 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 - $AUTHOR_ID_DUPLICATION$ + * E-mail address and Author ID that have already been registered cannot be registered again. + * Rows without errors have been successfully registered. Therefore, if you use the same CSV file and register the user that has been successfully registered, a duplicate error will occur. Please create a CSV file containing only the lines where the error occurred, or manually register them one by one. - *既に登録済みのE-mail address, Author IDを再登録することはできません。 + 3. An unexpected error occurred during user registration on the following line. If it does not succeed after trying again, please contact your dealer. +   $UNEXPECTED_ERROR$ - - 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 -   $UNEXPECTED_ERROR$ - -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 +If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. -Dear $CUSTOMER_NAME$, +Sehr geehrte(r) $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Die Massenregistrierung von Benutzern mithilfe der CSV-Datei ist fehlgeschlagen. Die Ursache und der Ort des Fehlers werden in 1, 2 und 3 unten angezeigt. (L = Linie) -CSVファイルによるユーザー一括登録に失敗しました。 + 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in einer anderen Zeile. + $EMAIL_DUPLICATION$ - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 - $EMAIL_DUPLICATION$ + 2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen Zeile. + $AUTHOR_ID_DUPLICATION$ - 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 - $AUTHOR_ID_DUPLICATION$ + * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden. + * Zeilen ohne Fehler wurden erfolgreich registriert. Wenn Sie daher dieselbe CSV-Datei verwenden und den erfolgreich registrierten Benutzer registrieren, tritt ein doppelter Fehler auf. Bitte erstellen Sie eine CSV-Datei, die nur die Zeilen enthält, in denen der Fehler aufgetreten ist, oder registrieren Sie sie einzeln manuell. - *既に登録済みのE-mail address, Author IDを再登録することはできません。 + 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch nach einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler. + $UNEXPECTED_ERROR$ +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. - 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 -   $UNEXPECTED_ERROR$ - -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. -Dear $CUSTOMER_NAME$, +Chère/Cher $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +L'enregistrement groupé des utilisateurs à l'aide du fichier CSV a échoué. La cause et l'emplacement de l'erreur sont indiqués aux points 1, 2 et 3 ci-dessous. ( L = Ligne ) -CSVファイルによるユーザー一括登録に失敗しました。 + 1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une autre ligne. + $EMAIL_DUPLICATION$ - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 - $EMAIL_DUPLICATION$ + 2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans une autre ligne. + $AUTHOR_ID_DUPLICATION$ - 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 - $AUTHOR_ID_DUPLICATION$ + * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau. + * Les lignes sans erreurs ont été enregistrées avec succès. Par conséquent, si vous utilisez le même fichier CSV et enregistrez l'utilisateur qui a été enregistré avec succès, une erreur en double se produira. Veuillez créer un fichier CSV contenant uniquement les lignes où l'erreur s'est produite, ou enregistrez-les manuellement une par une. - *既に登録済みのE-mail address, Author IDを再登録することはできません。 + 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela ne fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur. + $UNEXPECTED_ERROR$ +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. - 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 -   $UNEXPECTED_ERROR$ - -ディーラーによるサポートが必要な場合は、 $DEALER_NAME$ にお問い合わせください。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. \ No newline at end of file +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122_no_parent.html b/dictation_server/src/templates/template_U_122_no_parent.html index cac57e5..bd9a2af 100644 --- a/dictation_server/src/templates/template_U_122_no_parent.html +++ b/dictation_server/src/templates/template_U_122_no_parent.html @@ -1,95 +1,128 @@ - - Storage Usage Exceeded Notification [U-119] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    CSVファイルによるユーザー一括登録に失敗しました。

    -

    - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail - addressと重複しています。
    - $EMAIL_DUPLICATION$ -

    -

    - 2. - 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    - $AUTHOR_ID_DUPLICATION$ -

    -

    - *既に登録済みのE-mail address, Author IDを再登録することはできません。 -

    -

    - 3. - 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    - $UNEXPECTED_ERROR$ -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system. This is an automatically generated e-mail, please do not - reply. -

    -
    -
    -

    <Deutsch>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    CSVファイルによるユーザー一括登録に失敗しました。

    -

    - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail - addressと重複しています。
    - $EMAIL_DUPLICATION$ -

    -

    - 2. - 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    - $AUTHOR_ID_DUPLICATION$ -

    -

    - *既に登録済みのE-mail address, Author IDを再登録することはできません。 -

    -

    - 3. - 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    - $UNEXPECTED_ERROR$ -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system. This is an automatically generated e-mail, please do not - reply. -

    -
    -
    -

    <Français>

    -

    Dear $CUSTOMER_NAME$,

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    CSVファイルによるユーザー一括登録に失敗しました。

    -

    - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail - addressと重複しています。
    - $EMAIL_DUPLICATION$ -

    -

    - 2. - 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。
    - $AUTHOR_ID_DUPLICATION$ -

    -

    - *既に登録済みのE-mail address, Author IDを再登録することはできません。 -

    -

    - 3. - 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。
    - $UNEXPECTED_ERROR$ -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system. This is an automatically generated e-mail, please do not - reply. -

    -
    - - + + User Bulk Registration Failed Notification [U-122] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

    + Bulk user registration using the CSV file has failed. The cause and location of the error is shown in 1, 2, and 3 + below. ( L = Line ) +

    +

    + 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in + another + line.
    + $EMAIL_DUPLICATION$ +

    +

    + 2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line.
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + * E-mail address and Author ID that have already been registered cannot be registered again.
    + * Rows without errors have been successfully registered. Therefore, if you use the same CSV file and register the + user + that has been successfully registered, a duplicate error will occur. Please create a CSV file containing only the + lines where the error occurred, or manually register them one by one. +

    +

    + 3. An unexpected error occurred during user registration on the following line. If it does not succeed after + trying + again, please contact your dealer.
    +    $UNEXPECTED_ERROR$ +

    +

    + If you received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

    + Die Massenregistrierung von Benutzern mithilfe der CSV-Datei ist fehlgeschlagen. Die Ursache und der Ort des + Fehlers + werden in 1, 2 und 3 unten angezeigt. (L = Linie) +

    +

    + 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in + einer + anderen Zeile.
    + $EMAIL_DUPLICATION$ +

    +

    + 2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen + Zeile.
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden.
    + * Zeilen ohne Fehler wurden erfolgreich registriert. Wenn Sie daher dieselbe CSV-Datei verwenden und den + erfolgreich + registrierten Benutzer registrieren, tritt ein doppelter Fehler auf. Bitte erstellen Sie eine CSV-Datei, die nur + die + Zeilen enthält, in denen der Fehler aufgetreten ist, oder registrieren Sie sie einzeln manuell. +

    +

    + 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch + nach + einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler.
    + $UNEXPECTED_ERROR$ +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

    + L'enregistrement groupé des utilisateurs à l'aide du fichier CSV a échoué. La cause et l'emplacement de l'erreur + sont + indiqués aux points 1, 2 et 3 ci-dessous. ( L = Ligne ) +

    +

    + 1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une + autre ligne.
    + $EMAIL_DUPLICATION$ +

    +

    + 2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans + une + autre ligne.
    + $AUTHOR_ID_DUPLICATION$ +

    +

    + * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau.
    + * Les lignes sans erreurs ont été enregistrées avec succès. Par conséquent, si vous utilisez le même fichier CSV + et + enregistrez l'utilisateur qui a été enregistré avec succès, une erreur en double se produira. Veuillez créer un + fichier CSV contenant uniquement les lignes où l'erreur s'est produite, ou enregistrez-les manuellement une par + une. +

    +

    + 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela + ne + fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur.
    + $UNEXPECTED_ERROR$ +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122_no_parent.txt b/dictation_server/src/templates/template_U_122_no_parent.txt index 3b54d39..f91ba7f 100644 --- a/dictation_server/src/templates/template_U_122_no_parent.txt +++ b/dictation_server/src/templates/template_U_122_no_parent.txt @@ -2,67 +2,61 @@ Dear $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Bulk user registration using the CSV file has failed. The cause and location of the error is shown in 1, 2, and 3 below. ( L = Line ) -CSVファイルによるユーザー一括登録に失敗しました。 + 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in another line. + $EMAIL_DUPLICATION$ - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 - $EMAIL_DUPLICATION$ + 2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line. + $AUTHOR_ID_DUPLICATION$ - 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 - $AUTHOR_ID_DUPLICATION$ + * E-mail address and Author ID that have already been registered cannot be registered again. + * Rows without errors have been successfully registered. Therefore, if you use the same CSV file and register the user that has been successfully registered, a duplicate error will occur. Please create a CSV file containing only the lines where the error occurred, or manually register them one by one. - *既に登録済みのE-mail address, Author IDを再登録することはできません。 - - - 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 -   $UNEXPECTED_ERROR$ + 3. An unexpected error occurred during user registration on the following line. If it does not succeed after trying again, please contact your dealer. +   $UNEXPECTED_ERROR$ If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. -Dear $CUSTOMER_NAME$, +Sehr geehrte(r) $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +Die Massenregistrierung von Benutzern mithilfe der CSV-Datei ist fehlgeschlagen. Die Ursache und der Ort des Fehlers werden in 1, 2 und 3 unten angezeigt. (L = Linie) -CSVファイルによるユーザー一括登録に失敗しました。 + 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in einer anderen Zeile. + $EMAIL_DUPLICATION$ - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 - $EMAIL_DUPLICATION$ + 2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen Zeile. + $AUTHOR_ID_DUPLICATION$ - 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 - $AUTHOR_ID_DUPLICATION$ + * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden. + * Zeilen ohne Fehler wurden erfolgreich registriert. Wenn Sie daher dieselbe CSV-Datei verwenden und den erfolgreich registrierten Benutzer registrieren, tritt ein doppelter Fehler auf. Bitte erstellen Sie eine CSV-Datei, die nur die Zeilen enthält, in denen der Fehler aufgetreten ist, oder registrieren Sie sie einzeln manuell. - *既に登録済みのE-mail address, Author IDを再登録することはできません。 + 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch nach einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler. + $UNEXPECTED_ERROR$ - - 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 -   $UNEXPECTED_ERROR$ - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. -Dear $CUSTOMER_NAME$, +Chère/Cher $CUSTOMER_NAME$, -ODMS Cloudをご利用いただきありがとうございます。 +L'enregistrement groupé des utilisateurs à l'aide du fichier CSV a échoué. La cause et l'emplacement de l'erreur sont indiqués aux points 1, 2 et 3 ci-dessous. ( L = Ligne ) -CSVファイルによるユーザー一括登録に失敗しました。 + 1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une autre ligne. + $EMAIL_DUPLICATION$ - 1. 以下の行にあるE-mail addressは既に登録済みか、別の行のE-mail addressと重複しています。 - $EMAIL_DUPLICATION$ + 2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans une autre ligne. + $AUTHOR_ID_DUPLICATION$ - 2. 以下の行にあるAuthorIDは既に登録済みか、別の行のAuthorIDと重複しています。 - $AUTHOR_ID_DUPLICATION$ + * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau. + * Les lignes sans erreurs ont été enregistrées avec succès. Par conséquent, si vous utilisez le même fichier CSV et enregistrez l'utilisateur qui a été enregistré avec succès, une erreur en double se produira. Veuillez créer un fichier CSV contenant uniquement les lignes où l'erreur s'est produite, ou enregistrez-les manuellement une par une. - *既に登録済みのE-mail address, Author IDを再登録することはできません。 + 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela ne fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur. + $UNEXPECTED_ERROR$ - - 3. 以下の行のユーザー登録時に想定外のエラーが発生しました。時間をおいて再度実行しても成功しない場合はディーラーにお問い合わせください。 -   $UNEXPECTED_ERROR$ - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. \ No newline at end of file +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_123.html b/dictation_server/src/templates/template_U_123.html index 2e46fc4..86c1e5b 100644 --- a/dictation_server/src/templates/template_U_123.html +++ b/dictation_server/src/templates/template_U_123.html @@ -1,65 +1,64 @@ - - Storage Usage Exceeded Notification [U-119] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - お客様のアカウント情報は$DEALER_NAME$によりODMS - Cloudから削除されました。 -

    -

    - 再度ODMS - Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM - Degital Solutionsに問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Deutsch>

    -

    Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - お客様のアカウント情報は$DEALER_NAME$によりODMS - Cloudから削除されました。 -

    -

    - 再度ODMS - Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM - Degital Solutionsに問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    -
    -

    <Français>

    -

    Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    -

    ODMS Cloudをご利用いただきありがとうございます。

    -

    - お客様のアカウント情報は$DEALER_NAME$によりODMS - Cloudから削除されました。 -

    -

    - 再度ODMS - Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM - Degital Solutionsに問い合わせください。 -

    -

    - If you received this e-mail in error, please delete this e-mail from - your system.
    - This is an automatically generated e-mail, please do not reply. -

    -
    - - + + Partner Account Deleted Notification [U-123] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +

    +

    + Your account information has been removed from the ODMS Cloud by $DEALER_NAME$. +

    +

    + If you would like to register your account with the ODMS Cloud again, please contact your $DEALER_NAME$ or contact + OM + System directly. +

    +

    + If you received this e-mail in error, please delete this e-mail from your system.
    + This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. +

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +

    +

    + Ihre Kontoinformationen wurden von $DEALER_NAME$ aus der ODMS Cloud entfernt. +

    +

    + Wenn Sie Ihr Konto erneut bei der ODMS Cloud registrieren möchten, wenden Sie sich bitte an Ihren $DEALER_NAME$ + oder + wenden Sie sich direkt an OM System. +

    +

    + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. +

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +

    +

    + Les informations de votre compte ont été supprimées du cloud ODMS par $DEALER_NAME$. +

    +

    + Si vous souhaitez enregistrer à nouveau votre compte sur ODMS Cloud, veuillez contacter votre $DEALER_NAME$ ou + contacter directement OM System. +

    +

    + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas + répondre. +

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_123.txt b/dictation_server/src/templates/template_U_123.txt index 3a98e16..ea9f994 100644 --- a/dictation_server/src/templates/template_U_123.txt +++ b/dictation_server/src/templates/template_U_123.txt @@ -2,37 +2,31 @@ Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ -ODMS Cloudをご利用いただきありがとうございます。 +Your account information has been removed from the ODMS Cloud by $DEALER_NAME$. -お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 - -再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 +If you would like to register your account with the ODMS Cloud again, please contact your $DEALER_NAME$ or contact OM System directly. If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. -Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ -ODMS Cloudをご利用いただきありがとうございます。 +Ihre Kontoinformationen wurden von $DEALER_NAME$ aus der ODMS Cloud entfernt. -お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 +Wenn Sie Ihr Konto erneut bei der ODMS Cloud registrieren möchten, wenden Sie sich bitte an Ihren $DEALER_NAME$ oder wenden Sie sich direkt an OM System. -再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. -Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ +Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ -ODMS Cloudをご利用いただきありがとうございます。 +Les informations de votre compte ont été supprimées du cloud ODMS par $DEALER_NAME$. -お客様のアカウント情報は$DEALER_NAME$によりODMS Cloudから削除されました。 +Si vous souhaitez enregistrer à nouveau votre compte sur ODMS Cloud, veuillez contacter votre $DEALER_NAME$ ou contacter directement OM System. -再度ODMS Cloudにアカウント登録する場合は、$DEALER_NAME$にご相談いただくか、OM Degital Solutionsに問い合わせください。 - -If you received this e-mail in error, please delete this e-mail from your system. -This is an automatically generated e-mail, please do not reply. \ No newline at end of file +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file From b7554e30ff66fc2b70397c51fe29ba321604ae61 Mon Sep 17 00:00:00 2001 From: makabe Date: Fri, 19 Apr 2024 13:38:24 +0900 Subject: [PATCH 083/109] =?UTF-8?q?CCB=E3=81=8B=E3=82=89DB=E3=81=AE?= =?UTF-8?q?=E5=90=91=E3=81=8D=E5=85=88=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/database/initializeDataSource.ts | 2 +- dictation_server/src/app.module.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dictation_function/src/database/initializeDataSource.ts b/dictation_function/src/database/initializeDataSource.ts index f323d54..45cb3b9 100644 --- a/dictation_function/src/database/initializeDataSource.ts +++ b/dictation_function/src/database/initializeDataSource.ts @@ -22,7 +22,7 @@ export async function initializeDataSource( port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, - database: process.env.DB_NAME_CCB, + database: process.env.DB_NAME, entities: [ User, UserArchive, diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index f35fe54..3a8dfd5 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -60,10 +60,10 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; useFactory: () => process.env.STAGE === 'local' ? [ - { - rootPath: join(__dirname, '..', 'build'), - }, - ] + { + rootPath: join(__dirname, '..', 'build'), + }, + ] : [], }), ConfigModule.forRoot({ @@ -100,7 +100,7 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; port: configService.get('DB_PORT'), username: configService.get('DB_USERNAME'), password: configService.get('DB_PASSWORD'), - database: configService.get('DB_NAME_CCB'), + database: configService.get('DB_NAME'), autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード synchronize: false, // trueにすると自動的にmigrationが行われるため注意 }), From d43cece48a9ae3eb5fdc9f95a893c56a491452e8 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 22 Apr 2024 08:01:37 +0000 Subject: [PATCH 084/109] =?UTF-8?q?Merged=20PR=20882:=20=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E9=96=8B=E7=99=BA=E5=88=86=E3=82=92=E3=82=B9=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=83=B3=E3=82=B0=E3=81=AB=E5=8F=8D=E6=98=A0=E3=81=99?= =?UTF-8?q?=E3=82=8BPipeline=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4016: 追加開発分をステージングに反映するPipelineを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4016) - 追加開発分CCBをステージングに反映するPipeline定義のYamlファイルを追加しました。 ## レビューポイント - 参照元ブランチを変えた以外はSTGデプロイそのままにしていますが、対応の不足はないでしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - マージ後に確認 --- azure-pipelines-staging-ccb.yml | 312 ++++++++++++++++++ .../src/database/initializeDataSource.ts | 2 +- dictation_server/db/dbconfig.yml | 6 +- dictation_server/src/app.module.ts | 10 +- 4 files changed, 323 insertions(+), 7 deletions(-) create mode 100644 azure-pipelines-staging-ccb.yml diff --git a/azure-pipelines-staging-ccb.yml b/azure-pipelines-staging-ccb.yml new file mode 100644 index 0000000..f4c7e80 --- /dev/null +++ b/azure-pipelines-staging-ccb.yml @@ -0,0 +1,312 @@ +# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと +# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと +trigger: + branches: + include: + - release-ccb + tags: + include: + - stage-* + +jobs: +- job: initialize + displayName: Initialize + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + persistCredentials: true + - script: | + git fetch origin release-ccb:release-ccb + if git merge-base --is-ancestor $(Build.SourceVersion) release-ccb; then + echo "This commit is in the release-ccb branch." + else + echo "This commit is not in the release-ccb branch." + exit 1 + fi + displayName: 'タグが付けられたCommitがrelease-ccbブランチに存在するか確認' +- job: backend_test + dependsOn: initialize + condition: succeeded('initialize') + displayName: UnitTest + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Bash@3 + displayName: Bash Script (Test) + inputs: + targetType: inline + workingDirectory: dictation_server/.devcontainer + script: | + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_server sudo npm ci + docker-compose exec -T dictation_server sudo npm run migrate:up:test + docker-compose exec -T dictation_server sudo npm run test +- job: backend_build + dependsOn: backend_test + condition: succeeded('backend_test') + displayName: Build And Push Backend Image + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Npm@1 + displayName: npm ci + inputs: + command: ci + workingDir: dictation_server + verbose: false + - task: Docker@0 + displayName: build + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + dockerFile: DockerfileServerDictation.dockerfile + imageName: odmscloud/staging/dictation:$(Build.SourceVersion) + buildArguments: | + BUILD_VERSION=$(Build.SourceVersion) + - task: Docker@0 + displayName: push + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + action: Push an image + imageName: odmscloud/staging/dictation:$(Build.SourceVersion) +- job: frontend_build_staging + dependsOn: backend_build + condition: succeeded('backend_build') + displayName: Build Frontend Files(staging) + variables: + storageAccountName: saomdspipeline + environment: staging + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Npm@1 + displayName: npm ci + inputs: + command: ci + workingDir: dictation_client + verbose: false + - task: Bash@3 + displayName: Bash Script + inputs: + targetType: inline + script: cd dictation_client && npm run build:stg + - task: ArchiveFiles@2 + inputs: + rootFolderOrFile: dictation_client/build + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip' + replaceExistingArchive: true + - task: AzureCLI@2 + inputs: + azureSubscription: 'omds-service-connection-stg' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + az storage blob upload \ + --auth-mode login \ + --account-name $(storageAccountName) \ + --container-name $(environment) \ + --name $(Build.SourceVersion).zip \ + --type block \ + --overwrite \ + --file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip +- job: function_test + dependsOn: frontend_build_staging + condition: succeeded('frontend_build_staging') + displayName: UnitTest + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Bash@3 + displayName: Bash Script (Test) + inputs: + targetType: inline + workingDirectory: dictation_function/.devcontainer + script: | + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_function sudo npm ci + docker-compose exec -T dictation_function sudo npm run test +- job: function_build + dependsOn: function_test + condition: succeeded('function_test') + displayName: Build And Push Function Image + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Npm@1 + displayName: npm ci + inputs: + command: ci + workingDir: dictation_function + verbose: false + - task: Docker@0 + displayName: build + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + dockerFile: DockerfileFunctionDictation.dockerfile + imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion) + buildArguments: | + BUILD_VERSION=$(Build.SourceVersion) + - task: Docker@0 + displayName: push + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + action: Push an image + imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion) +- job: backend_deploy + dependsOn: function_build + condition: succeeded('function_build') + displayName: Backend Deploy + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureWebAppContainer@1 + inputs: + azureSubscription: 'omds-service-connection-stg' + appName: 'app-odms-dictation-stg' + deployToSlotOrASE: true + resourceGroupName: 'stg-application-rg' + slotName: 'staging' + containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)' +- job: frontend_deploy + dependsOn: backend_deploy + condition: succeeded('backend_deploy') + displayName: Deploy Frontend Files + variables: + storageAccountName: saomdspipeline + environment: staging + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureCLI@2 + inputs: + azureSubscription: 'omds-service-connection-stg' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + az storage blob download \ + --auth-mode login \ + --account-name $(storageAccountName) \ + --container-name $(environment) \ + --name $(Build.SourceVersion).zip \ + --file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip + - task: Bash@3 + displayName: Bash Script + inputs: + targetType: inline + script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion) + - task: AzureStaticWebApp@0 + displayName: 'Static Web App: ' + inputs: + workingDirectory: '$(Build.SourcesDirectory)' + app_location: '/$(Build.SourceVersion)' + config_file_location: /dictation_client + skip_app_build: true + skip_api_build: true + is_static_export: false + verbose: false + azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN) +- job: function_deploy + dependsOn: frontend_deploy + condition: succeeded('frontend_deploy') + displayName: Function Deploy + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureFunctionAppContainer@1 + inputs: + azureSubscription: 'omds-service-connection-stg' + appName: 'func-odms-dictation-stg' + imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)' +- job: smoke_test + dependsOn: function_deploy + condition: succeeded('function_deploy') + displayName: 'smoke test' + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + # スモークテスト用にjobを確保 +- job: swap_slot + dependsOn: smoke_test + condition: succeeded('smoke_test') + displayName: 'Swap Staging and Production' + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureAppServiceManage@0 + displayName: 'Azure App Service Manage: app-odms-dictation-stg' + inputs: + azureSubscription: 'omds-service-connection-stg' + action: 'Swap Slots' + WebAppName: 'app-odms-dictation-stg' + ResourceGroupName: 'stg-application-rg' + SourceSlot: 'staging' + SwapWithProduction: true +- job: migration + dependsOn: swap_slot + condition: succeeded('swap_slot') + displayName: DB migration + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureKeyVault@2 + displayName: 'Azure Key Vault: kv-odms-secret-stg' + inputs: + ConnectedServiceName: 'omds-service-connection-stg' + KeyVaultName: kv-odms-secret-stg + - task: CmdLine@2 + displayName: migration + inputs: + script: >2 + # DB接続情報書き換え + sed -i -e "s/DB_NAME_CCB/$(db-name-ccb)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml + sql-migrate --version + cat ./dictation_server/db/dbconfig.yml + # migration実行 + sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci_ccb \ No newline at end of file diff --git a/dictation_function/src/database/initializeDataSource.ts b/dictation_function/src/database/initializeDataSource.ts index 45cb3b9..f323d54 100644 --- a/dictation_function/src/database/initializeDataSource.ts +++ b/dictation_function/src/database/initializeDataSource.ts @@ -22,7 +22,7 @@ export async function initializeDataSource( port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, + database: process.env.DB_NAME_CCB, entities: [ User, UserArchive, diff --git a/dictation_server/db/dbconfig.yml b/dictation_server/db/dbconfig.yml index 38bd96b..48f97aa 100644 --- a/dictation_server/db/dbconfig.yml +++ b/dictation_server/db/dbconfig.yml @@ -10,7 +10,11 @@ ci: dialect: mysql dir: ./dictation_server/db/migrations datasource: DB_USERNAME:DB_PASS@tcp(DB_HOST:DB_PORT)/DB_NAME?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true +ci_ccb: + dialect: mysql + dir: ./dictation_server/db/migrations + datasource: DB_USERNAME:DB_PASS@tcp(DB_HOST:DB_PORT)/DB_NAME_CCB?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true test: dialect: mysql dir: /app/dictation_server/db/migrations - datasource: user:password@tcp(test_mysql_db:3306)/odms?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true \ No newline at end of file + datasource: user:password@tcp(test_mysql_db:3306)/odms?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 3a8dfd5..f35fe54 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -60,10 +60,10 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; useFactory: () => process.env.STAGE === 'local' ? [ - { - rootPath: join(__dirname, '..', 'build'), - }, - ] + { + rootPath: join(__dirname, '..', 'build'), + }, + ] : [], }), ConfigModule.forRoot({ @@ -100,7 +100,7 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; port: configService.get('DB_PORT'), username: configService.get('DB_USERNAME'), password: configService.get('DB_PASSWORD'), - database: configService.get('DB_NAME'), + database: configService.get('DB_NAME_CCB'), autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード synchronize: false, // trueにすると自動的にmigrationが行われるため注意 }), From 8122f6f4e1803be84afea91af5d70cef02ae138f Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 7 May 2024 00:05:17 +0000 Subject: [PATCH 085/109] =?UTF-8?q?Merged=20PR=20884:=20Function=E3=81=ABX?= =?UTF-8?q?-Requested-With=E3=83=98=E3=83=83=E3=83=80=E3=82=92=E9=81=A9?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4142: FunctionにX-Requested-Withヘッダを適用](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4142) - Functionのユーザー一括登録の処理中でOMDS CloudのAPIを呼び出す処理があるので、X-Requested-Withヘッダを適用しました。 - 一括登録失敗時のメール文面の翻訳でエラーがない場合のメッセージが日本語のままになっていたので各言語に対応しました。 ## レビューポイント - ヘッダの適用は適切でしょうか? - 翻訳の適用方法で、言語ごとに割り当てる内容を定数としていますが、文面の置き換え方法に問題はないでしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - ローカルからAPIを叩い検証証 --- .../src/functions/importUsers.ts | 10 ++ .../src/features/users/users.service.ts | 4 +- .../src/gateways/sendgrid/sendgrid.service.ts | 106 +++++++++++++----- dictation_server/src/templates/constants.ts | 18 ++- .../src/templates/template_U_122.html | 18 +-- .../src/templates/template_U_122.txt | 18 +-- .../templates/template_U_122_no_parent.html | 18 +-- .../templates/template_U_122_no_parent.txt | 18 +-- 8 files changed, 141 insertions(+), 69 deletions(-) diff --git a/dictation_function/src/functions/importUsers.ts b/dictation_function/src/functions/importUsers.ts index 38716b9..0a34802 100644 --- a/dictation_function/src/functions/importUsers.ts +++ b/dictation_function/src/functions/importUsers.ts @@ -18,6 +18,16 @@ import { sign, getJwtKey } from "../common/jwt"; import { AccessToken, SystemAccessToken } from "../common/jwt/types"; import { isImportJson, isStageJson } from "../blobstorage/types/guards"; import https from "https"; +import globalAxios from "axios"; + +// すべてのリクエストのヘッダーにX-Requested-Withを追加 +globalAxios.interceptors.request.use((config) => { + // headersがあれば追加、なければ新規作成 + config.headers = config.headers || {}; + // X-Requested-Withを追加 + config.headers["X-Requested-With"] = "XMLHttpRequest"; + return config; +}); export async function importUsersProcessing( context: InvocationContext, diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 957dde1..6896e98 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -63,7 +63,6 @@ import { import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; -import { Account } from '../../repositories/accounts/entity/account.entity'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; @Injectable() @@ -195,7 +194,6 @@ export class UsersService { ); //DBよりアクセス者の所属するアカウントIDを取得する let adminUser: EntityUser; - let account: Account | null; try { adminUser = await this.usersRepository.findUserByExternalId( context, @@ -210,7 +208,7 @@ export class UsersService { } const accountId = adminUser.account_id; - account = adminUser.account; + const account = adminUser.account; //authorIdが重複していないかチェックする if (authorId) { diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index b399326..7432e72 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -20,10 +20,19 @@ import { TYPIST_NAME, VERIFY_LINK, TEMPORARY_PASSWORD, - EMAIL_DUPLICATION, - AUTHOR_ID_DUPLICATION, - UNEXPECTED_ERROR, + EMAIL_DUPLICATION_EN, + AUTHOR_ID_DUPLICATION_EN, + UNEXPECTED_ERROR_EN, + EMAIL_DUPLICATION_DE, + AUTHOR_ID_DUPLICATION_DE, + UNEXPECTED_ERROR_DE, + EMAIL_DUPLICATION_FR, + AUTHOR_ID_DUPLICATION_FR, + UNEXPECTED_ERROR_FR, REQUEST_TIME, + NO_ERROR_MESSAGE_EN, + NO_ERROR_MESSAGE_DE, + NO_ERROR_MESSAGE_FR, } from '../../templates/constants'; import { URL } from 'node:url'; @@ -1387,18 +1396,37 @@ export class SendGridService { `[IN] [${context.getTrackingId()}] ${this.sendMailWithU122.name}`, ); try { - const duplicateEmailsMsg = - duplicateEmails.length === 0 - ? 'エラーはありません' - : duplicateEmails.map((x) => `L${x}`).join(', '); - const duplicateAuthorIdsMsg = - duplicateAuthorIds.length === 0 - ? 'エラーはありません' - : duplicateAuthorIds.map((x) => `L${x}`).join(', '); - const otherErrorsMsg = - otherErrors.length === 0 - ? 'エラーはありません' - : otherErrors.map((x) => `L${x}`).join(', '); + let duplicateEmailsMsgEn = NO_ERROR_MESSAGE_EN; + let duplicateAuthorIdsMsgEn = NO_ERROR_MESSAGE_EN; + let otherErrorsMsgEn = NO_ERROR_MESSAGE_EN; + let duplicateEmailsMsgDe = NO_ERROR_MESSAGE_DE; + let duplicateAuthorIdsMsgDe = NO_ERROR_MESSAGE_DE; + let otherErrorsMsgDe = NO_ERROR_MESSAGE_DE; + let duplicateEmailsMsgFr = NO_ERROR_MESSAGE_FR; + let duplicateAuthorIdsMsgFr = NO_ERROR_MESSAGE_FR; + let otherErrorsMsgFr = NO_ERROR_MESSAGE_FR; + + if (duplicateEmails.length !== 0) { + duplicateEmailsMsgEn = duplicateEmails.map((x) => `L${x}`).join(', '); + duplicateEmailsMsgDe = duplicateEmails.map((x) => `L${x}`).join(', '); + duplicateEmailsMsgFr = duplicateEmails.map((x) => `L${x}`).join(', '); + } + if (duplicateAuthorIds.length !== 0) { + duplicateAuthorIdsMsgEn = duplicateAuthorIds + .map((x) => `L${x}`) + .join(', '); + duplicateAuthorIdsMsgDe = duplicateAuthorIds + .map((x) => `L${x}`) + .join(', '); + duplicateAuthorIdsMsgFr = duplicateAuthorIds + .map((x) => `L${x}`) + .join(', '); + } + if (otherErrors.length !== 0) { + otherErrorsMsgEn = otherErrors.map((x) => `L${x}`).join(', '); + otherErrorsMsgDe = otherErrors.map((x) => `L${x}`).join(', '); + otherErrorsMsgFr = otherErrors.map((x) => `L${x}`).join(', '); + } const subject = 'User Bulk Registration Failed Notification [U-122]'; @@ -1408,27 +1436,51 @@ export class SendGridService { if (!dealerAccountName) { html = this.templateU122NoParentHtml .replaceAll(CUSTOMER_NAME, customerAccountName) - .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) - .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) - .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); text = this.templateU122NoParentText .replaceAll(CUSTOMER_NAME, customerAccountName) - .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) - .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) - .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); } else { html = this.templateU122Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) - .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) - .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) - .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); text = this.templateU122Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) - .replaceAll(EMAIL_DUPLICATION, duplicateEmailsMsg) - .replaceAll(AUTHOR_ID_DUPLICATION, duplicateAuthorIdsMsg) - .replaceAll(UNEXPECTED_ERROR, otherErrorsMsg); + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); } // メールを送信する diff --git a/dictation_server/src/templates/constants.ts b/dictation_server/src/templates/constants.ts index d6650a6..47ebb6d 100644 --- a/dictation_server/src/templates/constants.ts +++ b/dictation_server/src/templates/constants.ts @@ -12,6 +12,18 @@ export const FILE_NAME = '$FILE_NAME$'; export const TYPIST_NAME = '$TYPIST_NAME$'; export const TEMPORARY_PASSWORD = '$TEMPORARY_PASSWORD$'; export const REQUEST_TIME = '$REQUEST_TIME$'; -export const EMAIL_DUPLICATION = `$EMAIL_DUPLICATION$`; -export const AUTHOR_ID_DUPLICATION = `$AUTHOR_ID_DUPLICATION$`; -export const UNEXPECTED_ERROR = `$UNEXPECTED_ERROR$`; +// 言語ごとに変更 +export const EMAIL_DUPLICATION_EN = `$EMAIL_DUPLICATION_EN$`; +export const AUTHOR_ID_DUPLICATION_EN = `$AUTHOR_ID_DUPLICATION_EN$`; +export const UNEXPECTED_ERROR_EN = `$UNEXPECTED_ERROR_EN$`; +export const EMAIL_DUPLICATION_DE = `$EMAIL_DUPLICATION_DE$`; +export const AUTHOR_ID_DUPLICATION_DE = `$AUTHOR_ID_DUPLICATION_DE$`; +export const UNEXPECTED_ERROR_DE = `$UNEXPECTED_ERROR_DE$`; +export const EMAIL_DUPLICATION_FR = `$EMAIL_DUPLICATION_FR$`; +export const AUTHOR_ID_DUPLICATION_FR = `$AUTHOR_ID_DUPLICATION_FR$`; +export const UNEXPECTED_ERROR_FR = `$UNEXPECTED_ERROR_FR$`; + +// 言語ごとに当てはまる値 +export const NO_ERROR_MESSAGE_EN = 'No errors'; +export const NO_ERROR_MESSAGE_DE = 'Keine Fehler'; +export const NO_ERROR_MESSAGE_FR = 'Aucune erreur'; diff --git a/dictation_server/src/templates/template_U_122.html b/dictation_server/src/templates/template_U_122.html index f33c4c3..b456eb5 100644 --- a/dictation_server/src/templates/template_U_122.html +++ b/dictation_server/src/templates/template_U_122.html @@ -18,11 +18,11 @@ 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in another line.
    - $EMAIL_DUPLICATION$ +   $EMAIL_DUPLICATION_EN$

    2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line.
    - $AUTHOR_ID_DUPLICATION$ +   $AUTHOR_ID_DUPLICATION_EN$

    * E-mail address and Author ID that have already been registered cannot be registered again.
    @@ -35,7 +35,7 @@ 3. An unexpected error occurred during user registration on the following line. If it does not succeed after trying again, please contact your dealer.
    -    $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_EN$

    If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. @@ -59,12 +59,12 @@ 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in einer anderen Zeile.
    - $EMAIL_DUPLICATION$ +   $EMAIL_DUPLICATION_DE$

    2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen Zeile.
    - $AUTHOR_ID_DUPLICATION$ +   $AUTHOR_ID_DUPLICATION_DE$

    * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden.
    @@ -78,7 +78,7 @@ 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch nach einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler.
    - $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_DE$

    Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. @@ -101,13 +101,13 @@

    1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une autre ligne.
    - $EMAIL_DUPLICATION$ +   $EMAIL_DUPLICATION_FR$

    2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans une autre ligne.
    - $AUTHOR_ID_DUPLICATION$ +   $AUTHOR_ID_DUPLICATION_FR$

    * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau.
    @@ -121,7 +121,7 @@ 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela ne fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur.
    - $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_FR$

    Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. diff --git a/dictation_server/src/templates/template_U_122.txt b/dictation_server/src/templates/template_U_122.txt index 6729c16..59315df 100644 --- a/dictation_server/src/templates/template_U_122.txt +++ b/dictation_server/src/templates/template_U_122.txt @@ -5,16 +5,16 @@ Dear $CUSTOMER_NAME$, Bulk user registration using the CSV file has failed. The cause and location of the error is shown in 1, 2, and 3 below. ( L = Line ) 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in another line. - $EMAIL_DUPLICATION$ + $EMAIL_DUPLICATION_EN$ 2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line. - $AUTHOR_ID_DUPLICATION$ + $AUTHOR_ID_DUPLICATION_EN$ * E-mail address and Author ID that have already been registered cannot be registered again. * Rows without errors have been successfully registered. Therefore, if you use the same CSV file and register the user that has been successfully registered, a duplicate error will occur. Please create a CSV file containing only the lines where the error occurred, or manually register them one by one. 3. An unexpected error occurred during user registration on the following line. If it does not succeed after trying again, please contact your dealer. -   $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_EN$ If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. @@ -28,16 +28,16 @@ Sehr geehrte(r) $CUSTOMER_NAME$, Die Massenregistrierung von Benutzern mithilfe der CSV-Datei ist fehlgeschlagen. Die Ursache und der Ort des Fehlers werden in 1, 2 und 3 unten angezeigt. (L = Linie) 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in einer anderen Zeile. - $EMAIL_DUPLICATION$ + $EMAIL_DUPLICATION_DE$ 2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen Zeile. - $AUTHOR_ID_DUPLICATION$ + $AUTHOR_ID_DUPLICATION_DE$ * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden. * Zeilen ohne Fehler wurden erfolgreich registriert. Wenn Sie daher dieselbe CSV-Datei verwenden und den erfolgreich registrierten Benutzer registrieren, tritt ein doppelter Fehler auf. Bitte erstellen Sie eine CSV-Datei, die nur die Zeilen enthält, in denen der Fehler aufgetreten ist, oder registrieren Sie sie einzeln manuell. 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch nach einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler. - $UNEXPECTED_ERROR$ + $UNEXPECTED_ERROR_DE$ Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. @@ -51,16 +51,16 @@ Chère/Cher $CUSTOMER_NAME$, L'enregistrement groupé des utilisateurs à l'aide du fichier CSV a échoué. La cause et l'emplacement de l'erreur sont indiqués aux points 1, 2 et 3 ci-dessous. ( L = Ligne ) 1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une autre ligne. - $EMAIL_DUPLICATION$ + $EMAIL_DUPLICATION_FR$ 2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans une autre ligne. - $AUTHOR_ID_DUPLICATION$ + $AUTHOR_ID_DUPLICATION_FR$ * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau. * Les lignes sans erreurs ont été enregistrées avec succès. Par conséquent, si vous utilisez le même fichier CSV et enregistrez l'utilisateur qui a été enregistré avec succès, une erreur en double se produira. Veuillez créer un fichier CSV contenant uniquement les lignes où l'erreur s'est produite, ou enregistrez-les manuellement une par une. 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela ne fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur. - $UNEXPECTED_ERROR$ + $UNEXPECTED_ERROR_FR$ Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. diff --git a/dictation_server/src/templates/template_U_122_no_parent.html b/dictation_server/src/templates/template_U_122_no_parent.html index bd9a2af..1b3903a 100644 --- a/dictation_server/src/templates/template_U_122_no_parent.html +++ b/dictation_server/src/templates/template_U_122_no_parent.html @@ -18,11 +18,11 @@ 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in another line.
    - $EMAIL_DUPLICATION$ +   $EMAIL_DUPLICATION_EN$

    2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line.
    - $AUTHOR_ID_DUPLICATION$ +   $AUTHOR_ID_DUPLICATION_EN$

    * E-mail address and Author ID that have already been registered cannot be registered again.
    @@ -35,7 +35,7 @@ 3. An unexpected error occurred during user registration on the following line. If it does not succeed after trying again, please contact your dealer.
    -    $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_EN$

    If you received this e-mail in error, please delete this e-mail from your system.
    @@ -56,12 +56,12 @@ 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in einer anderen Zeile.
    - $EMAIL_DUPLICATION$ +   $EMAIL_DUPLICATION_DE$

    2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen Zeile.
    - $AUTHOR_ID_DUPLICATION$ +   $AUTHOR_ID_DUPLICATION_DE$

    * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden.
    @@ -75,7 +75,7 @@ 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch nach einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler.
    - $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_DE$

    Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
    @@ -95,13 +95,13 @@

    1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une autre ligne.
    - $EMAIL_DUPLICATION$ +   $EMAIL_DUPLICATION_FR$

    2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans une autre ligne.
    - $AUTHOR_ID_DUPLICATION$ +   $AUTHOR_ID_DUPLICATION_FR$

    * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau.
    @@ -115,7 +115,7 @@ 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela ne fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur.
    - $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_FR$

    Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
    diff --git a/dictation_server/src/templates/template_U_122_no_parent.txt b/dictation_server/src/templates/template_U_122_no_parent.txt index f91ba7f..f9b3461 100644 --- a/dictation_server/src/templates/template_U_122_no_parent.txt +++ b/dictation_server/src/templates/template_U_122_no_parent.txt @@ -5,16 +5,16 @@ Dear $CUSTOMER_NAME$, Bulk user registration using the CSV file has failed. The cause and location of the error is shown in 1, 2, and 3 below. ( L = Line ) 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in another line. - $EMAIL_DUPLICATION$ + $EMAIL_DUPLICATION_EN$ 2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line. - $AUTHOR_ID_DUPLICATION$ + $AUTHOR_ID_DUPLICATION_EN$ * E-mail address and Author ID that have already been registered cannot be registered again. * Rows without errors have been successfully registered. Therefore, if you use the same CSV file and register the user that has been successfully registered, a duplicate error will occur. Please create a CSV file containing only the lines where the error occurred, or manually register them one by one. 3. An unexpected error occurred during user registration on the following line. If it does not succeed after trying again, please contact your dealer. -   $UNEXPECTED_ERROR$ +   $UNEXPECTED_ERROR_EN$ If you received this e-mail in error, please delete this e-mail from your system. This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. @@ -26,16 +26,16 @@ Sehr geehrte(r) $CUSTOMER_NAME$, Die Massenregistrierung von Benutzern mithilfe der CSV-Datei ist fehlgeschlagen. Die Ursache und der Ort des Fehlers werden in 1, 2 und 3 unten angezeigt. (L = Linie) 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in einer anderen Zeile. - $EMAIL_DUPLICATION$ + $EMAIL_DUPLICATION_DE$ 2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen Zeile. - $AUTHOR_ID_DUPLICATION$ + $AUTHOR_ID_DUPLICATION_DE$ * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden. * Zeilen ohne Fehler wurden erfolgreich registriert. Wenn Sie daher dieselbe CSV-Datei verwenden und den erfolgreich registrierten Benutzer registrieren, tritt ein doppelter Fehler auf. Bitte erstellen Sie eine CSV-Datei, die nur die Zeilen enthält, in denen der Fehler aufgetreten ist, oder registrieren Sie sie einzeln manuell. 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch nach einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler. - $UNEXPECTED_ERROR$ + $UNEXPECTED_ERROR_DE$ Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. @@ -47,16 +47,16 @@ Chère/Cher $CUSTOMER_NAME$, L'enregistrement groupé des utilisateurs à l'aide du fichier CSV a échoué. La cause et l'emplacement de l'erreur sont indiqués aux points 1, 2 et 3 ci-dessous. ( L = Ligne ) 1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une autre ligne. - $EMAIL_DUPLICATION$ + $EMAIL_DUPLICATION_FR$ 2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans une autre ligne. - $AUTHOR_ID_DUPLICATION$ + $AUTHOR_ID_DUPLICATION_FR$ * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau. * Les lignes sans erreurs ont été enregistrées avec succès. Par conséquent, si vous utilisez le même fichier CSV et enregistrez l'utilisateur qui a été enregistré avec succès, une erreur en double se produira. Veuillez créer un fichier CSV contenant uniquement les lignes où l'erreur s'est produite, ou enregistrez-les manuellement une par une. 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela ne fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur. - $UNEXPECTED_ERROR$ + $UNEXPECTED_ERROR_FR$ Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file From 279a9ab037e870b5417368aca15e8dacff4a68d0 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 7 May 2024 07:11:39 +0000 Subject: [PATCH 086/109] =?UTF-8?q?Merged=20PR=20889:=20function=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4164: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4164) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 - 修正箇所がほかの機能に影響していないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば --- dictation_function/package.json | 3 ++- .../src/functions/licenseAutoAllocation.ts | 18 +----------------- .../licenseAutoAllocationManualRetry.ts | 18 +----------------- 3 files changed, 4 insertions(+), 35 deletions(-) diff --git a/dictation_function/package.json b/dictation_function/package.json index 0be5119..1f1524a 100644 --- a/dictation_function/package.json +++ b/dictation_function/package.json @@ -9,7 +9,8 @@ "clean": "rimraf dist", "prestart": "npm run clean && npm run build", "start": "func start", - "test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test && jest -w 1", + "test": "tsc --noEmit && sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test && jest -w 1", + "typecheck": "tsc --noEmit", "codegen": "sh codegen.sh" }, "dependencies": { diff --git a/dictation_function/src/functions/licenseAutoAllocation.ts b/dictation_function/src/functions/licenseAutoAllocation.ts index 80d4414..8b1b053 100644 --- a/dictation_function/src/functions/licenseAutoAllocation.ts +++ b/dictation_function/src/functions/licenseAutoAllocation.ts @@ -97,23 +97,7 @@ export async function licenseAutoAllocation( context.log("[IN]licenseAutoAllocation"); dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - entities: [User, Account, License, LicenseAllocationHistory], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } + const datasource = await initializeDataSource(context); let redisClient: RedisClient; try { // redis接続 diff --git a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts index b9d74e8..c4b4514 100644 --- a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts +++ b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts @@ -41,23 +41,7 @@ export async function licenseAutoAllocationManualRetry( context.log("[IN]licenseAutoAllocationManualRetry"); dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - entities: [User, Account, License, LicenseAllocationHistory], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } + const datasource = await initializeDataSource(context); let redisClient: RedisClient; try { // redis接続 From ffd6eb4e684f2ddc9653635d720f78bf24143070 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 9 May 2024 01:20:57 +0000 Subject: [PATCH 087/109] Merged PR 886: GET /tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3980: GET /tasks](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3980) - `GET /tasks` のバリデータのUTを追加しました。 ## レビューポイント - テスト項目は適切でしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - テストの修正のみなので影響なし --- .../features/tasks/tasks.controller.spec.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/dictation_server/src/features/tasks/tasks.controller.spec.ts b/dictation_server/src/features/tasks/tasks.controller.spec.ts index 72b8ac5..c753ab4 100644 --- a/dictation_server/src/features/tasks/tasks.controller.spec.ts +++ b/dictation_server/src/features/tasks/tasks.controller.spec.ts @@ -2,6 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TasksController } from './tasks.controller'; import { TasksService } from './tasks.service'; import { ConfigModule } from '@nestjs/config'; +import { TasksRequest } from './types/types'; +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; describe('TasksController', () => { let controller: TasksController; @@ -27,4 +30,89 @@ describe('TasksController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation getTasks', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new TasksRequest(); + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('設定可能な全てのパラメータが指定されている場合、リクエストが成功する', async () => { + const request = new TasksRequest(); + request.limit = 1; + request.offset = 1; + request.status = 'Uploaded'; + request.direction = 'ASC'; + request.paramName = 'JOB_NUMBER'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('limitが0より小さい場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.limit = -1; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('limitが文字列の場合、リクエストが失敗する', async () => { + const request = { limit: 'test' }; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('offsetが0より小さい場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.offset = -1; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('offsetが文字列の場合、リクエストが失敗する', async () => { + const request = { offset: 'test' }; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('statusがタスクのステータス文字列以外の場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.status = 'test'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('directionがASC,DESC以外の場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.direction = 'test'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('paramNameがTASK_LIST_SORTABLE_ATTRIBUTES以外の場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.paramName = 'test'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); From 68df7cd7281849f4247dbb3e9aaefe49b80ec9e4 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 9 May 2024 05:28:03 +0000 Subject: [PATCH 088/109] Merged PR 887: POST /auth/token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3981: POST /auth/token](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3981) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - `POST /auth/token` のバリデータのUTを追加しました。 ## レビューポイント - テスト項目は適切でしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - テストの修正のみなので影響なし --- .../src/features/auth/auth.controller.spec.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/dictation_server/src/features/auth/auth.controller.spec.ts b/dictation_server/src/features/auth/auth.controller.spec.ts index a762b74..aa4aeae 100644 --- a/dictation_server/src/features/auth/auth.controller.spec.ts +++ b/dictation_server/src/features/auth/auth.controller.spec.ts @@ -6,6 +6,9 @@ import { makeDefaultAdB2cMockValue, } from './test/auth.service.mock'; import { ConfigModule } from '@nestjs/config'; +import { TokenRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('AuthController', () => { let controller: AuthController; @@ -30,4 +33,57 @@ describe('AuthController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation token', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new TokenRequest(); + request.idToken = 'test'; + request.type = 'web'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('idTokenが指定されていない場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.type = 'web'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('idTokenが空文字の場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.idToken = ''; + request.type = 'web'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('idTokenが文字列でない場合、リクエストが失敗する', async () => { + const request = { idToken: 1, type: 'web' }; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('typeが指定されていない場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.idToken = 'test'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('typeがweb,mobile,desktop以外の場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.idToken = 'test'; + request.type = 'invalid'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); From 228e21ba783d16c36729682167f9ccbdd34fed9a Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Fri, 10 May 2024 03:56:48 +0000 Subject: [PATCH 089/109] =?UTF-8?q?Merged=20PR=20892:=20migration=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4035: migration修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4035) - job_numberテーブル作成 - マイグレーションのコマンド修正 ## レビューポイント - インデックス・ユニーク制約・外部キー制約の認識は合っているか - マイグレーションのコマンドは基本的にccbで認識あっているか ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - マイグレーションファイル作成のみでほかに影響なし ## 補足 - 相談、参考資料などがあれば --- .../db/migrations/065-create_job_number.sql | 13 +++++++++++++ dictation_server/package.json | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 dictation_server/db/migrations/065-create_job_number.sql diff --git a/dictation_server/db/migrations/065-create_job_number.sql b/dictation_server/db/migrations/065-create_job_number.sql new file mode 100644 index 0000000..3ed6b7d --- /dev/null +++ b/dictation_server/db/migrations/065-create_job_number.sql @@ -0,0 +1,13 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS `job_number` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'ID', + `account_id` BIGINT UNSIGNED NOT NULL COMMENT 'アカウントID', + `job_number` VARCHAR(10) NOT NULL COMMENT 'JOBナンバー', + `updated_at` TIMESTAMP DEFAULT now() COMMENT '更新時刻', + CONSTRAINT `unique_account_id` UNIQUE (`account_id`), + CONSTRAINT `fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`), + INDEX `idx_account_id` (`account_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; + +-- +migrate Down +DROP TABLE `job_number`; \ No newline at end of file diff --git a/dictation_server/package.json b/dictation_server/package.json index 7ca9f5b..a8d50bd 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -24,9 +24,9 @@ "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\"", - "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", - "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=local", + "migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=ccb", "migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test" }, "dependencies": { From 35e2d626a0ff365f8dfff3cb96790635919bff80 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 13 May 2024 05:04:16 +0000 Subject: [PATCH 090/109] =?UTF-8?q?Merged=20PR=20893:=20API=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=EF=BC=88upload-finished=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4033: API修正(upload-finished)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4033) - JobNumberテーブルから取得したJOBNUMBERを使用してタスクを作成する。 - テスト追加 ## レビューポイント - テストケースは足りているか - JOBNUMBERの採番ロジックに誤りはないか ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - JobNumberテーブルからの取得と更新クエリを追加した - 既存のクエリを変更はしていない ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - テストケースを修正し、既存テストがすべて通ることを確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/test/utility.ts | 25 + .../accounts/accounts.service.spec.ts | 2 +- .../src/features/files/files.service.spec.ts | 506 +++++++++++++++++- .../src/features/files/test/utility.ts | 13 + .../src/features/tasks/test/utility.ts | 9 - .../templates/templates.service.spec.ts | 4 +- .../job_number/entity/job_number.entity.ts | 25 + .../job_number.repository.module.ts | 11 + .../job_number.repository.service.ts | 7 + .../tasks/tasks.repository.service.ts | 34 +- 10 files changed, 611 insertions(+), 25 deletions(-) create mode 100644 dictation_server/src/repositories/job_number/entity/job_number.entity.ts create mode 100644 dictation_server/src/repositories/job_number/job_number.repository.module.ts create mode 100644 dictation_server/src/repositories/job_number/job_number.repository.service.ts diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index 28c8b0c..6046a4f 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -9,6 +9,8 @@ import { } from '../../constants'; import { License } from '../../repositories/licenses/entity/license.entity'; import { AccountArchive } from '../../repositories/accounts/entity/account_archive.entity'; +import { Task } from '../../repositories/tasks/entity/task.entity'; +import { JobNumber } from '../../repositories/job_number/entity/job_number.entity'; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -421,3 +423,26 @@ export const getLicenses = async ( }); return licenses; }; + +export const getTasks = async ( + datasource: DataSource, + account_id: number, +): Promise => { + const tasks = await datasource.getRepository(Task).find({ + where: { account_id: account_id }, + }); + return tasks; +}; + +// job_numberを取得する +export const getJobNumber = async ( + datasource: DataSource, + account_id: number, +): Promise => { + const jobNumber = await datasource.getRepository(JobNumber).findOne({ + where: { + account_id: account_id, + }, + }); + return jobNumber; +}; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 60ca795..99ed015 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -41,6 +41,7 @@ import { getLicenses, getUserArchive, getAccountArchive, + getTasks, } from '../../common/test/utility'; import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; @@ -94,7 +95,6 @@ import { truncateAllTable } from '../../common/test/init'; import { createTask, getCheckoutPermissions, - getTasks, } from '../tasks/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; import { TestLogger } from '../../common/test/logger'; diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 0539eed..01d2b1e 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -3,6 +3,7 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { makeBlobstorageServiceMockValue } from './test/files.service.mock'; import { DataSource } from 'typeorm'; import { + createJobNumber, createLicense, createTask, createUserGroupAndMember, @@ -13,6 +14,8 @@ import { import { FilesService } from './files.service'; import { makeContext } from '../../common/log'; import { + getJobNumber, + getTasks, makeHierarchicalAccounts, makeTestAccount, makeTestSimpleAccount, @@ -50,6 +53,7 @@ import { } from '../../constants'; import { truncateAllTable } from '../../common/test/init'; import { TestLogger } from '../../common/test/logger'; +import { TasksService } from '../tasks/tasks.service'; describe('publishUploadSas', () => { let source: DataSource | null = null; @@ -322,6 +326,10 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); // ワークフロータイピストを作成 await createWorkflowTypist(source, workflowId, typistUserId); + + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -335,6 +343,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { const notificationHubService = module.get( NotificationhubService, ); + const result = await service.uploadFinished( makeContext('trackingId', 'requestId'), authorExternalId, @@ -404,7 +413,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { accountId, 'http://blob/url/file.zip', 'file.zip', - '01', + 'Uploaded', typistUserId, authorAuthorId ?? '', ); @@ -423,6 +432,10 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); // ワークフロータイピストを作成 await createWorkflowTypist(source, workflowId, typistUserId); + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000001'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -546,6 +559,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { userGroupId, // ルーティング先のユーザーグループIDを設定 ); + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -668,6 +684,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { userGroupId, // ルーティング先のユーザーグループIDを設定 ); + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -736,6 +755,10 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { role: 'author', author_id: 'AUTHOR_ID', }); + + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -835,6 +858,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000003'); + const service = module.get(FilesService); const spy = jest .spyOn(service['sendGridService'], 'sendMail') @@ -918,6 +944,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000003'); + const service = module.get(FilesService); // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 const spy = jest @@ -1004,6 +1033,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000004'); + const service = module.get(FilesService); // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 const spy = jest @@ -1095,6 +1127,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000004'); + const service = module.get(FilesService); // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 const spy = jest @@ -1361,7 +1396,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { accountId, `http://blob/url/file_${i}.zip`, `file_${i}.zip`, - '01', + 'Uploaded', typistUserId, authorAuthorId ?? '', undefined, @@ -1381,6 +1416,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { false, ); + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + const module = await makeTestingModuleWithBlobAndNotification( source, blobParam, @@ -1487,7 +1525,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { accountId, `http://blob/url/file_${i}.zip`, `file_${i}.zip`, - '01', + 'Uploaded', typistUserId, authorAuthorId ?? '', undefined, @@ -1496,6 +1534,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + const module = await makeTestingModuleWithBlobAndNotification( source, blobParam, @@ -1621,6 +1662,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { 'Backup', false, ); + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); const module = await makeTestingModuleWithBlobAndNotification( source, @@ -1747,6 +1790,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { 'Backup', false, ); + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '99999999'); const module = await makeTestingModuleWithBlobAndNotification( source, @@ -1844,6 +1889,10 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); // ワークフロータイピストを作成 await createWorkflowTypist(source, workflowId, typistUserId); + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '99999999'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -1915,6 +1964,457 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { expect(resultCheckoutPermission.length).toEqual(1); expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); }); + + it('タスク作成時に採番されるジョブナンバーが常に最新タスクのジョブナンバー + 1となる(最新タスクが削除されている場合)', async () => { + if (!source) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { + external_id: authorExternalId, + id: authorUserId, + author_id: authorAuthorId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + author_id: undefined, + }); + // ワークタイプを作成 + const { id: worktypeId, custom_worktype_id } = await createWorktype( + source, + accountId, + 'worktypeId', + ); + // テンプレートファイルを作成 + const { id: templateFileId } = await createTemplateFile( + source, + accountId, + 'templateFile', + 'http://blob/url/templateFile.zip', + ); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + worktypeId, + templateFileId, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, typistUserId); + const blobParam = makeBlobstorageServiceMockValue(); + const notificationParam = makeDefaultNotificationhubServiceMockValue(); + + // タスクを10件作成, ジョブナンバーは00000001から00000010まで + for (let i = 1; i <= 10; i++) { + await createTask( + source, + accountId, + `http://blob/url/file_${i}.zip`, + `file_${i}.zip`, + 'Uploaded', + typistUserId, + authorAuthorId ?? '', + undefined, + undefined, + i.toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + + { + // 初期データ確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(10); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000010'); + } + + const module = await makeTestingModuleWithBlobAndNotification( + source, + blobParam, + notificationParam, + ); + if (!module) fail(); + const service = module.get(FilesService); + const taskService = module.get(TasksService); + const notificationHubService = module.get( + NotificationhubService, + ); + + // 最新のジョブナンバーのタスクを取得 + const latestTask = await getTaskFromJobNumber(source, '00000010'); + await taskService.deleteTask( + makeContext('trackingId', 'requestId'), + authorExternalId, + latestTask?.audio_file_id ?? 0, // 最新タスクのaudioFileId + ); + + { + // タスク削除確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(9); + // JobNumberが00000010のタスクが存在しないことを確認 + expect( + tasks.find((task) => task.job_number === '00000010'), + ).toBeUndefined(); + } + + const result = await service.uploadFinished( + makeContext('trackingId', 'requestId'), + authorExternalId, + 'http://blob/url/file.zip', + authorAuthorId ?? '', + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + 256, + '01', + 'DS2', + 'comment', + custom_worktype_id, + optionItemList, + false, + ); + expect(result.jobNumber).toEqual('00000011'); + // job_numberテーブルが正しく更新されているか確認 + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000011'); + + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(notificationHubService.notify).toHaveBeenCalledWith( + makeContext('trackingId', 'requestId'), + [`user_${typistUserId}`], + { + authorId: 'AUTHOR_ID', + filename: 'file', + priority: 'High', + uploadedAt: '2023-05-26T11:22:33.444', + }, + ); + // 作成したタスクを取得 + const resultTask = await getTaskFromJobNumber(source, result.jobNumber); + // タスクのチェックアウト権限を取得 + const resultCheckoutPermission = await getCheckoutPermissions( + source, + resultTask?.id ?? 0, + ); + // タスクのテンプレートファイルIDを確認 + expect(resultTask?.template_file_id).toEqual(templateFileId); + // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 + expect(resultCheckoutPermission.length).toEqual(1); + expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); + }); + it('タスク作成時に採番されるジョブナンバーが常に最新タスクのジョブナンバー + 1となる(途中のタスクが削除されている場合)', async () => { + if (!source) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { + external_id: authorExternalId, + id: authorUserId, + author_id: authorAuthorId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + author_id: undefined, + }); + // ワークタイプを作成 + const { id: worktypeId, custom_worktype_id } = await createWorktype( + source, + accountId, + 'worktypeId', + ); + // テンプレートファイルを作成 + const { id: templateFileId } = await createTemplateFile( + source, + accountId, + 'templateFile', + 'http://blob/url/templateFile.zip', + ); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + worktypeId, + templateFileId, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, typistUserId); + const blobParam = makeBlobstorageServiceMockValue(); + const notificationParam = makeDefaultNotificationhubServiceMockValue(); + + // タスクを10件作成, ジョブナンバーは00000001から00000010まで + for (let i = 1; i <= 10; i++) { + await createTask( + source, + accountId, + `http://blob/url/file_${i}.zip`, + `file_${i}.zip`, + 'Uploaded', + typistUserId, + authorAuthorId ?? '', + undefined, + undefined, + i.toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + + { + // 初期データ確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(10); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000010'); + } + + const module = await makeTestingModuleWithBlobAndNotification( + source, + blobParam, + notificationParam, + ); + if (!module) fail(); + const service = module.get(FilesService); + const taskService = module.get(TasksService); + const notificationHubService = module.get( + NotificationhubService, + ); + + // 途中のジョブナンバーのタスクを取得 + const latestTask = await getTaskFromJobNumber(source, '00000005'); + await taskService.deleteTask( + makeContext('trackingId', 'requestId'), + authorExternalId, + latestTask?.audio_file_id ?? 0, // 最新タスクのaudioFileId + ); + + { + // タスク削除確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(9); + // JobNumberが00000005のタスクが存在しないことを確認 + expect( + tasks.find((task) => task.job_number === '00000005'), + ).toBeUndefined(); + } + + const result = await service.uploadFinished( + makeContext('trackingId', 'requestId'), + authorExternalId, + 'http://blob/url/file.zip', + authorAuthorId ?? '', + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + 256, + '01', + 'DS2', + 'comment', + custom_worktype_id, + optionItemList, + false, + ); + expect(result.jobNumber).toEqual('00000011'); + // job_numberテーブルが正しく更新されているか確認 + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000011'); + + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(notificationHubService.notify).toHaveBeenCalledWith( + makeContext('trackingId', 'requestId'), + [`user_${typistUserId}`], + { + authorId: 'AUTHOR_ID', + filename: 'file', + priority: 'High', + uploadedAt: '2023-05-26T11:22:33.444', + }, + ); + // 作成したタスクを取得 + const resultTask = await getTaskFromJobNumber(source, result.jobNumber); + // タスクのチェックアウト権限を取得 + const resultCheckoutPermission = await getCheckoutPermissions( + source, + resultTask?.id ?? 0, + ); + // タスクのテンプレートファイルIDを確認 + expect(resultTask?.template_file_id).toEqual(templateFileId); + // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 + expect(resultCheckoutPermission.length).toEqual(1); + expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); + }); + it('タスク作成時に採番されるジョブナンバーが常に最新タスクのジョブナンバー + 1となる(最新タスクのジョブナンバーが99999999で削除されている場合)', async () => { + if (!source) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { + external_id: authorExternalId, + id: authorUserId, + author_id: authorAuthorId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + author_id: undefined, + }); + // ワークタイプを作成 + const { id: worktypeId, custom_worktype_id } = await createWorktype( + source, + accountId, + 'worktypeId', + ); + // テンプレートファイルを作成 + const { id: templateFileId } = await createTemplateFile( + source, + accountId, + 'templateFile', + 'http://blob/url/templateFile.zip', + ); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + worktypeId, + templateFileId, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, typistUserId); + const blobParam = makeBlobstorageServiceMockValue(); + const notificationParam = makeDefaultNotificationhubServiceMockValue(); + + // タスクを10件作成, ジョブナンバーは99999989から99999999まで + for (let i = 99999989; i <= 99999999; i++) { + await createTask( + source, + accountId, + `http://blob/url/file_${i}.zip`, + `file_${i}.zip`, + 'Uploaded', + typistUserId, + authorAuthorId ?? '', + undefined, + undefined, + i.toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '99999999'); + + { + // 初期データ確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(11); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('99999999'); + } + + + + const module = await makeTestingModuleWithBlobAndNotification( + source, + blobParam, + notificationParam, + ); + if (!module) fail(); + const service = module.get(FilesService); + const taskService = module.get(TasksService); + const notificationHubService = module.get( + NotificationhubService, + ); + + // 最新のジョブナンバーのタスクを取得 + const latestTask = await getTaskFromJobNumber(source, '99999999'); + await taskService.deleteTask( + makeContext('trackingId', 'requestId'), + authorExternalId, + latestTask?.audio_file_id ?? 0, // 最新タスクのaudioFileId + ); + + { + // タスク削除確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(10); + // JobNumberが99999999のタスクが存在しないことを確認 + expect( + tasks.find((task) => task.job_number === '99999999'), + ).toBeUndefined(); + } + + const result = await service.uploadFinished( + makeContext('trackingId', 'requestId'), + authorExternalId, + 'http://blob/url/file.zip', + authorAuthorId ?? '', + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + 256, + '01', + 'DS2', + 'comment', + custom_worktype_id, + optionItemList, + false, + ); + expect(result.jobNumber).toEqual('00000001'); + // job_numberテーブルが正しく更新されているか確認 + // 最新タスクのジョブナンバーが99999999の時、新規作成されたタスクのジョブナンバーは00000001になる + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000001'); + + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(notificationHubService.notify).toHaveBeenCalledWith( + makeContext('trackingId', 'requestId'), + [`user_${typistUserId}`], + { + authorId: 'AUTHOR_ID', + filename: 'file', + priority: 'High', + uploadedAt: '2023-05-26T11:22:33.444', + }, + ); + // 作成したタスクを取得 + const resultTask = await getTaskFromJobNumber(source, result.jobNumber); + // タスクのチェックアウト権限を取得 + const resultCheckoutPermission = await getCheckoutPermissions( + source, + resultTask?.id ?? 0, + ); + // タスクのテンプレートファイルIDを確認 + expect(resultTask?.template_file_id).toEqual(templateFileId); + // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 + expect(resultCheckoutPermission.length).toEqual(1); + expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); + }); }); describe('音声ファイルダウンロードURL取得', () => { diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index c001e6a..9239a1f 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -44,6 +44,7 @@ import { import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity'; import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity'; import { License } from '../../../repositories/licenses/entity/license.entity'; +import { JobNumber } from '../../../repositories/job_number/entity/job_number.entity'; export const createTask = async ( datasource: DataSource, @@ -109,6 +110,18 @@ export const createTask = async ( return { audioFileId: audioFile.id, taskId: task.id }; }; +// job_numberテーブルにレコードを作成する +export const createJobNumber = async ( + datasource: DataSource, + accountId: number, + jobNumber: string, +): Promise => { + await datasource.getRepository(JobNumber).insert({ + account_id: accountId, + job_number: jobNumber, + }); +}; + export const getTaskFromJobNumber = async ( datasource: DataSource, jobNumber: string, diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 31928e0..676f1ad 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -263,15 +263,6 @@ export const getTask = async ( return task; }; -export const getTasks = async ( - datasource: DataSource, - account_id: number, -): Promise => { - const tasks = await datasource.getRepository(Task).find({ - where: { account_id: account_id }, - }); - return tasks; -}; export const getCheckoutPermissions = async ( datasource: DataSource, diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index 37c3813..442dc87 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -6,7 +6,7 @@ import { getTemplateFiles, updateTaskTemplateFile, } from './test/utility'; -import { makeTestAccount, makeTestUser } from '../../common/test/utility'; +import { getTasks, makeTestAccount, makeTestUser } from '../../common/test/utility'; import { makeContext } from '../../common/log'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; import { HttpException, HttpStatus } from '@nestjs/common'; @@ -14,7 +14,7 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; import { overrideBlobstorageService } from '../../common/test/overrides'; import { TASK_STATUS, USER_ROLES } from '../../constants'; -import { createTask, getTasks } from '../tasks/test/utility'; +import { createTask } from '../tasks/test/utility'; import { createWorkflow, createWorkflowTypist, diff --git a/dictation_server/src/repositories/job_number/entity/job_number.entity.ts b/dictation_server/src/repositories/job_number/entity/job_number.entity.ts new file mode 100644 index 0000000..ec2373e --- /dev/null +++ b/dictation_server/src/repositories/job_number/entity/job_number.entity.ts @@ -0,0 +1,25 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { bigintTransformer } from '../../../common/entity'; + +@Entity({ name: 'job_number' }) +export class JobNumber { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'bigint', transformer: bigintTransformer }) + account_id: number; + + @Column() + job_number: string; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: 'datetime', + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; +} diff --git a/dictation_server/src/repositories/job_number/job_number.repository.module.ts b/dictation_server/src/repositories/job_number/job_number.repository.module.ts new file mode 100644 index 0000000..a39f9bd --- /dev/null +++ b/dictation_server/src/repositories/job_number/job_number.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { JobNumber } from './entity/job_number.entity'; +import { JobNumberRepositoryService } from './job_number.repository.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([JobNumber])], + providers: [JobNumberRepositoryService], + exports: [JobNumberRepositoryService], +}) +export class JobNumberRepositoryModule {} diff --git a/dictation_server/src/repositories/job_number/job_number.repository.service.ts b/dictation_server/src/repositories/job_number/job_number.repository.service.ts new file mode 100644 index 0000000..5790f00 --- /dev/null +++ b/dictation_server/src/repositories/job_number/job_number.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class JobNumberRepositoryService { + constructor(private dataSource: DataSource) {} +} diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index d5b2a37..1e30896 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -54,6 +54,7 @@ import { } from '../../common/repository'; import { Context } from '../../common/log'; import { UserNotFoundError } from '../users/errors/types'; +import { JobNumber } from '../job_number/entity/job_number.entity'; @Injectable() export class TasksRepositoryService { @@ -905,25 +906,29 @@ export class TasksRepositoryService { const taskRepo = entityManager.getRepository(Task); - // バグ 3954: [4/8リリース]タスクをすべてBackupした後、タスクを作成するとジョブナンバーが1から採番される(暫定対応) - // アカウント内で最新タスクのタスクを取得し、そのJOBナンバーをインクリメントして新しいタスクのJOBナンバーを設定する - const lastTask = await taskRepo.findOne({ + // バグ 3954: [4/8リリース]タスクをすべてBackupした後、タスクを作成するとジョブナンバーが1から採番される(恒久対応) + // job_numberテーブルから最新のJOBナンバーを取得 + const jobNumberRepo = entityManager.getRepository(JobNumber); + const currentJobNumberData = await jobNumberRepo.findOne({ where: { account_id: account_id }, - order: { created_at: 'DESC', job_number: 'DESC' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, lock: { mode: 'pessimistic_write' }, }); + // JOBナンバーが存在しない場合はエラー + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!currentJobNumberData) { + throw new Error(`JobNumber not exists. account_id:${account_id}`); + } - let newJobNumber = '00000001'; - if (!lastTask) { - // 初回は00000001 - newJobNumber = '00000001'; - } else if (lastTask.job_number === '99999999') { + let newJobNumber: string = ''; + if (currentJobNumberData.job_number === '99999999') { // 末尾なら00000001に戻る newJobNumber = '00000001'; } else { // 最新のJOBナンバーをインクリメントして次の番号とする - newJobNumber = `${Number(lastTask.job_number) + 1}`.padStart(8, '0'); + newJobNumber = `${ + Number(currentJobNumberData.job_number) + 1 + }`.padStart(8, '0'); } task.job_number = newJobNumber; @@ -935,6 +940,15 @@ export class TasksRepositoryService { context, ); + // JobNumberを更新 + await updateEntity( + jobNumberRepo, + { account_id: account_id }, + { job_number: newJobNumber }, + this.isCommentOut, + context, + ); + const optionItems = paramOptionItems.map((x) => { return { audio_file_id: persisted.audio_file_id, From dfdc6a33ad6eb9f5efdd32ff70931a98505f4225 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Tue, 14 May 2024 02:12:41 +0000 Subject: [PATCH 091/109] =?UTF-8?q?Merged=20PR=20894:=20API=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3(=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E7=B3=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4034: API修正(アカウント削除系)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4034) - アカウント削除時にJobNumberテーブルのレコードも削除するように修正 - パートナー削除時にJobNumberテーブルのレコードも削除するように修正 - テスト修正 ## レビューポイント - テストケースに不足はないか - ジョブナンバーテーブルの削除順に問題はないか ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - JobNumberテーブルのレコードを削除する処理を追加した - 既存のクエリに影響はなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 既存のテストが通ることを確認 - テストしていなかった観点(ソート条件も削除されているか等)も確認するように修正 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/test/modules.ts | 2 + dictation_server/src/common/test/utility.ts | 78 ++++++ .../accounts/accounts.service.spec.ts | 233 +++++++++++++++++- .../src/features/accounts/accounts.service.ts | 6 +- .../src/features/accounts/test/utility.ts | 2 +- .../src/features/auth/test/utility.ts | 2 + .../src/features/files/files.service.spec.ts | 21 +- .../src/features/files/test/utility.ts | 12 - .../src/features/tasks/tasks.service.spec.ts | 7 +- .../src/features/tasks/test/utility.ts | 1 - .../templates/templates.service.spec.ts | 6 +- .../src/features/users/test/utility.ts | 13 - .../accounts/accounts.repository.service.ts | 19 ++ 13 files changed, 346 insertions(+), 56 deletions(-) diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 8d50139..149c8de 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -40,6 +40,7 @@ import { TermsModule } from '../../features/terms/terms.module'; import { CacheModule } from '@nestjs/common'; import { RedisModule } from '../../gateways/redis/redis.module'; import { RedisService } from '../../gateways/redis/redis.service'; +import { JobNumberRepositoryModule } from '../../repositories/job_number/job_number.repository.module'; export const makeTestingModule = async ( datasource: DataSource, @@ -79,6 +80,7 @@ export const makeTestingModule = async ( SortCriteriaRepositoryModule, WorktypesRepositoryModule, TermsRepositoryModule, + JobNumberRepositoryModule, RedisModule, CacheModule.register({ isGlobal: true, ttl: 86400 }), ], diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index 6046a4f..d784663 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -11,6 +11,7 @@ import { License } from '../../repositories/licenses/entity/license.entity'; import { AccountArchive } from '../../repositories/accounts/entity/account_archive.entity'; import { Task } from '../../repositories/tasks/entity/task.entity'; import { JobNumber } from '../../repositories/job_number/entity/job_number.entity'; +import { SortCriteria } from '../../repositories/sort_criteria/entity/sort_criteria.entity'; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -239,6 +240,11 @@ export const makeTestAccount = async ( if (!account || !admin) { throw new Error('Unexpected null'); } + // sort_criteriaテーブルにデータを追加 + await createSortCriteria(datasource, userId, 'JOB_NUMBER', 'ASC'); + + // job_numberテーブルにデータを追加 + await createJobNumber(datasource, accountId, '00000000'); return { account: account, @@ -325,6 +331,8 @@ export const makeTestUser = async ( if (!user) { throw new Error('Unexpected null'); } + // sort_criteriaテーブルにデータを追加 + await createSortCriteria(datasource, user.id, 'FILE_LENGTH', 'ASC'); return user; }; @@ -434,6 +442,33 @@ export const getTasks = async ( return tasks; }; +// job_numberテーブルにレコードを作成する +export const createJobNumber = async ( + datasource: DataSource, + accountId: number, + jobNumber: string, +): Promise => { + await datasource.getRepository(JobNumber).insert({ + account_id: accountId, + job_number: jobNumber, + }); +}; + + +// job_numberテーブルのレコードを更新する +export const updateJobNumber = async ( + datasource: DataSource, + accountId: number, + jobNumber: string, +): Promise => { + await datasource.getRepository(JobNumber).update( + { account_id: accountId }, + { + job_number: jobNumber, + }, + ); +}; + // job_numberを取得する export const getJobNumber = async ( datasource: DataSource, @@ -446,3 +481,46 @@ export const getJobNumber = async ( }); return jobNumber; }; + +// sort_criteriaを作成する +export const createSortCriteria = async ( + datasource: DataSource, + userId: number, + parameter: string, + direction: string, +): Promise => { + await datasource.getRepository(SortCriteria).insert({ + user_id: userId, + parameter: parameter, + direction: direction, + }); +}; + +// 指定したユーザーのsort_criteriaを更新する +export const updateSortCriteria = async ( + datasource: DataSource, + userId: number, + parameter: string, + direction: string, +): Promise => { + await datasource.getRepository(SortCriteria).update( + { user_id: userId }, + { + parameter: parameter, + direction: direction, + }, + ); +}; + +// 指定したユーザーのsort_criteriaを取得する +export const getSortCriteria = async ( + datasource: DataSource, + userId: number, +): Promise => { + const sortCriteria = await datasource.getRepository(SortCriteria).findOne({ + where: { + user_id: userId, + }, + }); + return sortCriteria; +}; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 99ed015..dcad6ac 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -21,7 +21,7 @@ import { createWorktype, getLicenseOrders, getOptionItems, - getSortCriteria, + getSortCriteriaList, getTypistGroup, getTypistGroupMember, getTypistGroupMembers, @@ -42,6 +42,8 @@ import { getUserArchive, getAccountArchive, getTasks, + getSortCriteria, + getJobNumber, } from '../../common/test/utility'; import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; @@ -92,10 +94,7 @@ import { } from '../workflows/test/utility'; import { UsersService } from '../users/users.service'; import { truncateAllTable } from '../../common/test/init'; -import { - createTask, - getCheckoutPermissions, -} from '../tasks/test/utility'; +import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; import { TestLogger } from '../../common/test/logger'; import { Account } from '../../repositories/accounts/entity/account.entity'; @@ -418,7 +417,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -486,7 +485,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -557,7 +556,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -630,7 +629,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(1); const users = await getUsers(source); expect(users.length).toBe(1); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(1); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -715,7 +714,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -803,7 +802,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(1); const users = await getUsers(source); expect(users.length).toBe(1); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(1); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -7227,6 +7226,27 @@ describe('deleteAccountAndData', () => { ); expect(LicenseAllocationHistoryRecordA.length).toBe(0); + // 第五階層のアカウントAの管理者ユーザーが持つsortCriteriaが削除されていること + const sortCriteriaAccuntAAdmin = await getSortCriteria( + source, + tier5AccountsA.admin.id, + ); + expect(sortCriteriaAccuntAAdmin).toBe(null); + + // 第五階層のアカウントAの一般ユーザーが持つsortCriteriaが削除されていること + const sortCriteriaAccuntAUser = await getSortCriteria( + source, + userA?.id ?? 0, + ); + expect(sortCriteriaAccuntAUser).toBe(null); + + // 第五階層のアカウントAのJobNumberが削除されていること + const jobNumberAccuntA = await getJobNumber( + source, + tier5AccountsA.account.id, + ); + expect(jobNumberAccuntA).toBe(null); + // 第五階層のアカウントBは削除されていないこと const accountRecordB = await getAccount(source, tier5AccountsB.account.id); expect(accountRecordB?.id).not.toBeNull(); @@ -7266,6 +7286,31 @@ describe('deleteAccountAndData', () => { await getLicenseAllocationHistoryArchive(source); expect(LicenseAllocationHistoryArchive.length).toBe(1); + // 第五階層のアカウントBの管理者ユーザーが持つsortCriteriaが削除されていないこと + const sortCriteriaAccuntBAdmin = await getSortCriteria( + source, + tier5AccountsB.admin.id, + ); + expect(sortCriteriaAccuntBAdmin?.user_id).toBe(tier5AccountsB.admin.id); + expect(sortCriteriaAccuntBAdmin?.direction).toBe('ASC'); + expect(sortCriteriaAccuntBAdmin?.parameter).toBe('JOB_NUMBER'); + // 第五階層のアカウントBの一般ユーザーが持つsortCriteriaが削除されていないこと + const sortCriteriaAccuntBUser = await getSortCriteria( + source, + userB?.id ?? 0, + ); + expect(sortCriteriaAccuntBUser?.user_id).toBe(userB?.id ?? 0); + expect(sortCriteriaAccuntBUser?.direction).toBe('ASC'); + expect(sortCriteriaAccuntBUser?.parameter).toBe('FILE_LENGTH'); + + // 第五階層のアカウントBのJobNumberが削除されていないこと + const jobNumberAccuntB = await getJobNumber( + source, + tier5AccountsB.account.id, + ); + expect(jobNumberAccuntB?.account_id).toBe(tier5AccountsB.account.id); + expect(jobNumberAccuntB?.job_number).toBe('00000000'); + expect(_subject).toBe('Account Deleted Notification [U-111]'); expect(_url).toBe('http://localhost:8081/'); }); @@ -8524,6 +8569,21 @@ describe('deletePartnerAccount', () => { tier4Account.id, ); expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); } // パートナーアカウント情報の削除 @@ -8539,6 +8599,25 @@ describe('deletePartnerAccount', () => { expect(account4Record).toBe(null); const userRecordA = await getUser(source, tier4Admin?.id ?? 0); expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); // パートナーアカウントのライセンスが削除されていること const licenseRecord = await source.manager.find(License, { @@ -8716,6 +8795,21 @@ describe('deletePartnerAccount', () => { tier4Account.id, ); expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); } try { @@ -8865,6 +8959,21 @@ describe('deletePartnerAccount', () => { tier4Account.id, ); expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); } try { @@ -9028,6 +9137,21 @@ describe('deletePartnerAccount', () => { tier4Account.id, ); expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); } // パートナーアカウント情報の削除 @@ -9047,6 +9171,25 @@ describe('deletePartnerAccount', () => { expect(account4Record).toBe(null); const userRecordA = await getUser(source, tier4Admin?.id ?? 0); expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); // パートナーアカウントのライセンスが削除されていること const licenseRecord = await source.manager.find(License, { @@ -9234,6 +9377,21 @@ describe('deletePartnerAccount', () => { tier4Account.id, ); expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); } // パートナーアカウント情報の削除 @@ -9253,6 +9411,25 @@ describe('deletePartnerAccount', () => { expect(account4Record).toBe(null); const userRecordA = await getUser(source, tier4Admin?.id ?? 0); expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); // パートナーアカウントのライセンスが削除されていること const licenseRecord = await source.manager.find(License, { @@ -9426,6 +9603,21 @@ describe('deletePartnerAccount', () => { tier4Account.id, ); expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); } // パートナーアカウント情報の削除 @@ -9441,6 +9633,25 @@ describe('deletePartnerAccount', () => { expect(account4Record).toBe(null); const userRecordA = await getUser(source, tier4Admin?.id ?? 0); expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); // パートナーアカウントのライセンスが削除されていること const licenseRecord = await source.manager.find(License, { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 75b53e9..6547f1c 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -2359,7 +2359,7 @@ export class AccountsService { country = targetAccount.country; } catch (e) { // アカウントの削除に失敗した場合はエラーを返す - this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log(`[${context.getTrackingId()}] error=${e}`); this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.deleteAccountAndData.name}`, ); @@ -2382,7 +2382,7 @@ export class AccountsService { ); } catch (e) { // ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 - this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log(`[${context.getTrackingId()}] error=${e}`); this.logger.log( `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete ADB2C users: ${accountId}, users_id: ${dbUsers.map( (x) => x.external_id, @@ -2398,7 +2398,7 @@ export class AccountsService { ); } catch (e) { // blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了 - this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log(`[${context.getTrackingId()}] error=${e}`); this.logger.log( `${MANUAL_RECOVERY_REQUIRED}[${context.getTrackingId()}] Failed to delete blob container: ${accountId}, country: ${country}`, ); diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index 046c2d2..473a258 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -17,7 +17,7 @@ import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.e * @param dataSource データソース * @returns 該当ソート条件一覧 */ -export const getSortCriteria = async (dataSource: DataSource) => { +export const getSortCriteriaList = async (dataSource: DataSource) => { return await dataSource.getRepository(SortCriteria).find(); }; diff --git a/dictation_server/src/features/auth/test/utility.ts b/dictation_server/src/features/auth/test/utility.ts index fbbc0e4..fc78418 100644 --- a/dictation_server/src/features/auth/test/utility.ts +++ b/dictation_server/src/features/auth/test/utility.ts @@ -2,6 +2,7 @@ import { DataSource } from 'typeorm'; import { Term } from '../../../repositories/terms/entity/term.entity'; import { Account } from '../../../repositories/accounts/entity/account.entity'; import { User } from '../../../repositories/users/entity/user.entity'; +import { JobNumber } from '../../../repositories/job_number/entity/job_number.entity'; export const createTermInfo = async ( datasource: DataSource, @@ -34,5 +35,6 @@ export const deleteAccount = async ( id: number, ): Promise => { await dataSource.getRepository(User).delete({ account_id: id }); + await dataSource.getRepository(JobNumber).delete({ account_id: id }); await dataSource.getRepository(Account).delete({ id: id }); }; diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 01d2b1e..813b155 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -3,7 +3,6 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { makeBlobstorageServiceMockValue } from './test/files.service.mock'; import { DataSource } from 'typeorm'; import { - createJobNumber, createLicense, createTask, createUserGroupAndMember, @@ -14,12 +13,14 @@ import { import { FilesService } from './files.service'; import { makeContext } from '../../common/log'; import { + createJobNumber, getJobNumber, getTasks, makeHierarchicalAccounts, makeTestAccount, makeTestSimpleAccount, makeTestUser, + updateJobNumber, } from '../../common/test/utility'; import { makeTestingModule } from '../../common/test/modules'; import { @@ -858,8 +859,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } - // 最新のジョブナンバーでjob_numberテーブルを作成 - await createJobNumber(source, accountId, '00000003'); + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000003'); const service = module.get(FilesService); const spy = jest @@ -944,8 +945,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } - // 最新のジョブナンバーでjob_numberテーブルを作成 - await createJobNumber(source, accountId, '00000003'); + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000003'); const service = module.get(FilesService); // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 @@ -1033,8 +1034,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } - // 最新のジョブナンバーでjob_numberテーブルを作成 - await createJobNumber(source, accountId, '00000004'); + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000004'); const service = module.get(FilesService); // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 @@ -1127,8 +1128,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); } - // 最新のジョブナンバーでjob_numberテーブルを作成 - await createJobNumber(source, accountId, '00000004'); + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000004'); const service = module.get(FilesService); // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 @@ -2335,8 +2336,6 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { expect(jobNumber?.job_number).toEqual('99999999'); } - - const module = await makeTestingModuleWithBlobAndNotification( source, blobParam, diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index 9239a1f..5b96e99 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -110,18 +110,6 @@ export const createTask = async ( return { audioFileId: audioFile.id, taskId: task.id }; }; -// job_numberテーブルにレコードを作成する -export const createJobNumber = async ( - datasource: DataSource, - accountId: number, - jobNumber: string, -): Promise => { - await datasource.getRepository(JobNumber).insert({ - account_id: accountId, - job_number: jobNumber, - }); -}; - export const getTaskFromJobNumber = async ( datasource: DataSource, jobNumber: string, diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 5fe8662..04af977 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -24,9 +24,11 @@ import { import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service'; import { makeContext } from '../../common/log'; import { + createSortCriteria, makeTestAccount, makeTestSimpleAccount, makeTestUser, + updateSortCriteria, } from '../../common/test/utility'; import { ADMIN_ROLES, @@ -36,7 +38,6 @@ import { USER_ROLES, } from '../../constants'; import { makeTestingModule } from '../../common/test/modules'; -import { createSortCriteria } from '../users/test/utility'; import { createWorktype } from '../accounts/test/utility'; import { createWorkflow, @@ -3863,7 +3864,7 @@ describe('getNextTask', () => { role: USER_ROLES.TYPIST, }); - await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + await updateSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); const { taskId: taskId1 } = await createTask( source, @@ -4007,7 +4008,7 @@ describe('getNextTask', () => { role: USER_ROLES.TYPIST, }); - await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + await updateSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask( source, diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 676f1ad..b0c9637 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -263,7 +263,6 @@ export const getTask = async ( return task; }; - export const getCheckoutPermissions = async ( datasource: DataSource, task_id: number, diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index 442dc87..aca4887 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -6,7 +6,11 @@ import { getTemplateFiles, updateTaskTemplateFile, } from './test/utility'; -import { getTasks, makeTestAccount, makeTestUser } from '../../common/test/utility'; +import { + getTasks, + makeTestAccount, + makeTestUser, +} from '../../common/test/utility'; import { makeContext } from '../../common/log'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; import { HttpException, HttpStatus } from '@nestjs/common'; diff --git a/dictation_server/src/features/users/test/utility.ts b/dictation_server/src/features/users/test/utility.ts index 849aa34..99a6413 100644 --- a/dictation_server/src/features/users/test/utility.ts +++ b/dictation_server/src/features/users/test/utility.ts @@ -165,16 +165,3 @@ export const makeTestingModuleWithAdb2c = async ( console.log(e); } }; - -export const createSortCriteria = async ( - datasource: DataSource, - userId: number, - parameter: string, - direction: string, -): Promise => { - await datasource.getRepository(SortCriteria).insert({ - user_id: userId, - parameter: parameter, - direction: direction, - }); -}; diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index b5cbe5d..ecd571f 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -67,6 +67,7 @@ import { PartnerLicenseInfoForRepository, } from '../../features/accounts/types/types'; import { AccountArchive } from './entity/account_archive.entity'; +import { JobNumber } from '../job_number/entity/job_number.entity'; @Injectable() export class AccountsRepositoryService { @@ -1245,6 +1246,15 @@ export class AccountsRepositoryService { context, ); + // jobNumberのテーブルのレコードを削除する + const jobNumberRepo = entityManager.getRepository(JobNumber); + await deleteEntity( + jobNumberRepo, + { account_id: accountId }, + this.isCommentOut, + context, + ); + // アカウントを削除 await deleteEntity( accountRepo, @@ -1624,6 +1634,15 @@ export class AccountsRepositoryService { context, ); + // JobNumberのテーブルのレコードを削除する + const jobNumberRepo = entityManager.getRepository(JobNumber); + await deleteEntity( + jobNumberRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + // アカウントを削除 await deleteEntity( accountRepo, From fe5e8b8e1c87731e3a3ebd2a8e8075d1df41adde Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Tue, 14 May 2024 07:18:57 +0000 Subject: [PATCH 092/109] =?UTF-8?q?Merged=20PR=20895:=20API=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3(=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E4=BD=9C=E6=88=90=E7=B3=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4043: API修正(アカウント作成系)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4043) - アカウント作成時にJobNumberの初期値を設定するように修正 - パートナーアカウント作成時にJobNumberの初期値を設定するように修正 - リカバリ処理にJobNumberのレコード削除を追加 - テスト修正 ## レビューポイント - JobNumber作成処理の追加する箇所に問題はないか - テストケースに不足はないか ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - 既存のクエリに修正はなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - 既存テストが通ることを確認 - パートナーアカウント作成のテストにメール送信内容のチェックを追加 - ソート条件が作成・削除されていることを確認するテストを追加 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/test/utility.ts | 1 - dictation_server/src/constants/index.ts | 13 +++++ .../accounts/accounts.service.spec.ts | 53 ++++++++++++++++++- .../accounts/accounts.repository.service.ts | 23 ++++++++ .../tasks/tasks.repository.service.ts | 3 +- 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index d784663..2d531a4 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -454,7 +454,6 @@ export const createJobNumber = async ( }); }; - // job_numberテーブルのレコードを更新する export const updateJobNumber = async ( datasource: DataSource, diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 6478a26..c91dd23 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -339,3 +339,16 @@ export const STORAGE_SIZE_PER_LICENSE = 5; * @const {number} */ export const STORAGE_WARNING_THRESHOLD_PERCENT = 80; + +/** + * JobNumberの初期値 + * @const {string} + */ +export const INITIAL_JOB_NUMBER = '00000000'; + + +/** + * JobNumberの最大値 + * @const {string} + */ +export const MAX_JOB_NUMBER = '99999999'; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index dcad6ac..1dc617b 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -49,6 +49,7 @@ import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; import { ADB2C_SIGN_IN_TYPE, + INITIAL_JOB_NUMBER, LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, @@ -223,6 +224,16 @@ describe('createAccount', () => { expect(user?.account_id).toBe(accountId); expect(user?.role).toBe(role); + // jobNumberの初期値が正しく設定されているか確認 + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); + + // sortCriteriaが正しく設定されているか確認 + const sortCriteria = await getSortCriteria(source, user?.id ?? 0); + expect(sortCriteria?.user_id).toBe(user?.id); + expect(sortCriteria?.direction).toBe('ASC'); + expect(sortCriteria?.parameter).toBe('JOB_NUMBER'); + // 想定通りのメールが送られているか確認 expect(_subject).toBe('User Registration Notification [U-102]'); expect( @@ -878,7 +889,26 @@ describe('createPartnerAccount', () => { }, }); - overrideSendgridService(service, {}); + let _subject = ''; + let _url: string | undefined = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); overrideBlobstorageService(service, { createContainer: async () => { @@ -918,6 +948,19 @@ describe('createPartnerAccount', () => { expect(createdAccount?.tier).toBe(2); expect(createdAccount?.primary_admin_user_id).toBe(createdUser?.id); expect(createdAccount?.secondary_admin_user_id).toBe(null); + const sortCriteria = await getSortCriteria(source, createdUser?.id ?? 0); + expect(sortCriteria).not.toBeNull(); + expect(sortCriteria?.user_id).toBe(createdUser?.id); + expect(sortCriteria?.direction).toBe('ASC'); + expect(sortCriteria?.parameter).toBe('JOB_NUMBER'); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); + + // 想定通りのメールが送られているか確認 + expect(_subject).toBe('User Registration Notification [U-114]'); + expect( + _url?.startsWith('http://localhost:8081/mail-confirm/user?verify='), + ).toBeTruthy(); } }); @@ -1308,6 +1351,10 @@ describe('createPartnerAccount', () => { expect(users.length).toBe(2); expect(users[0].external_id).toBe(parentExternalId); expect(users[1].external_id).toBe(partnerExternalId); + const sortCriteria = await getSortCriteriaList(source); + expect(sortCriteria.length).toBe(2); + const jobNumber = await getJobNumber(source, accounts[1].id); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); } @@ -1487,6 +1534,10 @@ describe('createPartnerAccount', () => { expect(users.length).toBe(2); expect(users[0].external_id).toBe(parentExternalId); expect(users[1].external_id).toBe(partnerExternalId); + const sortCriteria = await getSortCriteriaList(source); + expect(sortCriteria.length).toBe(2); + const jobNumber = await getJobNumber(source, accounts[1].id); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); // コンテナ削除メソッドが呼ばれているか確認 diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index ecd571f..06ea6ef 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -26,6 +26,7 @@ import { getTaskListSortableAttribute, } from '../../common/types/sort/util'; import { + INITIAL_JOB_NUMBER, LICENSE_ALLOCATED_STATUS, LICENSE_EXPIRATION_THRESHOLD_DAYS, LICENSE_ISSUE_STATUS, @@ -210,6 +211,20 @@ export class AccountsRepositoryService { throw new Error(`invalid update. result.affected=${result.affected}`); } + // job_numberの初期値を設定 + const jobNumberRepo = entityManager.getRepository(JobNumber); + const initialJobNumber = jobNumberRepo.create({ + account_id: persistedAccount.id, + job_number: INITIAL_JOB_NUMBER, + }); + await insertEntity( + JobNumber, + jobNumberRepo, + initialJobNumber, + this.isCommentOut, + context, + ); + // ユーザーのタスクソート条件を作成 const sortCriteria = new SortCriteria(); { @@ -245,6 +260,14 @@ export class AccountsRepositoryService { const accountsRepo = entityManager.getRepository(Account); const usersRepo = entityManager.getRepository(User); const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + const jobNumberRepo = entityManager.getRepository(JobNumber); + // JobNumberを削除 + await deleteEntity( + jobNumberRepo, + { account_id: accountId }, + this.isCommentOut, + context, + ); // ソート条件を削除 await deleteEntity( sortCriteriaRepo, diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 1e30896..aef734f 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -12,6 +12,7 @@ import { import { Task } from './entity/task.entity'; import { ADMIN_ROLES, + MAX_JOB_NUMBER, NODE_ENV_TEST, TASK_STATUS, USER_ROLES, @@ -921,7 +922,7 @@ export class TasksRepositoryService { } let newJobNumber: string = ''; - if (currentJobNumberData.job_number === '99999999') { + if (currentJobNumberData.job_number === MAX_JOB_NUMBER) { // 末尾なら00000001に戻る newJobNumber = '00000001'; } else { From 0cca61517c8b47fe8ba412cd68083d340baab439 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Wed, 15 May 2024 06:15:22 +0000 Subject: [PATCH 093/109] =?UTF-8?q?Merged=20PR=20896:=20=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=83=A7=E3=83=B3=E3=82=A2=E3=83=83=E3=83=97=E7=94=A8?= =?UTF-8?q?SQL=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4044: バージョンアップ用SQLを作成](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4044) - jobNumberの初期値を設定するマイグレーションファイル作成 - タスクテーブルにレコードがある(=タスクを作成したことがある)アカウントに対しては最新のJobNumberで初期値をセットする - タスクテーブルにレコードがない(=タスク作成をしたことがない)アカウントに対しては`00000000`をセットする ## レビューポイント - セットする初期値は認識あっているか - migrate downの処理は問題ないか ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - マイグレーションファイルの作成のみなのでほかに影響はない想定 ## 補足 - 相談、参考資料などがあれば --- .../066-insert_initial_job_number.sql | 19 +++++++++++++++++++ dictation_server/src/constants/index.ts | 1 - 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 dictation_server/db/migrations/066-insert_initial_job_number.sql diff --git a/dictation_server/db/migrations/066-insert_initial_job_number.sql b/dictation_server/db/migrations/066-insert_initial_job_number.sql new file mode 100644 index 0000000..985026e --- /dev/null +++ b/dictation_server/db/migrations/066-insert_initial_job_number.sql @@ -0,0 +1,19 @@ +-- +migrate Up +INSERT INTO job_number (account_id, job_number) +SELECT + a.id AS account_id, + COALESCE(t.max_job_number, '00000000') AS job_number +FROM + accounts a +LEFT JOIN ( + SELECT + account_id, + MAX(job_number) AS max_job_number + FROM + tasks + GROUP BY + account_id +) t ON a.id = t.account_id; + +-- +migrate Down +TRUNCATE TABLE job_number; \ No newline at end of file diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index c91dd23..f295ed5 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -346,7 +346,6 @@ export const STORAGE_WARNING_THRESHOLD_PERCENT = 80; */ export const INITIAL_JOB_NUMBER = '00000000'; - /** * JobNumberの最大値 * @const {string} From ddf338bc726eb16b694d378bc384e838e85e7afc Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 15 May 2024 07:15:35 +0000 Subject: [PATCH 094/109] =?UTF-8?q?Merged=20PR=20890:=20=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4058: 翻訳情報反映](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4058) - 6月末リリース分の翻訳をクライアントに適用しました。 - いただいた翻訳内容は開発の管理ファイルに反映しています。(メンテナンスについては未反映です。) [ラベル・メッセージ管理](https://sonyjpn.sharepoint.com/:x:/r/sites/S127-OMDS-EXTERNAL-NDS/_layouts/15/doc2.aspx?sourcedoc=%7BCBFBB1F9-3AB4-4E2E-A21D-572684D20B29%7D&file=%E3%83%A9%E3%83%99%E3%83%AB%E3%83%BB%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E7%AE%A1%E7%90%86_dictation.xlsx&action=default&mobileredirect=true) ## レビューポイント - 翻訳の適用は適切か? ## UIの変更 - 翻訳の適用 ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - リテラルの内容のみなので影響なし --- dictation_client/src/translation/de.json | 156 +++++++++++----------- dictation_client/src/translation/en.json | 112 ++++++++-------- dictation_client/src/translation/es.json | 160 ++++++++++++----------- dictation_client/src/translation/fr.json | 156 +++++++++++----------- 4 files changed, 300 insertions(+), 284 deletions(-) diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index f6ba44f..67396de 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -49,6 +49,10 @@ "newUser": "Neuer Benutzer", "signUpButton": "Benutzerkonto erstellen", "logoAlt": "OM Dictation Management System in the Cloud" + }, + "text": { + "maintenanceNotificationTitle": "(de)サービス停止のお知らせ", + "maintenanceNotification": "Aufgrund von Systemwartungsarbeiten wird ODMS Cloud ab dem 12. Juni, 6:00 Uhr UTC-Zeit, etwa eine Stunde lang nicht verfügbar sein. Wir entschuldigen uns für etwaige Unannehmlichkeiten, die während der Wartung entstanden sind." } }, "signupPage": { @@ -129,18 +133,18 @@ "roleChangeError": "Die Benutzerrolle kann nicht geändert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", "encryptionPasswordCorrectError": "Das Verschlüsselungskennwort entspricht nicht den Regeln.", "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", - "userDeletionLicenseActiveError": "(de)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "typistDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "adminUserDeletionError": "(de)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "typistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "authorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "typistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", - "importSuccess": "(de)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", - "duplicateEmailError": "(de)以下の行のメールアドレスがCSV中で重複しています。", - "duplicateAuthorIdError": "(de)以下の行のAuthorIDがCSV中で重複しています。", - "overMaxUserError": "(de)一度に追加できるユーザーは100件までです。", - "invalidInputError": "(de)以下の行のユーザー情報が入力ルールに準拠していません。" + "userDeletionLicenseActiveError": "Der Benutzer konnte nicht gelöscht werden. Bitte heben Sie die Lizenzzuweisung vom Benutzer auf.", + "typistDeletionRoutingRuleError": "Der Benutzer konnte nicht gelöscht werden. Dieser Benutzer ist als Transkriptionist registriert, der in den Routing-Regeln enthalten ist. Bitte entfernen Sie den Transcriptionist aus der entsprechenden Routing-Regel auf der Registerkarte „Workflow“.", + "adminUserDeletionError": "Der Benutzer konnte nicht gelöscht werden. Bitte entfernen Sie den Benutzer vom primären oder sekundären Administrator auf der Registerkarte „Konto“.", + "typistUserDeletionTranscriptionTaskError": "Der Benutzer konnte nicht gelöscht werden. Diesem Transkriptionisten ist eine Aufgabe zugewiesen. Bitte ändern Sie die für die Aufgabe verantwortliche Person auf der Registerkarte „Diktieren“ in einen anderen Transkriptionisten.", + "authorUserDeletionTranscriptionTaskError": "Der Benutzer konnte nicht gelöscht werden. Es gibt von diesem Autor erstellte Aufgaben, die unvollständig sind. Bitte löschen Sie die Aufgaben, die von diesem Autor erstellt wurden, oder markieren Sie sie als erledigt.", + "typistUserDeletionTranscriptionistGroupError": "Der Benutzer konnte nicht gelöscht werden. Bitte entfernen Sie diesen Transkriptionisten aus der Transkriptionistengruppe auf der Registerkarte „Workflow“.", + "authorDeletionRoutingRuleError": "Der Benutzer konnte nicht gelöscht werden. Bitte entfernen Sie diesen Autor aus den Weiterleitungsregeln auf der Registerkarte „Workflow“.", + "importSuccess": "Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. Bitte überprüfen Sie Ihre E-Mails, da Sie eine E-Mail erhalten, sobald der Registrierungsprozess abgeschlossen ist.", + "duplicateEmailError": "Die E-Mail-Adressen in den folgenden Zeilen werden in der CSV-Datei dupliziert.", + "duplicateAuthorIdError": "Die Autoren-ID in der folgenden Zeile wird in der CSV-Datei dupliziert.", + "overMaxUserError": "Durch die Benutzerregistrierung per CSV-Datei können bis zu 100 Benutzer gleichzeitig registriert werden.", + "invalidInputError": "Die Benutzerinformationen in der folgenden Zeile entsprechen nicht den Eingaberegeln." }, "label": { "title": "Benutzer", @@ -174,32 +178,32 @@ "none": "Keiner", "encryptionPassword": "Passwort", "encryptionPasswordTerm": "Bitte legen Sie Ihr Passwort mit 4 bis 16 alphanumerischen Zeichen und Symbolen fest.", - "bulkImport": "(de)Bulk import", - "downloadCsv": "(de)Download CSV", - "importCsv": "(de)Import CSV", - "inputRules": "(de)Input rules", - "nameLabel": "(de)Name", - "emailAddressLabel": "(de)Email Address", - "roleLabel": "(de)Role", - "authorIdLabel": "(de)Author ID", - "autoRenewLabel": "(de)Auto Renew", - "notificationLabel": "(de)Notification", - "encryptionLabel": "(de)Encryption", - "encryptionPasswordLabel": "(de)Encryption Password", - "promptLabel": "(de)Prompt", - "addUsers": "(de)Add users" + "bulkImport": "Benutzer-Massenregistrierung", + "downloadCsv": "Laden Sie eine Beispiel-CSV-Datei herunter", + "importCsv": "CSV-Datei importieren", + "inputRules": "Eingaberegeln", + "nameLabel": "Name", + "emailAddressLabel": "E-Mail-Addresse", + "roleLabel": "Rolle", + "authorIdLabel": "Autoren-ID", + "autoRenewLabel": "Automatisch zuweisen", + "notificationLabel": "Benachrichtigung", + "encryptionLabel": "Verschlüsselung", + "encryptionPasswordLabel": "Verschlüsselungspasswort", + "promptLabel": "Eingabeaufforderung", + "addUsers": "Benutzer hinzufügen" }, "text": { - "downloadExplain": "(de)Download the csv format and enter it according to the rules below.", - "nameRule": "(de)Maximum 225 characters", - "emailAddressRule": "(de)Maximum 225 characters\nCannot use an email address that is already in use.", - "roleRule": "(de)None : 0\nAuthor : 1\nTranscriptionist : 2", - "authorIdRule": "(de)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", - "autoRenewRule": "(de)0 or 1", - "notificationRule": "(de)0 or 1", - "encryptionRule": "(de)Required only when Role=Author(1)\n0 or 1", - "encryptionPasswordRule": "(de)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", - "promptRule": "(de)Required only when Role=Author(1)\n0 or 1" + "downloadExplain": "Bitte laden Sie die CSV-Beispieldatei herunter und geben Sie die erforderlichen Informationen gemäß den folgenden Regeln ein.", + "nameRule": "> Maximal 225 Zeichen", + "emailAddressRule": "> Maximal 225 Zeichen\n> Eine bereits verwendete E-Mail-Adresse kann nicht verwendet werden.", + "roleRule": "Keiner : 0\nAutor : 1\nTranskriptionist : 2", + "authorIdRule": "> In diesem Element kann nur dann ein Wert festgelegt werden, wenn die „Rolle“ „Autor“ ist.\n> Maximal 16 Zeichen\n> Es dürfen nur großgeschriebene alphanumerische Zeichen und „_“ verwendet werden.\n> Eine bereits verwendete Autoren-ID kann nicht verwendet werden.", + "autoRenewRule": "> Wert : 0 oder 1 (1=EIN)", + "notificationRule": "> Wert : 0 oder 1 (1=EIN)", + "encryptionRule": "> In diesem Element kann nur dann ein Wert festgelegt werden, wenn die „Rolle“ „Autor“ ist.\n> Wert : 0 oder 1 (1=EIN)", + "encryptionPasswordRule": "> In diesem Element kann nur dann ein Wert festgelegt werden, wenn die „Rolle“ „Autor“ ist und die Verschlüsselung EIN ist.\n> Es können nur 4 bis 16 Buchstaben, Zahlen und Symbole eingegeben werden.", + "promptRule": "> In diesem Element kann nur dann ein Wert festgelegt werden, wenn die „Rolle“ „Autor“ ist.\n> Wert : 0 oder 1 (1=EIN)" } }, "LicenseSummaryPage": { @@ -220,10 +224,10 @@ "storageAvailable": "Speicher nicht verfügbar (Menge überschritten)", "licenseLabel": "Lizenz", "storageLabel": "Lagerung", - "storageUnavailableCheckbox": "(de)Storage Unavailable" + "storageUnavailableCheckbox": "Beschränken Sie die Kontonutzung" }, "message": { - "storageUnavalableSwitchingConfirm": "(de)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" + "storageUnavalableSwitchingConfirm": "Sind Sie sicher, dass Sie den Speichernutzungsstatus für dieses Konto ändern möchten?" } }, "licenseOrderPage": { @@ -251,12 +255,12 @@ "taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.", "backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.", "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.", - "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "Die Aufgabe konnte nicht gelöscht werden. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.", "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.", - "fileAlreadyDeletedError": "(de)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", - "fileRenameFailedError": "(de)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", - "fileNameAleadyExistsError": "(de)このファイル名は既に登録されています。他のファイル名で登録してください。" + "fileAlreadyDeletedError": "Die Bildschirminformationen sind nicht aktuell, sie enthalten bereits gelöschte Audiodateien. Bitte aktualisieren Sie den Bildschirm und wählen Sie die zu löschenden Dateien erneut aus.", + "fileRenameFailedError": "Da die Bildschirminformationen nicht aktuell sind, ist eine Inkonsistenz in den Dateiinformationen aufgetreten und die Datei konnte nicht umbenannt werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut.", + "fileNameAleadyExistsError": "Dieser Dateiname ist bereits registriert. Bitte registrieren Sie sich mit einem anderen Dateinamen." }, "label": { "title": "Diktate", @@ -298,13 +302,13 @@ "changeTranscriptionist": "Transkriptionist ändern", "deleteDictation": "Diktat löschen", "selectedTranscriptionist": "Ausgewählter transkriptionist", - "poolTranscriptionist": "Transkriptionsliste", + "poolTranscriptionist": "Liste der Transkriptionisten", "fileBackup": "Dateisicherung", "downloadForBackup": "Zur Sicherung herunterladen", "applications": "Desktopanwendung", "cancelDictation": "Transkription abbrechen", - "rawFileName": "(de)Raw File Name", - "fileNameSave": "(de)Save" + "rawFileName": "Ursprünglicher Dateiname", + "fileNameSave": "Führen Sie eine Dateiumbenennung durch" } }, "cardLicenseIssuePopupPage": { @@ -374,7 +378,7 @@ "issueRequesting": "Lizenzen auf Bestellung", "viewDetails": "Details anzeigen", "accounts": "konten", - "changeOwnerButton": "(de)Change Owner" + "changeOwnerButton": "Change Owner" } }, "orderHistoriesPage": { @@ -440,13 +444,13 @@ "templateOptional": "Vorlage (Optional)", "editRule": "Regel bearbeiten", "selected": "Ausgewählter transkriptionist", - "pool": "Transkriptionsliste", + "pool": "Liste der Transkriptionisten", "selectAuthor": "Autoren-ID auswählen", "selectWorktypeId": "Aufgabentypkennung auswählen", "selectTemplate": "Vorlage auswählen" }, "message": { - "selectedTypistEmptyError": "Transkriptionist oder Transkriptionistgruppe wurde nicht ausgewählt. Bitte wählen Sie eine oder mehrere aus der Transkriptionsliste aus.", + "selectedTypistEmptyError": "Transkriptionist oder Transkriptionistgruppe wurde nicht ausgewählt. Bitte wählen Sie eine oder mehrere aus der Transkriptionistenliste aus.", "workflowConflictError": "Eine Routing-Regel wurde bereits mit der angegebenen Kombination aus AuthorID und WorktypeID registriert. Bitte registrieren Sie sich mit einer anderen Kombination.", "inputEmptyError": "Pflichtfeld", "saveFailedError": "Die Routing-Regel konnte nicht gespeichert werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut." @@ -460,7 +464,7 @@ "addTypistGroup": "Transkriptionist Gruppe hinzufügen", "transcriptionist": "Transkriptionist", "selected": "Ausgewählter transkriptionist", - "pool": "Transkriptionsliste", + "pool": "Liste der Transkriptionisten", "add": "Hinzufügen", "remove": "Entfernen", "editTypistGroup": "Transkriptionistengruppe bearbeiten" @@ -469,8 +473,8 @@ "selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.", "groupSaveFailedError": "Die Transkriptionistengruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", "GroupNameAlreadyExistError": "Der Name dieser Transkriptionistengruppe ist bereits registriert. Bitte registrieren Sie sich mit einem anderen Namen der Transkriptionistengruppe.", - "deleteFailedWorkflowAssigned": "(de)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", - "deleteFailedCheckoutPermissionExisted": "(de)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" + "deleteFailedWorkflowAssigned": "Die Transkriptionistengruppe konnte nicht gelöscht werden. Bitte entfernen Sie die Transcriptionist-Gruppe aus der Routing-Regel auf der Registerkarte „Workflow“.", + "deleteFailedCheckoutPermissionExisted": "Die Transkriptionistengruppe konnte nicht gelöscht werden. Dieser Transkriptionistengruppe ist eine Aufgabe zugewiesen. Bitte weisen Sie die Aufgabe über die Registerkarte „Diktieren“ einem anderen Transkriptionisten oder einer anderen Transkriptionistengruppe zu." } }, "worktypeIdSetting": { @@ -520,8 +524,8 @@ "fileEmptyError": "Dateiauswahl ist erforderlich. Bitte wählen Sie eine Datei aus." }, "message": { - "deleteFailedWorkflowAssigned": "(de)テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", - "deleteFailedTaskAssigned": "(de)テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" + "deleteFailedWorkflowAssigned": "Die Vorlagendatei konnte nicht gelöscht werden. Bitte entfernen Sie die Vorlagendatei aus den Routing-Regeln auf der Registerkarte „Workflow“.", + "deleteFailedTaskAssigned": "Die Vorlagendatei konnte nicht gelöscht werden. Mit dieser Vorlage sind Aufgaben verbunden. Bitte löschen Sie die Aufgaben, die mit dieser Vorlage verknüpft sind, oder markieren Sie sie als erledigt." } }, "partnerPage": { @@ -537,19 +541,19 @@ "dealerManagement": "Erlauben Sie dem Händler, Änderungen vorzunehmen", "partners": "Partner", "deleteAccount": "Konto löschen", - "editAccount": "(de)Edit Account", - "accountInformation": "(de)Account information", - "primaryAdminInfo": "(de)Primary administrator's information", - "adminName": "(de)Admin Name", - "saveChanges": "(de)Save Changes" + "editAccount": "Konto bearbeiten", + "accountInformation": "Kontoinformationen", + "primaryAdminInfo": "Informationen des primären Administrators", + "adminName": "Name des Administrators", + "saveChanges": "Änderungen speichern" }, "message": { "delegateNotAllowedError": "Aktionen im Namen des Partners sind nicht zulässig. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "deleteFailedError": "Der Delegierungsvorgang ist fehlgeschlagen. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "delegateCancelError": "Der delegierte Vorgang wurde beendet, da die Berechtigung für den delegierten Vorgang widerrufen wurde.", - "partnerDeleteConfirm": "(de)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", - "editFailedError": "(de)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" + "partnerDeleteConfirm": "Löschen Sie das ausgewählte Konto. Ein gelöschtes Konto kann nicht wiederhergestellt werden. Sind Sie sicher? Zielkonto:", + "partnerDeleteFailedError": "Dieses Konto kann nicht gelöscht werden, da untergeordnete Konten mit diesem übergeordneten Konto verknüpft sind. Sie müssen die untergeordneten Konten verschieben oder löschen, bevor Sie dieses übergeordnete Konto löschen. Für weitere Informationen wenden Sie sich bitte an OMDS.", + "editFailedError": "Da die Bildschirminformationen nicht aktuell sind, ist beim Bearbeiten Ihres Partnerkontos eine Inkonsistenz aufgetreten. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut." } }, "accountPage": { @@ -571,7 +575,7 @@ "selectSecondaryAdministrator": "Sekundäradministrator auswählen", "saveChanges": "Änderungen speichern", "deleteAccount": "Konto löschen", - "fileRetentionDays": "(de)自動ファイル削除までの保持日数" + "fileRetentionDays": "Anzahl der Tage, die Dateien aufbewahrt werden, bevor sie automatisch gelöscht werden." }, "message": { "updateAccountFailedError": "Kontoinformationen konnten nicht gespeichert werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut." @@ -626,26 +630,26 @@ }, "fileDeleteSettingPopup": { "label": { - "title": "(de)Auto File Delete Setting", - "autoFileDeleteCheck": "(de)Auto file delete", - "daysAnnotation": "(de)Number of days from transcription finished to delete the files.", - "days": "(de)Days", - "saveButton": "(de)Save Settings", - "daysValidationError": "(de)Daysには1~999の数字を入力してください。" + "title": "Einstellung zum automatischen Löschen von Dateien", + "autoFileDeleteCheck": "Automatisches Löschen von Dateien", + "daysAnnotation": "Anzahl der Tage nach Abschluss der Transkription bis zum Löschen der Dateien.", + "days": "Tage", + "saveButton": "Einstellungen speichern", + "daysValidationError": "Bitte geben Sie für Tage eine Zahl zwischen 1 und 999 ein." } }, "changeOwnerPopup": { "message": { - "accountNotFoundError": "(de)変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "(de)パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "(de)パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "(de)パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" + "accountNotFoundError": "The account ID specified to change does not exist.", + "hierarchyMismatchError": "Failed to change partner account.\nPlease specify the account one level above the child account as the switch destination.。", + "regionMismatchError": "Failed to change partner account.\nPlease specify an account in the same region as the child account to switch to.", + "countryMismatchError": "Failed to change partner account.\nPlease specify an account in the same country as the child account to switch to." }, "label": { - "invalidInputError": "(de)変更先アカウントIDには1~9999999の数字を入力してください。", - "title": "(de)Change Owner", - "upperLayerId": "(de)Upper Layer ID", - "lowerLayerId": "(de)Lower Layer ID" + "invalidInputError": "Please enter a number between 1 and 9999999 for the destination account ID.", + "title": "Change Owner", + "upperLayerId": "Upper Layer ID", + "lowerLayerId": "Lower Layer ID" } } } \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index cee7426..041783f 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -49,6 +49,10 @@ "newUser": "New User", "signUpButton": "Create Account", "logoAlt": "OM Dictation Management System in the Cloud" + }, + "text": { + "maintenanceNotificationTitle": "サービス停止のお知らせ", + "maintenanceNotification": "Due to system maintenance, ODMS Cloud will be unavailable for approximately one hour starting from June 12th, 6:00AM UTC time. We apologize for any inconvenience caused during the maintenance." } }, "signupPage": { @@ -129,18 +133,18 @@ "roleChangeError": "Unable to change the User Role. The displayed information may be outdated, so please refresh the screen to see the latest status.", "encryptionPasswordCorrectError": "Encryption password does not meet the rules.", "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status.", - "userDeletionLicenseActiveError": "ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "typistDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "adminUserDeletionError": "ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "typistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "authorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "typistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", - "importSuccess": "ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", - "duplicateEmailError": "以下の行のメールアドレスがCSV中で重複しています。", - "duplicateAuthorIdError": "以下の行のAuthorIDがCSV中で重複しています。", - "overMaxUserError": "一度に追加できるユーザーは100件までです。", - "invalidInputError": "以下の行のユーザー情報が入力ルールに準拠していません。" + "userDeletionLicenseActiveError": "Failed to delete the user. Please unassign the license from the user.", + "typistDeletionRoutingRuleError": "Failed to delete the user. This user is registered as a Transcriptionist that is included in the routing rules. Please remove the Transcriptionist from the corresponding routing rule from the Workflow tab.", + "adminUserDeletionError": "Failed to delete the user. Please remove the user from the Primary or Secondary Administrator from the Account tab.", + "typistUserDeletionTranscriptionTaskError": "Failed to delete the user. There is a task assigned to this Transcriptionist. Please change the person in charge of the task to another Transcriptionist from the Dictation tab.", + "authorUserDeletionTranscriptionTaskError": "Failed to delete the user. There are tasks created by this Author that are incomplete. Please delete or mark the tasks as finished for tasks created by this Author.", + "typistUserDeletionTranscriptionistGroupError": "Failed to delete the user. Please remove this Transcriptionist from the Transcriptionist Group from the Workflow tab.", + "authorDeletionRoutingRuleError": "Failed to delete the user. Please remove this Author from the routing rules from the Workflow tab.", + "importSuccess": "We have received your bulk user registration request. Please check your email as you will receive an email once the registration process is complete.", + "duplicateEmailError": "The email addresses in the following lines are duplicated in the CSV file.", + "duplicateAuthorIdError": "The Author ID in the following line is duplicated in the CSV file.", + "overMaxUserError": "Up to 100 users can be registered at one time by user registration via CSV file.", + "invalidInputError": "The user information in the following line does not comply with the input rules." }, "label": { "title": "User", @@ -174,32 +178,32 @@ "none": "None", "encryptionPassword": "Password", "encryptionPasswordTerm": "Please set your password using 4 to 16 alphanumeric and symbols.", - "bulkImport": "Bulk import", - "downloadCsv": "Download CSV", - "importCsv": "Import CSV", + "bulkImport": "User Bulk Registration", + "downloadCsv": "Download sample CSV file", + "importCsv": "Import CSV file", "inputRules": "Input rules", "nameLabel": "Name", "emailAddressLabel": "Email Address", "roleLabel": "Role", "authorIdLabel": "Author ID", - "autoRenewLabel": "Auto Renew", + "autoRenewLabel": "Auto Assign", "notificationLabel": "Notification", "encryptionLabel": "Encryption", "encryptionPasswordLabel": "Encryption Password", "promptLabel": "Prompt", - "addUsers": "Add users" + "addUsers": "Add User" }, "text": { - "downloadExplain": "Download the csv format and enter it according to the rules below.", - "nameRule": "Maximum 225 characters", - "emailAddressRule": "Maximum 225 characters\nCannot use an email address that is already in use.", + "downloadExplain": "Please download the sample CSV file and apply the required information according to the rules below.", + "nameRule": "> Maximum 225 characters", + "emailAddressRule": "> Maximum 225 characters\n> Cannot use an email address that is already in use.", "roleRule": "None : 0\nAuthor : 1\nTranscriptionist : 2", - "authorIdRule": "Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", - "autoRenewRule": "0 or 1", - "notificationRule": "0 or 1", - "encryptionRule": "Required only when Role=Author(1)\n0 or 1", - "encryptionPasswordRule": "Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", - "promptRule": "Required only when Role=Author(1)\n0 or 1" + "authorIdRule": "> A value can be set in this item only when the \"Role\" is ”Author”.\n> Maximum 16 characters.\n> Only uppercase alphanumeric characters and \"_\" can be used.\n> Cannot use an Author ID that is already in use.", + "autoRenewRule": "> Value : 0 or 1 (1=ON)", + "notificationRule": "> Value : 0 or 1 (1=ON)", + "encryptionRule": "> A value can be set in this item only when the \"Role\" is ”Author”.\n> Value : 0 or 1 (1=ON)", + "encryptionPasswordRule": "> A value can be set in this item only when the \"Role\" is ”Author” and Encryption is ON.\n> Only 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "> A value can be set in this item only when the \"Role\" is ”Author”.\n> Value : 0 or 1 (1=ON)" } }, "LicenseSummaryPage": { @@ -220,10 +224,10 @@ "storageAvailable": "Storage Unavailable (Exceeded Amount)", "licenseLabel": "License", "storageLabel": "Storage", - "storageUnavailableCheckbox": "Storage Unavailable" + "storageUnavailableCheckbox": "Restrict account usage" }, "message": { - "storageUnavalableSwitchingConfirm": "対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" + "storageUnavalableSwitchingConfirm": "Are you sure you would like to change the storage usage status for this account?" } }, "licenseOrderPage": { @@ -251,12 +255,12 @@ "taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.", "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.", "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.", - "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "Failed to delete the task. Please refresh the screen and check again.", "licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.", "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.", - "fileAlreadyDeletedError": "既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", - "fileRenameFailedError": "ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", - "fileNameAleadyExistsError": "このファイル名は既に登録されています。他のファイル名で登録してください。" + "fileAlreadyDeletedError": "The screen information is not up to date, it contains audio files that have already been deleted. Please refresh the screen, and select the files to delete again.", + "fileRenameFailedError": "Since the screen information is not up-to-date, an inconsistency occurred in the file information and failed to rename the file. Please refresh the screen and try again.", + "fileNameAleadyExistsError": "This file name is already registered. Please register with a different file name." }, "label": { "title": "Dictations", @@ -298,13 +302,13 @@ "changeTranscriptionist": "Change Transcriptionist", "deleteDictation": "Delete Dictation", "selectedTranscriptionist": "Selected Transcriptionist", - "poolTranscriptionist": "Transcription List", + "poolTranscriptionist": "Transcriptionist List", "fileBackup": "File Backup", "downloadForBackup": "Download for backup", "applications": "Desktop Application", "cancelDictation": "Cancel Transcription", - "rawFileName": "Raw File Name", - "fileNameSave": "Save" + "rawFileName": "Original File Name", + "fileNameSave": "Execute file rename" } }, "cardLicenseIssuePopupPage": { @@ -440,13 +444,13 @@ "templateOptional": "Template (Optional)", "editRule": "Edit Rule", "selected": "Selected Transcriptionist", - "pool": "Transcription List", + "pool": "Transcriptionist List", "selectAuthor": "Select Author ID", "selectWorktypeId": "Select Worktype ID", "selectTemplate": "Select Template" }, "message": { - "selectedTypistEmptyError": "Transcriptionist, or Transcriptionist Group has not been selected. Please select one or more from the Transcription List.", + "selectedTypistEmptyError": "Transcriptionist, or Transcriptionist Group has not been selected. Please select one or more from the Transcriptionist List.", "workflowConflictError": "A routing rule has already been registered with the specified AuthorID and WorktypeID combination. Please register with different combination.", "inputEmptyError": "Mandatory Field", "saveFailedError": "Failed to save the routing rule. Please refresh the screen and try again." @@ -460,7 +464,7 @@ "addTypistGroup": "Add Transcriptionist Group", "transcriptionist": "Transcriptionist", "selected": "Selected Transcriptionist", - "pool": "Transcription List", + "pool": "Transcriptionist List", "add": "Add", "remove": "Remove", "editTypistGroup": "Edit Transcriptionist Group" @@ -469,8 +473,8 @@ "selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.", "groupSaveFailedError": "Transcriptionist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status.", "GroupNameAlreadyExistError": "This Transcriptionist Group name is already registered. Please register with another Transcriptionist Group name.", - "deleteFailedWorkflowAssigned": "TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", - "deleteFailedCheckoutPermissionExisted": "TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" + "deleteFailedWorkflowAssigned": "Failed to delete the Transcriptionist Group. Please remove the Transcriptionist Group from the routing rule from the Workflow tab.", + "deleteFailedCheckoutPermissionExisted": "Failed to delete the Transcriptionist Group. There is a task assigned to this Transcriptionist Group. Please reassign the task to another Transcriptionist or Transcriptionist Group from the Dictation tab." } }, "worktypeIdSetting": { @@ -520,8 +524,8 @@ "fileEmptyError": "File selection is required. Please select a file." }, "message": { - "deleteFailedWorkflowAssigned": "テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", - "deleteFailedTaskAssigned": "テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" + "deleteFailedWorkflowAssigned": "Failed to delete the template file. Please remove the template file from the routing rules from the Workflow tab.", + "deleteFailedTaskAssigned": "Failed to delete the template file. There are tasks associated with this template. Please delete or mark the tasks as finished for tasks associated with this template. " } }, "partnerPage": { @@ -538,18 +542,18 @@ "partners": "Partners", "deleteAccount": "Delete Account", "editAccount": "Edit Account", - "accountInformation": "Account information", + "accountInformation": "Account Information", "primaryAdminInfo": "Primary administrator's information", - "adminName": "Admin Name", + "adminName": "Administrator‘s Name", "saveChanges": "Save Changes" }, "message": { "delegateNotAllowedError": "Actions on behalf of partner are not allowed. Please refresh the screen and check again.", "deleteFailedError": "Delegate operation failed. Please refresh the screen and check again.", "delegateCancelError": "The delegated operation has been terminated because permission for the delegated operation has been revoked.", - "partnerDeleteConfirm": "選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", - "editFailedError": "パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" + "partnerDeleteConfirm": "Delete the selected account. A deleted account cannot be restored, are you sure? Target account:", + "partnerDeleteFailedError": "This account cannot be deleted because child accounts are associated with this parent account. You need to move or delete the child accounts prior to deleting this parent account. Please contact OMDS for more information.", + "editFailedError": "Since the screen information is not up to date, an inconsistency occurred when editing your partner account. Please refresh the screen and try again." } }, "accountPage": { @@ -571,7 +575,7 @@ "selectSecondaryAdministrator": "Select Secondary Administrator", "saveChanges": "Save Changes", "deleteAccount": "Delete Account", - "fileRetentionDays": "自動ファイル削除までの保持日数" + "fileRetentionDays": "Number of days files are kept before they are automatically deleted." }, "message": { "updateAccountFailedError": "Failed to save account information. Please refresh the screen and try again." @@ -631,18 +635,18 @@ "daysAnnotation": "Number of days from transcription finished to delete the files.", "days": "Days", "saveButton": "Save Settings", - "daysValidationError": "Daysには1~999の数字を入力してください。" + "daysValidationError": "Please enter a number between 1 and 999 for Days." } }, "changeOwnerPopup": { "message": { - "accountNotFoundError": "変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" + "accountNotFoundError": "The account ID specified to change does not exist.", + "hierarchyMismatchError": "Failed to change partner account.\nPlease specify the account one level above the child account as the switch destination.。", + "regionMismatchError": "Failed to change partner account.\nPlease specify an account in the same region as the child account to switch to.", + "countryMismatchError": "Failed to change partner account.\nPlease specify an account in the same country as the child account to switch to." }, "label": { - "invalidInputError": "変更先アカウントIDには1~9999999の数字を入力してください。", + "invalidInputError": "Please enter a number between 1 and 9999999 for the destination account ID.", "title": "Change Owner", "upperLayerId": "Upper Layer ID", "lowerLayerId": "Lower Layer ID" diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index c6bfe33..2c8f63c 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -49,6 +49,10 @@ "newUser": "Nuevo usuario", "signUpButton": "Crear una cuenta", "logoAlt": "OM Dictation Management System in the Cloud" + }, + "text": { + "maintenanceNotificationTitle": "(es)サービス停止のお知らせ", + "maintenanceNotification": "Debido al mantenimiento del sistema, ODMS Cloud no estará disponible durante aproximadamente una hora a partir del 12 de junio a las 6:00 am, hora UTC. Pedimos disculpas por cualquier inconveniente causado durante el mantenimiento." } }, "signupPage": { @@ -129,18 +133,18 @@ "roleChangeError": "No se puede cambiar la función de usuario. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", "encryptionPasswordCorrectError": "La contraseña de cifrado no cumple con las reglas.", "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", - "userDeletionLicenseActiveError": "(es)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "typistDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "adminUserDeletionError": "(es)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "typistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "authorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "typistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", - "importSuccess": "(es)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", - "duplicateEmailError": "(es)以下の行のメールアドレスがCSV中で重複しています。", - "duplicateAuthorIdError": "(es)以下の行のAuthorIDがCSV中で重複しています。", - "overMaxUserError": "(es)一度に追加できるユーザーは100件までです。", - "invalidInputError": "(es)以下の行のユーザー情報が入力ルールに準拠していません。" + "userDeletionLicenseActiveError": "No se pudo eliminar el usuario. Desasignar la licencia al usuario.", + "typistDeletionRoutingRuleError": "No se pudo eliminar el usuario. Este usuario está registrado como Transcriptor que está incluido en las reglas de enrutamiento. Elimine al transcriptor de la regla de enrutamiento correspondiente en la pestaña Flujo de trabajo.", + "adminUserDeletionError": "No se pudo eliminar el usuario. Elimine el usuario del administrador principal o secundario desde la pestaña Cuenta.", + "typistUserDeletionTranscriptionTaskError": "No se pudo eliminar el usuario. Hay una tarea asignada a este transcriptor. Cambie la persona a cargo de la tarea a otro Transcriptor desde la pestaña Dictado.", + "authorUserDeletionTranscriptionTaskError": "No se pudo eliminar el usuario. Hay tareas creadas por este Autor que están incompletas. Elimine o marque las tareas como finalizadas para las tareas creadas por este autor.", + "typistUserDeletionTranscriptionistGroupError": "No se pudo eliminar el usuario. Elimine a este transcriptor del grupo de transcriptores de la pestaña Flujo de trabajo.", + "authorDeletionRoutingRuleError": "No se pudo eliminar el usuario. Elimine a este autor de las reglas de enrutamiento de la pestaña Flujo de trabajo.", + "importSuccess": "Hemos recibido su solicitud de registro de usuario masivo. Por favor revise su correo electrónico ya que recibirá un correo electrónico una vez que se complete el proceso de registro.", + "duplicateEmailError": "Las direcciones de correo electrónico de las siguientes líneas están duplicadas en el archivo CSV.", + "duplicateAuthorIdError": "El ID del autor en la siguiente línea está duplicado en el archivo CSV.", + "overMaxUserError": "Se pueden registrar hasta 100 usuarios a la vez mediante el registro de usuario mediante un archivo CSV.", + "invalidInputError": "La información del usuario en la siguiente línea no cumple con las reglas de entrada." }, "label": { "title": "Usuario", @@ -165,7 +169,7 @@ "addToGroup": "Agregar grupo (opcional)", "author": "Autor", "transcriptionist": "Transcriptor", - "encryption": "Codificación", + "encryption": "Cifrado", "prompt": "Solicitar", "emailVerified": "Correo electrónico verificado", "editUser": "Editar usuario", @@ -174,32 +178,32 @@ "none": "Ninguno", "encryptionPassword": "Contraseña", "encryptionPasswordTerm": "Configure su contraseña utilizando de 4 a 16 símbolos alfanuméricos y.", - "bulkImport": "(es)Bulk import", - "downloadCsv": "(es)Download CSV", - "importCsv": "(es)Import CSV", - "inputRules": "(es)Input rules", - "nameLabel": "(es)Name", - "emailAddressLabel": "(es)Email Address", - "roleLabel": "(es)Role", - "authorIdLabel": "(es)Author ID", - "autoRenewLabel": "(es)Auto Renew", - "notificationLabel": "(es)Notification", - "encryptionLabel": "(es)Encryption", - "encryptionPasswordLabel": "(es)Encryption Password", - "promptLabel": "(es)Prompt", - "addUsers": "(es)Add users" + "bulkImport": "Registro masivo de usuarios", + "downloadCsv": "Descargar archivo CSV de muestra", + "importCsv": "Importar archivo CSV", + "inputRules": "Reglas de entrada", + "nameLabel": "Nombre", + "emailAddressLabel": "Dirección de correo electrónico", + "roleLabel": "Role", + "authorIdLabel": "ID de autor", + "autoRenewLabel": "Asignación automática", + "notificationLabel": "Notificación", + "encryptionLabel": "Cifrado ", + "encryptionPasswordLabel": "Contraseña de cifrado", + "promptLabel": "Solicitar", + "addUsers": "Agregar usuario" }, "text": { - "downloadExplain": "(es)Download the csv format and enter it according to the rules below.", - "nameRule": "(es)Maximum 225 characters", - "emailAddressRule": "(es)Maximum 225 characters\nCannot use an email address that is already in use.", - "roleRule": "(es)None : 0\nAuthor : 1\nTranscriptionist : 2", - "authorIdRule": "(es)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", - "autoRenewRule": "(es)0 or 1", - "notificationRule": "(es)0 or 1", - "encryptionRule": "(es)Required only when Role=Author(1)\n0 or 1", - "encryptionPasswordRule": "(es)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", - "promptRule": "(es)Required only when Role=Author(1)\n0 or 1" + "downloadExplain": "Descargue el archivo CSV de muestra y aplique la información requerida de acuerdo con las reglas siguientes.", + "nameRule": "> Máximo 225 caracteres", + "emailAddressRule": "> Máximo 225\n> No se puede utilizar una dirección de correo electrónico que ya esté en uso.", + "roleRule": "Ninguno : 0\nAutor : 1\nTranscriptor : 2", + "authorIdRule": "> Se puede establecer un valor en este elemento sólo cuando el \"Rol\" es \"Autor\".\n> Máximo 16 caracteres\n> Sólo se pueden utilizar caracteres alfanuméricos en mayúsculas y \"_\".\n> No se puede utilizar una ID de autor que ya esté en uso.", + "autoRenewRule": "> Valor : 0 o 1 (1=ENCENDIDO)", + "notificationRule": "> Valor : 0 o 1 (1=ENCENDIDO)", + "encryptionRule": "> Se puede establecer un valor en este elemento sólo cuando el \"Rol\" es \"Autor\".\n> Valor : 0 o 1 (1=ENCENDIDO)", + "encryptionPasswordRule": "> Se puede establecer un valor en este elemento solo cuando el \"Rol\" es \"Autor\" y el cifrado está activado.\n> Sólo se pueden ingresar de 4 a 16 letras, números y símbolos.", + "promptRule": "> Se puede establecer un valor en este elemento sólo cuando el \"Rol\" es \"Autor\".\n> Valor : 0 o 1 (1=ENCENDIDO)" } }, "LicenseSummaryPage": { @@ -220,10 +224,10 @@ "storageAvailable": "Almacenamiento no disponible (cantidad excedida)", "licenseLabel": "Licencia", "storageLabel": "Almacenamiento", - "storageUnavailableCheckbox": "(es)Storage Unavailable" + "storageUnavailableCheckbox": "Restringir el uso de la cuenta" }, "message": { - "storageUnavalableSwitchingConfirm": "(es)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" + "storageUnavalableSwitchingConfirm": "¿Está seguro de que desea cambiar el estado de uso del almacenamiento de esta cuenta?" } }, "licenseOrderPage": { @@ -251,12 +255,12 @@ "taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.", "backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.", "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.", - "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "No se pudo eliminar la tarea. Actualice la pantalla y verifique nuevamente.", "licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.", "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.", - "fileAlreadyDeletedError": "(es)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", - "fileRenameFailedError": "(es)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", - "fileNameAleadyExistsError": "(es)このファイル名は既に登録されています。他のファイル名で登録してください。" + "fileAlreadyDeletedError": "La información de la pantalla no está actualizada, contiene archivos de audio que ya han sido eliminados. Actualice la pantalla y seleccione los archivos que desea eliminar nuevamente.", + "fileRenameFailedError": "Dado que la información de la pantalla no está actualizada, se produjo una inconsistencia en la información del archivo y no se pudo cambiar el nombre del archivo. Actualice la pantalla e inténtelo de nuevo.", + "fileNameAleadyExistsError": "Este nombre de archivo ya está registrado. Regístrese con un nombre de archivo diferente." }, "label": { "title": "Dictado", @@ -264,7 +268,7 @@ "jobNumber": "Número de trabajo", "status": "Estado", "priority": "Prioridad", - "encryption": "Codificación", + "encryption": "Cifrado ", "authorId": "ID de autor", "workType": "ID de tipo de trabajo", "fileName": "Nombre de archivo", @@ -298,13 +302,13 @@ "changeTranscriptionist": "Cambiar transcriptor", "deleteDictation": "Borrar dictado", "selectedTranscriptionist": "Transcriptor seleccionado", - "poolTranscriptionist": "Lista de transcriptor", + "poolTranscriptionist": "Lista de transcriptores", "fileBackup": "Copia de seguridad de archivos", "downloadForBackup": "Descargar para respaldo", "applications": "Aplicación de escritorio", "cancelDictation": "Cancelar transcripción", - "rawFileName": "(es)Raw File Name", - "fileNameSave": "(es)Save" + "rawFileName": "Nombre de archivo original", + "fileNameSave": "Ejecutar cambio de nombre de archivo" } }, "cardLicenseIssuePopupPage": { @@ -374,7 +378,7 @@ "issueRequesting": "Licencias en Pedido", "viewDetails": "Ver detalles", "accounts": "cuentas", - "changeOwnerButton": "(es)Change Owner" + "changeOwnerButton": "Change Owner" } }, "orderHistoriesPage": { @@ -440,13 +444,13 @@ "templateOptional": "Plantilla (Opcional)", "editRule": "Editar regla", "selected": "Transcriptor seleccionado", - "pool": "Lista de transcriptor", + "pool": "Lista de transcriptores", "selectAuthor": "Seleccionar ID de autor", "selectWorktypeId": "Seleccionar ID de tipo de trabajo", "selectTemplate": "Seleccionar Plantilla" }, "message": { - "selectedTypistEmptyError": "No se ha seleccionado el transcriptor o el grupo de transcriptores. Seleccione uno o más de la lista de transcripción.", + "selectedTypistEmptyError": "No se ha seleccionado el transcriptor o el grupo de transcriptores. Seleccione uno o más de la lista de transcriptores.", "workflowConflictError": "Ya se ha registrado una regla de enrutamiento con la combinación AuthorID y WorktypeID especificada. Regístrese con una combinación diferente.", "inputEmptyError": "Campo obligatorio", "saveFailedError": "No se pudo guardar la regla de enrutamiento. Actualice la pantalla e inténtelo de nuevo." @@ -460,7 +464,7 @@ "addTypistGroup": "Agregar grupo transcriptor", "transcriptionist": "Transcriptor", "selected": "Transcriptor seleccionado", - "pool": "Lista de transcriptor", + "pool": "Lista de transcriptores", "add": "Añadir", "remove": "Eliminar", "editTypistGroup": "Editar grupo transcriptor" @@ -469,8 +473,8 @@ "selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.", "groupSaveFailedError": "El grupo transcriptor no se pudo salvar. La información mostrada puede estar desactualizada. Así que actualice la pantalla para ver el estado más reciente.", "GroupNameAlreadyExistError": "El nombre de este grupo transcriptor ya está registrado. Regístrese con otro nombre de grupo transcriptor.", - "deleteFailedWorkflowAssigned": "(es)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", - "deleteFailedCheckoutPermissionExisted": "(es)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" + "deleteFailedWorkflowAssigned": "No se pudo eliminar el grupo transcriptor. Elimine el grupo de transcriptores de la regla de enrutamiento de la pestaña Flujo de trabajo.", + "deleteFailedCheckoutPermissionExisted": "No se pudo eliminar el grupo transcriptor. Hay una tarea asignada a este Grupo Transcripcionista. Reasigne la tarea a otro transcriptor o grupo de transcriptores desde la pestaña Dictado." } }, "worktypeIdSetting": { @@ -520,8 +524,8 @@ "fileEmptyError": "Se requiere selección de archivos. Por favor seleccione un archivo." }, "message": { - "deleteFailedWorkflowAssigned": "(es)テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", - "deleteFailedTaskAssigned": "(es)テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" + "deleteFailedWorkflowAssigned": "No se pudo eliminar el archivo de plantilla. Elimine el archivo de plantilla adjunto a la regla de enrutamiento de la pestaña Flujo de trabajo.", + "deleteFailedTaskAssigned": "No se pudo eliminar el archivo de plantilla. Hay tareas asociadas con esta plantilla. Elimine o marque las tareas como finalizadas para las tareas asociadas con esta plantilla." } }, "partnerPage": { @@ -537,19 +541,19 @@ "dealerManagement": "Permitir que el distribuidor realice los cambios", "partners": "Socios", "deleteAccount": "Borrar cuenta", - "editAccount": "(es)Edit Account", - "accountInformation": "(es)Account information", - "primaryAdminInfo": "(es)Primary administrator's information", - "adminName": "(es)Admin Name", - "saveChanges": "(es)Save Changes" + "editAccount": "Editar cuenta", + "accountInformation": "Información de la cuenta", + "primaryAdminInfo": "Información del administrador principal", + "adminName": "Nombre del administrador", + "saveChanges": "Guardar cambios" }, "message": { "delegateNotAllowedError": "No se permiten acciones en nombre del socio. Actualice la pantalla y verifique nuevamente.", "deleteFailedError": "La operación del delegado falló. Actualice la pantalla y verifique nuevamente.", "delegateCancelError": "La operación delegada finalizó porque se revocó el permiso para la operación delegada.", - "partnerDeleteConfirm": "(es)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(es)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", - "editFailedError": "(es)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" + "partnerDeleteConfirm": "Eliminar la cuenta seleccionada. Una cuenta eliminada no se puede restaurar, ¿estás seguro? Cuenta objetivo:", + "partnerDeleteFailedError": "Esta cuenta no se puede eliminar porque las cuentas infantiles están asociadas con esta cuenta principal. Debe mover o eliminar las cuentas infantiles antes de eliminar esta cuenta principal. Comuníquese con OMDS para obtener más información.", + "editFailedError": "Dado que la información de la pantalla no está actualizada, se produjo una inconsistencia al editar su cuenta de socio. Actualice la pantalla e inténtelo de nuevo." } }, "accountPage": { @@ -571,7 +575,7 @@ "selectSecondaryAdministrator": "Seleccionar administrador secundario", "saveChanges": "Guardar cambios", "deleteAccount": "Borrar cuenta", - "fileRetentionDays": "(es)自動ファイル削除までの保持日数" + "fileRetentionDays": "Número de días que se conservan los archivos antes de que se eliminen automáticamente." }, "message": { "updateAccountFailedError": "No se pudo guardar la información de la cuenta. Actualice la pantalla e inténtelo de nuevo." @@ -626,26 +630,26 @@ }, "fileDeleteSettingPopup": { "label": { - "title": "(es)Auto File Delete Setting", - "autoFileDeleteCheck": "(es)Auto file delete", - "daysAnnotation": "(es)Number of days from transcription finished to delete the files.", - "days": "(es)Days", - "saveButton": "(es)Save Settings", - "daysValidationError": "(es)Daysには1~999の数字を入力してください。" + "title": "Configuración de eliminación automática de archivos", + "autoFileDeleteCheck": "Eliminación automática de archivos", + "daysAnnotation": "Número de días desde que finalizó la transcripción para eliminar los archivos.", + "days": "Días", + "saveButton": "Guardar ajustes", + "daysValidationError": "Ingrese un número entre 1 y 999 para Días." } }, "changeOwnerPopup": { "message": { - "accountNotFoundError": "(es)変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "(es)パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "(es)パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "(es)パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" + "accountNotFoundError": "The account ID specified to change does not exist.", + "hierarchyMismatchError": "Failed to change partner account.\nPlease specify the account one level above the child account as the switch destination.。", + "regionMismatchError": "Failed to change partner account.\nPlease specify an account in the same region as the child account to switch to.", + "countryMismatchError": "Failed to change partner account.\nPlease specify an account in the same country as the child account to switch to." }, "label": { - "invalidInputError": "(es)変更先アカウントIDには1~9999999の数字を入力してください。", - "title": "(es)Change Owner", - "upperLayerId": "(es)Upper Layer ID", - "lowerLayerId": "(es)Lower Layer ID" + "invalidInputError": "Please enter a number between 1 and 9999999 for the destination account ID.", + "title": "Change Owner", + "upperLayerId": "Upper Layer ID", + "lowerLayerId": "Lower Layer ID" } } } \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 071295b..9ee60f0 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -49,6 +49,10 @@ "newUser": "Nouvel utilisateur", "signUpButton": "Créer un compte", "logoAlt": "OM Dictation Management System in the Cloud" + }, + "text": { + "maintenanceNotificationTitle": "(fr)サービス停止のお知らせ", + "maintenanceNotification": "En raison de la maintenance du système, ODMS Cloud sera indisponible pendant environ une heure à partir du 12 juin à 6h00, heure UTC. Nous nous excusons pour tout inconvénient causé lors de la maintenance." } }, "signupPage": { @@ -129,18 +133,18 @@ "roleChangeError": "Impossible de modifier le rôle de l'utilisateur. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", "encryptionPasswordCorrectError": "Le mot de passe de cryptage n'est pas conforme aux règles.", "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", - "userDeletionLicenseActiveError": "(fr)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", - "typistDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", - "adminUserDeletionError": "(fr)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", - "typistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", - "authorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", - "typistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", - "importSuccess": "(fr)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", - "duplicateEmailError": "(fr)以下の行のメールアドレスがCSV中で重複しています。", - "duplicateAuthorIdError": "(fr)以下の行のAuthorIDがCSV中で重複しています。", - "overMaxUserError": "(fr)一度に追加できるユーザーは100件までです。", - "invalidInputError": "(fr)以下の行のユーザー情報が入力ルールに準拠していません。" + "userDeletionLicenseActiveError": "Échec de la suppression de l'utilisateur. Veuillez annuler l'attribution de la licence à l'utilisateur.", + "typistDeletionRoutingRuleError": "Échec de la suppression de l'utilisateur. Cet utilisateur est enregistré en tant que Transcripteur qui est inclus dans les règles de routage. Supprimez le transcripteur de la règle de routage correspondante dans l'onglet Workflow. Routing-Regel auf der Registerkarte „Workflow“.", + "adminUserDeletionError": "Échec de la suppression de l'utilisateur. Veuillez supprimer l'utilisateur de l'administrateur principal ou secondaire de l'onglet Compte.", + "typistUserDeletionTranscriptionTaskError": "Échec de la suppression de l'utilisateur. Une tâche est assignée à ce transcripteur. Veuillez remplacer la personne en charge de la tâche par un autre transcripteur depuis l'onglet Dictée.", + "authorUserDeletionTranscriptionTaskError": "Échec de la suppression de l'utilisateur. Certaines tâches créées par cet auteur sont incomplètes. Veuillez supprimer ou marquer les tâches comme terminées pour les tâches créées par cet auteur.", + "typistUserDeletionTranscriptionistGroupError": "Échec de la suppression de l'utilisateur. Veuillez supprimer ce transcripteur du groupe transcripteur de l'onglet Workflow.", + "authorDeletionRoutingRuleError": "Échec de la suppression de l'utilisateur. Supprimez cet auteur des règles de routage dans l'onglet Workflow.", + "importSuccess": "Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. Veuillez vérifier votre courrier électronique car vous recevrez un e-mail une fois le processus d'inscription terminé.", + "duplicateEmailError": "Les adresses email des lignes suivantes sont dupliquées dans le fichier CSV.", + "duplicateAuthorIdError": "L'ID d'auteur dans la ligne suivante est dupliqué dans le fichier CSV.", + "overMaxUserError": "Jusqu'à 100 utilisateurs peuvent être enregistrés en même temps par enregistrement d'utilisateur via un fichier CSV.", + "invalidInputError": "Les informations utilisateur de la ligne suivante ne sont pas conformes aux règles de saisie." }, "label": { "title": "Utilisateur", @@ -174,32 +178,32 @@ "none": "Aucun", "encryptionPassword": "Mot de passe", "encryptionPasswordTerm": "Veuillez définir votre mot de passe en utilisant 4 à 16 caractères alphanumériques et symboles.", - "bulkImport": "(fr)Bulk import", - "downloadCsv": "(fr)Download CSV", - "importCsv": "(fr)Import CSV", - "inputRules": "(fr)Input rules", - "nameLabel": "(fr)Name", - "emailAddressLabel": "(fr)Email Address", - "roleLabel": "(fr)Role", - "authorIdLabel": "(fr)Author ID", - "autoRenewLabel": "(fr)Auto Renew", - "notificationLabel": "(fr)Notification", - "encryptionLabel": "(fr)Encryption", - "encryptionPasswordLabel": "(fr)Encryption Password", - "promptLabel": "(fr)Prompt", - "addUsers": "(fr)Add users" + "bulkImport": "Inscription des utilisateurs de groupe", + "downloadCsv": "Télécharger un exemple de fichier CSV", + "importCsv": "Importer un fichier CSV", + "inputRules": "Règles de saisie", + "nameLabel": "Nom", + "emailAddressLabel": "Adresse e-mail", + "roleLabel": "Rôle", + "authorIdLabel": "Identifiant Auteur", + "autoRenewLabel": "Assignation automatique", + "notificationLabel": "Notification", + "encryptionLabel": "Chiffrement", + "encryptionPasswordLabel": "Mot de passe de chiffrement", + "promptLabel": "Invite", + "addUsers": "Ajouter un utilisateur" }, "text": { - "downloadExplain": "(fr)Download the csv format and enter it according to the rules below.", - "nameRule": "(fr)Maximum 225 characters", - "emailAddressRule": "(fr)Maximum 225 characters\nCannot use an email address that is already in use.", - "roleRule": "(fr)None : 0\nAuthor : 1\nTranscriptionist : 2", - "authorIdRule": "(fr)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", - "autoRenewRule": "(fr)0 or 1", - "notificationRule": "(fr)0 or 1", - "encryptionRule": "(fr)Required only when Role=Author(1)\n0 or 1", - "encryptionPasswordRule": "(fr)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", - "promptRule": "(fr)Required only when Role=Author(1)\n0 or 1" + "downloadExplain": "Veuillez télécharger l'exemple de fichier CSV et appliquer les informations requises conformément aux règles ci-dessous.", + "nameRule": "> 225 caractères maximum", + "emailAddressRule": "> 225 caractères maximum\n> Impossible d'utiliser une adresse email déjà utilisée.", + "roleRule": "Aucun : 0\nAuteur : 1\nTranscriptionniste : 2", + "authorIdRule": "> Une valeur peut être définie dans cet élément uniquement lorsque le « Rôle » est « Auteur »\n> 16 caractères maximum\n> Seuls les caractères alphanumériques majuscules et \"_\" peuvent être utilisés.\n> Impossible d'utiliser un identifiant d'auteur déjà utilisé.", + "autoRenewRule": "> Valeur : 0 or 1 (1=ON)", + "notificationRule": "> Valeur : 0 or 1 (1=ON)", + "encryptionRule": "> Une valeur peut être définie dans cet élément uniquement lorsque le « Rôle » est « Auteur ».\n> Valeur : 0 ou 1 (1=ON)", + "encryptionPasswordRule": "> Une valeur peut être définie dans cet élément uniquement lorsque le « Rôle » est « Auteur » et que le cryptage est activé.\n> Seuls 4 à 16 lettres, chiffres et symboles peuvent être saisis.", + "promptRule": "> Une valeur peut être définie dans cet élément uniquement lorsque le « Rôle » est « Auteur ».\n> Valeur : 0 ou 1 (1=ON)" } }, "LicenseSummaryPage": { @@ -220,10 +224,10 @@ "storageAvailable": "Stockage indisponible (montant dépassée)", "licenseLabel": "Licence", "storageLabel": "Stockage", - "storageUnavailableCheckbox": "(fr)Storage Unavailable" + "storageUnavailableCheckbox": "Restreindre l'utilisation du compte" }, "message": { - "storageUnavalableSwitchingConfirm": "(fr)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" + "storageUnavalableSwitchingConfirm": "Êtes-vous sûr de vouloir modifier l'état d'utilisation du stockage pour ce compte ?" } }, "licenseOrderPage": { @@ -251,12 +255,12 @@ "taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.", "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.", "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.", - "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "deleteFailedError": "Échec de la suppression de la tâche. Veuillez actualiser l'écran et vérifier à nouveau.", "licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.", "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.", - "fileAlreadyDeletedError": "(fr)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", - "fileRenameFailedError": "(fr)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", - "fileNameAleadyExistsError": "(fr)このファイル名は既に登録されています。他のファイル名で登録してください。" + "fileAlreadyDeletedError": "Les informations à l'écran ne sont pas à jour, elles contiennent des fichiers audio qui ont déjà été supprimés. Veuillez actualiser l'écran et sélectionner à nouveau les fichiers à supprimer.", + "fileRenameFailedError": "Étant donné que les informations à l'écran ne sont pas à jour, une incohérence s'est produite dans les informations du fichier et il n'a pas été possible de renommer le fichier. Veuillez actualiser l'écran et réessayer.", + "fileNameAleadyExistsError": "Ce nom de fichier est déjà enregistré. Veuillez vous inscrire avec un nom de fichier différent." }, "label": { "title": "Dictées", @@ -298,13 +302,13 @@ "changeTranscriptionist": "Changer de transcriptionniste ", "deleteDictation": "Supprimer la dictée", "selectedTranscriptionist": "Transcriptionniste sélectionné", - "poolTranscriptionist": "Liste de transcriptionniste", + "poolTranscriptionist": "Liste des transcripteurs", "fileBackup": "Sauvegarde de fichiers", "downloadForBackup": "Télécharger pour sauvegarde", "applications": "Application de bureau", "cancelDictation": "Annuler la transcription", - "rawFileName": "(fr)Raw File Name", - "fileNameSave": "(fr)Save" + "rawFileName": "Nom du fichier d'origine", + "fileNameSave": "Exécuter le changement de nom du fichier" } }, "cardLicenseIssuePopupPage": { @@ -374,7 +378,7 @@ "issueRequesting": "Licences en commande", "viewDetails": "Voir les détails", "accounts": "comptes", - "changeOwnerButton": "(fr)Change Owner" + "changeOwnerButton": "Change Owner" } }, "orderHistoriesPage": { @@ -440,13 +444,13 @@ "templateOptional": "Masque (Facultatif)", "editRule": "Modifier la règle", "selected": "Transcriptionniste sélectionné", - "pool": "Liste de transcriptionniste", + "pool": "Liste des transcripteurs", "selectAuthor": "Sélectionner le Identifiant Auteur", "selectWorktypeId": "Sélectionner le Identifiant du Type de travail", "selectTemplate": "Sélectionner le Masque" }, "message": { - "selectedTypistEmptyError": "Transcriptionist ou Transcriptionist Group n’a pas été sélectionné. Veuillez en sélectionner un ou plusieurs dans la liste de transcription.", + "selectedTypistEmptyError": "Transcriptionist ou Transcriptionist Group n’a pas été sélectionné. Veuillez en sélectionner un ou plusieurs dans la liste de transcripteurs.", "workflowConflictError": "Une règle de routage a déjà été enregistrée avec la combinaison AuthorID et WorktypeID spécifiée. Veuillez vous inscrire avec une combinaison différente.", "inputEmptyError": "Champ obligatoire", "saveFailedError": "Échec de l'enregistrement de la règle de routage. Veuillez actualiser l'écran et réessayer." @@ -460,7 +464,7 @@ "addTypistGroup": "Ajouter un groupe de transcripteurs", "transcriptionist": "Transcriptionniste", "selected": "Transcriptionniste sélectionné", - "pool": "Liste de transcriptionniste", + "pool": "Liste des transcripteurs", "add": "Ajouter", "remove": "Supprimer", "editTypistGroup": "Modifier le groupe de transcripteurs" @@ -469,8 +473,8 @@ "selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.", "groupSaveFailedError": "Le groupe de transcriptionniste n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", "GroupNameAlreadyExistError": "Ce nom de groupe transcripteur est déjà enregistré. Veuillez vous inscrire avec un autre nom de groupe transcripteur.", - "deleteFailedWorkflowAssigned": "(fr)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", - "deleteFailedCheckoutPermissionExisted": "(fr)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" + "deleteFailedWorkflowAssigned": "Échec de la suppression du groupe transcripteur. Veuillez supprimer le groupe Transcriptionist de la règle de routage depuis l'onglet Workflow.", + "deleteFailedCheckoutPermissionExisted": "Échec de la suppression du groupe transcripteur. Une tâche est assignée à ce groupe transcripteur. Veuillez réaffecter la tâche à un autre transcripteur ou groupe de transcripteurs à partir de l'onglet Dictée." } }, "worktypeIdSetting": { @@ -520,8 +524,8 @@ "fileEmptyError": "La sélection de fichiers est requise. Veuillez sélectionner un fichier." }, "message": { - "deleteFailedWorkflowAssigned": "(fr)テンプレートファイルの削除に失敗しました。Workflow画面でルーティングルールから対象テンプレートファイルを外してください。", - "deleteFailedTaskAssigned": "(fr)テンプレートファイルの削除に失敗しました。Dictation画面で対象テンプレートファイルが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。" + "deleteFailedWorkflowAssigned": "Échec de la suppression du fichier modèle. Veuillez supprimer le fichier modèle joint à la règle de routage de l'onglet Workflow.", + "deleteFailedTaskAssigned": "Échec de la suppression du fichier modèle. Certaines tâches sont associées à ce modèle. Veuillez supprimer ou marquer les tâches comme terminées pour les tâches associées à ce modèle." } }, "partnerPage": { @@ -537,19 +541,19 @@ "dealerManagement": "Autoriser le revendeur à modifier les paramètres", "partners": "Partenaires", "deleteAccount": "Supprimer le compte", - "editAccount": "(fr)Edit Account", - "accountInformation": "(fr)Account information", - "primaryAdminInfo": "(fr)Primary administrator's information", - "adminName": "(fr)Admin Name", - "saveChanges": "(fr)Save Changes" + "editAccount": "Modifier le compte", + "accountInformation": "Information sur le compte", + "primaryAdminInfo": "Informations sur l'administrateur principal", + "adminName": "Nom de l'administrateur", + "saveChanges": "Sauvegarder les modifications" }, "message": { "delegateNotAllowedError": "Les actions au nom du partenaire ne sont pas autorisées. Veuillez actualiser l'écran et vérifier à nouveau.", "deleteFailedError": "L’opération de délégation a échoué. Veuillez actualiser l'écran et vérifier à nouveau.", "delegateCancelError": "L'opération déléguée a été interrompue car l'autorisation pour l'opération déléguée a été révoquée.", - "partnerDeleteConfirm": "(fr)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", - "editFailedError": "(fr)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" + "partnerDeleteConfirm": "Supprimez le compte sélectionné. Un compte supprimé ne peut pas être restauré, en êtes-vous sûr ? Compte cible :", + "partnerDeleteFailedError": "Ce compte ne peut pas être supprimé car des comptes enfants sont associés à ce compte parent. Vous devez déplacer ou supprimer les comptes enfants avant de supprimer ce compte parent. Veuillez contacter l'OMDS pour plus d'informations.", + "editFailedError": "Les informations à l'écran n'étant pas à jour, une incohérence est survenue lors de la modification de votre compte partenaire. Veuillez actualiser l'écran et réessayer." } }, "accountPage": { @@ -571,7 +575,7 @@ "selectSecondaryAdministrator": "Sélectionner le administrateur secondaire", "saveChanges": "Sauvegarder les modifications", "deleteAccount": "Supprimer le compte", - "fileRetentionDays": "(fr)自動ファイル削除までの保持日数" + "fileRetentionDays": "Nombre de jours pendant lesquels les fichiers sont conservés avant d'être automatiquement supprimés." }, "message": { "updateAccountFailedError": "Échec de l'enregistrement des informations du compte. Veuillez actualiser l'écran et réessayer." @@ -626,26 +630,26 @@ }, "fileDeleteSettingPopup": { "label": { - "title": "(fr)Auto File Delete Setting", - "autoFileDeleteCheck": "(fr)Auto file delete", - "daysAnnotation": "(fr)Number of days from transcription finished to delete the files.", - "days": "(fr)Days", - "saveButton": "(fr)Save Settings", - "daysValidationError": "(fr)Daysには1~999の数字を入力してください。" + "title": "Paramètre de suppression automatique de fichiers", + "autoFileDeleteCheck": "Suppression automatique des fichiers", + "daysAnnotation": "Número de días desde que finalizó la transcripción para eliminar los archivos.", + "days": "Jours", + "saveButton": "Enregistrer les paramètres", + "daysValidationError": "Veuillez saisir un nombre compris entre 1 et 999 pour les jours." } }, "changeOwnerPopup": { "message": { - "accountNotFoundError": "(fr)変更先のアカウントIDは存在しません。", - "hierarchyMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\nLower layerの1階層上のアカウントを切り替え先に指定してください。", - "regionMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\nLower layerと同じリージョンのアカウントを切り替え先に指定してください。", - "countryMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\nLower layerと同じ国のアカウントを切り替え先に指定してください。" + "accountNotFoundError": "The account ID specified to change does not exist.", + "hierarchyMismatchError": "Failed to change partner account.\nPlease specify the account one level above the child account as the switch destination.。", + "regionMismatchError": "Failed to change partner account.\nPlease specify an account in the same region as the child account to switch to.", + "countryMismatchError": "Failed to change partner account.\nPlease specify an account in the same country as the child account to switch to." }, "label": { - "invalidInputError": "(fr)変更先アカウントIDには1~9999999の数字を入力してください。", - "title": "(fr)Change Owner", - "upperLayerId": "(fr)Upper Layer ID", - "lowerLayerId": "(fr)Lower Layer ID" + "invalidInputError": "Please enter a number between 1 and 9999999 for the destination account ID.", + "title": "Change Owner", + "upperLayerId": "Upper Layer ID", + "lowerLayerId": "Lower Layer ID" } } } \ No newline at end of file From 4dac420bedad3d26993aedb32d4a400b1a870b99 Mon Sep 17 00:00:00 2001 From: "SAITO-PC-3\\saito.k" Date: Wed, 15 May 2024 17:00:20 +0900 Subject: [PATCH 095/109] =?UTF-8?q?typeORM=E3=81=AE=E3=83=A2=E3=82=B8?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=83=AB=E8=A8=AD=E5=AE=9A=E3=81=8C=E6=BC=8F?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=81=9F=E3=81=AE=E3=81=A7=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictation_server/src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index f35fe54..cdc19b7 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -54,6 +54,7 @@ import { RedisModule } from './gateways/redis/redis.module'; import * as redisStore from 'cache-manager-redis-store'; import { SystemAccessGuardsModule } from './common/guards/system/accessguards.module'; import { CheckHeaderMiddleware } from './common/check-header.middleware'; +import { JobNumberRepositoryModule } from './repositories/job_number/job_number.repository.module'; @Module({ imports: [ ServeStaticModule.forRootAsync({ @@ -140,6 +141,7 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; WorktypesRepositoryModule, TermsModule, RedisModule, + JobNumberRepositoryModule, ], controllers: [ HealthController, From 5860da285eb073faa32531444dd50313de937a51 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Fri, 17 May 2024 06:49:26 +0000 Subject: [PATCH 096/109] =?UTF-8?q?Merged=20PR=20899:=20=E9=9A=8E=E5=B1=A4?= =?UTF-8?q?=E6=A7=8B=E9=80=A0=E5=A4=89=E6=9B=B4API=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4179: 階層構造変更API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4179) - 階層構造変更API修正 - 第四⇔第五の切り替え処理の時は国・リージョンの一致を確認しない - 不要なエラーコード・エラーハンドリングを削除 - テスト修正 ## レビューポイント - 修正箇所の認識は合っているか - テストケースは足りているか ## 動作確認状況 - ローカルで確認 - 国の違うDealerに付け替える処理を行い成功した - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - 国の違う第四⇔第五間の付け替えのテストが成功することを確認 - ほかのテストが成功することを確認 ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/common/errors/code.ts | 1 - .../license/partnerLicense/operations.ts | 6 - dictation_server/src/common/error/code.ts | 1 - dictation_server/src/common/error/message.ts | 1 - .../accounts/accounts.service.spec.ts | 103 ++++++++++-------- .../src/features/accounts/accounts.service.ts | 19 +--- .../src/repositories/accounts/errors/types.ts | 9 -- 7 files changed, 63 insertions(+), 77 deletions(-) diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index fcef6f2..b1a366f 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -80,7 +80,6 @@ export const errorCodes = [ "E017001", // 親アカウント変更不可エラー(指定したアカウントが存在しない) "E017002", // 親アカウント変更不可エラー(階層関係が不正) "E017003", // 親アカウント変更不可エラー(リージョンが同一でない) - "E017004", // 親アカウント変更不可エラー(国が同一でない) "E018001", // パートナーアカウント削除エラー(削除条件を満たしていない) "E019001", // パートナーアカウント取得不可エラー(階層構造が不正) "E020001", // パートナーアカウント変更エラー(変更条件を満たしていない) diff --git a/dictation_client/src/features/license/partnerLicense/operations.ts b/dictation_client/src/features/license/partnerLicense/operations.ts index 8ceb0a8..84b5e73 100644 --- a/dictation_client/src/features/license/partnerLicense/operations.ts +++ b/dictation_client/src/features/license/partnerLicense/operations.ts @@ -174,12 +174,6 @@ export const switchParentAsync = createAsyncThunk< ); } - if (error.code === "E017004") { - errorMessage = getTranslationID( - "changeOwnerPopup.message.countryMismatchError" - ); - } - thunkApi.dispatch( openSnackbar({ level: "error", diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 0b5902e..740e842 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -85,7 +85,6 @@ export const ErrorCodes = [ 'E017001', // 親アカウント変更不可エラー(指定したアカウントが存在しない) 'E017002', // 親アカウント変更不可エラー(階層関係が不正) 'E017003', // 親アカウント変更不可エラー(リージョンが同一でない) - 'E017004', // 親アカウント変更不可エラー(国が同一でない) 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) 'E019001', // パートナーアカウント取得不可エラー(階層構造が不正) 'E020001', // パートナーアカウント変更エラー(変更条件を満たしていない) diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 6731596..89e51c0 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -75,7 +75,6 @@ export const errors: Errors = { E017001: 'Parent account switch failed Error: account not found', E017002: 'Parent account switch failed Error: hierarchy mismatch', E017003: 'Parent account switch failed Error: region mismatch', - E017004: 'Parent account switch failed Error: country mismatch', E018001: 'Partner account delete failed Error: not satisfied conditions', E019001: 'Partner account get failed Error: hierarchy mismatch', E020001: 'Partner account change failed Error: not satisfied conditions', diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 1dc617b..d185c6e 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -8220,6 +8220,65 @@ describe('switchParent', () => { expect(child2LicenseOrderStatuses).toBeTruthy(); }); + it('第四<->第五の切り替えで、親子で国が異なる場合でも階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 5, + country: `NZ`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 5, + country: `GB`, + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + it('切り替え先親アカウントが存在しない場合は400エラー(親アカウント不在エラー)を返す', async () => { if (!source) fail(); const module = await makeTestingModule(source); @@ -8403,50 +8462,6 @@ describe('switchParent', () => { } } }); - - it('第四<->第五の切り替えで、親子で国が異なる場合は400エラー(国関係不一致エラー)を返す', async () => { - if (!source) fail(); - const module = await makeTestingModule(source); - if (!module) fail(); - - const accountsRepositoryService = module.get( - AccountsRepositoryService, - ); - const child1 = new Account(); - child1.id = 1; - child1.tier = 5; - child1.country = 'AU'; - - const child2 = new Account(); - child2.id = 2; - child2.tier = 5; - child2.country = 'NZ'; // このアカウントだけ国が異なるようにしておく - - accountsRepositoryService.findAccountsById = jest - .fn() - .mockResolvedValue([child1, child2]); - - const context = makeContext('external_id', 'requestId'); - const service = module.get(AccountsService); - - const parent = new Account(); - parent.id = 10; - parent.tier = 4; - parent.country = 'AU'; - try { - accountsRepositoryService.findAccountById = jest - .fn() - .mockResolvedValue(parent); - await service.switchParent(context, parent.id, [child1.id, child2.id]); - } catch (e) { - if (e instanceof HttpException) { - expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); - expect(e.getResponse()).toEqual(makeErrorResponse('E017004')); - } else { - fail(); - } - } - }); }); describe('deletePartnerAccount', () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 6547f1c..3277dc9 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -51,7 +51,6 @@ import { LicensesRepositoryService } from '../../repositories/licenses/licenses. import { AccountNotFoundError, AdminUserNotFoundError, - CountryMismatchError, DealerAccountNotFoundError, HierarchyMismatchError, RegionMismatchError, @@ -2769,11 +2768,6 @@ export class AccountsService { makeErrorResponse('E017003'), HttpStatus.BAD_REQUEST, ); - case CountryMismatchError: - throw new HttpException( - makeErrorResponse('E017004'), - HttpStatus.BAD_REQUEST, - ); } } @@ -2825,9 +2819,10 @@ export class AccountsService { children: Account[], ): { success: boolean; - errorType: null | RegionMismatchError | CountryMismatchError; + errorType: null | RegionMismatchError; } { - // 第三<->第四の切り替えはリージョンの一致を確認し、第四<->第五の切り替えは国の一致を確認する。 + // 「プロダクト バックログ項目 4171: 階層構造切り替えのリージョンの制約を修正する」の対応で第五階層の親(第四階層)を切り替えるときはリージョン・国の制約をなくす。 2024年5月16日 + // 第三<->第四の切り替えはリージョンの一致を確認する。 if (parent.tier === TIERS.TIER3) { if ( !children.every( @@ -2843,13 +2838,7 @@ export class AccountsService { return { success: true, errorType: null }; } else if (parent.tier === TIERS.TIER4) { - if (!children.every((child) => child.country === parent.country)) { - return { - success: false, - errorType: new CountryMismatchError('Invalid country relation'), - }; - } - + // 第四<->第五の切り替えは国・リージョンの一致を確認しない。 return { success: true, errorType: null }; } else { // 親アカウントの階層が想定外の場合、本関数の使い方が間違っているので例外を投げる diff --git a/dictation_server/src/repositories/accounts/errors/types.ts b/dictation_server/src/repositories/accounts/errors/types.ts index 4b59a1b..2c661e3 100644 --- a/dictation_server/src/repositories/accounts/errors/types.ts +++ b/dictation_server/src/repositories/accounts/errors/types.ts @@ -52,12 +52,3 @@ export class RegionMismatchError extends Error { this.name = 'RegionMismatchError'; } } -/** - * 所属国不一致エラー - */ -export class CountryMismatchError extends Error { - constructor(message: string) { - super(message); - this.name = 'CountryMismatchError'; - } -} From bd8f035c469ef3618294e6241e72311ceff54ed4 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Thu, 23 May 2024 07:26:04 +0000 Subject: [PATCH 097/109] Merged PR 902: POST /files/audio/upload-finished MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3982: POST /files/audio/upload-finished](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3982) - /files/audio/upload-finished のバリデーションテスト作成 ## レビューポイント - テストケースで抜けているところはないか - IsNotEmpty()は一つに省略 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - テスト追加のため既存機能に影響なし ## 補足 - 相談、参考資料などがあれば --- .../features/files/files.controller.spec.ts | 188 +++++++++++++++++- .../workflows/workflows.service.spec.ts | 2 +- 2 files changed, 188 insertions(+), 2 deletions(-) diff --git a/dictation_server/src/features/files/files.controller.spec.ts b/dictation_server/src/features/files/files.controller.spec.ts index d79bf29..e5b87e6 100644 --- a/dictation_server/src/features/files/files.controller.spec.ts +++ b/dictation_server/src/features/files/files.controller.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FilesController } from './files.controller'; import { FilesService } from './files.service'; import { ConfigModule } from '@nestjs/config'; -import { FileRenameRequest } from './types/types'; +import { AudioUploadFinishedRequest, FileRenameRequest } from './types/types'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @@ -111,3 +111,189 @@ describe('valdation FileRenameRequest', () => { expect(errors.length).toBe(1); }); }); + +describe('valdation AudioUploadFinishedRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('必須パラメータが空文字の場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = ''; //空文字 + request.authorId = ''; //空文字 + request.fileName = ''; //空文字 + request.duration = ''; //空文字 + request.createdDate = ''; //空文字 + request.finishedDate = ''; //空文字 + request.uploadedDate = ''; //空文字 + request.priority = ''; //空文字 + // fileSizeは設定しない + request.audioFormat = ''; //空文字 + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(10); + }); + it('durationが数値変換不可の場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = 'aaaaa'; //数値変換不可 + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('fileSizeが数値でない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 'aaaa' as any; //数値でない + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('fileSizeがintではない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 5.01; //integerでない + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('priorityが00,01ではない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '001'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('optionItemListが10件ではない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '01'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); +}); diff --git a/dictation_server/src/features/workflows/workflows.service.spec.ts b/dictation_server/src/features/workflows/workflows.service.spec.ts index 633c13b..1ff4fe7 100644 --- a/dictation_server/src/features/workflows/workflows.service.spec.ts +++ b/dictation_server/src/features/workflows/workflows.service.spec.ts @@ -36,7 +36,7 @@ describe('getWorkflows', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 - logger: new TestLogger('file'), + logger: new TestLogger('none'), logging: true, }); return await s.initialize(); From 261a5afbddf41450939ff7ac992d643f95b4be52 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 28 May 2024 05:38:33 +0000 Subject: [PATCH 098/109] Merged PR 903: POST /notification/register MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3983: POST /notification/register](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3983) - `POST /notification/register`APIのバリデータに対するテストを追加しました。 ## レビューポイント - テストケースに不足はないか。 ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - テストの追加のみなのでほかに影響なし。 --- .../notification.controller.spec.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/dictation_server/src/features/notification/notification.controller.spec.ts b/dictation_server/src/features/notification/notification.controller.spec.ts index bbf71a7..7f95d10 100644 --- a/dictation_server/src/features/notification/notification.controller.spec.ts +++ b/dictation_server/src/features/notification/notification.controller.spec.ts @@ -2,6 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NotificationController } from './notification.controller'; import { NotificationService } from './notification.service'; import { ConfigModule } from '@nestjs/config'; +import { RegisterRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('NotificationController', () => { let controller: NotificationController; @@ -28,3 +31,57 @@ describe('NotificationController', () => { expect(controller).toBeDefined(); }); }); + +describe('valdation register', () => { + it('有効なリクエストが成功する(wns)', async () => { + const request = new RegisterRequest(); + request.pns = 'wns'; + request.handler = 'test'; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('有効なリクエストが成功する(apn)', async () => { + const request = new RegisterRequest(); + request.pns = 'apns'; + request.handler = 'test'; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('pnsが不正(wns or apns以外)な場合、リクエストが失敗する', async () => { + const request = new RegisterRequest(); + request.pns = 'invalid'; + request.handler = 'test'; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('handlerが空文字の場合、リクエストが失敗する', async () => { + const request = new RegisterRequest(); + request.pns = 'apns'; + request.handler = ''; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('handlerが文字列でない場合、リクエストが失敗する', async () => { + const request = { + pns: 'apns', + handler: 123, + }; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); +}); From 9736d236539c3cc1f7c859e8fd6176c2e13eb12b Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 28 May 2024 05:58:17 +0000 Subject: [PATCH 099/109] Merged PR 904: POST /accounts/worktypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3985: POST /accounts/worktypes](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3985) - `POST /accounts/worktypes`のリクエストバリデータにテストを追加しました。 ## レビューポイント - テストケースに不足はないか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - テストの追加のみなので影響なし --- .../accounts/accounts.controller.spec.ts | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index 454fd63..8ea840c 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -8,6 +8,7 @@ import { DeletePartnerAccountRequest, GetPartnerUsersRequest, UpdatePartnerInfoRequest, + CreateWorktypesRequest, } from './types/types'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @@ -41,6 +42,88 @@ describe('AccountsController', () => { expect(controller).toBeDefined(); }); + describe('valdation CreateWorktypesRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'TEST'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdが指定されていない場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが空文字の場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = ''; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字を超える場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = '123456789A1234567'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字の場合、リクエストが成功する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = '123456789A123456'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdに使用不可文字が含まれる場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'test.test'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字を超える場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A123456'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字の場合、リクエストが成功する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A12345'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + }); + describe('valdation switchParentRequest', () => { it('最低限の有効なリクエストが成功する', async () => { const request = new SwitchParentRequest(); From 0e68f26c571df5268730608c4f7687f281788e04 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Thu, 30 May 2024 00:18:59 +0000 Subject: [PATCH 100/109] =?UTF-8?q?Merged=20PR=20906:=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E8=AA=8D=E8=A8=BCAPI=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4182: ユーザー認証API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4182) - 認証済みチェックをパスワード変更より先に行うように修正 - パスワード変更に失敗したら、認証済みフラグをfalseにするリカバリ処理追加 - リカバリに失敗したら手動復旧ログを出力 - メール送信に失敗したらエラーを返すように修正 - メール送信に失敗したらリカバリ処理を行うように修正 - リカバリに失敗したら手動復旧ログを出力 - テスト修正 - リカバリ処理を考慮したケースを追加 ## レビューポイント - リカバリ処理の記述 - メール送信でエラーが起きたときにエラーを握りつぶさないようにしたが問題ないか - メール送信で失敗したときにエラーを握りつぶすと、ユーザーは届かないメールを待つしかなくなる - 失敗を伝えて、リカバリをしてあげると再実行してもらうことができる。 ## クエリの変更 - クエリの変更はなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 既存のテストケースをDBを使うテストに置き換え - 結果は変えずに通ることを確認 - テストケースを追加し、新たな観点でテストを作成 ## 補足 - 相談、参考資料などがあれば --- .../src/pages/UserVerifyPage/index.tsx | 5 +- dictation_server/src/common/test/overrides.ts | 25 + .../src/features/users/users.service.spec.ts | 717 +++++++++++++----- .../src/features/users/users.service.ts | 124 ++- .../users/users.repository.service.ts | 36 +- 5 files changed, 682 insertions(+), 225 deletions(-) diff --git a/dictation_client/src/pages/UserVerifyPage/index.tsx b/dictation_client/src/pages/UserVerifyPage/index.tsx index f573ffb..5e57b3b 100644 --- a/dictation_client/src/pages/UserVerifyPage/index.tsx +++ b/dictation_client/src/pages/UserVerifyPage/index.tsx @@ -15,11 +15,8 @@ const UserVerifyPage: React.FC = (): JSX.Element => { const jwt = query.get("verify") ?? ""; useEffect(() => { - if (!jwt) { - navigate("/mail-confirm/failed"); - } dispatch(userVerifyAsync({ jwt })); - }, [navigate, dispatch, jwt]); + }, [dispatch, jwt]); const verifyState = useSelector(VerifyStateSelector); diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index 4c65c7c..07e2025 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -35,6 +35,11 @@ export const overrideAdB2cService = ( externalIds: string[], ) => Promise; getUser?: (context: Context, externalId: string) => Promise; + changePassword?: ( + context: Context, + externalId: string, + password: string, + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -69,6 +74,12 @@ export const overrideAdB2cService = ( writable: true, }); } + if (overrides.changePassword) { + Object.defineProperty(obj, obj.changePassword.name, { + value: overrides.changePassword, + writable: true, + }); + } }; /** @@ -122,6 +133,8 @@ export const overrideUsersRepositoryService = ( overrides: { createNormalUser?: (user: newUser) => Promise; deleteNormalUser?: (userId: number) => Promise; + updateUserVerified?: (context: Context, userId: number) => Promise; + updateUserUnverified?: (context: Context, userId: number) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -138,6 +151,18 @@ export const overrideUsersRepositoryService = ( writable: true, }); } + if (overrides.updateUserVerified) { + Object.defineProperty(obj, obj.updateUserVerified.name, { + value: overrides.updateUserVerified, + writable: true, + }); + } + if (overrides.updateUserUnverified) { + Object.defineProperty(obj, obj.updateUserUnverified.name, { + value: overrides.updateUserUnverified, + writable: true, + }); + } }; /** diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 5246e06..e85a332 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -8,7 +8,6 @@ import { makeDefaultUsersRepositoryMockValue, makeUsersServiceMock, } from './test/users.service.mock'; -import { EmailAlreadyVerifiedError } from '../../repositories/users/errors/types'; import { createLicense, createUserGroup, @@ -22,6 +21,7 @@ import { LICENSE_ALLOCATED_STATUS, LICENSE_EXPIRATION_THRESHOLD_DAYS, LICENSE_TYPE, + MANUAL_RECOVERY_REQUIRED, TASK_STATUS, USER_AUDIO_FORMAT, USER_LICENSE_EXPIRY_STATUS, @@ -59,6 +59,7 @@ import { createTask } from '../files/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; import { MultipleImportErrors } from './types/types'; import { TestLogger } from '../../common/test/logger'; +import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; describe('UsersService.confirmUser', () => { let source: DataSource | null = null; @@ -254,216 +255,564 @@ describe('UsersService.confirmUser', () => { }); describe('UsersService.confirmUserAndInitPassword', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); it('ユーザーが発行されたパスワードでログインできるようにする', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: false, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), - auto_renew: true, - notification: true, - encryption: false, - prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + let _subject: string = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; - expect( + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + expect(_subject).toBe('Temporary password [U-113]'); + const user = await getUserFromExternalId(source, 'externalId_user1'); + expect(user?.email_verified).toBe(true); + }); + it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + + const token = 'invalid.id.token'; + + try { await service.confirmUserAndInitPassword( makeContext('trackingId', 'requestId'), token, - ), - ).toEqual(undefined); - }); - - it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: false, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), - auto_renew: true, - notification: true, - encryption: false, - prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, - ); - const token = 'invalid.id.token'; - await expect( - service.confirmUserAndInitPassword( - makeContext('trackingId', 'requestId'), - token, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST), - ); + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E000101')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // パスワードが変更されていないことを確認 + expect(adB2cService.changePassword).toBeCalledTimes(0); }); it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。(メール認証API)', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: true, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, auto_renew: true, - notification: true, encryption: false, + encryption_password: undefined, prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - usersRepositoryMockValue.updateUserVerified = new EmailAlreadyVerifiedError( - `Email already verified user`, - ); + email_verified: true, // emailを認証済みにする + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, - ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; - await expect( - service.confirmUserAndInitPassword( + + try { + await service.confirmUserAndInitPassword( makeContext('trackingId', 'requestId'), token, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST), + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010202')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていることを確認 + expect(user?.email_verified).toBe(true); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // パスワードが変更されていないことを確認 + expect(adB2cService.changePassword).toBeCalledTimes(0); + }); + it('ADB2Cユーザーのパスワード更新に失敗した場合、リカバリ処理を行い、メールを未認証のままにする。(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: async () => { + throw new Error('ADB2C Error'); + }, + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + }); + it('ADB2Cユーザーのパスワード更新に失敗した場合、リカバリ処理を行うが、リカバリ処理に失敗すると認証のままになる(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + overrideAdB2cService(service, { + changePassword: async () => { + throw new Error('ADB2C Error'); + }, + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideUsersRepositoryService(service, { + updateUserUnverified: async () => { + throw new Error('DB Error'); + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されたままであることを確認 + expect(user?.email_verified).toBe(true); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe(true); }); it('DBネットワークエラーとなる場合、エラーとなる。(メール認証API)', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: false, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), - auto_renew: true, - notification: true, - encryption: false, - prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - usersRepositoryMockValue.updateUserVerified = new Error('DB error'); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + // DBエラーを発生させる + overrideUsersRepositoryService(service, { + updateUserVerified: async () => { + throw new Error('DB Error'); + }, + }); + const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; - await expect( - service.confirmUserAndInitPassword( + try { + await service.confirmUserAndInitPassword( makeContext('trackingId', 'requestId'), token, - ), - ).rejects.toEqual( - new HttpException( - makeErrorResponse('E009999'), - HttpStatus.INTERNAL_SERVER_ERROR, - ), + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // パスワードが変更されていないことを確認 + expect(adB2cService.changePassword).toBeCalledTimes(0); + }); + it('メール送信に失敗した場合、リカバリ処理を行い、メールを未認証の状態にする。(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const adb2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: async () => { + throw new Error('SendGrid Error'); + }, + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // ADB2Cのパスワードが変更されていることを確認(パスワードは変更されても、ユーザーにメールが届いていないので問題ない) + expect(adb2cService.changePassword).toBeCalledTimes(1); + }); + it('メール送信に失敗した場合、リカバリ処理を行うが、リカバリ処理に失敗するとADB2Cのパスワードが変更され、DB上も認証された状態になる(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideUsersRepositoryService(service, { + updateUserUnverified: async () => { + throw new Error('DB Error'); + }, + }); + overrideSendgridService(service, { + sendMail: async () => { + throw new Error('SendGrid Error'); + }, + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されたままであることを確認 + expect(user?.email_verified).toBe(true); + // ADB2Cのパスワードが変更されていることを確認 + expect(adB2cService.changePassword).toBeCalledTimes(1); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe(true); }); }); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 6896e98..699efe6 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -516,46 +516,10 @@ export class UsersService { ); } - // ランダムなパスワードを生成する - const ramdomPassword = makePassword(); const { accountId, userId, email } = decodedToken; - try { - // ユーザー情報からAzure AD B2CのIDを特定する - const user = await this.usersRepository.findUserById(context, userId); - const extarnalId = user.external_id; - // パスワードを変更する - await this.adB2cService.changePassword( - context, - extarnalId, - ramdomPassword, - ); // ユーザを認証済みにする await this.usersRepository.updateUserVerified(context, userId); - - // メール送信処理 - try { - const { external_id: primaryAdminUserExternalId } = - await this.getPrimaryAdminUser(context, accountId); - - const adb2cUser = await this.adB2cService.getUser( - context, - primaryAdminUserExternalId, - ); - - const { displayName: primaryAdminName } = - getUserNameAndMailAddress(adb2cUser); - - await this.sendgridService.sendMailWithU113( - context, - email, - primaryAdminName, - ramdomPassword, - ); - } catch (e) { - this.logger.error(`[${context.getTrackingId()}] error=${e}`); - // メール送信に関する例外はログだけ出して握りつぶす - } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { @@ -572,6 +536,62 @@ export class UsersService { ); } } + } + + // ランダムなパスワードを生成する + const ramdomPassword = makePassword(); + try { + // ユーザー情報からAzure AD B2CのIDを特定する + const user = await this.usersRepository.findUserById(context, userId); + const extarnalId = user.external_id; + // パスワードを変更する + await this.adB2cService.changePassword( + context, + extarnalId, + ramdomPassword, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.error( + `[${context.getTrackingId()}] change password failed. userId=${userId}`, + ); + // リカバリー処理 + // ユーザを未認証に戻す + await this.updateUserUnverified(context, userId); + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + // メール送信処理 + try { + const { external_id: primaryAdminUserExternalId } = + await this.getPrimaryAdminUser(context, accountId); + + const adb2cUser = await this.adB2cService.getUser( + context, + primaryAdminUserExternalId, + ); + + const { displayName: primaryAdminName } = + getUserNameAndMailAddress(adb2cUser); + + await this.sendgridService.sendMailWithU113( + context, + email, + primaryAdminName, + ramdomPassword, + ); + } catch (e) { + // リカバリー処理 + // ユーザーを未認証に戻す + await this.updateUserUnverified(context, userId); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${ @@ -1791,4 +1811,36 @@ export class UsersService { return primaryAdmin; } + + /** + * ユーザーを未認証にする + * @param context + * @param userId + * @returns void + */ + private async updateUserUnverified( + context: Context, + userId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updateUserUnverified.name + } | params: { userId: ${userId} };`, + ); + try { + await this.usersRepository.updateUserUnverified(context, userId); + this.logger.log( + `[${context.getTrackingId()}] update user unverified: ${userId}`, + ); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to update user unverified: ${userId}`, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.updateUserUnverified.name}`, + ); + } + } } diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 01b0ff9..32de564 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -399,7 +399,7 @@ export class UsersRepositoryService { } /** - * 管理ユーザーがメール認証済みなら認証情報を更新する + * ユーザーがメール認証済みなら認証情報を更新する * @param user * @returns update */ @@ -437,6 +437,40 @@ export class UsersRepositoryService { }); } + /** + * ユーザーをメール未認証にする + * @param user + * @param context + * @param id + * @returns void + */ + async updateUserUnverified(context: Context, id: number): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + const targetUser = await userRepo.findOne({ + where: { + id: id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!targetUser) { + throw new UserNotFoundError(`User not Found.`); + } + + targetUser.email_verified = false; + + await updateEntity( + userRepo, + { id: targetUser.id }, + targetUser, + this.isCommentOut, + context, + ); + }); + } + /** * Emailを認証済みにして、トライアルライセンスを作成する * @param id From b7925f311b32c1258ffe5864bf05724b23455882 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 3 Jun 2024 05:07:08 +0000 Subject: [PATCH 101/109] =?UTF-8?q?Merged=20PR=20907:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=EF=BC=88=E3=82=BF=E3=82=B9=E3=82=AF=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=AE=E6=B4=BB=E6=80=A7?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E3=82=92=E4=BF=AE=E6=AD=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4200: 画面修正(タスク削除ボタンの活性条件を修正)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4200) - タスク削除ボタンの活性条件を修正 - Pendingの時は削除できないようにする ## レビューポイント - 特になし ## UIの変更 - https://ndstokyo.sharepoint.com/:i:/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/Task4200/Pending%E5%89%8A%E9%99%A4%E4%B8%8D%E5%8F%AF.png?csf=1&web=1&e=ATzEZ8 ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - Inprogressのステータスで削除できないこと - Uploaded・Finished・backupで削除できる ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/pages/DictationPage/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index f73dd27..1c8a47b 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -1266,9 +1266,11 @@ const DictationPage: React.FC = (): JSX.Element => {

  • {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} Date: Mon, 3 Jun 2024 05:39:53 +0000 Subject: [PATCH 102/109] =?UTF-8?q?Merged=20PR=20908:=20API=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=EF=BC=88=E3=82=BF=E3=82=B9=E3=82=AF=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E3=81=AE=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=B9=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4201: API修正(タスク削除条件のステータスチェックを修正)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4201) - タスク削除不可条件を修正 - Pendingの時も削除できないようにする ## レビューポイント - 修正の認識があっているか ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - タスクの各ステータスで削除可否を確かめられるように修正 ## 補足 - 相談、参考資料などがあれば --- .../src/features/tasks/tasks.service.spec.ts | 166 +++++++++++++++++- .../tasks/tasks.repository.service.ts | 10 +- 2 files changed, 168 insertions(+), 8 deletions(-) diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 04af977..cb59c38 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -4336,7 +4336,7 @@ describe('deleteTask', () => { source = null; }); - it('管理者として、アカウント内のタスクを削除できる', async () => { + it('管理者として、アカウント内のタスクを削除できる(タスクのStatusがUploaded)', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); @@ -4420,7 +4420,7 @@ describe('deleteTask', () => { ); } }); - it('Authorとして、自身が追加したタスクを削除できる', async () => { + it('Authorとして、自身が追加したタスクを削除できる(タスクのStatusがFinished)', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); @@ -4448,7 +4448,7 @@ describe('deleteTask', () => { '', '01', '00000001', - TASK_STATUS.UPLOADED, + TASK_STATUS.FINISHED, ); await createCheckoutPermissions(source, taskId, typistUserId); @@ -4460,7 +4460,92 @@ describe('deleteTask', () => { const optionItems = await getAudioOptionItems(source, taskId); expect(task?.id).toBe(taskId); - expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.status).toBe(TASK_STATUS.FINISHED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const blobStorageService = + module.get(BlobstorageService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + await service.deleteTask(context, authorExternalId, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'y.zip', + ); + } + }); + it('Authorとして、自身が追加したタスクを削除できる(タスクのStatusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.BACKUP, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.BACKUP); expect(task?.audio_file_id).toBe(audioFileId); expect(audioFile?.id).toBe(audioFileId); @@ -4578,6 +4663,79 @@ describe('deleteTask', () => { } } }); + it('ステータスがPendingのタスクを削除しようとした場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.PENDING, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.PENDING); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010601')); + } else { + fail(); + } + } + }); it('Authorが自身が作成したタスク以外を削除しようとした場合、エラーとなること', async () => { if (!source) fail(); const module = await makeTestingModule(source); diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index aef734f..e43c2cf 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -1436,13 +1436,15 @@ export class TasksRepositoryService { } } - // タスクのステータスがInProgressの場合はエラー - if (task.status === TASK_STATUS.IN_PROGRESS) { + // タスクのステータスがInProgress・Pendingの時はエラー + if ( + task.status === TASK_STATUS.IN_PROGRESS || + task.status === TASK_STATUS.PENDING + ) { throw new StatusNotMatchError( - `task status is InProgress. audio_file_id:${audioFileId}`, + `task status is InProgress or Pending. status:${task.status} , audio_file_id:${audioFileId}`, ); } - // タスクに紐づくオプションアイテムを削除 const optionItemRepo = entityManager.getRepository(AudioOptionItem); await deleteEntity( From 909c2a6d55a3dfc1d8addfba0863d1ca075d6c21 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 3 Jun 2024 06:36:05 +0000 Subject: [PATCH 103/109] =?UTF-8?q?Merged=20PR=20909:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=EF=BC=88CSV=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E5=A4=89=E6=8F=9B=E5=87=A6=E7=90=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4202: 画面修正(CSVファイルの変換処理)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4202) - CSVデータの変換関数を修正 - author_id,encryption_passwordが数値のみの場合でも、文字列として扱うようにする ## レビューポイント - ほかに確認したほうが良いテストケースはあるか ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - 既存のテストはすべて通ることを確認 ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/common/parser.test.ts | 19 +++++++++++++++++++ dictation_client/src/common/parser.ts | 19 ++++++++++++++++++- dictation_client/src/common/test/test_008.csv | 2 ++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 dictation_client/src/common/test/test_008.csv diff --git a/dictation_client/src/common/parser.test.ts b/dictation_client/src/common/parser.test.ts index d4aff39..a318a76 100644 --- a/dictation_client/src/common/parser.test.ts +++ b/dictation_client/src/common/parser.test.ts @@ -131,4 +131,23 @@ describe("parse", () => { expect(actualValue.role).toEqual(expectValue.role); } }); + + it("author_id,encryption_passwordが数値のみの場合でも、文字列として変換できる", async () => { + const text = fs.readFileSync("src/common/test/test_008.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: "1111", + auto_renew: 1, + notification: 1, + encryption: 1, + encryption_password: "222222", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); }); diff --git a/dictation_client/src/common/parser.ts b/dictation_client/src/common/parser.ts index 15fe85b..30e0b43 100644 --- a/dictation_client/src/common/parser.ts +++ b/dictation_client/src/common/parser.ts @@ -42,7 +42,24 @@ export const parseCSV = async (csvString: string): Promise => download: false, worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定 header: true, - dynamicTyping: true, + dynamicTyping: { + // author_id, encryption_passwordは数値のみの場合、numberに変換されたくないためdynamicTypingをtrueにしない + role: true, + auto_renew: true, + notification: true, + encryption: true, + prompt: true, + }, + // dynamicTypingがfalseの場合、空文字をnullに変換できないためtransformを使用する + transform: (value, field) => { + if (field === "author_id" || field === "encryption_password") { + // 空文字の場合はnullに変換する + if (value === "") { + return null; + } + } + return value; + }, complete: (results: ParseResult) => { // ヘッダーがCSVTypeFieldsと一致しない場合はエラーを返す if (!equals(results.meta.fields ?? [], CSVTypeFields)) { diff --git a/dictation_client/src/common/test/test_008.csv b/dictation_client/src/common/test/test_008.csv new file mode 100644 index 0000000..ccc9f52 --- /dev/null +++ b/dictation_client/src/common/test/test_008.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,1111,1,1,1,222222,0 \ No newline at end of file From 4f7d65f0e88b349804ce5de6eda87a51089cbb9b Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 4 Jun 2024 04:25:54 +0000 Subject: [PATCH 104/109] Merged PR 905: POST /accounts/worktypes/{id} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3986: POST /accounts/worktypes/{id}](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3986) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 - 修正箇所がほかの機能に影響していないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば --- .../accounts/accounts.controller.spec.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index 8ea840c..24bf667 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -9,6 +9,8 @@ import { GetPartnerUsersRequest, UpdatePartnerInfoRequest, CreateWorktypesRequest, + UpdateWorktypesRequest, + UpdateWorktypeRequestParam, } from './types/types'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @@ -124,6 +126,101 @@ describe('AccountsController', () => { }); }); + describe('valdation UpdateWorktypesRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'TEST'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdが指定されていない場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが空文字の場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = ''; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字を超える場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = '123456789A1234567'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字の場合、リクエストが成功する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = '123456789A123456'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdに使用不可文字が含まれる場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'test.test'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字を超える場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A123456'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字の場合、リクエストが成功する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A12345'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + }); + + describe('valdation UpdateWorktypeRequestParam', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new UpdateWorktypeRequestParam(); + request.id = 1; + + const valdationObject = plainToClass(UpdateWorktypeRequestParam, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('idが1より小さい場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypeRequestParam(); + request.id = 0; + + const valdationObject = plainToClass(UpdateWorktypeRequestParam, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('idが数値でない場合、リクエストが失敗する', async () => { + const request = { id: '0' }; + + const valdationObject = plainToClass(UpdateWorktypeRequestParam, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + describe('valdation switchParentRequest', () => { it('最低限の有効なリクエストが成功する', async () => { const request = new SwitchParentRequest(); From 6b1650a6342a50f52226a327a5f6c7705bd64c16 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Tue, 4 Jun 2024 06:54:42 +0000 Subject: [PATCH 105/109] =?UTF-8?q?Merged=20PR=20910:=20=E3=83=A1=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E6=96=87=E9=9D=A2=E4=BF=AE=E6=AD=A3=EF=BC=86=E3=83=A6?= =?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E4=B8=80=E6=8B=AC=E7=99=BB=E9=8C=B2?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4190: メール文面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4190) [Task4191: ユーザー一括登録のテンプレートファイル修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4191) - ユーザー一括追加時に送信するメールの内容を修正 - SCVとなっていたところをCSVに修正 - ユーザー一括追加用のエクセルの項目名を画面上の項目名と合わせる - auto_renewとなっていたところをauto_assignに修正 ## レビューポイント - 修正内容の認識は合っているか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - CSV変換のテストが通ることを確認 - メール内容はdev動作確認で確認 ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/common/parser.test.ts | 14 +++++++------- dictation_client/src/common/parser.ts | 6 +++--- dictation_client/src/common/test/test_001.csv | 2 +- dictation_client/src/common/test/test_002.csv | 2 +- dictation_client/src/common/test/test_003.csv | 2 +- dictation_client/src/common/test/test_004.csv | 2 +- dictation_client/src/common/test/test_005.csv | 2 +- dictation_client/src/common/test/test_006.csv | 2 +- dictation_client/src/common/test/test_007.csv | 2 +- dictation_client/src/common/test/test_008.csv | 2 +- dictation_client/src/features/user/operations.ts | 2 +- dictation_client/src/features/user/selectors.ts | 4 ++-- .../src/pages/UserListPage/importPopup.tsx | 2 +- dictation_server/src/templates/template_U_120.html | 6 +++--- dictation_server/src/templates/template_U_120.txt | 6 +++--- .../src/templates/template_U_120_no_parent.html | 6 +++--- .../src/templates/template_U_120_no_parent.txt | 6 +++--- dictation_server/src/templates/template_U_121.html | 6 +++--- dictation_server/src/templates/template_U_121.txt | 6 +++--- .../src/templates/template_U_121_no_parent.html | 6 +++--- .../src/templates/template_U_121_no_parent.txt | 6 +++--- 21 files changed, 46 insertions(+), 46 deletions(-) diff --git a/dictation_client/src/common/parser.test.ts b/dictation_client/src/common/parser.test.ts index a318a76..d690cd3 100644 --- a/dictation_client/src/common/parser.test.ts +++ b/dictation_client/src/common/parser.test.ts @@ -13,7 +13,7 @@ describe("parse", () => { email: "sample@example.com", role: 1, author_id: "HOGE", - auto_renew: 1, + auto_assign: 1, notification: 1, encryption: 1, encryption_password: "abcd", @@ -58,7 +58,7 @@ describe("parse", () => { email: "sample@example.com", role: 1, author_id: null, - auto_renew: 1, + auto_assign: 1, notification: 1, encryption: 1, encryption_password: "abcd", @@ -76,7 +76,7 @@ describe("parse", () => { email: "sample@example.com", role: null, author_id: "HOGE", - auto_renew: 1, + auto_assign: 1, notification: 1, encryption: 1, encryption_password: "abcd", @@ -94,7 +94,7 @@ describe("parse", () => { email: "sample@example.com", role: 1, author_id: "HOGE", - auto_renew: 1, + auto_assign: 1, notification: 1, encryption: 1, encryption_password: "abcd", @@ -105,7 +105,7 @@ describe("parse", () => { email: "sample2@example.com", role: 1, author_id: "HOGE2", - auto_renew: 1, + auto_assign: 1, notification: 1, encryption: 1, encryption_password: "abcd2", @@ -119,7 +119,7 @@ describe("parse", () => { const actualValue = actualData[i]; const expectValue = expectData[i]; expect(actualValue.author_id).toEqual(expectValue.author_id); - expect(actualValue.auto_renew).toEqual(expectValue.auto_renew); + expect(actualValue.auto_assign).toEqual(expectValue.auto_assign); expect(actualValue.email).toEqual(expectValue.email); expect(actualValue.encryption).toEqual(expectValue.encryption); expect(actualValue.encryption_password).toEqual( @@ -141,7 +141,7 @@ describe("parse", () => { email: "sample@example.com", role: 1, author_id: "1111", - auto_renew: 1, + auto_assign: 1, notification: 1, encryption: 1, encryption_password: "222222", diff --git a/dictation_client/src/common/parser.ts b/dictation_client/src/common/parser.ts index 30e0b43..8d76b30 100644 --- a/dictation_client/src/common/parser.ts +++ b/dictation_client/src/common/parser.ts @@ -6,7 +6,7 @@ export type CSVType = { email: string | null; role: number | null; author_id: string | null; - auto_renew: number | null; + auto_assign: number | null; notification: number; encryption: number | null; encryption_password: string | null; @@ -19,7 +19,7 @@ const CSVTypeFields: (keyof CSVType)[] = [ "email", "role", "author_id", - "auto_renew", + "auto_assign", "notification", "encryption", "encryption_password", @@ -45,7 +45,7 @@ export const parseCSV = async (csvString: string): Promise => dynamicTyping: { // author_id, encryption_passwordは数値のみの場合、numberに変換されたくないためdynamicTypingをtrueにしない role: true, - auto_renew: true, + auto_assign: true, notification: true, encryption: true, prompt: true, diff --git a/dictation_client/src/common/test/test_001.csv b/dictation_client/src/common/test/test_001.csv index 7f52660..4b48912 100644 --- a/dictation_client/src/common/test/test_001.csv +++ b/dictation_client/src/common/test/test_001.csv @@ -1,2 +1,2 @@ -name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_002.csv b/dictation_client/src/common/test/test_002.csv index 6a385a7..31409e4 100644 --- a/dictation_client/src/common/test/test_002.csv +++ b/dictation_client/src/common/test/test_002.csv @@ -1,2 +1,2 @@ -name,email,role,auto_renew,notification,encryption,encryption_password,prompt +name,email,role,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_003.csv b/dictation_client/src/common/test/test_003.csv index 4fb47b2..443c07f 100644 --- a/dictation_client/src/common/test/test_003.csv +++ b/dictation_client/src/common/test/test_003.csv @@ -1,2 +1,2 @@ -name,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +name,role,author_id,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_004.csv b/dictation_client/src/common/test/test_004.csv index fec0f30..a35bfa1 100644 --- a/dictation_client/src/common/test/test_004.csv +++ b/dictation_client/src/common/test/test_004.csv @@ -1,2 +1,2 @@ -name,emeil,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +name,emeil,role,author_id,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_005.csv b/dictation_client/src/common/test/test_005.csv index b8165d9..4e9f9c7 100644 --- a/dictation_client/src/common/test/test_005.csv +++ b/dictation_client/src/common/test/test_005.csv @@ -1,2 +1,2 @@ -name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,1,,1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_006.csv b/dictation_client/src/common/test/test_006.csv index 18ee411..93e65f3 100644 --- a/dictation_client/src/common/test/test_006.csv +++ b/dictation_client/src/common/test/test_006.csv @@ -1,2 +1,2 @@ -name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_007.csv b/dictation_client/src/common/test/test_007.csv index ea049ff..9447f41 100644 --- a/dictation_client/src/common/test/test_007.csv +++ b/dictation_client/src/common/test/test_007.csv @@ -1,3 +1,3 @@ -name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0,x hoge2,sample2@example.com,1,"HOGE2",1,1,1,abcd2,0,1,32,4,aa \ No newline at end of file diff --git a/dictation_client/src/common/test/test_008.csv b/dictation_client/src/common/test/test_008.csv index ccc9f52..a40daec 100644 --- a/dictation_client/src/common/test/test_008.csv +++ b/dictation_client/src/common/test/test_008.csv @@ -1,2 +1,2 @@ -name,email,role,author_id,auto_renew,notification,encryption,encryption_password,prompt +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt hoge,sample@example.com,1,1111,1,1,1,222222,0 \ No newline at end of file diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index 8b9c782..6e2cf2c 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -534,7 +534,7 @@ export const importUsersAsync = createAsyncThunk< email: user.email ?? "", role: user.role ?? 0, authorId: user.author_id ?? undefined, - autoRenew: user.auto_renew ?? 0, + autoRenew: user.auto_assign ?? 0, notification: user.notification ?? 0, encryption: user.encryption ?? undefined, encryptionPassword: user.encryption_password ?? undefined, diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index eb9852c..8cd9bdf 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -493,8 +493,8 @@ export const selectImportValidationErrors = (state: RootState) => { } } - // auto_renew - if (csvUser.auto_renew === null || ![0, 1].includes(csvUser.auto_renew)) { + // auto_assign + if (csvUser.auto_assign === null || ![0, 1].includes(csvUser.auto_assign)) { invalidInput.push(rowNumber); // eslint-disable-next-line no-continue continue; diff --git a/dictation_client/src/pages/UserListPage/importPopup.tsx b/dictation_client/src/pages/UserListPage/importPopup.tsx index fcc879a..c844730 100644 --- a/dictation_client/src/pages/UserListPage/importPopup.tsx +++ b/dictation_client/src/pages/UserListPage/importPopup.tsx @@ -52,7 +52,7 @@ export const ImportPopup: React.FC = (props) => { "email", "role", "author_id", - "auto_renew", + "auto_assign", "notification", "encryption", "encryption_password", diff --git a/dictation_server/src/templates/template_U_120.html b/dictation_server/src/templates/template_U_120.html index 3f5c3ad..03dd097 100644 --- a/dictation_server/src/templates/template_U_120.html +++ b/dictation_server/src/templates/template_U_120.html @@ -13,7 +13,7 @@

    We have received your bulk user registration request.
    - Date and time: $REQUEST_TIME$
    - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$

    ・Please wait until the registration is complete. This may take a few minutes to process.
    @@ -36,7 +36,7 @@

    Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten.
    - Datum und Uhrzeit: $REQUEST_TIME$
    - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$

    ・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern.
    @@ -59,7 +59,7 @@

    Nous avons reçu votre demande d'enregistrement groupé d'utilisateur.
    - Date et heure : $REQUEST_TIME$
    - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$

    ・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes.
    diff --git a/dictation_server/src/templates/template_U_120.txt b/dictation_server/src/templates/template_U_120.txt index e33e7e0..2d856d9 100644 --- a/dictation_server/src/templates/template_U_120.txt +++ b/dictation_server/src/templates/template_U_120.txt @@ -4,7 +4,7 @@ Dear $CUSTOMER_NAME$, We have received your bulk user registration request. - Date and time: $REQUEST_TIME$ - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$ ・Please wait until the registration is complete. This may take a few minutes to process. ・Notification will be sent separately upon completion. @@ -21,7 +21,7 @@ Sehr geehrte(r) $CUSTOMER_NAME$, Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. - Datum und Uhrzeit: $REQUEST_TIME$ - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$ ・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern. ・Eine Benachrichtigung wird nach Abschluss separat verschickt. @@ -38,7 +38,7 @@ Chère/Cher $CUSTOMER_NAME$, Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. - Date et heure : $REQUEST_TIME$ - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$ ・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes. ・Une notification sera envoyée séparément une fois terminée. diff --git a/dictation_server/src/templates/template_U_120_no_parent.html b/dictation_server/src/templates/template_U_120_no_parent.html index 730449c..ae319b0 100644 --- a/dictation_server/src/templates/template_U_120_no_parent.html +++ b/dictation_server/src/templates/template_U_120_no_parent.html @@ -13,7 +13,7 @@

    We have received your bulk user registration request.
    - Date and time: $REQUEST_TIME$
    - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$

    ・Please wait until the registration is complete. This may take a few minutes to process.
    @@ -33,7 +33,7 @@

    Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten.
    - Datum und Uhrzeit: $REQUEST_TIME$
    - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$

    ・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern.
    @@ -53,7 +53,7 @@

    Nous avons reçu votre demande d'enregistrement groupé d'utilisateur.
    - Date et heure : $REQUEST_TIME$
    - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$

    ・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes.
    diff --git a/dictation_server/src/templates/template_U_120_no_parent.txt b/dictation_server/src/templates/template_U_120_no_parent.txt index d5d85fa..4e46961 100644 --- a/dictation_server/src/templates/template_U_120_no_parent.txt +++ b/dictation_server/src/templates/template_U_120_no_parent.txt @@ -4,7 +4,7 @@ Dear $CUSTOMER_NAME$, We have received your bulk user registration request. - Date and time: $REQUEST_TIME$ - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$ ・Please wait until the registration is complete. This may take a few minutes to process. ・Notification will be sent separately upon completion. @@ -19,7 +19,7 @@ Sehr geehrte(r) $CUSTOMER_NAME$, Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. - Datum und Uhrzeit: $REQUEST_TIME$ - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$ ・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern. ・Eine Benachrichtigung wird nach Abschluss separat verschickt. @@ -34,7 +34,7 @@ Chère/Cher $CUSTOMER_NAME$, Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. - Date et heure : $REQUEST_TIME$ - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$ ・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes. ・Une notification sera envoyée séparément une fois terminée. diff --git a/dictation_server/src/templates/template_U_121.html b/dictation_server/src/templates/template_U_121.html index 4598b6b..e2eac21 100644 --- a/dictation_server/src/templates/template_U_121.html +++ b/dictation_server/src/templates/template_U_121.html @@ -10,7 +10,7 @@

    Bulk user registration using the CSV file has been completed.
    - Date and time: $REQUEST_TIME$
    - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$

    ・User Registration Notification [U-114] will be sent to the registered users.
    @@ -33,7 +33,7 @@

    Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen.
    - Datum und Uhrzeit: $REQUEST_TIME$
    - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$

    ・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet.
    @@ -57,7 +57,7 @@

    L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé.
    - Date et heure : $REQUEST_TIME$
    - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$

    ・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés.
    diff --git a/dictation_server/src/templates/template_U_121.txt b/dictation_server/src/templates/template_U_121.txt index 63e9176..b04a729 100644 --- a/dictation_server/src/templates/template_U_121.txt +++ b/dictation_server/src/templates/template_U_121.txt @@ -4,7 +4,7 @@ Dear $CUSTOMER_NAME$, Bulk user registration using the CSV file has been completed. - Date and time: $REQUEST_TIME$ - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$ ・User Registration Notification [U-114] will be sent to the registered users. ・Registration will not be completed unless the user verifies their email address. @@ -21,7 +21,7 @@ Sehr geehrte(r) $CUSTOMER_NAME$, Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen. - Datum und Uhrzeit: $REQUEST_TIME$ - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$ ・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet. ・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt. @@ -38,7 +38,7 @@ Chère/Cher $CUSTOMER_NAME$, L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé. - Date et heure : $REQUEST_TIME$ - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$ ・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés. ・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail. diff --git a/dictation_server/src/templates/template_U_121_no_parent.html b/dictation_server/src/templates/template_U_121_no_parent.html index 25393c9..2f52ad9 100644 --- a/dictation_server/src/templates/template_U_121_no_parent.html +++ b/dictation_server/src/templates/template_U_121_no_parent.html @@ -10,7 +10,7 @@

    Bulk user registration using the CSV file has been completed.
    - Date and time: $REQUEST_TIME$
    - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$

    ・User Registration Notification [U-114] will be sent to the registered users.
    @@ -30,7 +30,7 @@

    Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen.
    - Datum und Uhrzeit: $REQUEST_TIME$
    - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$

    ・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet.
    @@ -51,7 +51,7 @@

    L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé.
    - Date et heure : $REQUEST_TIME$
    - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$

    ・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés.
    diff --git a/dictation_server/src/templates/template_U_121_no_parent.txt b/dictation_server/src/templates/template_U_121_no_parent.txt index c1f7987..650d6b7 100644 --- a/dictation_server/src/templates/template_U_121_no_parent.txt +++ b/dictation_server/src/templates/template_U_121_no_parent.txt @@ -4,7 +4,7 @@ Dear $CUSTOMER_NAME$, Bulk user registration using the CSV file has been completed. - Date and time: $REQUEST_TIME$ - - SCV file name: $FILE_NAME$ + - CSV file name: $FILE_NAME$ ・User Registration Notification [U-114] will be sent to the registered users. ・Registration will not be completed unless the user verifies their email address. @@ -19,7 +19,7 @@ Sehr geehrte(r) $CUSTOMER_NAME$, Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen. - Datum und Uhrzeit: $REQUEST_TIME$ - - SCV-Dateiname: $FILE_NAME$ + - CSV-Dateiname: $FILE_NAME$ ・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet. ・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt. @@ -34,7 +34,7 @@ Chère/Cher $CUSTOMER_NAME$, L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé. - Date et heure : $REQUEST_TIME$ - - Nom du fichier SCV : $FILE_NAME$ + - Nom du fichier CSV : $FILE_NAME$ ・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés. ・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail. From 83732fa1b5ac45aecaf1ffe5ec9b0ea2a729e24d Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Thu, 6 Jun 2024 07:36:17 +0000 Subject: [PATCH 106/109] =?UTF-8?q?Merged=20PR=20911:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=EF=BC=88=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=90=8D=E5=A4=89=E6=9B=B4=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=AE?= =?UTF-8?q?=E3=83=87=E3=82=B6=E3=82=A4=E3=83=B3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4213: 画面修正(ファイル名変更ボタンのデザイン)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4213) - ファイル名変更ボタンのデザイン修正 - 言語を英語以外にすると、ファイル名の入力欄と重なってしまうのを修正 - ボタンの位置を入力欄の下に修正 - リテラル修正も含めて対応 - https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%2036-1?workitem=4214 ## レビューポイント - 特になし ## UIの変更 - Before/Afterのスクショなど - 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/Task4213?csf=1&web=1&e=ftBwWm ## 動作確認状況 - ローカルで確認 - デザインの修正のみのためデグレは発生しない ## 補足 - 修正したデザインに対してはOMDS様に確認済み --- .../src/pages/DictationPage/filePropertyPopup.tsx | 9 +++++++++ dictation_client/src/translation/de.json | 2 +- dictation_client/src/translation/en.json | 2 +- dictation_client/src/translation/es.json | 4 ++-- dictation_client/src/translation/fr.json | 4 ++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx index 4f3e9bb..e3b2e09 100644 --- a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx +++ b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx @@ -104,6 +104,15 @@ export const FilePropertyPopup: React.FC = (props) => { name="submit" value={t(getTranslationID("dictationPage.label.fileNameSave"))} className={`${styles.formSubmit} ${styles.isActive}`} + style={{ + position: "relative", + marginTop: "0.2rem", + right: "auto", + maxWidth: "18rem", + whiteSpace: "normal", + overflowWrap: "break-word", + fontSize: "small", + }} onClick={saveFileName} /> {isPushSaveButton && fileName.length === 0 && ( diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 1a3a348..67b2367 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -654,4 +654,4 @@ "lowerLayerId": "Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index ec99d53..2059296 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -654,4 +654,4 @@ "lowerLayerId": "Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 930f15b..86d5a23 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -15,7 +15,7 @@ "copyRight": "© OM Digital Solutions Corporation", "edit": "Editar", "save": "Ahorrar", - "delete": "Delete", + "delete": "Borrar", "return": "Devolver", "operationInsteadOf": "Operar la nube ODMS en nombre de:", "headerAccount": "Cuenta", @@ -654,4 +654,4 @@ "lowerLayerId": "Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 7bc9f65..083360c 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -15,7 +15,7 @@ "copyRight": "© OM Digital Solutions Corporation", "edit": "Éditer", "save": "Sauvegarder", - "delete": "Delete", + "delete": "Supprimer", "return": "Retour", "operationInsteadOf": "Exploiter le Cloud ODMS pour le compte de :", "headerAccount": "Compte", @@ -654,4 +654,4 @@ "lowerLayerId": "Lower Layer ID" } } -} +} \ No newline at end of file From 474cdd56c6133187992b24089dfa00e1cbfdc087 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 10 Jun 2024 02:31:35 +0000 Subject: [PATCH 107/109] =?UTF-8?q?Merged=20PR=20913:=20=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=BB=E3=83=B3=E3=82=B9=E5=8F=96=E5=BE=97API=E3=81=AE?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4220: ライセンス取得APIの修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4220) - 割り当て可能ライセンス取得APIを修正 - 有効期限が当日のライセンスは割り当て可能としないように修正 - 有効期限が翌日以降のライセンスを取得 - テスト修正 - ライセンス割り当ての処理にコメント追加 - 結果として有効期限が当日のライセンスは割り当てられないので問題はないが、記述方法が紛らわしいのでコメントを追加した。 ## レビューポイント - テストケースは足りているか - 追加したコメントの意図は伝わるか ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - クエリ部分を修正したが、基準となる日付を変更したのみなので、クエリ自体に変更はなし - Before/Afterで確認したが、変更はなかった ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - テストを追加して、デグレがないことを検証した ## 補足 - 相談、参考資料などがあれば --- .../licenses/licenses.service.spec.ts | 415 +++++++++++++++++- .../licenses/licenses.repository.service.ts | 14 +- 2 files changed, 418 insertions(+), 11 deletions(-) diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 72ac6ea..37912c3 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -25,6 +25,7 @@ import { } from '../../constants'; import { makeHierarchicalAccounts, + makeTestAccount, makeTestSimpleAccount, makeTestUser, } from '../../common/test/utility'; @@ -387,8 +388,12 @@ describe('カードライセンスを取り込む', () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); + // 明日の日付を取得 + // ミリ秒以下は切り捨てる + const tommorow = new Date(); + tommorow.setDate(tommorow.getDate() + 1); + tommorow.setMilliseconds(0); - const now = new Date(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: externalId } = await makeTestUser(source, { account_id: accountId, @@ -402,7 +407,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 1, - new Date(now.getTime() + 60 * 60 * 1000), + new Date(tommorow.getTime() + 60 * 60 * 1000), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -428,7 +433,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 3, - new Date(now.getTime() + 60 * 60 * 1000), + new Date(tommorow.getTime() + 60 * 60 * 1000), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -441,7 +446,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 4, - new Date(now.getTime() + 60 * 60 * 1000 * 2), + new Date(tommorow.getTime() + 60 * 60 * 1000 * 2), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -467,7 +472,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 6, - new Date(now.getTime() + 60 * 60 * 1000 * 2), + new Date(tommorow.getTime() + 60 * 60 * 1000 * 2), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.ALLOCATED, @@ -480,7 +485,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 7, - new Date(now.getTime() + 60 * 60 * 1000 * 2), + new Date(tommorow.getTime() + 60 * 60 * 1000 * 2), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.DELETED, @@ -493,7 +498,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 8, - new Date(now.getTime() + 60 * 60 * 1000), + new Date(tommorow.getTime() + 60 * 60 * 1000), accountId + 1, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -506,7 +511,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 9, - new Date(now.getTime() - 60 * 60 * 1000 * 24), + new Date(tommorow.getTime() - 60 * 60 * 1000 * 24), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -1610,3 +1615,397 @@ describe('ライセンス注文キャンセル', () => { ); }); }); + +describe('割り当て可能なライセンス取得', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('割り当て可能なライセンスを取得できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // 有効期限が当日のライセンスを1つ、有効期限が翌日のライセンスを1つ、有効期限が翌々日のライセンスを1つ作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const today = new Date(); + today.setMilliseconds(0); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setMilliseconds(0); + const dayAfterTomorrow = new Date(); + dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 2); + dayAfterTomorrow.setMilliseconds(0); + await createLicense( + source, + 1, + today, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + await createLicense( + source, + 2, + tomorrow, + account.id, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + await createLicense( + source, + 3, + dayAfterTomorrow, + account.id, + LICENSE_TYPE.TRIAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + // ライセンス作成したデータを確認 + { + const license1 = await selectLicense(source, 1); + expect(license1.license?.id).toBe(1); + expect(license1.license?.expiry_date).toEqual(today); + expect(license1.license?.allocated_user_id).toBe(null); + expect(license1.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license1.license?.account_id).toBe(account.id); + expect(license1.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license2 = await selectLicense(source, 2); + expect(license2.license?.id).toBe(2); + expect(license2.license?.expiry_date).toEqual(tomorrow); + expect(license2.license?.allocated_user_id).toBe(null); + expect(license2.license?.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE); + expect(license2.license?.account_id).toBe(account.id); + expect(license2.license?.type).toBe(LICENSE_TYPE.CARD); + const license3 = await selectLicense(source, 3); + expect(license3.license?.id).toBe(3); + expect(license3.license?.expiry_date).toEqual(dayAfterTomorrow); + expect(license3.license?.allocated_user_id).toBe(null); + expect(license3.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license3.license?.account_id).toBe(account.id); + expect(license3.license?.type).toBe(LICENSE_TYPE.TRIAL); + } + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + + // 有効期限が当日のライセンスは取得されない + // 有効期限が長い順に取得される + expect(response.allocatableLicenses.length).toBe(2); + expect(response.allocatableLicenses[0].licenseId).toBe(3); + expect(response.allocatableLicenses[0].expiryDate).toEqual( + dayAfterTomorrow, + ); + expect(response.allocatableLicenses[1].licenseId).toBe(2); + expect(response.allocatableLicenses[1].expiryDate).toEqual(tomorrow); + }); + + it('割り当て可能なライセンスが存在しない場合、空の配列を返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // 有効期限が当日のライセンスを3つ、有効期限が昨日のライセンスを1つ作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const today = new Date(); + today.setMilliseconds(0); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setMilliseconds(0); + for (let i = 1; i <= 3; i++) { + await createLicense( + source, + i, + today, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + } + await createLicense( + source, + 4, + yesterday, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + // ライセンス作成したデータを確認 + { + const license1 = await selectLicense(source, 1); + expect(license1.license?.id).toBe(1); + expect(license1.license?.expiry_date).toEqual(today); + expect(license1.license?.allocated_user_id).toBe(null); + expect(license1.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license1.license?.account_id).toBe(account.id); + expect(license1.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license2 = await selectLicense(source, 2); + expect(license2.license?.id).toBe(2); + expect(license2.license?.expiry_date).toEqual(today); + expect(license2.license?.allocated_user_id).toBe(null); + expect(license2.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license2.license?.account_id).toBe(account.id); + expect(license2.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license3 = await selectLicense(source, 3); + expect(license3.license?.id).toBe(3); + expect(license3.license?.expiry_date).toEqual(today); + expect(license3.license?.allocated_user_id).toBe(null); + expect(license3.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license3.license?.account_id).toBe(account.id); + expect(license3.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license4 = await selectLicense(source, 4); + expect(license4.license?.id).toBe(4); + expect(license4.license?.expiry_date).toEqual(yesterday); + expect(license4.license?.allocated_user_id).toBe(null); + expect(license4.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license4.license?.account_id).toBe(account.id); + expect(license4.license?.type).toBe(LICENSE_TYPE.NORMAL); + } + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + // 有効期限が当日のライセンスは取得されない + // 有効期限が切れているライセンスは取得されない + expect(response.allocatableLicenses.length).toBe(0); + expect(response.allocatableLicenses).toEqual([]); + }); + + it('割り当て可能なライセンスを100件取得できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // 有効期限が30日後のライセンスを100件作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const date = new Date(); + date.setDate(date.getDate() + 30); + date.setMilliseconds(0); + for (let i = 1; i <= 100; i++) { + await createLicense( + source, + i, + date, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + } + // ライセンス作成したデータを確認 + for (let i = 1; i <= 100; i++) { + const license = await selectLicense(source, i); + expect(license.license?.id).toBe(i); + expect(license.license?.expiry_date).toEqual(date); + expect(license.license?.allocated_user_id).toBe(null); + expect(license.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license.license?.account_id).toBe(account.id); + expect(license.license?.type).toBe(LICENSE_TYPE.NORMAL); + } + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + // 100件取得できる + expect(response.allocatableLicenses.length).toBe(100); + }); + it('既に割り当てられているライセンスは取得されない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // ライセンスを5件作成(10日後から1日ずつ有効期限を設定) + // 有効期限が設定されていないライセンス(新規ライセンス)を1件作成 + // 既に割り当てられているライセンスを1件作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const date = new Date(); + date.setMinutes(0); + date.setSeconds(0); + date.setDate(date.getDate() + 10); + date.setMilliseconds(0); + for (let i = 1; i <= 5; i++) { + await createLicense( + source, + i, + date, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + admin.id, + null, + null, + null, + ); + date.setDate(date.getDate() + 1); + } + // 新規ライセンス + await createLicense( + source, + 6, + null, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + // 既に割り当てられているライセンス + await createLicense( + source, + 7, + date, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + admin.id, + null, + null, + null, + ); + // ライセンス作成したデータを確認 + { + const date = new Date(); + date.setMinutes(0); + date.setSeconds(0); + date.setDate(date.getDate() + 10); + date.setMilliseconds(0); + for (let i = 1; i <= 5; i++) { + const license = await selectLicense(source, i); + expect(license.license?.id).toBe(i); + expect(license.license?.expiry_date).toEqual(date); + expect(license.license?.allocated_user_id).toBe(admin.id); + expect(license.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license.license?.account_id).toBe(account.id); + expect(license.license?.type).toBe(LICENSE_TYPE.NORMAL); + date.setDate(date.getDate() + 1); + } + const newLicense = await selectLicense(source, 6); + expect(newLicense.license?.id).toBe(6); + expect(newLicense.license?.expiry_date).toBe(null); + expect(newLicense.license?.allocated_user_id).toBe(null); + expect(newLicense.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + const allocatedLicense = await selectLicense(source, 7); + expect(allocatedLicense.license?.id).toBe(7); + expect(allocatedLicense.license?.expiry_date).toEqual(date); + expect(allocatedLicense.license?.allocated_user_id).toBe(admin.id); + expect(allocatedLicense.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.ALLOCATED, + ); + expect(allocatedLicense.license?.account_id).toBe(account.id); + expect(allocatedLicense.license?.type).toBe(LICENSE_TYPE.NORMAL); + } + + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + // 既に割り当てられているライセンスは取得されない + // 新規ライセンスは取得される + // 有効期限が長い順に取得される + expect(response.allocatableLicenses.length).toBe(6); + expect(response.allocatableLicenses[0].licenseId).toBe(6); + expect(response.allocatableLicenses[0].expiryDate).toBe(undefined); + expect(response.allocatableLicenses[1].licenseId).toBe(5); // 有効期限が最も長い + expect(response.allocatableLicenses[2].licenseId).toBe(4); + expect(response.allocatableLicenses[3].licenseId).toBe(3); + expect(response.allocatableLicenses[4].licenseId).toBe(2); + expect(response.allocatableLicenses[5].licenseId).toBe(1); + }); +}); diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 5d20eb4..5b6e873 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -525,9 +525,14 @@ export class LicensesRepositoryService { context: Context, myAccountId: number, ): Promise { - const nowDate = new DateWithZeroTime(); const licenseRepo = this.dataSource.getRepository(License); // EntityManagerではorderBy句で、expiry_dateに対して複数条件でソートを使用するため出来ない為、createQueryBuilderを使用する。 + // プロダクト バックログ項目 4218: [FB対応]有効期限当日のライセンスは一覧に表示しない の対応 + // 有効期限が当日のライセンスは取得しない + // 明日の00:00:00を取得 + const tomorrowDate = new DateWithZeroTime( + new Date().setDate(new Date().getDate() + 1), + ); const queryBuilder = licenseRepo .createQueryBuilder('license') .where('license.account_id = :accountId', { accountId: myAccountId }) @@ -538,8 +543,8 @@ export class LicensesRepositoryService { ], }) .andWhere( - '(license.expiry_date >= :nowDate OR license.expiry_date IS NULL)', - { nowDate }, + '(license.expiry_date >= :tomorrowDate OR license.expiry_date IS NULL)', + { tomorrowDate }, ) .comment(`${context.getTrackingId()}_${new Date().toUTCString()}`) .orderBy('license.expiry_date IS NULL', 'DESC') @@ -597,6 +602,9 @@ export class LicensesRepositoryService { } // 期限切れの場合はエラー + // 有効期限が当日のライセンスは割り当て不可 + // XXX 記述は「有効期限が過去のライセンスは割り当て不可」のような意図だと思われるが、実際の処理は「有効期限が当日のライセンスは割り当て不可」になっている + // より正確な記述に修正したほうが良いが、リリース後のため、修正は保留(2024年6月7日) if (targetLicense.expiry_date) { const currentDay = new Date(); currentDay.setHours(23, 59, 59, 999); From aea66f46169d1c1cfc7133b663134e69a403ea08 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Tue, 11 Jun 2024 05:05:53 +0000 Subject: [PATCH 108/109] =?UTF-8?q?Merged=20PR=20914:=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E5=89=8A=E9=99=A4API=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4223: ユーザー削除API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4223) - Authorの削除条件の修正 - Authorが作成したタスクがある場合は削除できない - Typistの削除条件の修正 - 文字起こし担当のタスクがある場合は削除できないように修正 - 挙動は変わらない(記述を修正した) - リテラル反映 - テストケース追加 ## レビューポイント - テストケースは足りているか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - 64行目のクエリ(Author/Typistともに) - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/Task4223?csf=1&web=1&e=UsjxWP ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - Authorが作成したタスクがある場合にそのAuthorは削除できないことを確認(タスクのステータスはFinished・Backup) - AuthorのAuthorIDが設定されているタスクがある場合はそのAuthorは削除できないことを確認(すべてのステータス) - Typistが文字起こし担当のタスクがある場合そのTypistは削除できないことを確認(タスクのステータスはFinished・Backup) ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/translation/de.json | 8 +- dictation_client/src/translation/en.json | 8 +- dictation_client/src/translation/es.json | 8 +- dictation_client/src/translation/fr.json | 8 +- .../src/features/users/users.service.spec.ts | 742 +++++++++++++++++- .../src/features/users/users.service.ts | 8 +- .../src/repositories/users/errors/types.ts | 12 +- .../users/users.repository.service.ts | 61 +- 8 files changed, 801 insertions(+), 54 deletions(-) diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 67b2367..db88543 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -54,7 +54,7 @@ }, "text": { "maintenanceNotificationTitle": "Hinweis auf geplante Wartungsarbeiten", - "maintenanceNotification": "Aufgrund von Systemwartungsarbeiten wird ODMS Cloud ab dem 12. Juni, 6:00 Uhr UTC-Zeit, etwa eine Stunde lang nicht verfügbar sein. Wir entschuldigen uns für etwaige Unannehmlichkeiten, die während der Wartung entstanden sind." + "maintenanceNotification": "Aufgrund von Systemwartungsarbeiten wird ODMS Cloud ab dem 17. Juni, 6:00 Uhr UTC-Zeit, etwa eine Stunde lang nicht verfügbar sein. Wir entschuldigen uns für etwaige Unannehmlichkeiten, die während der Wartung entstanden sind." } }, "signupPage": { @@ -138,8 +138,8 @@ "userDeletionLicenseActiveError": "Der Benutzer konnte nicht gelöscht werden. Bitte heben Sie die Lizenzzuweisung vom Benutzer auf.", "typistDeletionRoutingRuleError": "Der Benutzer konnte nicht gelöscht werden. Dieser Benutzer ist als Transkriptionist registriert, der in den Routing-Regeln enthalten ist. Bitte entfernen Sie den Transcriptionist aus der entsprechenden Routing-Regel auf der Registerkarte „Workflow“.", "adminUserDeletionError": "Der Benutzer konnte nicht gelöscht werden. Bitte entfernen Sie den Benutzer vom primären oder sekundären Administrator auf der Registerkarte „Konto“.", - "typistUserDeletionTranscriptionTaskError": "Der Benutzer konnte nicht gelöscht werden. Diesem Transkriptionisten ist eine Aufgabe zugewiesen. Bitte ändern Sie die für die Aufgabe verantwortliche Person auf der Registerkarte „Diktieren“ in einen anderen Transkriptionisten.", - "authorUserDeletionTranscriptionTaskError": "Der Benutzer konnte nicht gelöscht werden. Es gibt von diesem Autor erstellte Aufgaben, die unvollständig sind. Bitte löschen Sie die Aufgaben, die von diesem Autor erstellt wurden, oder markieren Sie sie als erledigt.", + "typistUserDeletionTranscriptionTaskError": "Der Benutzer konnte nicht gelöscht werden. Diesem Transkriptionisten ist eine Aufgabe zugewiesen. Bitte ändern Sie die für die Aufgabe verantwortliche Person auf der Registerkarte „Diktat“ in einen anderen Transkriptionisten, wenn noch nicht abgeschlossene Aufgaben vorhanden sind. Falls der Status der Diktate als „Abgeschlossen“ oder „Backup“ markiert ist, löschen Sie bitte die Aufgabe.", + "authorUserDeletionTranscriptionTaskError": "Der Benutzer konnte nicht gelöscht werden. Es gibt von diesem Autor erstellte Aufgaben, die noch nicht abgeschlossen sind. Bitte markieren Sie die Aufgaben als abgeschlossen und löschen Sie sie, bevor Sie den Autor löschen.", "typistUserDeletionTranscriptionistGroupError": "Der Benutzer konnte nicht gelöscht werden. Bitte entfernen Sie diesen Transkriptionisten aus der Transkriptionistengruppe auf der Registerkarte „Workflow“.", "authorDeletionRoutingRuleError": "Der Benutzer konnte nicht gelöscht werden. Bitte entfernen Sie diesen Autor aus den Weiterleitungsregeln auf der Registerkarte „Workflow“.", "importSuccess": "Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. Bitte überprüfen Sie Ihre E-Mails, da Sie eine E-Mail erhalten, sobald der Registrierungsprozess abgeschlossen ist.", @@ -253,7 +253,7 @@ "dictationPage": { "message": { "noPlaybackAuthorization": "Sie haben keine Berechtigung zum Abspielen dieser Datei.", - "taskToPlaybackNoExists": "Die Datei kann nicht abgespielt werden, da sie bereits transkribiert wurde oder nicht existiert.", + "taskToPlaybackNoExists": "Die Datei kann nicht abgespielt werden, weil sie bereits transkribiert wurde, nicht existiert oder Sie eine andere Datei in Bearbeitung haben. Da Sie nicht mehr als eine Datei gleichzeitig in Bearbeitung haben können, ändern Sie die andere Datei in „Ausstehend“ und versuchen Sie es erneut.", "taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.", "backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.", "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.", diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 2059296..9d7fbe7 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -54,7 +54,7 @@ }, "text": { "maintenanceNotificationTitle": "Notice of scheduled maintenance", - "maintenanceNotification": "Due to system maintenance, ODMS Cloud will be unavailable for approximately one hour starting from June 12th, 6:00AM UTC time. We apologize for any inconvenience caused during the maintenance." + "maintenanceNotification": "Due to system maintenance, ODMS Cloud will be unavailable for approximately one hour starting from June 17th, 6:00AM UTC time. We apologize for any inconvenience caused during the maintenance." } }, "signupPage": { @@ -138,8 +138,8 @@ "userDeletionLicenseActiveError": "Failed to delete the user. Please unassign the license from the user.", "typistDeletionRoutingRuleError": "Failed to delete the user. This user is registered as a Transcriptionist that is included in the routing rules. Please remove the Transcriptionist from the corresponding routing rule from the Workflow tab.", "adminUserDeletionError": "Failed to delete the user. Please remove the user from the Primary or Secondary Administrator from the Account tab.", - "typistUserDeletionTranscriptionTaskError": "Failed to delete the user. There is a task assigned to this Transcriptionist. Please change the person in charge of the task to another Transcriptionist from the Dictation tab.", - "authorUserDeletionTranscriptionTaskError": "Failed to delete the user. There are tasks created by this Author that are incomplete. Please delete or mark the tasks as finished for tasks created by this Author.", + "typistUserDeletionTranscriptionTaskError": "Failed to delete the user. There is a task assigned to this Transcriptionist. Please change the person in charge of the task to another Transcriptionist from the Dictation tab if there are unfinished tasks. In case the status of dictations is marked as Finished or Backup, please delete the task.", + "authorUserDeletionTranscriptionTaskError": "Failed to delete the user. There are tasks created by this Author that are incomplete. Please mark the tasks as finished and then delete them before deleting the Author.", "typistUserDeletionTranscriptionistGroupError": "Failed to delete the user. Please remove this Transcriptionist from the Transcriptionist Group from the Workflow tab.", "authorDeletionRoutingRuleError": "Failed to delete the user. Please remove this Author from the routing rules from the Workflow tab.", "importSuccess": "We have received your bulk user registration request. Please check your email as you will receive an email once the registration process is complete.", @@ -253,7 +253,7 @@ "dictationPage": { "message": { "noPlaybackAuthorization": "You do not have permission to playback this file.", - "taskToPlaybackNoExists": "The file cannot be played because it has already been transcribed or does not exist.", + "taskToPlaybackNoExists": "The file cannot be played because it has already been transcribed, does not exist, or you have another file that is in progress. Since you cannot have more than one file in progress at a time, please change the other file to Pending and try again.", "taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.", "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.", "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.", diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 86d5a23..676b1b2 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -54,7 +54,7 @@ }, "text": { "maintenanceNotificationTitle": "Aviso de mantenimiento programado", - "maintenanceNotification": "Debido al mantenimiento del sistema, ODMS Cloud no estará disponible durante aproximadamente una hora a partir del 12 de junio a las 6:00 am, hora UTC. Pedimos disculpas por cualquier inconveniente causado durante el mantenimiento." + "maintenanceNotification": "Debido al mantenimiento del sistema, ODMS Cloud no estará disponible durante aproximadamente una hora a partir del 17 de junio a las 6:00 am, hora UTC. Pedimos disculpas por cualquier inconveniente causado durante el mantenimiento." } }, "signupPage": { @@ -138,8 +138,8 @@ "userDeletionLicenseActiveError": "No se pudo eliminar el usuario. Desasignar la licencia al usuario.", "typistDeletionRoutingRuleError": "No se pudo eliminar el usuario. Este usuario está registrado como Transcriptor que está incluido en las reglas de enrutamiento. Elimine al transcriptor de la regla de enrutamiento correspondiente en la pestaña Flujo de trabajo.", "adminUserDeletionError": "No se pudo eliminar el usuario. Elimine el usuario del administrador principal o secundario desde la pestaña Cuenta.", - "typistUserDeletionTranscriptionTaskError": "No se pudo eliminar el usuario. Hay una tarea asignada a este transcriptor. Cambie la persona a cargo de la tarea a otro Transcriptor desde la pestaña Dictado.", - "authorUserDeletionTranscriptionTaskError": "No se pudo eliminar el usuario. Hay tareas creadas por este Autor que están incompletas. Elimine o marque las tareas como finalizadas para las tareas creadas por este autor.", + "typistUserDeletionTranscriptionTaskError": "No se pudo eliminar el usuario. Hay una tarea asignada a este transcriptor. Cambie la persona a cargo de la tarea a otro Transcriptor desde la pestaña Dictado si hay tareas pendientes. En caso de que el estado de los dictados esté marcado como Finalizado o Copia de seguridad, elimine la tarea.", + "authorUserDeletionTranscriptionTaskError": "No se pudo eliminar el usuario. Hay tareas creadas por este autor que están incompletas. Marque las tareas como finalizadas y luego elimínelas antes de eliminar al autor.", "typistUserDeletionTranscriptionistGroupError": "No se pudo eliminar el usuario. Elimine a este transcriptor del grupo de transcriptores de la pestaña Flujo de trabajo.", "authorDeletionRoutingRuleError": "No se pudo eliminar el usuario. Elimine a este autor de las reglas de enrutamiento de la pestaña Flujo de trabajo.", "importSuccess": "Hemos recibido su solicitud de registro de usuario masivo. Por favor revise su correo electrónico ya que recibirá un correo electrónico una vez que se complete el proceso de registro.", @@ -253,7 +253,7 @@ "dictationPage": { "message": { "noPlaybackAuthorization": "No tienes permiso para reproducir este archivo.", - "taskToPlaybackNoExists": "El archivo no se puede reproducir porque ya ha sido transcrito o no existe.", + "taskToPlaybackNoExists": "El archivo no se puede reproducir porque ya se ha transcrito, no existe o hay otro archivo en progreso. Dado que no puede tener más de un archivo en progreso a la vez, cambie el otro archivo a Pendiente e inténtelo nuevamente.", "taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.", "backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.", "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.", diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 083360c..302ef18 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -54,7 +54,7 @@ }, "text": { "maintenanceNotificationTitle": "Avis de maintenance programmée", - "maintenanceNotification": "En raison de la maintenance du système, ODMS Cloud sera indisponible pendant environ une heure à partir du 12 juin à 6h00, heure UTC. Nous nous excusons pour tout inconvénient causé lors de la maintenance." + "maintenanceNotification": "En raison de la maintenance du système, ODMS Cloud sera indisponible pendant environ une heure à partir du 17 juin à 6h00, heure UTC. Nous nous excusons pour tout inconvénient causé lors de la maintenance." } }, "signupPage": { @@ -138,8 +138,8 @@ "userDeletionLicenseActiveError": "Échec de la suppression de l'utilisateur. Veuillez annuler l'attribution de la licence à l'utilisateur.", "typistDeletionRoutingRuleError": "Échec de la suppression de l'utilisateur. Cet utilisateur est enregistré en tant que Transcripteur qui est inclus dans les règles de routage. Supprimez le transcripteur de la règle de routage correspondante dans l'onglet Workflow. Routing-Regel auf der Registerkarte „Workflow“.", "adminUserDeletionError": "Échec de la suppression de l'utilisateur. Veuillez supprimer l'utilisateur de l'administrateur principal ou secondaire de l'onglet Compte.", - "typistUserDeletionTranscriptionTaskError": "Échec de la suppression de l'utilisateur. Une tâche est assignée à ce transcripteur. Veuillez remplacer la personne en charge de la tâche par un autre transcripteur depuis l'onglet Dictée.", - "authorUserDeletionTranscriptionTaskError": "Échec de la suppression de l'utilisateur. Certaines tâches créées par cet auteur sont incomplètes. Veuillez supprimer ou marquer les tâches comme terminées pour les tâches créées par cet auteur.", + "typistUserDeletionTranscriptionTaskError": "Échec de la suppression de l'utilisateur. Une tâche est assignée à ce transcripteur. Veuillez remplacer la personne en charge de la tâche par un autre transcripteur depuis l'onglet Dictée s'il y a des tâches inachevées. Si le statut des dictées est marqué comme Terminé ou Sauvegarde, veuillez supprimer la tâche.", + "authorUserDeletionTranscriptionTaskError": "Échec de la suppression de l'utilisateur. Certaines tâches créées par cet auteur sont incomplètes. Veuillez marquer les tâches comme terminées, puis supprimez-les avant de supprimer l'auteur.", "typistUserDeletionTranscriptionistGroupError": "Échec de la suppression de l'utilisateur. Veuillez supprimer ce transcripteur du groupe transcripteur de l'onglet Workflow.", "authorDeletionRoutingRuleError": "Échec de la suppression de l'utilisateur. Supprimez cet auteur des règles de routage dans l'onglet Workflow.", "importSuccess": "Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. Veuillez vérifier votre courrier électronique car vous recevrez un e-mail une fois le processus d'inscription terminé.", @@ -253,7 +253,7 @@ "dictationPage": { "message": { "noPlaybackAuthorization": "Vous n'êtes pas autorisé à lire ce fichier.", - "taskToPlaybackNoExists": "Le fichier ne peut pas être lu car il a déjà été transcrit ou n'existe pas.", + "taskToPlaybackNoExists": "Le fichier ne peut pas être lu car il a déjà été transcrit, n'existe pas ou vous avez un autre fichier en cours. Étant donné que vous ne pouvez pas avoir plus d'un fichier en cours à la fois, veuillez modifier l'autre fichier en En attente et réessayer.", "taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.", "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.", "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.", diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index e85a332..899167e 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -3287,7 +3287,7 @@ describe('UsersService.deleteUser', () => { source = null; }); - it('ユーザーを削除できる', async () => { + it('ユーザーを削除できる(Author)', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); @@ -3298,6 +3298,7 @@ describe('UsersService.deleteUser', () => { const { id: user1, external_id } = await makeTestUser(source, { account_id: account.id, role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_1', }); const service = module.get(UsersService); @@ -3352,6 +3353,71 @@ describe('UsersService.deleteUser', () => { expect(userArchive[0].external_id).toBe(external_id); } }); + it('ユーザーを削除できる(Typist)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.TYPIST, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + + // ユーザーが削除されたことを確認 + { + const user = await getUser(source, user1); + expect(user).toBeNull(); + // ユーザアーカイブが作成されたことを確認 + const userArchive = await getUserArchive(source); + expect(userArchive[0].external_id).toBe(external_id); + } + },600000); it('存在しないユーザは削除できない', async () => { if (!source) fail(); const module = await makeTestingModule(source); @@ -3486,7 +3552,7 @@ describe('UsersService.deleteUser', () => { } } }); - it('WorkFlowに割り当てられているユーザ(Auhtor)は削除できない', async () => { + it('WorkFlowに割り当てられているユーザ(Author)は削除できない', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); @@ -3749,7 +3815,292 @@ describe('UsersService.deleteUser', () => { } } }); - it('削除対象ユーザーが有効なタスクをまだ持っている場合、削除できない', async () => { + it('削除対象ユーザー(Author)のAuthorIDが設定されているタスクがある場合、削除できない(statusがInprogress)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.IN_PROGRESS, + user2, + 'AUTHOR', + admin.id, // 作成者がadmin + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)のAuthorIDが設定されているタスクがある場合、削除できない(statusがFinished)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.FINISHED, + user2, + 'AUTHOR', + admin.id, // 作成者がadmin + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)のAuthorIDが設定されているタスクがある場合、削除できない(statusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.BACKUP, + user2, + 'AUTHOR', + admin.id, // 作成者がadmin + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)が有効なタスクをまだ持っている場合、削除できない(statusがInprogress)', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); @@ -3844,7 +4195,197 @@ describe('UsersService.deleteUser', () => { } } }); - it('削除対象ユーザータスクのチェックアウト権限まだ持っている場合、削除できない', async () => { + it('削除対象ユーザー(Author)が有効なタスクをまだ持っている場合、削除できない(statusがFinished)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.FINISHED, + user2, + 'AUTHOR', + user1, + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)が有効なタスクをまだ持っている場合、削除できない(statusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.BACKUP, + user2, + 'AUTHOR', + user1, + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Typist)タスクのチェックアウト権限まだ持っている場合、削除できない', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); @@ -3931,6 +4472,199 @@ describe('UsersService.deleteUser', () => { } } }); + + it('削除対象ユーザー(Typist)が文字起こし担当のタスクがまだ持っている場合、削除できない(statusがFinished)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // user2が文字起こし担当のタスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.FINISHED, + user2, + 'AUTHOR', + user1, + ); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014009')); + } else { + fail(); + } + } + },600000); + it('削除対象ユーザー(Typist)が文字起こし担当のタスクがまだ持っている場合、削除できない(statusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // user2が文字起こし担当のタスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.BACKUP, + user2, + 'AUTHOR', + user1, + ); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014009')); + } else { + fail(); + } + } + }); it('削除対象ユーザーが有効なライセンスをまだ持っている場合、削除できない', async () => { if (!source) fail(); const module = await makeTestingModule(source); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 699efe6..ecea95d 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -36,10 +36,10 @@ import { AuthorIdAlreadyExistsError, EmailAlreadyVerifiedError, EncryptionPasswordNeedError, - ExistsCheckoutPermissionDeleteFailedError, ExistsGroupMemberDeleteFailedError, + ExistsTaskDeleteFailedError, + ExistsValidCheckoutDeleteFailedError, ExistsValidLicenseDeleteFailedError, - ExistsValidTaskDeleteFailedError, InvalidRoleChangeError, UpdateTermsVersionNotSetError, UserNotFoundError, @@ -1446,7 +1446,7 @@ export class UsersService { makeErrorResponse('E014005'), HttpStatus.BAD_REQUEST, ); - case ExistsValidTaskDeleteFailedError: + case ExistsTaskDeleteFailedError: throw new HttpException( makeErrorResponse('E014006'), HttpStatus.BAD_REQUEST, @@ -1456,7 +1456,7 @@ export class UsersService { makeErrorResponse('E014007'), HttpStatus.BAD_REQUEST, ); - case ExistsCheckoutPermissionDeleteFailedError: + case ExistsValidCheckoutDeleteFailedError: throw new HttpException( makeErrorResponse('E014009'), HttpStatus.BAD_REQUEST, diff --git a/dictation_server/src/repositories/users/errors/types.ts b/dictation_server/src/repositories/users/errors/types.ts index 5468900..c5352fa 100644 --- a/dictation_server/src/repositories/users/errors/types.ts +++ b/dictation_server/src/repositories/users/errors/types.ts @@ -87,19 +87,19 @@ export class ExistsGroupMemberDeleteFailedError extends Error { } } -// 削除対象ユーザー(Author)に未完了のタスクがまだ残っている事が原因の削除失敗エラー -export class ExistsValidTaskDeleteFailedError extends Error { +// 削除対象ユーザー(Author)が作成したタスクがまだ残っている事が原因の削除失敗エラー +export class ExistsTaskDeleteFailedError extends Error { constructor(message: string) { super(message); - this.name = 'ExistsValidTaskDeleteFailedError'; + this.name = 'ExistsTaskDeleteFailedError'; } } -// 削除対象ユーザーがチェックアウト権限を持っている事が原因の削除失敗エラー -export class ExistsCheckoutPermissionDeleteFailedError extends Error { +// 削除対象ユーザーがチェックアウト権限を持っているor文字起こし担当者としてアサインされている事が原因の削除失敗エラー +export class ExistsValidCheckoutDeleteFailedError extends Error { constructor(message: string) { super(message); - this.name = 'ExistsCheckoutPermissionDeleteFailedError'; + this.name = 'ExistsValidCheckoutDeleteFailedError'; } } diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 32de564..9e4bc39 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -3,7 +3,6 @@ import { User, UserArchive, newUser } from './entity/user.entity'; import { DataSource, FindOptionsWhere, - In, IsNull, Not, UpdateResult, @@ -26,9 +25,9 @@ import { AssignedWorkflowWithTypistDeleteFailedError, AssignedWorkflowWithAuthorDeleteFailedError, AdminDeleteFailedError, - ExistsValidTaskDeleteFailedError, - ExistsCheckoutPermissionDeleteFailedError, ExistsValidLicenseDeleteFailedError, + ExistsValidCheckoutDeleteFailedError, + ExistsTaskDeleteFailedError, } from './errors/types'; import { LICENSE_ALLOCATED_STATUS, @@ -742,37 +741,51 @@ export class UsersRepositoryService { // 削除対象ユーザーがAuthorであった時、 if (target.role === USER_ROLES.AUTHOR) { const taskRepo = entityManager.getRepository(Task); - // 自分が所有者のタスクの一覧を取得する - const tasks = await taskRepo.find({ + // 自分が所有者のタスクが存在するか確認 + const ownerTasksExist = await taskRepo.exist({ relations: { file: true, }, - where: { - account_id: target.account_id, - status: Not(TASK_STATUS.BACKUP), // 数が膨大になりうる&有効なタスクへの状態遷移ができないBACKUPは除いて取得 - file: { - owner_user_id: target.id, + where: [ + { + account_id: target.account_id, + file: { + owner_user_id: target.id, + }, }, - }, + { + account_id: target.account_id, + file: { + author_id: target.author_id ?? undefined, + }, + }, + ], lock: { mode: 'pessimistic_write' }, // lockする事で状態遷移の競合をブロックし、新規追加以外で所有タスク群の状態変更を防ぐ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); - // 未完了タスクが残っていたら削除できない - const enableStatus: string[] = [ - TASK_STATUS.UPLOADED, - TASK_STATUS.IN_PROGRESS, - TASK_STATUS.PENDING, - ]; - // 未完了タスクを列挙 - const enableTasks = tasks.filter((task) => - enableStatus.includes(task.status), - ); - if (enableTasks.length > 0) { - throw new ExistsValidTaskDeleteFailedError('User has valid tasks.'); + // 削除対象のユーザーが所有者のタスクが存在する場合は削除できない + if (ownerTasksExist) { + throw new ExistsTaskDeleteFailedError('User has tasks.'); } } // 削除対象ユーザーがTypistであった時、 if (target.role === USER_ROLES.TYPIST) { + const taskRepo = entityManager.getRepository(Task); + // 削除対象ユーザーが文字起こし担当のタスクが存在するか確認 + const transcriptionTasksExist = await taskRepo.exist({ + where: { + account_id: target.account_id, + typist_user_id: target.id, + }, + lock: { mode: 'pessimistic_write' }, // lockする事で状態遷移の競合をブロックし、新規追加以外で所有タスク群の状態変更を防ぐ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 削除対象のユーザーが文字起こし担当のタスクが存在する場合は削除できない + if (transcriptionTasksExist) { + throw new ExistsValidCheckoutDeleteFailedError('User has tasks.'); + } + const checkoutPermissionRepo = entityManager.getRepository(CheckoutPermission); const permissions = await checkoutPermissionRepo.find({ @@ -785,7 +798,7 @@ export class UsersRepositoryService { // タスクのチェックアウト権限が残っていたら削除できない if (permissions.length !== 0) { - throw new ExistsCheckoutPermissionDeleteFailedError( + throw new ExistsValidCheckoutDeleteFailedError( 'User has checkout permissions.', ); } From 254cbdf0d6375f7071d67e8906cd83556395ef44 Mon Sep 17 00:00:00 2001 From: "SAITO-PC-3\\saito.k" Date: Wed, 12 Jun 2024 14:04:16 +0900 Subject: [PATCH 109/109] =?UTF-8?q?=E3=83=AA=E3=83=86=E3=83=A9=E3=83=AB?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=E6=BC=8F=E3=82=8C=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictation_client/src/pages/UserListPage/importPopup.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dictation_client/src/pages/UserListPage/importPopup.tsx b/dictation_client/src/pages/UserListPage/importPopup.tsx index c844730..a8be391 100644 --- a/dictation_client/src/pages/UserListPage/importPopup.tsx +++ b/dictation_client/src/pages/UserListPage/importPopup.tsx @@ -153,7 +153,9 @@ export const ImportPopup: React.FC = (props) => { {t(getTranslationID("userListPage.label.importCsv"))} -

    Input rules
    +
    + {t(getTranslationID("userListPage.label.inputRules"))} +
    {t(getTranslationID("userListPage.label.nameLabel"))}
    {t(getTranslationID("userListPage.text.nameRule"))}
    • - {/* パートナーアカウント削除はCCB後回し分なので非表示 {isVisibleButton && (
    • - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onDeleteAccount(x.accountId, x.name); + }} + > {t( getTranslationID( "partnerPage.label.deleteAccount" @@ -197,7 +227,6 @@ const PartnerPage: React.FC = (): JSX.Element => {
    • )} - */} {isVisibleDealerManagement && (
    • {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index a96efdc..5acb001 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -537,7 +537,9 @@ "message": { "delegateNotAllowedError": "Aktionen im Namen des Partners sind nicht zulässig. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "deleteFailedError": "Der Delegierungsvorgang ist fehlgeschlagen. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", - "delegateCancelError": "Der delegierte Vorgang wurde beendet, da die Berechtigung für den delegierten Vorgang widerrufen wurde." + "delegateCancelError": "Der delegierte Vorgang wurde beendet, da die Berechtigung für den delegierten Vorgang widerrufen wurde.", + "partnerDeleteConfirm": "(de)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", + "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" } }, "accountPage": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 57615cc..8936250 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -537,7 +537,9 @@ "message": { "delegateNotAllowedError": "Actions on behalf of partner are not allowed. Please refresh the screen and check again.", "deleteFailedError": "Delegate operation failed. Please refresh the screen and check again.", - "delegateCancelError": "The delegated operation has been terminated because permission for the delegated operation has been revoked." + "delegateCancelError": "The delegated operation has been terminated because permission for the delegated operation has been revoked.", + "partnerDeleteConfirm": "(en)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", + "partnerDeleteFailedError": "(en)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" } }, "accountPage": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 47200a8..cb1651b 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -537,7 +537,9 @@ "message": { "delegateNotAllowedError": "No se permiten acciones en nombre del socio. Actualice la pantalla y verifique nuevamente.", "deleteFailedError": "La operación del delegado falló. Actualice la pantalla y verifique nuevamente.", - "delegateCancelError": "La operación delegada finalizó porque se revocó el permiso para la operación delegada." + "delegateCancelError": "La operación delegada finalizó porque se revocó el permiso para la operación delegada.", + "partnerDeleteConfirm": "(es)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", + "partnerDeleteFailedError": "(es)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" } }, "accountPage": { diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 4560e2a..034755c 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -537,7 +537,9 @@ "message": { "delegateNotAllowedError": "Les actions au nom du partenaire ne sont pas autorisées. Veuillez actualiser l'écran et vérifier à nouveau.", "deleteFailedError": "L’opération de délégation a échoué. Veuillez actualiser l'écran et vérifier à nouveau.", - "delegateCancelError": "L'opération déléguée a été interrompue car l'autorisation pour l'opération déléguée a été révoquée." + "delegateCancelError": "L'opération déléguée a été interrompue car l'autorisation pour l'opération déléguée a été révoquée.", + "partnerDeleteConfirm": "(fr)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", + "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" } }, "accountPage": { From 133db833eea29d1bd747fbeccdc02c100448033b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Mon, 25 Mar 2024 08:09:16 +0000 Subject: [PATCH 064/109] =?UTF-8?q?Merged=20PR=20850:=20staging-pipeline?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E5=AE=9F=E8=A1=8C=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3948: staging-pipelineのテスト実行修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3948) - docker-composeを使用したテストは `ubuntu-latest` で実行する必要があったため、pipelineを修正 ## レビューポイント - 問題がありそうな記述はあるか - 実行順の依存関係等が壊れていないか ## 動作確認状況 - 未動作確認。4月以降、CCB対応時に動かして確認を想定。 --- azure-pipelines-staging.yml | 38 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/azure-pipelines-staging.yml b/azure-pipelines-staging.yml index 16550b1..042a731 100644 --- a/azure-pipelines-staging.yml +++ b/azure-pipelines-staging.yml @@ -170,9 +170,29 @@ jobs: --type block \ --overwrite \ --file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip -- job: function_build +- job: function_test dependsOn: frontend_build_production condition: succeeded('frontend_build_production') + displayName: UnitTest + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Bash@3 + displayName: Bash Script (Test) + inputs: + targetType: inline + workingDirectory: dictation_function/.devcontainer + script: | + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_function sudo npm ci + docker-compose exec -T dictation_function sudo npm run test +- job: function_build + dependsOn: function_test + condition: succeeded('function_test') displayName: Build And Push Function Image pool: name: odms-deploy-pipeline @@ -186,22 +206,6 @@ jobs: command: ci workingDir: dictation_function verbose: false - - task: AzureKeyVault@2 - displayName: 'Azure Key Vault: kv-odms-secret-stg' - inputs: - ConnectedServiceName: 'omds-service-connection-stg' - KeyVaultName: kv-odms-secret-stg - SecretsFilter: '*' - - task: Bash@3 - displayName: Bash Script (Test) - inputs: - targetType: inline - workingDirectory: dictation_function/.devcontainer - script: | - docker-compose -f pipeline-docker-compose.yml build - docker-compose -f pipeline-docker-compose.yml up -d - docker-compose exec -T dictation_function sudo npm ci - docker-compose exec -T dictation_function sudo npm run test - task: Docker@0 displayName: build inputs: From 114ded790e3a8995cdfbb7ac35d8c1c67a1d060a Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Tue, 26 Mar 2024 06:22:07 +0000 Subject: [PATCH 065/109] =?UTF-8?q?Merged=20PR=20855:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E3=83=91=E3=83=BC=E3=83=88=E3=83=8A=E3=83=BC?= =?UTF-8?q?=E3=82=92=E7=B7=A8=E9=9B=86=E3=81=97=E3=81=9F=E3=81=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3930: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3930) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 新規追加API2本のIFを作成、controllerの返却値は仮実装(別タスクで実装) - 影響範囲(他の機能にも影響があるか)  新規追加のみなので影響はなし ## レビューポイント - 特筆する点はありません ## UIの変更 なし ## クエリの変更 なし ## 動作確認状況 - ローカルで確認  バリデーションテストとPOSTMANからの起動の確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか 完全新規のIFの実装のみなのでデグレはない想定 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/api/odms/openapi.json | 161 ++++++++++++++++ .../accounts/accounts.controller.spec.ts | 148 +++++++++++++++ .../features/accounts/accounts.controller.ts | 173 ++++++++++++++++++ .../src/features/accounts/types/types.ts | 58 ++++++ 4 files changed, 540 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 6940f83..1d10bdb 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1772,6 +1772,118 @@ "security": [{ "bearer": [] }] } }, + "/accounts/partner/users": { + "post": { + "operationId": "getPartnerUsers", + "summary": "", + "description": "パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用)", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerUsersRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerUsersResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/API実行者と取得対象が親子関係ではない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner/update": { + "post": { + "operationId": "updatePartnerInfo", + "summary": "", + "description": "パートナーアカウントの情報を更新します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePartnerInfoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePartnerInfoResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/API実行者と取得対象が親子関係ではない/アカウントが不在/プライマリ管理者が同一アカウント内にいない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -4677,6 +4789,55 @@ "required": ["targetAccountId"] }, "DeletePartnerAccountResponse": { "type": "object", "properties": {} }, + "GetPartnerUsersRequest": { + "type": "object", + "properties": { + "targetAccountId": { + "type": "number", + "description": "取得対象のアカウントID" + } + }, + "required": ["targetAccountId"] + }, + "PartnerUser": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "ユーザーID" }, + "name": { "type": "string", "description": "ユーザー名" }, + "email": { "type": "string", "description": "メールアドレス" }, + "isPrimaryAdmin": { + "type": "boolean", + "description": "プライマリ管理者かどうか" + } + }, + "required": ["id", "name", "email", "isPrimaryAdmin"] + }, + "GetPartnerUsersResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/PartnerUser" } + } + }, + "required": ["users"] + }, + "UpdatePartnerInfoRequest": { + "type": "object", + "properties": { + "targetAccountId": { + "type": "number", + "description": "変更対象アカウントID" + }, + "primaryAdminUserId": { + "type": "number", + "description": "プライマリ管理者ID" + }, + "companyName": { "type": "string", "description": "会社名" } + }, + "required": ["targetAccountId", "primaryAdminUserId", "companyName"] + }, + "UpdatePartnerInfoResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { "type": "object", "properties": { "token": { "type": "string" } }, diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index 3b89aef..454fd63 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -6,6 +6,8 @@ import { AuthService } from '../auth/auth.service'; import { SwitchParentRequest, DeletePartnerAccountRequest, + GetPartnerUsersRequest, + UpdatePartnerInfoRequest, } from './types/types'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @@ -129,4 +131,150 @@ describe('AccountsController', () => { expect(errors.length).toBe(1); }); }); + describe('valdation getPartnerUsers', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = { + targetAccountId: 1, + }; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('取得対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = {}; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('取得対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = { + userId: 0, + }; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('取得対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + const request = { + userId: 'a', + }; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + describe('valdation updatePartnerInfo', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('更新対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: undefined, + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('更新対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 0, + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('更新対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 'a', + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + // primaryAdminUserIdのテスト + it('更新対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: undefined, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('更新対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 0, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('更新対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 'a', + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + // companyNameのテスト + it('更新対象アカウントが文字列以外場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 2, + companyName: 1, + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); }); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 0f47c2e..db02299 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -81,6 +81,10 @@ import { SwitchParentResponse, DeletePartnerAccountRequest, DeletePartnerAccountResponse, + GetPartnerUsersResponse, + GetPartnerUsersRequest, + UpdatePartnerInfoRequest, + UpdatePartnerInfoResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -2479,4 +2483,173 @@ export class AccountsController { return {}; } + + @Post('partner/users') + @ApiResponse({ + status: HttpStatus.OK, + type: GetPartnerUsersResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正/API実行者と取得対象が親子関係ではない', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'getPartnerUsers', + description: + 'パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用)', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3], + }), + ) + async getPartnerUsers( + @Req() req: Request, + @Body() body: GetPartnerUsersRequest, + ): Promise { + const { targetAccountId } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: 仮実装 + /*await this.accountService.getPartnerUsers( + context, + targetAccountId, + ); + */ + //仮の返却値 + return { users: [] }; + } + + @Post('partner/update') + @ApiResponse({ + status: HttpStatus.OK, + type: UpdatePartnerInfoResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + 'パラメータ不正/API実行者と取得対象が親子関係ではない/アカウントが不在/プライマリ管理者が同一アカウント内にいない', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'updatePartnerInfo', + description: 'パートナーアカウントの情報を更新します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3], + }), + ) + async updatePartnerInfo( + @Req() req: Request, + @Body() body: UpdatePartnerInfoRequest, + ): Promise { + const { targetAccountId } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: 仮実装 + /*await this.accountService.updatePartnerAccount( + context, + targetAccountId, + ); + */ + return {}; + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 2ba21c6..03c2954 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -13,6 +13,9 @@ import { ArrayMaxSize, ValidateNested, Max, + IsString, + IsNotEmpty, + IsBoolean, } from 'class-validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator'; @@ -398,6 +401,31 @@ export class DeletePartnerAccountRequest { targetAccountId: number; } +export class GetPartnerUsersRequest { + @ApiProperty({ description: '取得対象のアカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + targetAccountId: number; +} + +export class UpdatePartnerInfoRequest { + @ApiProperty({ description: '変更対象アカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + targetAccountId: number; + + @ApiProperty({ description: 'プライマリ管理者ID' }) + @Type(() => Number) + @IsInt() + @Min(1) + primaryAdminUserId: number; + + @ApiProperty({ description: '会社名' }) + @MaxLength(255) + companyName: string; +} // ============================== // RESPONSE // ============================== @@ -718,7 +746,37 @@ export class UpdateRestrictionStatusResponse {} export class SwitchParentResponse {} export class DeletePartnerAccountResponse {} +export class PartnerUser { + @ApiProperty({ description: 'ユーザーID' }) + @Type(() => Number) + @IsInt() + @IsNotEmpty() + id: number; + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'メールアドレス' }) + @IsEmail({ blacklisted_chars: '*' }) + @IsNotEmpty() + email: string; + + @ApiProperty({ description: 'プライマリ管理者かどうか' }) + @Type(() => Boolean) + isPrimaryAdmin: boolean; +} + +export class GetPartnerUsersResponse { + @ApiProperty({ type: [PartnerUser] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PartnerUser) + users: PartnerUser[]; +} + +export class UpdatePartnerInfoResponse {} // ============================== // Request/Response外の型 // TODO: Request/Response/その他の型を別ファイルに分ける From 1d71bef7aaafb6b4b306cab5a28924032374e7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Tue, 26 Mar 2024 07:05:39 +0000 Subject: [PATCH 066/109] =?UTF-8?q?Merged=20PR=20857:=20=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3977: テスト追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3977) - エラー時にアラーム発報ログが出力されているかを確認するテストを追加 - contextを継承して、ログに吐き出す内容をstring型の配列として溜め込むテスト用のクラスを追加 ## レビューポイント - テストの内容は妥当か ## クエリの変更 - テストのみ追加なので変更なし ## 動作確認状況 - npm run testが通過したこと --- dictation_function/src/test/common/context.ts | 16 +++ .../src/test/deleteAudioFiles.spec.ts | 135 +++++++++++++++++- 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 dictation_function/src/test/common/context.ts diff --git a/dictation_function/src/test/common/context.ts b/dictation_function/src/test/common/context.ts new file mode 100644 index 0000000..7d2f017 --- /dev/null +++ b/dictation_function/src/test/common/context.ts @@ -0,0 +1,16 @@ +import { InvocationContext } from "@azure/functions"; + +export class TestInvocationContext extends InvocationContext { + contents: string[] = []; + getLogs(): string[] { + return this.contents; + } + log(...args: any[]): void { + super.log(args); + this.contents.push(args.toString()); + } + error(...args: any[]): void { + super.error(args); + this.contents.push(args.toString()); + } +} diff --git a/dictation_function/src/test/deleteAudioFiles.spec.ts b/dictation_function/src/test/deleteAudioFiles.spec.ts index f165e4b..a6f2ad7 100644 --- a/dictation_function/src/test/deleteAudioFiles.spec.ts +++ b/dictation_function/src/test/deleteAudioFiles.spec.ts @@ -15,7 +15,7 @@ import { makeTestAccount, makeTestTask, } from "./common/utility"; -import { TASK_STATUS } from "../constants"; +import { MANUAL_RECOVERY_REQUIRED, TASK_STATUS } from "../constants"; import { TestLogger } from "./common/logger"; import { AudioFile } from "../entity/audio_file.entity"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; @@ -30,6 +30,7 @@ import { LicenseArchive, } from "../entity/license.entity"; import { AudioOptionItem } from "../entity/audio_option_item.entity"; +import { TestInvocationContext } from "./common/context"; describe("getProcessTargets | 削除対象を特定するQueryが正常に動作するか確認する", () => { let source: DataSource | null = null; @@ -1493,4 +1494,136 @@ describe("deleteAudioFilesProcessing", () => { expect(optionItems.length).toEqual(4); } }); + + it("Blobの削除でエラーが発生した場合、アラーム発報用のログが出力される", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file1 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + const args: { accountId: number; fileName: string; country: string }[] = []; + const blobstorage = new AudioBlobStorageService(); + Object.defineProperty(blobstorage, blobstorage.deleteFile.name, { + value: async ( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise => { + throw new Error( + `delete blob failed. succeeded: ${false}, errorCode: ${"ERROR_CODE"}, date: ${"DATE"}` + ); + }, + writable: true, + }); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(1); + const files = await getAudioFiles(source); + expect(files.length).toEqual(1); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(2); + } + + const context = new TestInvocationContext(); + await deleteAudioFilesProcessing(context, source, blobstorage, now); + + const log = context + .getLogs() + .find((log) => log.includes(MANUAL_RECOVERY_REQUIRED)); + expect(log).toBeDefined(); + expect(log).toEqual( + `[MANUAL_RECOVERY_REQUIRED] file delete failed. target={"id":"1","audio_file_id":"1","account_id":"1","country":"US","file_name":"testcase03.wav"}` + ); + }); + + it("DBの削除でエラーが発生した場合、アラーム発報用のログが出力される", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + // 2日と1秒前(削除対象)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + const args: { accountId: number; fileName: string; country: string }[] = []; + const blobstorage = new AudioBlobStorageService(); + Object.defineProperty(blobstorage, blobstorage.deleteFile.name, { + value: async ( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise => { + args.push({ accountId, country, fileName }); + }, + writable: true, + }); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(1); + const files = await getAudioFiles(source); + expect(files.length).toEqual(1); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(2); + } + + Object.defineProperty(source, "transaction", { + value: async () => { + throw new Error("transaction error"); + }, + writable: true, + }); + + const context = new TestInvocationContext(); + + await expect( + deleteAudioFilesProcessing(context, source, blobstorage, now) + ).rejects.toThrow(); + + const log = context + .getLogs() + .find((log) => log.includes(MANUAL_RECOVERY_REQUIRED)); + expect(log).toBeDefined(); + expect(log).toEqual( + `[MANUAL_RECOVERY_REQUIRED] Failed to execute auto file deletion function. error=Error: transaction error` + ); + }); }); From 8752448eed0a56909c1f6bcd40dbf234ba408454 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Tue, 26 Mar 2024 11:30:30 +0000 Subject: [PATCH 067/109] =?UTF-8?q?Merged=20PR=20858:=20=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4013: エラーメッセージ修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4013) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) なし ## レビューポイント - とくになし ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 UIなのでなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか メッセージの修正のみなのでデグレなし ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/translation/de.json | 2 +- dictation_client/src/translation/en.json | 2 +- dictation_client/src/translation/es.json | 2 +- dictation_client/src/translation/fr.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 5acb001..a3b0e67 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -539,7 +539,7 @@ "deleteFailedError": "Der Delegierungsvorgang ist fehlgeschlagen. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "delegateCancelError": "Der delegierte Vorgang wurde beendet, da die Berechtigung für den delegierten Vorgang widerrufen wurde.", "partnerDeleteConfirm": "(de)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" + "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" } }, "accountPage": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 8936250..a02319e 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -539,7 +539,7 @@ "deleteFailedError": "Delegate operation failed. Please refresh the screen and check again.", "delegateCancelError": "The delegated operation has been terminated because permission for the delegated operation has been revoked.", "partnerDeleteConfirm": "(en)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(en)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" + "partnerDeleteFailedError": "(en)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" } }, "accountPage": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index cb1651b..fb4578b 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -539,7 +539,7 @@ "deleteFailedError": "La operación del delegado falló. Actualice la pantalla y verifique nuevamente.", "delegateCancelError": "La operación delegada finalizó porque se revocó el permiso para la operación delegada.", "partnerDeleteConfirm": "(es)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(es)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" + "partnerDeleteFailedError": "(es削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" } }, "accountPage": { diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 034755c..597d578 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -539,7 +539,7 @@ "deleteFailedError": "L’opération de délégation a échoué. Veuillez actualiser l'écran et vérifier à nouveau.", "delegateCancelError": "L'opération déléguée a été interrompue car l'autorisation pour l'opération déléguée a été révoquée.", "partnerDeleteConfirm": "(fr)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除や階層構造変更を行ってください。" + "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" } }, "accountPage": { From 0288292058d858a1f57544d205d4fe3dd387014e Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Wed, 3 Apr 2024 00:50:53 +0000 Subject: [PATCH 068/109] =?UTF-8?q?Merged=20PR=20859:=20=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=8A=E3=83=BC=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E5=8F=96=E5=BE=97API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3936: パートナーユーザー取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3936) - このPull Requestでの対象/対象外 パートナー変更APIの修正は別タスクで対応 - 影響範囲(他の機能にも影響があるか) 新規APIのため他の機能に影響はない ## レビューポイント - パートナーのアカウントIDからユーザー一覧を取得する際に、Repository層ではEmai認証状態を意識した取得は行わない  →service層でフィルタリングする実装にしたが  (アカウントIDからユーザー一覧を取得する処理がいままでなかったので、あったほうがいいかなと思い) ## クエリの変更 新規APIのためクエリの変更はない ## 動作確認状況 - ローカルで確認 UT+POSTMAN - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか 既存機能には手を入れていない ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/error/code.ts | 1 + dictation_server/src/common/error/message.ts | 1 + .../features/accounts/accounts.controller.ts | 9 +- .../accounts/accounts.service.spec.ts | 192 +++++++++++++++++- .../src/features/accounts/accounts.service.ts | 109 ++++++++++ .../src/features/accounts/types/types.ts | 1 - .../users/users.repository.service.ts | 19 ++ 7 files changed, 322 insertions(+), 10 deletions(-) diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 6cdd041..00d1c62 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -87,4 +87,5 @@ export const ErrorCodes = [ 'E017003', // 親アカウント変更不可エラー(リージョンが同一でない) 'E017004', // 親アカウント変更不可エラー(国が同一でない) 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) + 'E019001', // パートナーアカウント取得不可エラー(階層構造が不正) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 6d3855d..123dc5c 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -77,4 +77,5 @@ export const errors: Errors = { E017003: 'Parent account switch failed Error: region mismatch', E017004: 'Parent account switch failed Error: country mismatch', E018001: 'Partner account delete failed Error: not satisfied conditions', + E019001: 'Partner account get failed Error: hierarchy mismatch', }; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index db02299..0cbee34 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2559,13 +2559,8 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: 仮実装 - /*await this.accountService.getPartnerUsers( - context, - targetAccountId, - ); - */ - //仮の返却値 + await this.accountService.getPartnerUsers(context, userId, targetAccountId); + return { users: [] }; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 7cd6f4a..066eb0b 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -161,7 +161,7 @@ describe('createAccount', () => { }, }); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -6261,7 +6261,7 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -9447,3 +9447,191 @@ describe('deletePartnerAccount', () => { } }); }); +describe('getPartnerUsers', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('パートナーアカウント情報が取得できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + const context = makeContext(tier3Admin.external_id, 'requestId'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // パートナーアカウント情報の取得 + const partnerUsers = await service.getPartnerUsers( + context, + tier3Admin.external_id, + tier4Account.id, + ); + expect(partnerUsers).toEqual([ + { + id: tier4Admin.id, + name: 'adb2c' + tier4Admin.external_id, + email: 'mail@example.com', + isPrimaryAdmin: true, + }, + { + id: typist.id, + name: 'adb2c' + typist.external_id, + email: 'mail@example.com', + isPrimaryAdmin: false, + }, + ]); + }); + + it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 第3階層のアカウント作成 + const { admin: tier3Admin } = await makeTestAccount(source, { tier: 3 }); + const { account: tier3Parent } = await makeTestAccount(source, { tier: 3 }); + // 第4階層のアカウント作成 + const { account: tier4Account } = await makeTestAccount( + source, + { + parent_account_id: tier3Parent.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + try { + // パートナーアカウント情報の取得 + await service.getPartnerUsers( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E019001')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index b4a943d..484dbf0 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -37,6 +37,7 @@ import { Author, Partner, GetCompanyNameResponse, + PartnerUser, } from './types/types'; import { DateWithZeroTime, @@ -2981,4 +2982,112 @@ export class AccountsService { ); } } + /** + * 指定したアカウントIDのユーザー情報を取得します + * @param context + * @param targetAccountId 取得対象パートナーのアカウントID + * @returns PartnerUser[] + */ + async getPartnerUsers( + context: Context, + externalId: string, + targetAccountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.getPartnerUsers.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId},};`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: myAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (myAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + // 指定したアカウントIDの情報を取得する + const targetAccount = await this.accountRepository.findAccountById( + context, + targetAccountId, + ); + + // 実行者のアカウントが対象アカウントの親アカウントであるか確認する。 + if (myAccount.id !== targetAccount.parent_account_id) { + throw new HierarchyMismatchError( + `Invalid hierarchy relation. accountId: ${targetAccountId}`, + ); + } + // 対象アカウントのユーザ一覧を取得する + const users = await this.usersRepository.findUsersByAccountId( + context, + targetAccountId, + ); + + //ADB2Cからユーザー情報を取得する + const externalIds = users.map((x) => x.external_id); + const adb2cUsers = await this.adB2cService.getUsers(context, externalIds); + + // ユーザー情報をマージする + const partnerUsers = users.map((user) => { + const adb2cUser = adb2cUsers.find( + (adb2c) => user.external_id === adb2c.id, + ); + if (!adb2cUser) { + throw new Error( + `adb2c user not found. externalId: ${user.external_id}`, + ); + } + const { displayName, emailAddress } = + getUserNameAndMailAddress(adb2cUser); + if (!emailAddress) { + throw new Error( + `adb2c user mail not found. externalId: ${user.external_id}`, + ); + } + return { + id: user.id, + name: displayName, + email: emailAddress, + isPrimaryAdmin: targetAccount.primary_admin_user_id === user.id, + }; + }); + + return partnerUsers; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E019001'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.getPartnerUsers.name}`, + ); + } + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 03c2954..2363279 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -15,7 +15,6 @@ import { Max, IsString, IsNotEmpty, - IsBoolean, } from 'class-validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator'; diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index e319156..01b0ff9 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -190,7 +190,26 @@ export class UsersRepositoryService { } return user; } + /** + * アカウントIDをもとにユーザー一覧を取得します。 + * @param context + * @param accountId 検索対象のアカウントID + * @returns users[] + */ + async findUsersByAccountId( + context: Context, + accountId: number, + ): Promise { + const users = await this.dataSource.getRepository(User).find({ + where: { + email_verified: true, + account_id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + return users; + } /** * AuthorIDをもとにユーザーを取得します。 * AuthorIDがセットされていない場合や、ユーザーが存在しない場合はエラーを返します。 From 915483c109d8e89c39510f71d693c403fb1517a3 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Fri, 5 Apr 2024 02:37:58 +0000 Subject: [PATCH 069/109] =?UTF-8?q?Merged=20PR=20860:=20=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=8A=E3=83=BC=E6=83=85=E5=A0=B1=E6=9B=B4=E6=96=B0?= =?UTF-8?q?API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3937: パートナー情報更新API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3937) - パートナーアカウント情報更新APIとUTを実装しました。 ## レビューポイント - エラーケースの出し分けは適切でしょうか? - テストケースは過不足ないでしょうか? ## UIの変更 - なし ## クエリの変更 - 新規追加のため変更はなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 新規追加なので問題なし。 --- dictation_server/src/common/error/code.ts | 1 + dictation_server/src/common/error/message.ts | 1 + .../features/accounts/accounts.controller.ts | 10 +- .../accounts/accounts.service.spec.ts | 489 ++++++++++++++++++ .../src/features/accounts/accounts.service.ts | 120 +++++ .../src/gateways/sendgrid/sendgrid.service.ts | 63 +++ .../accounts/accounts.repository.service.ts | 71 +++ .../src/templates/template_U_124.html | 58 +++ .../src/templates/template_U_124.txt | 35 ++ 9 files changed, 844 insertions(+), 4 deletions(-) create mode 100644 dictation_server/src/templates/template_U_124.html create mode 100644 dictation_server/src/templates/template_U_124.txt diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 00d1c62..8bd934c 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -88,4 +88,5 @@ export const ErrorCodes = [ 'E017004', // 親アカウント変更不可エラー(国が同一でない) 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) 'E019001', // パートナーアカウント取得不可エラー(階層構造が不正) + 'E020001', // パートナーアカウント変更エラー(変更条件を満たしていない) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 123dc5c..d384512 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -78,4 +78,5 @@ export const errors: Errors = { E017004: 'Parent account switch failed Error: country mismatch', E018001: 'Partner account delete failed Error: not satisfied conditions', E019001: 'Partner account get failed Error: hierarchy mismatch', + E020001: 'Partner account change failed Error: not satisfied conditions', }; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 0cbee34..3523dc9 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2602,7 +2602,7 @@ export class AccountsController { @Req() req: Request, @Body() body: UpdatePartnerInfoRequest, ): Promise { - const { targetAccountId } = body; + const { targetAccountId, primaryAdminUserId, companyName } = body; const accessToken = retrieveAuthorizationToken(req); if (!accessToken) { @@ -2639,12 +2639,14 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: 仮実装 - /*await this.accountService.updatePartnerAccount( + await this.accountService.updatePartnerInfo( context, + userId, targetAccountId, + primaryAdminUserId, + companyName, ); - */ + return {}; } } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 066eb0b..cd50a2f 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -9635,3 +9635,492 @@ describe('getPartnerUsers', () => { } }); }); + +describe('updatePartnerInfo', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('パートナーアカウントの会社名を変更できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { + tier: 4, + parent_account_id: parent.id, + company_name: 'oldCompanyName', + }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('oldCompanyName'); + } + + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + 'newCompanyName', + ); + + { + // DB内が想定通りになっているか確認 + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('newCompanyName'); + + // パートナーアカウント情報変更完了通知が送信されていること + expect(_subject).toBe('Partner Account Edit Notification [U-124]'); + } + }); + + it('パートナーアカウントのプライマリ管理者を変更できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { + tier: 4, + parent_account_id: parent.id, + }, + ); + const newPartnerAdmin = await makeTestUser(source, { + account_id: partner.id, + }); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id); + } + + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + newPartnerAdmin.id, + partner.company_name, + ); + + { + // DB内が想定通りになっているか確認 + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(newPartnerAdmin.id); + + // パートナーアカウント情報変更完了通知が送信されていること + expect(_subject).toBe('Partner Account Edit Notification [U-124]'); + } + }); + + it('変更対象アカウントが実行者のパートナーアカウントでない場合、エラーなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { tier: 4 }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id); + } + + const service = module.get(AccountsService); + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => {}, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + try { + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + partner.company_name, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E020001')); + } else { + fail(); + } + } + }); + + it('DBアクセスがエラーの場合、エラーなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { tier: 4, parent_account_id: parent.id }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id); + } + + const service = module.get(AccountsService); + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => {}, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + //DBアクセスに失敗するようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + accountsRepositoryService.updatePartnerInfo = jest + .fn() + .mockRejectedValue('DB failed'); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + try { + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + partner.company_name, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + + it('メール送信に失敗した場合でも、エラーとならず成功となること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { tier: 4, parent_account_id: parent.id, company_name: 'oldCompanyName' }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('oldCompanyName'); + } + + const service = module.get(AccountsService); + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + throw new Error('sendMail failed'); + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + 'newCompanyName', + ); + + { + // DB内が想定通りになっているか確認 + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('newCompanyName'); + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 484dbf0..a69eb2a 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -3090,4 +3090,124 @@ export class AccountsService { ); } } + /** + * 指定したパートナーアカウントの情報を更新する + * @param context + * @param externalId + * @param targetAccountId // 更新対象のアカウントID + * @param primaryAdminUserId // 更新後のプライマリ管理者のユーザーID + * @param companyName // 更新後の会社名 + * @returns partner account + */ + async updatePartnerInfo( + context: Context, + externalId: string, + targetAccountId: number, + primaryAdminUserId: number, + companyName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updatePartnerInfo.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId}, ` + + `primaryAdminUserId: ${primaryAdminUserId}, ` + + `companyName: ${companyName}, };`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: parentAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (parentAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + + // アカウント情報更新処理 + await this.accountRepository.updatePartnerInfo( + context, + parentAccount.id, + targetAccountId, + primaryAdminUserId, + companyName, + ); + + // メール送信処理 + try { + // 実行者のアカウント情報 + const { companyName: parentCompanyName, adminEmails: parentEmails } = + await this.getAccountInformation(context, parentAccount.id); + + // 更新後のパートナーのプライマリ管理者 + const primaryAdmin = await this.usersRepository.findUserById( + context, + primaryAdminUserId, + ); + const adb2cAdmin = await this.adB2cService.getUser( + context, + primaryAdmin.external_id, + ); + const { + displayName: primaryAdminName, + emailAddress: primaryAdminEmail, + } = getUserNameAndMailAddress(adb2cAdmin); + + if (!primaryAdminEmail) { + throw new Error( + `adb2c user mail not found. externalId: ${primaryAdmin.external_id}`, + ); + } + + await this.sendgridService.sendMailWithU124( + context, + companyName, + primaryAdminName, + primaryAdminEmail, + parentCompanyName, + parentEmails, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + case AdminUserNotFoundError: + throw new HttpException( + makeErrorResponse('E010502'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E020001'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.updatePartnerInfo.name}`, + ); + } + } } diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index 2309c23..f5c219b 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -89,6 +89,8 @@ export class SendGridService { private readonly templateU122NoParentText: string; private readonly templateU123Html: string; private readonly templateU123Text: string; + private readonly templateU124Html: string; + private readonly templateU124Text: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); @@ -338,6 +340,14 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_123.txt`), 'utf-8', ); + this.templateU124Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_124.html`), + 'utf-8', + ); + this.templateU124Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_124.txt`), + 'utf-8', + ); } } @@ -1488,6 +1498,59 @@ export class SendGridService { } } + /** + * U-124のテンプレートを使用したメールを送信する + * @param context + * @param partnerAccountName + * @param partnerPrimaryName + * @param partnerPrimaryMail + * @param dealerAccountName + * @param dealerEmails + * @returns mail with u124 + */ + async sendMailWithU124( + context: Context, + partnerAccountName: string, + partnerPrimaryName: string, + partnerPrimaryMail: string, + dealerAccountName: string, + dealerEmails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU124.name}`, + ); + try { + const subject = 'Partner Account Edit Notification [U-124]'; + const url = new URL(this.appDomain).href; + + const html = this.templateU124Html + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(TOP_URL, url); + const text = this.templateU124Text + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(TOP_URL, url); + + // メールを送信する + await this.sendMail( + context, + [partnerPrimaryMail], + dealerEmails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU124.name}`, + ); + } + } + /** * メールを送信する * @param context diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index ab49646..b5cbe5d 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -35,6 +35,7 @@ import { AccountNotFoundError, AdminUserNotFoundError, DealerAccountNotFoundError, + HierarchyMismatchError, PartnerAccountDeletionError, } from './errors/types'; import { @@ -1768,4 +1769,74 @@ export class AccountsRepositoryService { return users; }); } + /** + * 指定したパートナーアカウントの情報を更新する + * @param context + * @param parentAccountId + * @param targetAccountId + * @param primaryAdminUserId + * @param companyName + * @returns partner info + */ + async updatePartnerInfo( + context: Context, + parentAccountId: number, + targetAccountId: number, + primaryAdminUserId: number, + companyName: string, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + const userRepo = entityManager.getRepository(User); + + // 指定したプライマリ管理者が対象アカウント内に存在するかチェック + const primaryAdminUser = await userRepo.findOne({ + where: { + id: primaryAdminUserId, + account_id: targetAccountId, + email_verified: true, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!primaryAdminUser) { + throw new AdminUserNotFoundError( + `Primary admin user is not found. id: ${primaryAdminUserId}, account_id: ${targetAccountId}`, + ); + } + + // 対象アカウントが存在するかチェック + const targetAccount = await accountRepo.findOne({ + where: { id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!targetAccount) { + throw new AccountNotFoundError( + `Account is not found. id: ${targetAccountId}`, + ); + } + + // 実行者のアカウントが対象アカウントの親アカウントであるか確認する + if (parentAccountId !== targetAccount.parent_account_id) { + throw new HierarchyMismatchError( + `Target account is not child account. parentAccountId: ${parentAccountId}, targetAccountId: ${targetAccountId}`, + ); + } + + // accountsテーブルレコード更新 + await updateEntity( + accountRepo, + { id: targetAccountId }, + { + company_name: companyName, + primary_admin_user_id: primaryAdminUserId, + }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/templates/template_U_124.html b/dictation_server/src/templates/template_U_124.html new file mode 100644 index 0000000..98d7236 --- /dev/null +++ b/dictation_server/src/templates/template_U_124.html @@ -0,0 +1,58 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
      +

      <English>

      +

      Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

      +

      Your account information has been edited by $DEALER_NAME$.

      +

      + To check or change your account information, please log in to the ODMS + Cloud.
      + URL: $TOP_URL$ +

      +

      + If you received this e-mail in error, please delete this e-mail from + your system.
      + This is an automatically generated e-mail and this mailbox is not + monitored. Please do not reply. +

      +
      +
      +

      <Deutsch>

      +

      Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

      +

      Ihre Kontoinformationen wurden von $DEALER_NAME$ bearbeitet.

      +

      + Um Ihre Kontoinformationen zu überprüfen oder zu ändern, melden Sie sich + bitte bei der ODMS Cloud an.
      + URL: $TOP_URL$ +

      +

      + Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese + E-Mail bitte aus Ihrem System.
      + Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht + überwacht. Bitte antworten Sie nicht. +

      +
      +
      +

      <Français>

      +

      Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

      +

      + Les informations de votre compte ont été modifiées par $DEALER_NAME$. +

      +

      + Pour vérifier ou modifier les informations de votre compte, veuillez + vous connecter au ODMS Cloud.
      + URL : $TOP_URL$ +

      +

      + Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail + de votre système.
      + Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres + n'est pas surveillée. Merci de ne pas répondre. +

      +
      + + diff --git a/dictation_server/src/templates/template_U_124.txt b/dictation_server/src/templates/template_U_124.txt new file mode 100644 index 0000000..e12dc2c --- /dev/null +++ b/dictation_server/src/templates/template_U_124.txt @@ -0,0 +1,35 @@ + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Your account information has been edited by $DEALER_NAME$. + +To check or change your account information, please log in to the ODMS Cloud. +URL: $TOP_URL$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Ihre Kontoinformationen wurden von $DEALER_NAME$ bearbeitet. + +Um Ihre Kontoinformationen zu überprüfen oder zu ändern, melden Sie sich bitte bei der ODMS Cloud an. +URL: $TOP_URL$ + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Les informations de votre compte ont été modifiées par $DEALER_NAME$. + +Pour vérifier ou modifier les informations de votre compte, veuillez vous connecter au ODMS Cloud. +URL : $TOP_URL$ + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file From e6d6e477d95f32aa428d6f06b730e0379b07dfe0 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 8 Apr 2024 07:41:05 +0000 Subject: [PATCH 070/109] =?UTF-8?q?Merged=20PR=20862:=20=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=8A=E3=83=BC=E4=B8=80=E8=A6=A7=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=EF=BC=86=E3=83=91=E3=83=BC=E3=83=88=E3=83=8A=E3=83=BC=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=83=9D=E3=83=83=E3=83=97=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3935: パートナー一覧画面&パートナー編集ポップアップ実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3935) - パートナー一覧画面からパートナー編集ポップアップを表示して情報を変更できる画面実装をしています。 ## レビューポイント - エラーの表示は適切でしょうか? - 画面イメージは認識通りでしょうか? ## UIの変更 - [Task3935](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/Task3935?csf=1&web=1&e=FdaUMT) ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 新規機能なので問題なし --- dictation_client/src/api/api.ts | 232 ++++++++++++++++++ dictation_client/src/common/errors/code.ts | 2 + dictation_client/src/common/errors/types.ts | 2 +- .../src/features/dictation/constants.ts | 8 +- .../src/features/partner/operations.ts | 109 ++++++++ .../src/features/partner/partnerSlice.ts | 66 +++++ .../src/features/partner/selectors.ts | 14 ++ .../src/features/partner/state.ts | 8 + dictation_client/src/features/user/state.ts | 6 +- dictation_client/src/features/user/types.ts | 4 +- .../src/features/workflow/worktype/types.ts | 2 +- .../PartnerPage/addPartnerAccountPopup.tsx | 1 + .../PartnerPage/editPartnerAccountPopup.tsx | 178 ++++++++++++++ .../src/pages/PartnerPage/index.tsx | 39 +++ dictation_client/src/translation/de.json | 12 +- dictation_client/src/translation/en.json | 14 +- dictation_client/src/translation/es.json | 12 +- dictation_client/src/translation/fr.json | 12 +- .../features/accounts/accounts.controller.ts | 8 +- 19 files changed, 701 insertions(+), 28 deletions(-) create mode 100644 dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index c363b52..993f4c0 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -1035,6 +1035,32 @@ export interface GetPartnerLicensesResponse { */ 'childrenPartnerLicenses': Array; } +/** + * + * @export + * @interface GetPartnerUsersRequest + */ +export interface GetPartnerUsersRequest { + /** + * 取得対象のアカウントID + * @type {number} + * @memberof GetPartnerUsersRequest + */ + 'targetAccountId': number; +} +/** + * + * @export + * @interface GetPartnerUsersResponse + */ +export interface GetPartnerUsersResponse { + /** + * + * @type {Array} + * @memberof GetPartnerUsersResponse + */ + 'users': Array; +} /** * * @export @@ -1585,6 +1611,37 @@ export interface PartnerLicenseInfo { */ 'issueRequesting': number; } +/** + * + * @export + * @interface PartnerUser + */ +export interface PartnerUser { + /** + * ユーザーID + * @type {number} + * @memberof PartnerUser + */ + 'id': number; + /** + * ユーザー名 + * @type {string} + * @memberof PartnerUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof PartnerUser + */ + 'email': string; + /** + * プライマリ管理者かどうか + * @type {boolean} + * @memberof PartnerUser + */ + 'isPrimaryAdmin': boolean; +} /** * * @export @@ -2289,6 +2346,31 @@ export interface UpdateOptionItemsRequest { */ 'optionItems': Array; } +/** + * + * @export + * @interface UpdatePartnerInfoRequest + */ +export interface UpdatePartnerInfoRequest { + /** + * 変更対象アカウントID + * @type {number} + * @memberof UpdatePartnerInfoRequest + */ + 'targetAccountId': number; + /** + * プライマリ管理者ID + * @type {number} + * @memberof UpdatePartnerInfoRequest + */ + 'primaryAdminUserId': number; + /** + * 会社名 + * @type {string} + * @memberof UpdatePartnerInfoRequest + */ + 'companyName': string; +} /** * * @export @@ -3318,6 +3400,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerUsers: async (getPartnerUsersRequest: GetPartnerUsersRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getPartnerUsersRequest' is not null or undefined + assertParamExists('getPartnerUsers', 'getPartnerUsersRequest', getPartnerUsersRequest) + const localVarPath = `/accounts/partner/users`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getPartnerUsersRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3710,6 +3832,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartnerInfo: async (updatePartnerInfoRequest: UpdatePartnerInfoRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updatePartnerInfoRequest' is not null or undefined + assertParamExists('updatePartnerInfo', 'updatePartnerInfoRequest', updatePartnerInfoRequest) + const localVarPath = `/accounts/partner/update`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerInfoRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -4092,6 +4254,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.getPartnerLicenses']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartnerUsers(getPartnerUsersRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getPartnerUsers']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -4221,6 +4396,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.updateOptionItems']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartnerInfo(updatePartnerInfoRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updatePartnerInfo']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -4459,6 +4647,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: any): AxiosPromise { return localVarFp.getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(axios, basePath)); }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: any): AxiosPromise { + return localVarFp.getPartnerUsers(getPartnerUsersRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4558,6 +4756,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: any): AxiosPromise { return localVarFp.updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(axios, basePath)); }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: any): AxiosPromise { + return localVarFp.updatePartnerInfo(updatePartnerInfoRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4825,6 +5033,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartnerUsers(getPartnerUsersRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -4944,6 +5164,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updatePartnerInfo(updatePartnerInfoRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index ed1570d..477282c 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -82,4 +82,6 @@ export const errorCodes = [ "E017003", // 親アカウント変更不可エラー(リージョンが同一でない) "E017004", // 親アカウント変更不可エラー(国が同一でない) "E018001", // パートナーアカウント削除エラー(削除条件を満たしていない) + "E019001", // パートナーアカウント取得不可エラー(階層構造が不正) + "E020001", // パートナーアカウント変更エラー(変更条件を満たしていない) ] as const; diff --git a/dictation_client/src/common/errors/types.ts b/dictation_client/src/common/errors/types.ts index 8cc801e..f4a42b4 100644 --- a/dictation_client/src/common/errors/types.ts +++ b/dictation_client/src/common/errors/types.ts @@ -6,4 +6,4 @@ export type ErrorObject = { statusCode?: number; }; -export type ErrorCodeType = typeof errorCodes[number]; +export type ErrorCodeType = (typeof errorCodes)[number]; diff --git a/dictation_client/src/features/dictation/constants.ts b/dictation_client/src/features/dictation/constants.ts index f7e22d1..ac273ce 100644 --- a/dictation_client/src/features/dictation/constants.ts +++ b/dictation_client/src/features/dictation/constants.ts @@ -6,7 +6,7 @@ export const STATUS = { BACKUP: "Backup", } as const; -export type StatusType = typeof STATUS[keyof typeof STATUS]; +export type StatusType = (typeof STATUS)[keyof typeof STATUS]; export const LIMIT_TASK_NUM = 100; @@ -26,7 +26,7 @@ export const SORTABLE_COLUMN = { TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE", } as const; export type SortableColumnType = - typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; + (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN]; export const isSortableColumnType = ( value: string @@ -36,14 +36,14 @@ export const isSortableColumnType = ( }; export type SortableColumnList = - typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; + (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN]; export const DIRECTION = { ASC: "ASC", DESC: "DESC", } as const; -export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; +export type DirectionType = (typeof DIRECTION)[keyof typeof DIRECTION]; // DirectionTypeの型チェック関数 export const isDirectionType = (arg: string): arg is DirectionType => diff --git a/dictation_client/src/features/partner/operations.ts b/dictation_client/src/features/partner/operations.ts index 2dac369..8c832f6 100644 --- a/dictation_client/src/features/partner/operations.ts +++ b/dictation_client/src/features/partner/operations.ts @@ -9,6 +9,7 @@ import { CreatePartnerAccountRequest, GetPartnersResponse, DeletePartnerAccountRequest, + GetPartnerUsersResponse, } from "../../api/api"; import { Configuration } from "../../api/configuration"; @@ -176,3 +177,111 @@ export const deletePartnerAccountAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +// パートナーアカウントユーザー取得 +export const getPartnerUsersAsync = createAsyncThunk< + GetPartnerUsersResponse, + { + // パラメータ + accountId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/getPartnerUsersAsync", async (args, thunkApi) => { + const { accountId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + const res = await accountApi.getPartnerUsers( + { targetAccountId: accountId }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + + return res.data; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + +// パートナーアカウントユーザー編集 +export const editPartnerInfoAsync = createAsyncThunk< + { + /* Empty Object */ + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/editPartnerInfoAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + const { id, companyName, selectedAdminId } = state.partner.apps.editPartner; + + try { + await accountApi.updatePartnerInfo( + { + targetAccountId: id, + primaryAdminUserId: selectedAdminId, + companyName, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + if (error.code === "E010502" || error.code === "E020001") { + errorMessage = getTranslationID("partnerPage.message.editFailedError"); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/partner/partnerSlice.ts b/dictation_client/src/features/partner/partnerSlice.ts index cf09f4a..5edf14b 100644 --- a/dictation_client/src/features/partner/partnerSlice.ts +++ b/dictation_client/src/features/partner/partnerSlice.ts @@ -4,6 +4,8 @@ import { createPartnerAccountAsync, getPartnerInfoAsync, deletePartnerAccountAsync, + getPartnerUsersAsync, + editPartnerInfoAsync, } from "./operations"; import { LIMIT_PARTNER_VIEW_NUM } from "./constants"; @@ -21,6 +23,13 @@ const initialState: PartnerState = { adminName: "", email: "", }, + editPartner: { + users: [], + id: 0, + companyName: "", + country: "", + selectedAdminId: 0, + }, limit: LIMIT_PARTNER_VIEW_NUM, offset: 0, isLoading: false, @@ -79,6 +88,37 @@ export const partnerSlice = createSlice({ state.apps.delegatedAccountId = undefined; state.apps.delegatedCompanyName = undefined; }, + changeEditPartner: ( + state, + action: PayloadAction<{ + id: number; + companyName: string; + country: string; + }> + ) => { + const { id, companyName, country } = action.payload; + + state.apps.editPartner.id = id; + state.apps.editPartner.companyName = companyName; + state.apps.editPartner.country = country; + }, + changeEditCompanyName: ( + state, + action: PayloadAction<{ companyName: string }> + ) => { + const { companyName } = action.payload; + state.apps.editPartner.companyName = companyName; + }, + changeSelectedAdminId: ( + state, + action: PayloadAction<{ adminId: number }> + ) => { + const { adminId } = action.payload; + state.apps.editPartner.selectedAdminId = adminId; + }, + cleanupPartnerAccount: (state) => { + state.apps.editPartner = initialState.apps.editPartner; + }, }, extraReducers: (builder) => { builder.addCase(createPartnerAccountAsync.pending, (state) => { @@ -110,6 +150,28 @@ export const partnerSlice = createSlice({ builder.addCase(deletePartnerAccountAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(getPartnerUsersAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(getPartnerUsersAsync.fulfilled, (state, action) => { + const { users } = action.payload; + state.apps.editPartner.users = users; + state.apps.editPartner.selectedAdminId = + users.find((user) => user.isPrimaryAdmin)?.id ?? 0; + state.apps.isLoading = false; + }); + builder.addCase(getPartnerUsersAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(editPartnerInfoAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(editPartnerInfoAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(editPartnerInfoAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); export const { @@ -121,5 +183,9 @@ export const { savePageInfo, changeDelegateAccount, cleanupDelegateAccount, + changeEditPartner, + changeEditCompanyName, + changeSelectedAdminId, + cleanupPartnerAccount, } = partnerSlice.actions; export default partnerSlice.reducer; diff --git a/dictation_client/src/features/partner/selectors.ts b/dictation_client/src/features/partner/selectors.ts index 061f8b1..0bd2ac4 100644 --- a/dictation_client/src/features/partner/selectors.ts +++ b/dictation_client/src/features/partner/selectors.ts @@ -62,3 +62,17 @@ export const selectDelegatedAccountId = (state: RootState) => state.partner.apps.delegatedAccountId; export const selectDelegatedCompanyName = (state: RootState) => state.partner.apps.delegatedCompanyName; + +// edit +export const selectEditPartnerId = (state: RootState) => + state.partner.apps.editPartner.id; +export const selectEditPartnerCompanyName = (state: RootState) => + state.partner.apps.editPartner.companyName; +export const selectEditPartnerCountry = (state: RootState) => + state.partner.apps.editPartner.country; + +export const selectEditPartnerUsers = (state: RootState) => + state.partner.apps.editPartner.users; + +export const selectSelectedAdminId = (state: RootState) => + state.partner.apps.editPartner.selectedAdminId; diff --git a/dictation_client/src/features/partner/state.ts b/dictation_client/src/features/partner/state.ts index 18a88dd..0ef2c2c 100644 --- a/dictation_client/src/features/partner/state.ts +++ b/dictation_client/src/features/partner/state.ts @@ -1,6 +1,7 @@ import { CreatePartnerAccountRequest, GetPartnersResponse, + PartnerUser, } from "../../api/api"; export interface PartnerState { @@ -19,4 +20,11 @@ export interface Apps { isLoading: boolean; delegatedAccountId?: number; delegatedCompanyName?: string; + editPartner: { + users: PartnerUser[]; + id: number; + companyName: string; + country: string; + selectedAdminId: number; + }; } diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index 5d619e1..87e197d 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -1,9 +1,5 @@ import { CSVType } from "common/parser"; -import { - User, - AllocatableLicenseInfo, - MultipleImportUser, -} from "../../api/api"; +import { User, AllocatableLicenseInfo } from "../../api/api"; import { AddUser, UpdateUser, LicenseAllocateUser } from "./types"; export interface UsersState { diff --git a/dictation_client/src/features/user/types.ts b/dictation_client/src/features/user/types.ts index a96e236..2fa06e7 100644 --- a/dictation_client/src/features/user/types.ts +++ b/dictation_client/src/features/user/types.ts @@ -54,14 +54,14 @@ export interface LicenseAllocateUser { remaining?: number; } -export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES]; +export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES]; // 受け取った値がUSER_ROLESの型であるかどうかを判定する export const isRoleType = (role: string): role is RoleType => Object.values(USER_ROLES).includes(role as RoleType); export type LicenseStatusType = - typeof LICENSE_STATUS[keyof typeof LICENSE_STATUS]; + (typeof LICENSE_STATUS)[keyof typeof LICENSE_STATUS]; // 受け取った値がLicenseStatusTypeの型であるかどうかを判定する export const isLicenseStatusType = ( diff --git a/dictation_client/src/features/workflow/worktype/types.ts b/dictation_client/src/features/workflow/worktype/types.ts index 239cb26..c1c8348 100644 --- a/dictation_client/src/features/workflow/worktype/types.ts +++ b/dictation_client/src/features/workflow/worktype/types.ts @@ -2,7 +2,7 @@ import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants"; // OPTION_ITEMS_DEFAULT_VALUE_TYPEからOptionItemDefaultValueTypeを作成する export type OptionItemsDefaultValueType = - typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE]; + (typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE)[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE]; // 受け取った値がOptionItemDefaultValueType型かどうかを判定する export const isOptionItemDefaultValueType = ( diff --git a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx index 48b36a2..2ea40a4 100644 --- a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx +++ b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx @@ -110,6 +110,7 @@ export const AddPartnerAccountPopup: React.FC = ( country, adminName, email, + offset, ]); return ( diff --git a/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx b/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx new file mode 100644 index 0000000..5736ac5 --- /dev/null +++ b/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx @@ -0,0 +1,178 @@ +import { AppDispatch } from "app/store"; +import React, { useCallback, useEffect } from "react"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { getTranslationID } from "translation"; +import { useTranslation } from "react-i18next"; +import { + changeEditCompanyName, + changeSelectedAdminId, + cleanupPartnerAccount, + getPartnerUsersAsync, + editPartnerInfoAsync, + selectEditPartnerCompanyName, + selectEditPartnerCountry, + selectEditPartnerId, + selectEditPartnerUsers, + selectIsLoading, + selectSelectedAdminId, + selectOffset, + getPartnerInfoAsync, + LIMIT_PARTNER_VIEW_NUM, +} from "features/partner"; +import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; +import { COUNTRY_LIST } from "../SignupPage/constants"; + +interface EditPartnerAccountPopup { + isOpen: boolean; + onClose: () => void; +} + +export const EditPartnerAccountPopup: React.FC = ( + props +) => { + const { isOpen, onClose } = props; + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + const isLoading = useSelector(selectIsLoading); + const offset = useSelector(selectOffset); + + const partnerId = useSelector(selectEditPartnerId); + const companyName = useSelector(selectEditPartnerCompanyName); + const country = useSelector(selectEditPartnerCountry); + + const users = useSelector(selectEditPartnerUsers); + const adminUser = users.find((user) => user.isPrimaryAdmin); + + const selectedAdminId = useSelector(selectSelectedAdminId); + + // ポップアップを閉じる処理 + const closePopup = useCallback(() => { + if (isLoading) { + return; + } + dispatch(cleanupPartnerAccount()); + onClose(); + }, [isLoading, onClose, dispatch]); + + useEffect(() => { + if (isOpen) { + dispatch(getPartnerUsersAsync({ accountId: partnerId })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + const onEditPartner = useCallback(async () => { + // eslint-disable-next-line no-alert + if (!window.confirm(t(getTranslationID("common.message.dialogConfirm")))) { + return; + } + + const { meta } = await dispatch(editPartnerInfoAsync()); + if (meta.requestStatus === "fulfilled") { + dispatch( + getPartnerInfoAsync({ + limit: LIMIT_PARTNER_VIEW_NUM, + offset, + }) + ); + closePopup(); + } + }, [dispatch, closePopup, t, offset]); + + return ( +
      +
      +

      + {t(getTranslationID("partnerPage.label.editAccount"))} + +

      +
      +
      +
      + {t(getTranslationID("partnerPage.label.accountInformation"))} +
      +
      {t(getTranslationID("partnerPage.label.name"))}
      +
      + { + dispatch( + changeEditCompanyName({ companyName: e.target.value }) + ); + }} + /> +
      +
      {t(getTranslationID("partnerPage.label.country"))}
      +
      + c.value === country)?.label} + className={styles.formInput} + readOnly + /> +
      +
      + {t(getTranslationID("partnerPage.label.primaryAdminInfo"))} +
      +
      {t(getTranslationID("partnerPage.label.adminName"))}
      +
      + +
      +
      {t(getTranslationID("partnerPage.label.email"))}
      +
      + +
      +
      + + Loading +
      +
      +
      +
      +
      + ); +}; diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx index d1006e3..dac23a2 100644 --- a/dictation_client/src/pages/PartnerPage/index.tsx +++ b/dictation_client/src/pages/PartnerPage/index.tsx @@ -20,20 +20,24 @@ import { } from "features/partner/index"; import { changeDelegateAccount, + changeEditPartner, savePageInfo, } from "features/partner/partnerSlice"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; import { getDelegationTokenAsync } from "features/auth/operations"; import { useNavigate } from "react-router-dom"; +import { Partner } from "api"; import personAdd from "../../assets/images/person_add.svg"; import { TIERS } from "../../components/auth/constants"; import { AddPartnerAccountPopup } from "./addPartnerAccountPopup"; +import { EditPartnerAccountPopup } from "./editPartnerAccountPopup"; import checkFill from "../../assets/images/check_fill.svg"; const PartnerPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); const [isPopupOpen, setIsPopupOpen] = useState(false); + const [isEditPopupOpen, setIsEditPopupOpen] = useState(false); const [t] = useTranslation(); const navigate = useNavigate(); const total = useSelector(selectTotal); @@ -72,6 +76,19 @@ const PartnerPage: React.FC = (): JSX.Element => { const onOpen = useCallback(() => { setIsPopupOpen(true); }, [setIsPopupOpen]); + const onOpenEditPopup = useCallback( + (editPartner: Partner) => { + dispatch( + changeEditPartner({ + id: editPartner.accountId, + companyName: editPartner.name, + country: editPartner.country, + }) + ); + setIsEditPopupOpen(true); + }, + [setIsEditPopupOpen, dispatch] + ); // パートナー取得APIを呼び出す useEffect(() => { @@ -144,6 +161,12 @@ const PartnerPage: React.FC = (): JSX.Element => { setIsPopupOpen(false); }} /> + { + setIsEditPopupOpen(false); + }} + />
      @@ -211,6 +234,22 @@ const PartnerPage: React.FC = (): JSX.Element => {
      + {isVisibleButton && ( +
    • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onOpenEditPopup(x); + }} + > + {t( + getTranslationID( + "partnerPage.label.editAccount" + ) + )} + +
    • + )} {isVisibleButton && (
    • {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index a3b0e67..e163161 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Erlauben Sie dem Händler, Änderungen vorzunehmen", "partners": "Partner", - "deleteAccount": "Konto löschen" + "deleteAccount": "Konto löschen", + "editAccount": "(de)Edit Account", + "accountInformation": "(de)Account information", + "primaryAdminInfo": "(de)Primary administrator's information", + "adminName": "(de)Admin Name", + "saveChanges": "(de)Save Changes" }, "message": { "delegateNotAllowedError": "Aktionen im Namen des Partners sind nicht zulässig. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "deleteFailedError": "Der Delegierungsvorgang ist fehlgeschlagen. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "delegateCancelError": "Der delegierte Vorgang wurde beendet, da die Berechtigung für den delegierten Vorgang widerrufen wurde.", "partnerDeleteConfirm": "(de)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "(de)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "(de)Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index a02319e..df0214b 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Dealer Management", "partners": "Partners", - "deleteAccount": "Delete Account" + "deleteAccount": "Delete Account", + "editAccount": "Edit Account", + "accountInformation": "Account information", + "primaryAdminInfo": "Primary administrator's information", + "adminName": "Admin Name", + "saveChanges": "Save Changes" }, "message": { "delegateNotAllowedError": "Actions on behalf of partner are not allowed. Please refresh the screen and check again.", "deleteFailedError": "Delegate operation failed. Please refresh the screen and check again.", "delegateCancelError": "The delegated operation has been terminated because permission for the delegated operation has been revoked.", - "partnerDeleteConfirm": "(en)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(en)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteConfirm": "選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", + "partnerDeleteFailedError": "削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index fb4578b..e526a1d 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Permitir que el distribuidor realice los cambios", "partners": "Socios", - "deleteAccount": "Borrar cuenta" + "deleteAccount": "Borrar cuenta", + "editAccount": "(es)Edit Account", + "accountInformation": "(es)Account information", + "primaryAdminInfo": "(es)Primary administrator's information", + "adminName": "(es)Admin Name", + "saveChanges": "(es)Save Changes" }, "message": { "delegateNotAllowedError": "No se permiten acciones en nombre del socio. Actualice la pantalla y verifique nuevamente.", "deleteFailedError": "La operación del delegado falló. Actualice la pantalla y verifique nuevamente.", "delegateCancelError": "La operación delegada finalizó porque se revocó el permiso para la operación delegada.", "partnerDeleteConfirm": "(es)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(es削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteFailedError": "(es)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "(es)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "(es)Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 597d578..393e5d9 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Autoriser le revendeur à modifier les paramètres", "partners": "Partenaires", - "deleteAccount": "Supprimer le compte" + "deleteAccount": "Supprimer le compte", + "editAccount": "(fr)Edit Account", + "accountInformation": "(fr)Account information", + "primaryAdminInfo": "(fr)Primary administrator's information", + "adminName": "(fr)Admin Name", + "saveChanges": "(fr)Save Changes" }, "message": { "delegateNotAllowedError": "Les actions au nom du partenaire ne sont pas autorisées. Veuillez actualiser l'écran et vérifier à nouveau.", "deleteFailedError": "L’opération de délégation a échoué. Veuillez actualiser l'écran et vérifier à nouveau.", "delegateCancelError": "L'opération déléguée a été interrompue car l'autorisation pour l'opération déléguée a été révoquée.", "partnerDeleteConfirm": "(fr)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "(fr)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "(fr)Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 3523dc9..9985180 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2559,9 +2559,13 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - await this.accountService.getPartnerUsers(context, userId, targetAccountId); + const users = await this.accountService.getPartnerUsers( + context, + userId, + targetAccountId, + ); - return { users: [] }; + return { users }; } @Post('partner/update') From 09c21eafa77cf03324299eb3cd7dec9620847232 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 10 Apr 2024 06:15:54 +0000 Subject: [PATCH 071/109] =?UTF-8?q?Merged=20PR=20864:=20DB=E3=81=AB?= =?UTF-8?q?=E3=82=AB=E3=83=A9=E3=83=A0=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4057: DBにカラム追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4057) - DBマイグレーションファイルとして音声ファイルテーブルに生ファイル名カラムを追加しました。 ## レビューポイント - カラムの要件は適切でしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルでmigrate:up/downできることを確認 --- .../db/migrations/062-add_audio_file_raw_file_name.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 dictation_server/db/migrations/062-add_audio_file_raw_file_name.sql diff --git a/dictation_server/db/migrations/062-add_audio_file_raw_file_name.sql b/dictation_server/db/migrations/062-add_audio_file_raw_file_name.sql new file mode 100644 index 0000000..015d4ec --- /dev/null +++ b/dictation_server/db/migrations/062-add_audio_file_raw_file_name.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `audio_files` ADD COLUMN `raw_file_name` VARCHAR(1024) NOT NULL COMMENT '生ファイル名' AFTER `file_name`; + +-- +migrate Down +ALTER TABLE `audio_files` DROP COLUMN `raw_file_name`; From 07bca1d638494228685b81c1baf0ec003304813c Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 10 Apr 2024 09:48:40 +0000 Subject: [PATCH 072/109] =?UTF-8?q?Merged=20PR=20866:=20DB=E3=83=9E?= =?UTF-8?q?=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4120: DBマイグレーションエラー修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4120) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 - 修正箇所がほかの機能に影響していないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば --- .../063-add_audio_file_raw_file_name_default.sql | 7 +++++++ dictation_server/src/features/tasks/tasks.service.spec.ts | 1 + .../src/features/tasks/test/tasks.service.mock.ts | 1 + .../repositories/audio_files/entity/audio_file.entity.ts | 2 ++ 4 files changed, 11 insertions(+) create mode 100644 dictation_server/db/migrations/063-add_audio_file_raw_file_name_default.sql diff --git a/dictation_server/db/migrations/063-add_audio_file_raw_file_name_default.sql b/dictation_server/db/migrations/063-add_audio_file_raw_file_name_default.sql new file mode 100644 index 0000000..d7beef4 --- /dev/null +++ b/dictation_server/db/migrations/063-add_audio_file_raw_file_name_default.sql @@ -0,0 +1,7 @@ +-- +migrate Up +ALTER TABLE `audio_files` DROP COLUMN `raw_file_name`; +ALTER TABLE `audio_files` ADD COLUMN `raw_file_name` VARCHAR(1024) DEFAULT '' NOT NULL COMMENT '生ファイル名' AFTER `file_name`; + +-- +migrate Down +ALTER TABLE `audio_files` DROP COLUMN `raw_file_name`; +ALTER TABLE `audio_files` ADD COLUMN `raw_file_name` VARCHAR(1024) NOT NULL COMMENT '生ファイル名' AFTER `file_name`; \ No newline at end of file diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 304b8ad..5f85918 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -251,6 +251,7 @@ describe('TasksService', () => { owner_user_id: 1, url: 'test/test.zip', file_name: 'test.zip', + raw_file_name: 'test.zip', author_id: 'AUTHOR', work_type_id: 'WorkType', started_at: new Date('2023-01-01T01:01:01.000'), diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index 1a0f4ab..f33c963 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -461,6 +461,7 @@ const defaultTasksRepositoryMockValue: { owner_user_id: 1, url: 'test/test.zip', file_name: 'test.zip', + raw_file_name: 'test.zip', author_id: 'AUTHOR', work_type_id: 'WorkType', started_at: new Date('2023-01-01T01:01:01.000Z'), 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 5a0d9f5..8332760 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 @@ -15,6 +15,8 @@ export class AudioFile { @Column() file_name: string; @Column() + raw_file_name: string; + @Column() author_id: string; @Column() work_type_id: string; From 33d4ab3d2fd6e512a299317b8def65e0b24a4cbc Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 11 Apr 2024 04:29:16 +0000 Subject: [PATCH 073/109] =?UTF-8?q?Merged=20PR=20863:=20=EF=BC=88Sprint31?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=EF=BC=89=E4=B8=80=E9=83=A8=E3=81=AEentity?= =?UTF-8?q?=E3=81=A7MySQL=E4=B8=8A=E3=81=AE=E5=9E=8B=E3=81=8Cbigint?= =?UTF-8?q?=E3=81=AE=E3=82=82=E3=81=AE=E3=81=AB=E5=AF=BE=E3=81=97=E3=81=A6?= =?UTF-8?q?bigintTransformer=E3=81=A7=E5=A4=89=E6=8F=9B=E3=81=99=E3=82=8B?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=81=8C=E5=85=A5=E3=81=A3=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3928: (Sprint31対応)一部のentityでMySQL上の型がbigintのものに対してbigintTransformerで変換する処理が入っていない](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3928) - 現在、bigint型で変換を適用していない部分へのTransformerの適用はうまくいかないため保留。 - 既存処理については問題ありません - Functionのテストで日付に依存している箇所があったので修正しています。 ## レビューポイント - 共有 ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - テストの修正のみ --- .../src/test/analysisLicenses.spec.ts | 132 +++++++++--------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/dictation_function/src/test/analysisLicenses.spec.ts b/dictation_function/src/test/analysisLicenses.spec.ts index 75e694c..56e2bea 100644 --- a/dictation_function/src/test/analysisLicenses.spec.ts +++ b/dictation_function/src/test/analysisLicenses.spec.ts @@ -2604,137 +2604,137 @@ describe("analysisLicenses", () => { expect(csvContentUS).toBe( '"アカウント","対象年月","カテゴリー1","カテゴリー2","ライセンス種別","役割","数量"' + "\r\n" + - '"test inc.","202402","有効ライセンス数","所有ライセンス数","Trial","","9"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","所有ライセンス数","Standard","","9"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","所有ライセンス数","Card","","7"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Trial","Author","1"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Trial","None","3"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Standard","Author","4"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Standard","None","2"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Card","Author","2"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Card","Typist","3"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","使用中ライセンス数","Card","None","1"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` + "\r\n" + - '"test inc.","202402","新規発行ライセンス数","","Trial","","3"' + + `"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` + "\r\n" + - '"test inc.","202402","新規発行ライセンス数","","Standard","","2"' + + `"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` + "\r\n" + - '"test inc.","202402","新規発行ライセンス数","","Card","","1"' + + `"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Trial","Author","5"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Trial","Typist","3"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Trial","None","2"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Trial","Unallocated","1"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Standard","Author","2"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Standard","Typist","1"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Standard","None","2"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Standard","Unallocated","2"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Card","Author","1"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Card","Typist","2"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Card","None","3"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` + "\r\n" + - '"test inc.","202402","失効ライセンス数","","Card","Unallocated","5"' + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","","トライアルから切り替え","Author","1"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","","トライアルから切り替え","Typist","2"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","","トライアルから切り替え","None","1"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","","カードから切り替え","Author","1"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","","カードから切り替え","Typist","2"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` + "\r\n" + - '"test inc.","202402","有効ライセンス数","","カードから切り替え","None","3"' + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` + "\r\n" + - '"1","202402","有効ライセンス数","所有ライセンス数","Trial","","9"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` + "\r\n" + - '"1","202402","有効ライセンス数","所有ライセンス数","Standard","","9"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` + "\r\n" + - '"1","202402","有効ライセンス数","所有ライセンス数","Card","","7"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Trial","Author","1"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Trial","None","3"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Standard","Author","4"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Standard","None","2"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Card","Author","2"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Card","Typist","3"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` + "\r\n" + - '"1","202402","有効ライセンス数","使用中ライセンス数","Card","None","1"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` + "\r\n" + - '"1","202402","新規発行ライセンス数","","Trial","","3"' + + `"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` + "\r\n" + - '"1","202402","新規発行ライセンス数","","Standard","","2"' + + `"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` + "\r\n" + - '"1","202402","新規発行ライセンス数","","Card","","1"' + + `"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` + "\r\n" + - '"1","202402","失効ライセンス数","","Trial","Author","5"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` + "\r\n" + - '"1","202402","失効ライセンス数","","Trial","Typist","3"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` + "\r\n" + - '"1","202402","失効ライセンス数","","Trial","None","2"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` + "\r\n" + - '"1","202402","失効ライセンス数","","Trial","Unallocated","1"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` + "\r\n" + - '"1","202402","失効ライセンス数","","Standard","Author","2"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` + "\r\n" + - '"1","202402","失効ライセンス数","","Standard","Typist","1"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` + "\r\n" + - '"1","202402","失効ライセンス数","","Standard","None","2"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` + "\r\n" + - '"1","202402","失効ライセンス数","","Standard","Unallocated","2"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` + "\r\n" + - '"1","202402","失効ライセンス数","","Card","Author","1"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` + "\r\n" + - '"1","202402","失効ライセンス数","","Card","Typist","2"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` + "\r\n" + - '"1","202402","失効ライセンス数","","Card","None","3"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` + "\r\n" + - '"1","202402","失効ライセンス数","","Card","Unallocated","5"' + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` + "\r\n" + - '"1","202402","有効ライセンス数","","トライアルから切り替え","Author","1"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` + "\r\n" + - '"1","202402","有効ライセンス数","","トライアルから切り替え","Typist","2"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` + "\r\n" + - '"1","202402","有効ライセンス数","","トライアルから切り替え","None","1"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` + "\r\n" + - '"1","202402","有効ライセンス数","","カードから切り替え","Author","1"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` + "\r\n" + - '"1","202402","有効ライセンス数","","カードから切り替え","Typist","2"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` + "\r\n" + - '"1","202402","有効ライセンス数","","カードから切り替え","None","3"' + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` + "\r\n" ); }); From f209c7359eec876b83e498bb02cbc60f352a1b6f Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 12 Apr 2024 01:36:49 +0000 Subject: [PATCH 074/109] =?UTF-8?q?Merged=20PR=20865:=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E3=83=BB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4049: IF実装・修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4049) - 音声ファイル名変更APIのIFを実装してリクエストパラメータのテストを実装しました。 - タスク一覧取得APIのレスポンスに生ファイル名を追加しました。 - OpenAPIの更新 ## レビューポイント - ファイル名変更APIのパスは適切でしょうか? - バリデータのチェックは適切でしょうか? ## UIの変更 - なし ## クエリの変更 - IFなのでなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - 既存テストを実施して、タスク一覧についてはレスポンス期待値を修正。 - タスク一覧画面が正常に見えることを確認 --- dictation_server/src/api/odms/openapi.json | 69 ++++++++++++++++ .../features/files/files.controller.spec.ts | 81 +++++++++++++++++++ .../src/features/files/files.controller.ts | 72 +++++++++++++++++ .../src/features/files/types/types.ts | 16 ++++ .../src/features/tasks/tasks.service.spec.ts | 3 + .../src/features/tasks/types/convert.ts | 1 + .../src/features/tasks/types/types.ts | 2 + 7 files changed, 244 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 1d10bdb..77983dc 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -2903,6 +2903,58 @@ "security": [{ "bearer": [] }] } }, + "/files/rename": { + "post": { + "operationId": "fileRename", + "summary": "", + "description": "音声ファイルの表示ファイル名を変更します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/FileRenameRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/FileRenameResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, "/tasks": { "get": { "operationId": "getTasks", @@ -5283,6 +5335,18 @@ "required": ["name", "url"] }, "TemplateUploadFinishedReqponse": { "type": "object", "properties": {} }, + "FileRenameRequest": { + "type": "object", + "properties": { + "audioFileId": { + "type": "number", + "description": "ファイル名変更対象の音声ファイルID" + }, + "fileName": { "type": "string", "description": "変更するファイル名" } + }, + "required": ["audioFileId", "fileName"] + }, + "FileRenameResponse": { "type": "object", "properties": {} }, "Assignee": { "type": "object", "properties": { @@ -5322,6 +5386,10 @@ "description": "音声ファイルのBlob Storage上での保存場所(ファイル名含む)のURL" }, "fileName": { "type": "string", "description": "音声ファイル名" }, + "rawFileName": { + "type": "string", + "description": "生(Blob Storage上の)音声ファイル名" + }, "audioDuration": { "type": "string", "description": "音声ファイルの録音時間(ミリ秒の整数値)" @@ -5382,6 +5450,7 @@ "optionItemList", "url", "fileName", + "rawFileName", "audioDuration", "audioCreatedDate", "audioFinishedDate", diff --git a/dictation_server/src/features/files/files.controller.spec.ts b/dictation_server/src/features/files/files.controller.spec.ts index 3986b38..39798dc 100644 --- a/dictation_server/src/features/files/files.controller.spec.ts +++ b/dictation_server/src/features/files/files.controller.spec.ts @@ -2,6 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FilesController } from './files.controller'; import { FilesService } from './files.service'; import { ConfigModule } from '@nestjs/config'; +import { FileRenameRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('FilesController', () => { let controller: FilesController; @@ -28,3 +31,81 @@ describe('FilesController', () => { expect(controller).toBeDefined(); }); }); + +describe('valdation FileRenameRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('音声ファイルIDが指定されていない場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイルIDが0の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 0; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイルIDが文字列の場合、リクエストが失敗する', async () => { + class InvalidFileRenameRequest { + audioFileId: string; + fileName: string; + } + + const request = new InvalidFileRenameRequest(); + request.audioFileId = 'invalid'; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイル名が空文字の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = ''; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイル名が50文字の場合、リクエストに成功する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('音声ファイル名が51文字の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5A'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); +}); diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index f8b4328..27906a5 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -27,6 +27,8 @@ import { AudioUploadFinishedResponse, AudioUploadLocationRequest, AudioUploadLocationResponse, + FileRenameRequest, + FileRenameResponse, TemplateDownloadLocationRequest, TemplateDownloadLocationResponse, TemplateUploadFinishedReqponse, @@ -533,4 +535,74 @@ export class FilesController { await this.filesService.templateUploadFinished(context, userId, url, name); return {}; } + + @ApiResponse({ + status: HttpStatus.OK, + type: FileRenameResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'fileRename', + description: '音声ファイルの表示ファイル名を変更します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @Post('rename') + async fileRename( + @Req() req: Request, + @Body() body: FileRenameRequest, + ): Promise { + const { audioFileId, fileName } = body; + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + // TODO: ファイル名変更処理を実装する + return {}; + } } diff --git a/dictation_server/src/features/files/types/types.ts b/dictation_server/src/features/files/types/types.ts index 3545a20..2199a75 100644 --- a/dictation_server/src/features/files/types/types.ts +++ b/dictation_server/src/features/files/types/types.ts @@ -8,6 +8,7 @@ import { IsInt, IsNotEmpty, IsNumberString, + IsString, MaxLength, Min, MinLength, @@ -141,3 +142,18 @@ export class TemplateUploadFinishedRequest { } export class TemplateUploadFinishedReqponse {} + +export class FileRenameRequest { + @ApiProperty({ description: 'ファイル名変更対象の音声ファイルID' }) + @Type(() => Number) + @Min(1) + @IsInt() + audioFileId: number; + @ApiProperty({ description: '変更するファイル名' }) + @IsString() + @MaxLength(50) + @MinLength(1) + fileName: string; +} + +export class FileRenameResponse {} diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 5f85918..0799356 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -104,6 +104,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', @@ -365,6 +366,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', @@ -500,6 +502,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', diff --git a/dictation_server/src/features/tasks/types/convert.ts b/dictation_server/src/features/tasks/types/convert.ts index 2ce6d99..c7fb43e 100644 --- a/dictation_server/src/features/tasks/types/convert.ts +++ b/dictation_server/src/features/tasks/types/convert.ts @@ -66,6 +66,7 @@ const createTask = ( audioFormat: file.audio_format, comment: file.comment ?? '', fileName: file.file_name, + rawFileName: file.raw_file_name, fileSize: file.file_size, isEncrypted: file.is_encrypted, url: file.url, diff --git a/dictation_server/src/features/tasks/types/types.ts b/dictation_server/src/features/tasks/types/types.ts index 33ba3d1..34d4252 100644 --- a/dictation_server/src/features/tasks/types/types.ts +++ b/dictation_server/src/features/tasks/types/types.ts @@ -110,6 +110,8 @@ export class Task { url: string; @ApiProperty({ description: '音声ファイル名' }) fileName: string; + @ApiProperty({ description: '生(Blob Storage上の)音声ファイル名' }) + rawFileName: string; @ApiProperty({ description: '音声ファイルの録音時間(ミリ秒の整数値)', }) From e6d27d78108944976ebf662508dba37809bf287a Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 15 Apr 2024 06:52:07 +0000 Subject: [PATCH 075/109] =?UTF-8?q?Merged=20PR=20867:=20=E9=9F=B3=E5=A3=B0?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E5=90=8D=E5=A4=89=E6=9B=B4?= =?UTF-8?q?API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4052: 音声ファイル名変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4052) - ファイル名変更APIとそのUTを実装しました。 ## レビューポイント - リポジトリ実装のチェック内容とその順序は適切でしょうか? - テスト項目は適切でしょうか? ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - UT実行 - ローカル実行 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/error/code.ts | 2 + dictation_server/src/common/error/message.ts | 2 + .../features/files/files.controller.spec.ts | 10 +- .../src/features/files/files.controller.ts | 2 +- .../src/features/files/files.module.ts | 1 + .../src/features/files/files.service.spec.ts | 464 ++++++++++++++++-- .../src/features/files/files.service.ts | 76 +++ .../src/features/files/test/utility.ts | 32 +- .../src/features/files/types/types.ts | 2 +- .../audio_files.repository.service.ts | 177 ++++++- .../repositories/audio_files/errors/types.ts | 35 ++ 11 files changed, 750 insertions(+), 53 deletions(-) create mode 100644 dictation_server/src/repositories/audio_files/errors/types.ts diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 8bd934c..0b5902e 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -89,4 +89,6 @@ export const ErrorCodes = [ 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) 'E019001', // パートナーアカウント取得不可エラー(階層構造が不正) 'E020001', // パートナーアカウント変更エラー(変更条件を満たしていない) + 'E021001', // 音声ファイル名変更不可エラー(権限不足) + 'E021002', // 音声ファイル名変更不可エラー(同名ファイルが存在) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index d384512..6731596 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -79,4 +79,6 @@ export const errors: Errors = { E018001: 'Partner account delete failed Error: not satisfied conditions', E019001: 'Partner account get failed Error: hierarchy mismatch', E020001: 'Partner account change failed Error: not satisfied conditions', + E021001: 'Audio file name change failed Error: insufficient permissions', + E021002: 'Audio file name change failed Error: same file name exists', }; diff --git a/dictation_server/src/features/files/files.controller.spec.ts b/dictation_server/src/features/files/files.controller.spec.ts index 39798dc..d79bf29 100644 --- a/dictation_server/src/features/files/files.controller.spec.ts +++ b/dictation_server/src/features/files/files.controller.spec.ts @@ -88,20 +88,22 @@ describe('valdation FileRenameRequest', () => { const errors = await validate(valdationObject); expect(errors.length).toBe(1); }); - it('音声ファイル名が50文字の場合、リクエストに成功する', async () => { + it('音声ファイル名が64文字の場合、リクエストに成功する', async () => { const request = new FileRenameRequest(); request.audioFileId = 1; - request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5'; + request.fileName = + 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5ABCDEFGHI6ABCD'; const valdationObject = plainToClass(FileRenameRequest, request); const errors = await validate(valdationObject); expect(errors.length).toBe(0); }); - it('音声ファイル名が51文字の場合、リクエストが失敗する', async () => { + it('音声ファイル名が65文字の場合、リクエストが失敗する', async () => { const request = new FileRenameRequest(); request.audioFileId = 1; - request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5A'; + request.fileName = + 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5ABCDEFGHI6ABCDE'; const valdationObject = plainToClass(FileRenameRequest, request); diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index 27906a5..ceb28ac 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -602,7 +602,7 @@ export class FilesController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: ファイル名変更処理を実装する + await this.filesService.fileRename(context, userId, audioFileId, fileName); return {}; } } diff --git a/dictation_server/src/features/files/files.module.ts b/dictation_server/src/features/files/files.module.ts index 9d087d8..2a7e224 100644 --- a/dictation_server/src/features/files/files.module.ts +++ b/dictation_server/src/features/files/files.module.ts @@ -28,6 +28,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r SendGridModule, AdB2cModule, AccountsRepositoryModule, + AudioFilesRepositoryModule, ], providers: [FilesService], controllers: [FilesController], diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index c0ce84a..88f048d 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -35,7 +35,11 @@ import { import { createWorktype } from '../accounts/test/utility'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service'; -import { getCheckoutPermissions, getTask } from '../tasks/test/utility'; +import { + createCheckoutPermissions, + getAudioFile, + getCheckoutPermissions, +} from '../tasks/test/utility'; import { DateWithZeroTime } from '../licenses/types/types'; import { LICENSE_ALLOCATED_STATUS, @@ -601,7 +605,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); // 音声ファイルの録音者のユーザー - const { author_id: authorAuthorId } = await makeTestUser(source, { + await makeTestUser(source, { account_id: accountId, external_id: 'author-user-external-id', role: 'author', @@ -724,16 +728,13 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); // 音声ファイルの録音者のユーザー - const { - external_id: authorExternalId, - id: authorUserId, - author_id: authorAuthorId, - } = await makeTestUser(source, { - account_id: accountId, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -1126,16 +1127,13 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { it('日付フォーマットが不正な場合、エラーを返却する', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); - const { - external_id: authorExternalId, - id: authorUserId, - author_id: authorAuthorId, - } = await makeTestUser(source, { - account_id: accountId, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -1173,16 +1171,13 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { it('オプションアイテムが10個ない場合、エラーを返却する', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); - const { - external_id: authorExternalId, - id: authorUserId, - author_id: authorAuthorId, - } = await makeTestUser(source, { - account_id: accountId, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -2199,3 +2194,408 @@ const optionItemList = [ optionItemValue: 'value_10', }, ]; + +describe('fileRename', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ファイル名を変更できる(管理者)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + await service.fileRename( + context, + admin.external_id, + task.audioFileId, + newFileName, + ); + + //実行結果を確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(newFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + }); + it('ファイル名を変更できる(Author)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: authorExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }); + + const context = makeContext(authorExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + undefined, + 'AUTHOR_ID', + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + await service.fileRename( + context, + authorExternalId, + task.audioFileId, + newFileName, + ); + + //実行結果を確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(newFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + }); + it('ファイル名を変更できる(Typist)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: typistExternalId, id: typistId } = await makeTestUser( + source, + { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }, + ); + + const context = makeContext(typistExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + await createCheckoutPermissions(source, task.taskId, typistId); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + await service.fileRename( + context, + typistExternalId, + task.audioFileId, + newFileName, + ); + + //実行結果を確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(newFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + }); + it('ユーザーが管理者でなくRoleがNoneの場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: noneExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'none-user-external-id', + role: USER_ROLES.NONE, + }); + + const context = makeContext(noneExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + try { + await service.fileRename( + context, + noneExternalId, + task.audioFileId, + newFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021001')); + } else { + fail(); + } + } + }); + it('Authorがファイル名変更をするときユーザーのAuthorIDとタスクのAuthorIDが異なる場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: authorExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }); + + const context = makeContext(authorExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + undefined, + 'AUTHOR_ID_XXX', + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + try { + await service.fileRename( + context, + authorExternalId, + task.audioFileId, + newFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021001')); + } else { + fail(); + } + } + }); + it('Typistがファイル名変更をするときユーザーがタスクのチェックアウト候補でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: typistExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(typistExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + try { + await service.fileRename( + context, + typistExternalId, + task.audioFileId, + newFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021001')); + } else { + fail(); + } + } + }); + it('変更するファイル名がすでに存在する場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + undefined, + undefined, + undefined, + undefined, + '00000001', + ); + + const alreadyExistFileName = 'already.zip'; + const alreadyExistTask = await createTask( + source, + account.id, + 'https://blob.url/account-1', + alreadyExistFileName, + TASK_STATUS.UPLOADED, + undefined, + undefined, + undefined, + undefined, + '00000002', + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + + const alreadyExistAudioFile = await getAudioFile( + source, + alreadyExistTask.audioFileId, + ); + expect(alreadyExistAudioFile?.file_name).toBe(alreadyExistFileName); + } + + try { + await service.fileRename( + context, + admin.external_id, + task.audioFileId, + alreadyExistFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021002')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 5736ac2..f6a023c 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -43,6 +43,12 @@ import { AccountsRepositoryService } from '../../repositories/accounts/accounts. import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; +import { AudioFilesRepositoryService } from '../../repositories/audio_files/audio_files.repository.service'; +import { + CheckoutPermissionNotFoundError, + FileNameAlreadyExistsError, + RoleNotMatchError, +} from '../../repositories/audio_files/errors/types'; @Injectable() export class FilesService { @@ -59,6 +65,7 @@ export class FilesService { private readonly notificationhubService: NotificationhubService, private readonly licensesRepository: LicensesRepositoryService, private readonly sendGridService: SendGridService, + private readonly audioFilesRepositoryService: AudioFilesRepositoryService, ) {} /** @@ -911,4 +918,73 @@ export class FilesService { ); } } + + /** + * 音声ファイルの表示ファイル名を変更する + * @param context + * @param externalId + * @param audioFileId + * @param fileName + * @returns rename + */ + async fileRename( + context: Context, + externalId: string, + audioFileId: number, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.fileRename.name + } | params: { externalId: ${externalId}, audioFileId: ${audioFileId}, fileName: ${fileName} };`, + ); + + try { + // ユーザー取得 + const { account_id: accountId, id: userId } = + await this.usersRepository.findUserByExternalId(context, externalId); + + // 音声ファイルの表示ファイル名を変更 + await this.audioFilesRepositoryService.renameAudioFile( + context, + accountId, + userId, + audioFileId, + fileName, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + + if (e instanceof Error) { + switch (e.constructor) { + case TasksNotFoundError: + case RoleNotMatchError: + case AuthorUserNotMatchError: + case CheckoutPermissionNotFoundError: + throw new HttpException( + makeErrorResponse('E021001'), + HttpStatus.BAD_REQUEST, + ); + case FileNameAlreadyExistsError: + throw new HttpException( + makeErrorResponse('E021002'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.fileRename.name}`, + ); + } + } } diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index c15f981..73416be 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -56,7 +56,7 @@ export const createTask = async ( owner_user_id?: number | undefined, fileSize?: number | undefined, jobNumber?: string | undefined, -): Promise<{ audioFileId: number }> => { +): Promise<{ audioFileId: number; taskId: number }> => { const { identifiers: audioFileIdentifiers } = await datasource .getRepository(AudioFile) .insert({ @@ -64,6 +64,7 @@ export const createTask = async ( owner_user_id: owner_user_id ?? 1, url: url, file_name: fileName, + raw_file_name: fileName, author_id: author_id ?? 'DEFAULT_ID', work_type_id: 'work_type_id', started_at: new Date(), @@ -89,20 +90,23 @@ export const createTask = async ( }); const templateFile = templateFileIdentifiers.pop() as TemplateFile; - await datasource.getRepository(Task).insert({ - job_number: jobNumber ?? '00000001', - account_id: account_id, - is_job_number_enabled: true, - audio_file_id: audioFile.id, - template_file_id: templateFile.id, - typist_user_id: typist_user_id, - status: status, - priority: '01', - started_at: new Date().toISOString(), - created_at: new Date(), - }); + const { identifiers: TaskIdentifiers } = await datasource + .getRepository(Task) + .insert({ + job_number: jobNumber ?? '00000001', + account_id: account_id, + is_job_number_enabled: true, + audio_file_id: audioFile.id, + template_file_id: templateFile.id, + typist_user_id: typist_user_id, + status: status, + priority: '01', + started_at: new Date().toISOString(), + created_at: new Date(), + }); + const task = TaskIdentifiers.pop() as Task; - return { audioFileId: audioFile.id }; + return { audioFileId: audioFile.id, taskId: task.id }; }; export const getTaskFromJobNumber = async ( diff --git a/dictation_server/src/features/files/types/types.ts b/dictation_server/src/features/files/types/types.ts index 2199a75..57bdb41 100644 --- a/dictation_server/src/features/files/types/types.ts +++ b/dictation_server/src/features/files/types/types.ts @@ -151,7 +151,7 @@ export class FileRenameRequest { audioFileId: number; @ApiProperty({ description: '変更するファイル名' }) @IsString() - @MaxLength(50) + @MaxLength(64) @MinLength(1) fileName: string; } diff --git a/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts b/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts index b9605e8..a6dded1 100644 --- a/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts +++ b/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts @@ -1,7 +1,182 @@ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, In, Not } from 'typeorm'; +import { AudioFile } from './entity/audio_file.entity'; +import { Task } from '../tasks/entity/task.entity'; +import { User } from '../users/entity/user.entity'; +import { USER_ROLES } from '../../constants'; +import { UserGroupMember } from '../user_groups/entity/user_group_member.entity'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; +import { Context } from '../../common/log'; +import { updateEntity } from '../../common/repository'; +import { + CheckoutPermissionNotFoundError, + FileNameAlreadyExistsError, + RoleNotMatchError, + TasksNotFoundError, +} from './errors/types'; +import { AuthorUserNotMatchError } from '../../features/files/errors/types'; @Injectable() export class AudioFilesRepositoryService { + //クエリログにコメントを出力するかどうか + private readonly isCommentOut = process.env.STAGE !== 'local'; constructor(private dataSource: DataSource) {} + + /** + * アカウント内のテンプレートファイルの一覧を取得する + * @param context + * @param accountId + * @param userId + * @param audioFileId + * @param fileName + * @returns audio file + */ + async renameAudioFile( + context: Context, + accountId: number, + userId: number, + audioFileId: number, + fileName: string, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + // 実行ユーザーの情報を取得 + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { account_id: accountId, id: userId }, + relations: { account: true }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!user) { + throw new Error( + `user not found. account_id: ${accountId}, user_id: ${userId}`, + ); + } + const account = user.account; + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!account) { + throw new Error(`account not found. account_id: ${accountId}`); + } + + // ユーザーがアカウントの管理者であるかどうか + const isAdmin = + account.primary_admin_user_id === userId || + account.secondary_admin_user_id === userId; + + // ユーザーがTypistである場合は、ユーザーの所属するグループを取得しておく + let groupIds: number[] = []; + if (user.role === USER_ROLES.TYPIST) { + const groupMemberRepo = entityManager.getRepository(UserGroupMember); + // ユーザーの所属するすべてのグループを列挙 + const groups = await groupMemberRepo.find({ + relations: { user: true }, + where: { user_id: userId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + // ユーザーの所属するすべてのグループIDを列挙 + groupIds = groups.map((member) => member.user_group_id); + } + + // リクエストの音声ファイル名と同じファイル名の音声ファイルの一覧を取得 + const audioFileRepo = entityManager.getRepository(AudioFile); + const audioFiles = await audioFileRepo.find({ + where: { + account_id: accountId, + id: Not(audioFileId), + file_name: fileName, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // ファイル名が重複している場合はエラー + if (audioFiles.length !== 0) { + throw new FileNameAlreadyExistsError( + `The file name already exists. accountId: ${accountId} file_name: ${fileName}`, + ); + } + + // タスク情報を取得 + const taskRepo = entityManager.getRepository(Task); + const task = await taskRepo.findOne({ + where: { account_id: accountId, audio_file_id: audioFileId }, + relations: { file: true }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!task) { + throw new TasksNotFoundError( + `task not found. account_id: ${accountId} audio_file_id: ${audioFileId}`, + ); + } + + // 音声ファイル情報を取得 + const audioFile = task.file; + if (!audioFile) { + throw new TasksNotFoundError( + `audioFile not found. audio_file_id: ${audioFileId}`, + ); + } + + if (isAdmin) { + // 管理者の場合は、ファイル名を変更できる + await updateEntity( + audioFileRepo, + { id: audioFileId }, + { file_name: fileName }, + this.isCommentOut, + context, + ); + return; + } + + // ユーザーが管理者でない場合は、ロールに応じた権限を確認 + + if (user.role === USER_ROLES.NONE) { + // NONEの場合はエラー + throw new RoleNotMatchError( + `The user does not have the required role. userId: ${userId}. role: ${user.role}`, + ); + } + if (user.role === USER_ROLES.AUTHOR) { + // ユーザーがAuthorである場合は、音声ファイルのAuthorIDが一致するか確認 + if (audioFile.author_id !== user.author_id) { + throw new AuthorUserNotMatchError( + `The user is not the author of the audio file. audioFileId: ${audioFileId}, userId: ${userId}`, + ); + } + } + if (user.role === USER_ROLES.TYPIST) { + // ユーザーがTypistである場合は、チェックアウト権限を確認 + const checkoutRepo = entityManager.getRepository(CheckoutPermission); + // ユーザーに対するチェックアウト権限、またはユーザーの所属するユーザーグループのチェックアウト権限を取得 + const checkoutPermissions = await checkoutRepo.find({ + where: [ + { task_id: task.id, user_id: userId }, // ユーザーがチェックアウト可能である + { task_id: task.id, user_group_id: In(groupIds) }, // ユーザーの所属するユーザーグループがチェックアウト可能である + ], + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // チェックアウト権限がない場合はエラー + if (checkoutPermissions.length === 0) { + throw new CheckoutPermissionNotFoundError( + `The user does not have checkout permission. taskId: ${task.id}, userId: ${userId}`, + ); + } + } + + // ファイル名を変更 + await updateEntity( + audioFileRepo, + { id: audioFileId }, + { file_name: fileName }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/repositories/audio_files/errors/types.ts b/dictation_server/src/repositories/audio_files/errors/types.ts new file mode 100644 index 0000000..9d47793 --- /dev/null +++ b/dictation_server/src/repositories/audio_files/errors/types.ts @@ -0,0 +1,35 @@ +// タスク未発見エラー +export class TasksNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'TasksNotFoundError'; + } +} +// ファイル名変更権限ロール不一致エラー +export class RoleNotMatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'RoleNotMatchError'; + } +} +// タスクAuthorID不一致エラー +export class TaskAuthorIdNotMatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'TaskAuthorIdNotMatchError'; + } +} +// チェックアウト権限未発見エラー +export class CheckoutPermissionNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'CheckoutPermissionNotFoundError'; + } +} +// 同名ファイルエラー +export class FileNameAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + this.name = 'StatusNotMatchError'; + } +} From 1d2089b0c4910a0af53b75cc81a74ee9a16a3993 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 16 Apr 2024 05:24:18 +0000 Subject: [PATCH 076/109] =?UTF-8?q?Merged=20PR=20868:=20=E9=9F=B3=E5=A3=B0?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E5=90=8D=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=AE=E3=81=AB=E4=BC=B4=E3=81=86API=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4053: 音声ファイル名変更のに伴うAPI修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4053) - 音声ファイル名について生ファイル名を利用するように修正しました。 - ファイルアップロード完了(タスク登録) - パラメータのファイル名で生ファイル名も登録 - 音声ファイルダウンロード先取得 - タスク削除 - blobストレージのファイル名に生ファイル名を利用 ## レビューポイント - 対象APIは認識通りか ## UIの変更 - なし ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - UTが通ることを確認 - 各APIで生ファイル名を使って実行できることを確認 --- dictation_server/src/features/files/files.service.ts | 4 ++-- dictation_server/src/features/tasks/tasks.service.spec.ts | 4 ++-- dictation_server/src/features/tasks/tasks.service.ts | 2 +- dictation_server/src/features/tasks/test/utility.ts | 1 + .../src/repositories/tasks/tasks.repository.service.ts | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index f6a023c..3d0084a 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -611,7 +611,7 @@ export class FilesService { } } - const filePath = `${file.file_name}`; + const filePath = `${file.raw_file_name}`; const isFileExist = await this.blobStorageService.fileExists( context, @@ -623,7 +623,7 @@ export class FilesService { if (!isFileExist) { this.logger.log(`[${context.getTrackingId()}] filePath:${filePath}`); throw new AudioFileNotFoundError( - `Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${file.file_name}`, + `Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${filePath}`, ); } diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 0799356..5fe8662 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -4415,7 +4415,7 @@ describe('deleteTask', () => { context, account.id, account.country, - 'x.zip', + 'y.zip', ); } }); @@ -4500,7 +4500,7 @@ describe('deleteTask', () => { context, account.id, account.country, - 'x.zip', + 'y.zip', ); } }); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index 11b42ea..871d83f 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -932,7 +932,7 @@ export class TasksService { Object.values(TASK_STATUS), ); - const targetFileName = task.file?.file_name; + const targetFileName = task.file?.raw_file_name; if (!targetFileName) { throw new Error(`target file not found. audioFileId: ${audioFileId}`); } diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 6a1e024..31928e0 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -120,6 +120,7 @@ export const createTask = async ( owner_user_id: owner_user_id, url: '', file_name: 'x.zip', + raw_file_name: 'y.zip', author_id: author_id, work_type_id: work_type_id, started_at: new Date(), diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index d712597..4a1e484 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -854,6 +854,7 @@ export class TasksRepositoryService { audioFile.owner_user_id = owner_user_id; audioFile.url = url; audioFile.file_name = file_name; + audioFile.raw_file_name = file_name; audioFile.author_id = author_id; audioFile.work_type_id = work_type_id; audioFile.started_at = started_at; From 0a714f8484588d9c0941e1f8919a171a6715524b Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 16 Apr 2024 10:12:44 +0000 Subject: [PATCH 077/109] =?UTF-8?q?Merged=20PR=20870:=20=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E6=83=85=E5=A0=B1=E3=83=9D=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E7=94=BB=E9=9D=A2=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4051: ファイル情報ポップアップ画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4051) - ファイル情報ポップアップを修正し、ファイル名を変更できるように修正しました。 - 音声ファイル名変更APIのファイル名の文字数を修正しました。 - フロントの入力欄で64文字にしたので、プラス拡張子で68文字としました。 ## レビューポイント - 画面イメージは認識通りでしょうか? - 表示では拡張子を取って、APIに渡す際にはつけているのですが処理として不自然ではないでしょうか? ## UIの変更 - [Task4051](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/Task4051?csf=1&web=1&e=nbYIE0) ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - タスク一覧、ファイル情報ポップアップが正常に見えることを確認 --- dictation_client/src/api/api.ts | 100 ++++++++++++++++++ dictation_client/src/common/errors/code.ts | 2 + .../src/features/dictation/dictationSlice.ts | 11 ++ .../src/features/dictation/operations.ts | 68 ++++++++++++ .../pages/DictationPage/filePropertyPopup.tsx | 65 +++++++++++- .../src/pages/DictationPage/index.tsx | 40 +++++-- dictation_client/src/translation/de.json | 8 +- dictation_client/src/translation/en.json | 8 +- dictation_client/src/translation/es.json | 8 +- dictation_client/src/translation/fr.json | 8 +- .../tasks/tasks.repository.service.ts | 2 +- 11 files changed, 302 insertions(+), 18 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 993f4c0..72982f3 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -725,6 +725,25 @@ export interface ErrorResponse { */ 'code': string; } +/** + * + * @export + * @interface FileRenameRequest + */ +export interface FileRenameRequest { + /** + * ファイル名変更対象の音声ファイルID + * @type {number} + * @memberof FileRenameRequest + */ + 'audioFileId': number; + /** + * 変更するファイル名 + * @type {string} + * @memberof FileRenameRequest + */ + 'fileName': string; +} /** * * @export @@ -1971,6 +1990,12 @@ export interface Task { * @memberof Task */ 'fileName': string; + /** + * 生(Blob Storage上の)音声ファイル名 + * @type {string} + * @memberof Task + */ + 'rawFileName': string; /** * 音声ファイルの録音時間(ミリ秒の整数値) * @type {string} @@ -5723,6 +5748,46 @@ export const FilesApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + fileRename: async (fileRenameRequest: FileRenameRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'fileRenameRequest' is not null or undefined + assertParamExists('fileRename', 'fileRenameRequest', fileRenameRequest) + const localVarPath = `/files/rename`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(fileRenameRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5907,6 +5972,19 @@ export const FilesApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['FilesApi.downloadTemplateLocation']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async fileRename(fileRenameRequest: FileRenameRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.fileRename(fileRenameRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.fileRename']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5987,6 +6065,16 @@ export const FilesApiFactory = function (configuration?: Configuration, basePath downloadTemplateLocation(audioFileId: number, options?: any): AxiosPromise { return localVarFp.downloadTemplateLocation(audioFileId, options).then((request) => request(axios, basePath)); }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + fileRename(fileRenameRequest: FileRenameRequest, options?: any): AxiosPromise { + return localVarFp.fileRename(fileRenameRequest, options).then((request) => request(axios, basePath)); + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -6059,6 +6147,18 @@ export class FilesApi extends BaseAPI { return FilesApiFp(this.configuration).downloadTemplateLocation(audioFileId, options).then((request) => request(this.axios, this.basePath)); } + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public fileRename(fileRenameRequest: FileRenameRequest, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).fileRename(fileRenameRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 477282c..fcef6f2 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -84,4 +84,6 @@ export const errorCodes = [ "E018001", // パートナーアカウント削除エラー(削除条件を満たしていない) "E019001", // パートナーアカウント取得不可エラー(階層構造が不正) "E020001", // パートナーアカウント変更エラー(変更条件を満たしていない) + "E021001", // 音声ファイル名変更不可エラー(権限不足) + "E021002", // 音声ファイル名変更不可エラー(同名ファイルが存在) ] as const; diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index b187c88..e8361af 100644 --- a/dictation_client/src/features/dictation/dictationSlice.ts +++ b/dictation_client/src/features/dictation/dictationSlice.ts @@ -12,6 +12,7 @@ import { updateAssigneeAsync, cancelAsync, deleteTaskAsync, + renameFileAsync, } from "./operations"; import { SORTABLE_COLUMN, @@ -228,6 +229,16 @@ export const dictationSlice = createSlice({ builder.addCase(deleteTaskAsync.rejected, (state) => { state.apps.isLoading = false; }); + + builder.addCase(renameFileAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(renameFileAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(renameFileAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 522b6d9..a25c5d3 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -689,3 +689,71 @@ export const deleteTaskAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const renameFileAsync = createAsyncThunk< + { + // empty + }, + { + // パラメータ + audioFileId: number; + fileName: string; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("dictations/renameFileAsync", async (args, thunkApi) => { + const { audioFileId, fileName } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const filesApi = new FilesApi(config); + + try { + await filesApi.fileRename( + { fileName, audioFileId }, + { headers: { authorization: `Bearer ${accessToken}` } } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let message = getTranslationID("common.message.internalServerError"); + + // 変更権限がない場合はエラー + if (error.code === "E021001") { + message = getTranslationID("dictationPage.message.fileRenameFailedError"); + } + + // ファイル名が既に存在する場合はエラー + if (error.code === "E021002") { + message = getTranslationID( + "dictationPage.message.fileNameAleadyExistsError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx index 979bbf1..ab234b2 100644 --- a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx +++ b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx @@ -1,13 +1,15 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import styles from "styles/app.module.scss"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { selectSelectedFileTask, selectIsLoading, PRIORITY, + renameFileAsync, } from "features/dictation"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; +import { AppDispatch } from "app/store"; import close from "../../assets/images/close.svg"; import lock from "../../assets/images/lock.svg"; @@ -19,14 +21,46 @@ interface FilePropertyPopupProps { export const FilePropertyPopup: React.FC = (props) => { const { onClose, isOpen } = props; const [t] = useTranslation(); + const dispatch: AppDispatch = useDispatch(); const isLoading = useSelector(selectIsLoading); + const [isPushSaveButton, setIsPushSaveButton] = useState(false); + // ポップアップを閉じる処理 const closePopup = useCallback(() => { + setIsPushSaveButton(false); onClose(false); }, [onClose]); const selectedFileTask = useSelector(selectSelectedFileTask); + const [fileName, setFileName] = useState(""); + + useEffect(() => { + if (isOpen) { + setFileName(selectedFileTask?.fileName ?? ""); + } + }, [selectedFileTask, isOpen]); + + // ファイル名の保存処理 + const saveFileName = useCallback(async () => { + setIsPushSaveButton(true); + if (fileName.length === 0) { + return; + } + const { meta } = await dispatch( + renameFileAsync({ + audioFileId: selectedFileTask?.audioFileId ?? 0, + fileName, + }) + ); + + setIsPushSaveButton(false); + + if (meta.requestStatus === "fulfilled") { + onClose(true); + } + }, [dispatch, onClose, fileName, selectedFileTask]); + return (
      @@ -45,7 +79,32 @@ export const FilePropertyPopup: React.FC = (props) => { {t(getTranslationID("filePropertyPopup.label.general"))}
      {t(getTranslationID("dictationPage.label.fileName"))}
      -
      {selectedFileTask?.fileName.replace(".zip", "") ?? ""}
      +
      + setFileName(e.target.value)} + /> + + {isPushSaveButton && fileName.length === 0 && ( + + {t(getTranslationID("common.message.inputEmptyError"))} + + )} +
      +
      {t(getTranslationID("dictationPage.label.rawFileName"))}
      +
      {selectedFileTask?.rawFileName ?? ""}
      {t(getTranslationID("dictationPage.label.fileSize"))}
      {selectedFileTask?.fileSize ?? ""}
      {t(getTranslationID("dictationPage.label.fileLength"))}
      diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index ec0ee93..f73dd27 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -499,9 +499,39 @@ const DictationPage: React.FC = (): JSX.Element => { setIsBackupPopupOpen(true); }, []); - const onCloseFilePropertyPopup = useCallback(() => { - setIsFilePropertyPopupOpen(false); - }, []); + const onCloseFilePropertyPopup = useCallback( + (isChanged: boolean) => { + if (isChanged) { + const filter = getFilter( + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup + ); + dispatch( + listTasksAsync({ + limit: LIMIT_TASK_NUM, + offset: 0, + filter, + direction: sortDirection, + paramName: sortableParamName, + }) + ); + } + setIsFilePropertyPopupOpen(false); + }, + [ + dispatch, + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup, + sortDirection, + sortableParamName, + ] + ); const sortIconClass = ( currentParam: SortableColumnType, @@ -1309,9 +1339,7 @@ const DictationPage: React.FC = (): JSX.Element => {
    {x.workType} - {x.fileName.replace(".zip", "")} - {x.fileName}{x.audioDuration}