Merge branch 'develop' into main
This commit is contained in:
commit
4dd3e646f4
@ -1,5 +1,6 @@
|
||||
// トークンの型やtypeGuardの関数を配置するファイル
|
||||
export interface Token {
|
||||
delegateUserId?: string;
|
||||
userId: string;
|
||||
role: string;
|
||||
tier: number;
|
||||
|
||||
@ -33,4 +33,20 @@ export const TIERS = {
|
||||
* 401エラー時にログアウトさせずに処理を継続するエラーコード
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = ["E010209"];
|
||||
export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [
|
||||
"E010209",
|
||||
"E010503",
|
||||
"E010501",
|
||||
];
|
||||
|
||||
/**
|
||||
* アクセストークンを更新する基準の秒数
|
||||
* @const {number}
|
||||
*/
|
||||
export const TOKEN_UPDATE_TIME = 5 * 60;
|
||||
|
||||
/**
|
||||
* アクセストークンの更新チェックを行う間隔(ミリ秒)
|
||||
* @const {number}
|
||||
*/
|
||||
export const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000;
|
||||
|
||||
@ -2,33 +2,56 @@ import React, { useCallback } from "react";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { decodeToken } from "common/decodeToken";
|
||||
import { useInterval } from "common/useInterval";
|
||||
import { updateTokenAsync, loadAccessToken } from "features/auth";
|
||||
import {
|
||||
updateTokenAsync,
|
||||
loadAccessToken,
|
||||
updateDelegationTokenAsync,
|
||||
} from "features/auth";
|
||||
import { DateTime } from "luxon";
|
||||
import { useDispatch } from "react-redux";
|
||||
// アクセストークンを更新する基準の秒数
|
||||
const TOKEN_UPDATE_TIME = 5 * 60;
|
||||
// アクセストークンの更新チェックを行う間隔(ミリ秒)
|
||||
const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000;
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { cleanupDelegateAccount } from "features/partner";
|
||||
import { TOKEN_UPDATE_INTERVAL_MS, TOKEN_UPDATE_TIME } from "./constants";
|
||||
|
||||
export const UpdateTokenTimer = () => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const delegattionToken = useSelector(selectDelegationAccessToken);
|
||||
|
||||
// 期限が5分以内であれば更新APIを呼ぶ
|
||||
const updateToken = useCallback(async () => {
|
||||
// localStorageからトークンを取得
|
||||
const jwt = loadAccessToken();
|
||||
// 現在時刻を取得
|
||||
const now = DateTime.local().toSeconds();
|
||||
// selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
|
||||
if (jwt) {
|
||||
const token = decodeToken(jwt);
|
||||
if (token) {
|
||||
const { exp } = token;
|
||||
const now = DateTime.local().toSeconds();
|
||||
if (exp - now <= TOKEN_UPDATE_TIME) {
|
||||
await dispatch(updateTokenAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
// 代行操作トークン更新処理
|
||||
if (delegattionToken) {
|
||||
const token = decodeToken(delegattionToken);
|
||||
if (token) {
|
||||
const { exp } = token;
|
||||
if (exp - now <= TOKEN_UPDATE_TIME) {
|
||||
const { meta } = await dispatch(updateDelegationTokenAsync());
|
||||
if (meta.requestStatus === "rejected") {
|
||||
dispatch(cleanupDelegateAccount());
|
||||
navigate("/partners");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [dispatch, delegattionToken, navigate]);
|
||||
|
||||
useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS);
|
||||
|
||||
|
||||
@ -21,10 +21,16 @@ export const DelegationBar: React.FC = (): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onClickExit = useCallback(() => {
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
dispatch(clearDelegationToken());
|
||||
dispatch(cleanupDelegateAccount());
|
||||
navigate("/partners");
|
||||
}, [dispatch, navigate]);
|
||||
}, [dispatch, navigate, t]);
|
||||
|
||||
return (
|
||||
<div className={styles.manageInfo}>
|
||||
|
||||
@ -70,3 +70,12 @@ export const TIER1_TO_TIER4_ONLY_TABS = [HEADER_MENUS_PARTNER];
|
||||
* admin,standardでなく、第1~5階層でもないアカウントに表示する空のヘッダータブ
|
||||
*/
|
||||
export const INVALID_ACCOUNT_TABS = [];
|
||||
|
||||
/**
|
||||
* 代行操作中に表示するヘッダータブ
|
||||
*/
|
||||
export const DELEGATE_TABS = [
|
||||
HEADER_MENUS_LICENSE,
|
||||
HEADER_MENUS_USER,
|
||||
HEADER_MENUS_WORKFLOW,
|
||||
];
|
||||
|
||||
@ -3,7 +3,7 @@ import styles from "styles/app.module.scss";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useMsal } from "@azure/msal-react";
|
||||
import { clearToken } from "features/auth";
|
||||
import { clearToken, loadAccessToken } from "features/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getUserInfoAsync,
|
||||
@ -12,6 +12,7 @@ import {
|
||||
clearUserInfo,
|
||||
} from "features/login";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { getFilteredMenus } from "./utils";
|
||||
import logo from "../../assets/images/OMS_logo_black.svg";
|
||||
import ac from "../../assets/images/account_circle.svg";
|
||||
@ -27,31 +28,41 @@ interface HeaderProps {
|
||||
// ログイン後のヘッダー
|
||||
const LoginedHeader: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
const { activePath } = props;
|
||||
const filterMenus = getFilteredMenus();
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { instance } = useMsal();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// メニューの代行操作表示制御
|
||||
const isDelegation = useSelector(selectDelegationAccessToken) !== null;
|
||||
const accessToken = loadAccessToken();
|
||||
const filterMenus = getFilteredMenus(isDelegation);
|
||||
|
||||
// Headerのユーザー情報を取得する
|
||||
const isUserNameEmpty = useSelector(selectIsUserNameEmpty);
|
||||
const userName = useSelector(selectUserName);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUserNameEmpty) {
|
||||
// ユーザー情報が空の場合、ユーザー情報を取得する(アクセストークンがある場合のみ)
|
||||
// ログアウト時にユーザー情報とアクセストークンをクリアするため、失敗するそのタイミングでユーザー情報を取得しないようにする
|
||||
if (accessToken !== null && isUserNameEmpty) {
|
||||
dispatch(getUserInfoAsync());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, isUserNameEmpty]);
|
||||
|
||||
// ログアウト
|
||||
const onSignoutButton = useCallback(
|
||||
async () => {
|
||||
// ダイアログ確認
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(t(getTranslationID("common.message.displayDialog")))) {
|
||||
instance.logoutRedirect({ postLogoutRedirectUri: "/" });
|
||||
if (
|
||||
!isDelegation &&
|
||||
// eslint-disable-next-line no-alert
|
||||
window.confirm(t(getTranslationID("common.message.displayDialog")))
|
||||
) {
|
||||
dispatch(clearToken());
|
||||
dispatch(clearUserInfo());
|
||||
instance.logoutRedirect({ postLogoutRedirectUri: "/" });
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -87,7 +98,11 @@ const LoginedHeader: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
<img src={ac} alt="" className={styles.accountIcon} />
|
||||
<span>{userName}</span>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<span className={styles.accountSignout} onClick={onSignoutButton}>
|
||||
<span
|
||||
className={styles.accountSignout}
|
||||
onClick={onSignoutButton}
|
||||
style={{ pointerEvents: isDelegation ? "none" : "auto" }}
|
||||
>
|
||||
<img src={logout} alt="" className={styles.accountIcon} />
|
||||
{t(getTranslationID("common.label.signOutButton"))}
|
||||
</span>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import styles from "styles/app.module.scss";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import logo from "../../assets/images/OMS_logo_black.svg";
|
||||
import { HEADER_NAME } from "./constants";
|
||||
|
||||
@ -12,12 +13,13 @@ const NotLoginHeader: React.FC<NotLoginHeaderProps> = (
|
||||
props: NotLoginHeaderProps
|
||||
) => {
|
||||
const { isMobile } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<header className={`${styles.header} ${isMobile && styles.home}`}>
|
||||
<div className={`${styles.headerLogo}`}>
|
||||
<img src={logo} alt="OM System" />
|
||||
</div>
|
||||
<p className={`${styles.headerSub}`}>{HEADER_NAME}</p>
|
||||
<p className={`${styles.headerSub}`}>{t(HEADER_NAME)}</p>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
HEADER_MENUS,
|
||||
TIER1_TO_TIER4_ONLY_TABS,
|
||||
INVALID_ACCOUNT_TABS,
|
||||
DELEGATE_TABS,
|
||||
} from "./constants";
|
||||
import { TIERS } from "../auth/constants";
|
||||
|
||||
@ -29,7 +30,7 @@ export const isLoginPaths = (d: string): d is LoginedPaths => {
|
||||
}
|
||||
};
|
||||
// 権限、階層ごとに表示するヘッダーをわける
|
||||
export const getFilteredMenus = () => {
|
||||
export const getFilteredMenus = (isDelegation: boolean) => {
|
||||
const isAdmin = isAdminUser();
|
||||
const isStandard = isStandardUser();
|
||||
const isTier5 = isApproveTier([TIERS.TIER5]);
|
||||
@ -40,6 +41,10 @@ export const getFilteredMenus = () => {
|
||||
TIERS.TIER4,
|
||||
]);
|
||||
|
||||
if (isDelegation) {
|
||||
return HEADER_MENUS.filter((item) => DELEGATE_TABS.includes(item.key));
|
||||
}
|
||||
|
||||
if (tier1ToTier4) {
|
||||
if (isAdmin) {
|
||||
return HEADER_MENUS;
|
||||
|
||||
@ -9,7 +9,11 @@ import {
|
||||
removeRefreshToken,
|
||||
} from "./utils";
|
||||
import type { AuthState } from "./state";
|
||||
import { getDelegationTokenAsync, updateTokenAsync } from "./operations";
|
||||
import {
|
||||
getDelegationTokenAsync,
|
||||
updateDelegationTokenAsync,
|
||||
updateTokenAsync,
|
||||
} from "./operations";
|
||||
|
||||
const initialState: AuthState = {
|
||||
configuration: initialConfig(),
|
||||
@ -61,6 +65,14 @@ export const authSlice = createSlice({
|
||||
state.delegationAccessToken = accessToken;
|
||||
state.delegationRefreshToken = refreshToken;
|
||||
});
|
||||
builder.addCase(updateDelegationTokenAsync.fulfilled, (state, action) => {
|
||||
const { accessToken } = action.payload;
|
||||
state.delegationAccessToken = accessToken;
|
||||
});
|
||||
builder.addCase(updateDelegationTokenAsync.rejected, (state) => {
|
||||
state.delegationAccessToken = null;
|
||||
state.delegationRefreshToken = null;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -6,10 +6,11 @@ import { ErrorObject, createErrorObject } from "common/errors";
|
||||
import {
|
||||
AccessTokenResponse,
|
||||
AuthApi,
|
||||
DelegationAccessTokenResponse,
|
||||
DelegationTokenResponse,
|
||||
} from "../../api/api";
|
||||
import { Configuration } from "../../api/configuration";
|
||||
import { getAccessToken, loadRefreshToken } from "./utils";
|
||||
import { getAccessToken, getRefreshToken, loadRefreshToken } from "./utils";
|
||||
|
||||
export const updateTokenAsync = createAsyncThunk<
|
||||
AccessTokenResponse,
|
||||
@ -40,6 +41,50 @@ export const updateTokenAsync = createAsyncThunk<
|
||||
}
|
||||
});
|
||||
|
||||
export const updateDelegationTokenAsync = createAsyncThunk<
|
||||
DelegationAccessTokenResponse,
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
/* Empty Object */
|
||||
};
|
||||
}
|
||||
>("auth/updateDelegationTokenAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const refreshToken = getRefreshToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const authApi = new AuthApi(config);
|
||||
|
||||
try {
|
||||
const { data } = await authApi.delegationAccessToken({
|
||||
headers: { authorization: `Bearer ${refreshToken}` },
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
if (error.code === "E010503") {
|
||||
errorMessage = getTranslationID(
|
||||
"partnerPage.message.delegateCancelError"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({});
|
||||
}
|
||||
});
|
||||
|
||||
// パートナーのアカウントを代理操作するトークンを取得する
|
||||
export const getDelegationTokenAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import type { RootState } from "app/store";
|
||||
import { setToken, getAccessToken } from "features/auth";
|
||||
import { getAccessToken, setToken } from "features/auth";
|
||||
import {
|
||||
AuthApi,
|
||||
UsersApi,
|
||||
@ -66,8 +66,7 @@ export const getUserInfoAsync = createAsyncThunk<
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const { configuration, accessToken } = state.auth;
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
|
||||
@ -92,6 +92,12 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
// 代理操作開始処理
|
||||
const startDealerManagement = useCallback(
|
||||
async (delegatedAccountId: number, delegatedCompanyName: string) => {
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
changeDelegateAccount({ delegatedAccountId, delegatedCompanyName })
|
||||
);
|
||||
@ -100,7 +106,7 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
navigate("/user");
|
||||
}
|
||||
},
|
||||
[dispatch, navigate]
|
||||
[dispatch, navigate, t]
|
||||
);
|
||||
|
||||
// HTML
|
||||
|
||||
@ -87,8 +87,9 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
|
||||
const users = useSelector(selectUserViews);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
// ユーザーが第5階層であるかどうかを判定する
|
||||
const isTier5 = isApproveTier([TIERS.TIER5]);
|
||||
// ユーザーが第5階層であるかどうかを判定する(代行操作中は第5階層として扱う)
|
||||
const isTier5 =
|
||||
isApproveTier([TIERS.TIER5]) || delegationAccessToken !== null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -14,6 +14,12 @@ services:
|
||||
- "8082"
|
||||
environment:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
networks:
|
||||
- external
|
||||
networks:
|
||||
external:
|
||||
name: omds_network
|
||||
external: true
|
||||
|
||||
# Data Volume として永続化する
|
||||
volumes:
|
||||
|
||||
5
dictation_function/.env
Normal file
5
dictation_function/.env
Normal file
@ -0,0 +1,5 @@
|
||||
DB_HOST=omds-mysql
|
||||
DB_PORT=3306
|
||||
DB_NAME=omds
|
||||
DB_USERNAME=omdsdbuser
|
||||
DB_PASSWORD=omdsdbpass
|
||||
7
dictation_function/.gitignore
vendored
7
dictation_function/.gitignore
vendored
@ -29,7 +29,6 @@ dist
|
||||
.python_packages/
|
||||
|
||||
# Python Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@ -45,4 +44,8 @@ __pycache__/
|
||||
# Azurite artifacts
|
||||
__blobstorage__
|
||||
__queuestorage__
|
||||
__azurite_db*__.json
|
||||
__azurite_db*__.json
|
||||
|
||||
# credentials
|
||||
credentials
|
||||
.env.local
|
||||
6429
dictation_function/package-lock.json
generated
6429
dictation_function/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,15 +9,41 @@
|
||||
"clean": "rimraf dist",
|
||||
"prestart": "npm run clean && npm run build",
|
||||
"start": "func start",
|
||||
"test": "echo \"No tests yet...\""
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/functions": "^4.0.0"
|
||||
"@azure/functions": "^4.0.0",
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"mysql2": "^2.3.3",
|
||||
"typeorm": "^0.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"azure-functions-core-tools": "^4.x",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/node": "18.x",
|
||||
"typescript": "^4.0.0",
|
||||
"rimraf": "^5.0.0"
|
||||
"azure-functions-core-tools": "^4.x",
|
||||
"jest": "^28.0.3",
|
||||
"rimraf": "^5.0.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^28.0.1",
|
||||
"typescript": "^4.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import { bigintTransformer } from '.';
|
||||
|
||||
describe('bigintTransformer', () => {
|
||||
describe('to', () => {
|
||||
it('number型を整数を表す文字列に変換できる', () => {
|
||||
expect(bigintTransformer.to(0)).toBe('0');
|
||||
expect(bigintTransformer.to(1)).toBe('1');
|
||||
expect(bigintTransformer.to(1234567890)).toBe('1234567890');
|
||||
expect(bigintTransformer.to(9007199254740991)).toBe('9007199254740991');
|
||||
expect(bigintTransformer.to(-1)).toBe('-1');
|
||||
});
|
||||
it('少数点以下がある場合はエラーとなる', () => {
|
||||
expect(() => bigintTransformer.to(1.1)).toThrowError(
|
||||
'1.1 is not integer.',
|
||||
);
|
||||
});
|
||||
it('Number.MAX_SAFE_INTEGERを超える値を変換しようとするとエラーになる', () => {
|
||||
expect(() => bigintTransformer.to(9007199254740992)).toThrowError(
|
||||
'value is greater than 9007199254740991.',
|
||||
);
|
||||
expect(() => bigintTransformer.to(9223372036854775807)).toThrowError(
|
||||
'value is greater than 9007199254740991.',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('from', () => {
|
||||
it('bigint型の文字列をnumber型に変換できる', () => {
|
||||
expect(bigintTransformer.from('0')).toBe(0);
|
||||
expect(bigintTransformer.from('1')).toBe(1);
|
||||
expect(bigintTransformer.from('1234567890')).toBe(1234567890);
|
||||
expect(bigintTransformer.from('-1')).toBe(-1);
|
||||
});
|
||||
it('Number.MAX_SAFE_INTEGERを超える値を変換しようとするとエラーになる', () => {
|
||||
expect(() => bigintTransformer.from('9007199254740992')).toThrowError(
|
||||
'9007199254740992 is greater than 9007199254740991.',
|
||||
);
|
||||
expect(() => bigintTransformer.from('9223372036854775807')).toThrowError(
|
||||
'9223372036854775807 is greater than 9007199254740991.',
|
||||
);
|
||||
});
|
||||
it('number型の場合はそのまま返す', () => {
|
||||
expect(bigintTransformer.from(0)).toBe(0);
|
||||
expect(bigintTransformer.from(1)).toBe(1);
|
||||
expect(bigintTransformer.from(1234567890)).toBe(1234567890);
|
||||
expect(bigintTransformer.from(-1)).toBe(-1);
|
||||
});
|
||||
it('nullの場合はそのまま返す', () => {
|
||||
expect(bigintTransformer.from(null)).toBe(null);
|
||||
});
|
||||
it('number型に変換できない場合はエラーとなる', () => {
|
||||
expect(() => bigintTransformer.from('a')).toThrowError('a is not int.');
|
||||
expect(() => bigintTransformer.from('')).toThrowError(' is not int.');
|
||||
expect(() => bigintTransformer.from(undefined)).toThrowError(
|
||||
'undefined is not string.',
|
||||
);
|
||||
expect(() => bigintTransformer.from({})).toThrowError(
|
||||
'[object Object] is not string.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
57
dictation_function/src/common/entity/index.ts
Normal file
57
dictation_function/src/common/entity/index.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { ValueTransformer } from 'typeorm';
|
||||
|
||||
// DBのbigint型をnumber型に変換するためのtransformer
|
||||
// DBのBigInt型をそのまま扱うと、JSのNumber型の最大値を超えると誤差が発生するため、本来はNumber型に変換すべきではないが、
|
||||
// 影響範囲を最小限に抑えるため、Number型に変換する。使用するのはAutoIncrementされるIDのみの想定のため、
|
||||
// Number.MAX_SAFE_INTEGERより大きい値は現実的には発生しない想定で変換する。
|
||||
export const bigintTransformer: ValueTransformer = {
|
||||
from: (value: any): number | null => {
|
||||
// valueがnullであればそのまま返す
|
||||
if (value === null) {
|
||||
return value;
|
||||
}
|
||||
// valueがnumber型かどうかを判定
|
||||
// 利用DBによってはbigint型であってもnumber型で返ってくる場合があるため、number型の場合はそのまま返す(sqliteの場合)
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
// valueが文字列かどうかを判定
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`${value} is not string.`);
|
||||
}
|
||||
// 数値に変換可能な文字列かどうかを判定
|
||||
if (Number.isNaN(parseInt(value))) {
|
||||
throw new Error(`${value} is not int.`);
|
||||
}
|
||||
|
||||
// 文字列ならbigintに変換
|
||||
// valueが整数でない場合は値が丸められてしまうが、TypeORMのEntityの定義上、整数を表す文字列以外はありえないため、少数点は考慮しない
|
||||
const bigIntValue = BigInt(value);
|
||||
// bigIntValueがNumber.MAX_SAFE_INTEGERより大きいかどうかを判定
|
||||
if (bigIntValue > Number.MAX_SAFE_INTEGER) {
|
||||
throw new Error(`${value} is greater than ${Number.MAX_SAFE_INTEGER}.`);
|
||||
}
|
||||
// number型で表現できる整数であればnumber型に変換して返す
|
||||
return Number(bigIntValue);
|
||||
},
|
||||
to: (value: any): string | null | undefined => {
|
||||
// valueがnullまたはundefinedであればそのまま返す
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
// valueがnumber型かどうかを判定
|
||||
if (typeof value !== 'number') {
|
||||
throw new Error(`${value} is not number.`);
|
||||
}
|
||||
|
||||
// valueがNumber.MAX_SAFE_INTEGERより大きいかどうかを判定
|
||||
if (value > Number.MAX_SAFE_INTEGER) {
|
||||
throw new Error(`value is greater than ${Number.MAX_SAFE_INTEGER}.`);
|
||||
}
|
||||
// valueが整数かどうかを判定
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error(`${value} is not integer.`);
|
||||
}
|
||||
return value.toString();
|
||||
},
|
||||
};
|
||||
71
dictation_function/src/common/test/utility.ts
Normal file
71
dictation_function/src/common/test/utility.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { DataSource } from "typeorm";
|
||||
import { User } from "../../entity/user.entity";
|
||||
import { Account } from "../../entity/account.entity";
|
||||
import { ADMIN_ROLES, USER_ROLES } from "../../constants";
|
||||
|
||||
type InitialTestDBState = {
|
||||
tier1Accounts: { account: Account; users: User[] }[];
|
||||
tier2Accounts: { account: Account; users: User[] }[];
|
||||
tier3Accounts: { account: Account; users: User[] }[];
|
||||
tier4Accounts: { account: Account; users: User[] }[];
|
||||
tier5Accounts: { account: Account; users: User[] }[];
|
||||
};
|
||||
|
||||
// 上書きされたら困る項目を除外したAccount型
|
||||
type OverrideAccount = Omit<
|
||||
Account,
|
||||
"id" | "primary_admin_user_id" | "secondary_admin_user_id" | "user"
|
||||
>;
|
||||
|
||||
// 上書きされたら困る項目を除外したUser型
|
||||
type OverrideUser = Omit<
|
||||
User,
|
||||
"id" | "account" | "license" | "userGroupMembers"
|
||||
>;
|
||||
|
||||
type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] };
|
||||
type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] };
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する
|
||||
* @param dataSource データソース
|
||||
* @param defaultUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
|
||||
* @returns 作成したユーザー
|
||||
*/
|
||||
export const makeTestUser = async (
|
||||
datasource: DataSource,
|
||||
defaultUserValue?: UserDefault
|
||||
): Promise<User> => {
|
||||
const d = defaultUserValue;
|
||||
const { identifiers } = await datasource.getRepository(User).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,
|
||||
license_alert: d?.license_alert ?? 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(),
|
||||
updated_by: d?.updated_by ?? "updater",
|
||||
updated_at: d?.updated_at ?? new Date(),
|
||||
});
|
||||
const result = identifiers.pop() as User;
|
||||
|
||||
const user = await datasource.getRepository(User).findOne({
|
||||
where: {
|
||||
id: result.id,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error("Unexpected null");
|
||||
}
|
||||
return user;
|
||||
};
|
||||
263
dictation_function/src/constants/index.ts
Normal file
263
dictation_function/src/constants/index.ts
Normal file
@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 階層
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIERS = {
|
||||
//OMDS東京
|
||||
TIER1: 1,
|
||||
//OMDS現地法人
|
||||
TIER2: 2,
|
||||
//代理店
|
||||
TIER3: 3,
|
||||
//販売店
|
||||
TIER4: 4,
|
||||
//エンドユーザー
|
||||
TIER5: 5,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 音声ファイルをEast USに保存する国リスト
|
||||
* @const {number}
|
||||
*/
|
||||
export const BLOB_STORAGE_REGION_US = ['CA', 'KY', 'US'];
|
||||
|
||||
/**
|
||||
* 音声ファイルをAustralia Eastに保存する国リスト
|
||||
* @const {number}
|
||||
*/
|
||||
export const BLOB_STORAGE_REGION_AU = ['AU', 'NZ'];
|
||||
|
||||
/**
|
||||
* 音声ファイルをNorth Europeに保存する国リスト
|
||||
* @const {number}
|
||||
*/
|
||||
export const BLOB_STORAGE_REGION_EU = [
|
||||
'AT',
|
||||
'BE',
|
||||
'BG',
|
||||
'HR',
|
||||
'CY',
|
||||
'CZ',
|
||||
'DK',
|
||||
'EE',
|
||||
'FI',
|
||||
'FR',
|
||||
'DE',
|
||||
'GR',
|
||||
'HU',
|
||||
'IS',
|
||||
'IE',
|
||||
'IT',
|
||||
'LV',
|
||||
'LI',
|
||||
'LT',
|
||||
'LU',
|
||||
'MT',
|
||||
'NL',
|
||||
'NO',
|
||||
'PL',
|
||||
'PT',
|
||||
'RO',
|
||||
'RS',
|
||||
'SK',
|
||||
'SI',
|
||||
'ZA',
|
||||
'ES',
|
||||
'SE',
|
||||
'CH',
|
||||
'TR',
|
||||
'GB',
|
||||
];
|
||||
|
||||
/**
|
||||
* 管理ロール
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const ADMIN_ROLES = {
|
||||
ADMIN: 'admin',
|
||||
STANDARD: 'standard',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ロール
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const USER_ROLES = {
|
||||
NONE: 'none',
|
||||
AUTHOR: 'author',
|
||||
TYPIST: 'typist',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ライセンス注文状態
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const LICENSE_ISSUE_STATUS = {
|
||||
ISSUE_REQUESTING: 'Issue Requesting',
|
||||
ISSUED: 'Issued',
|
||||
CANCELED: 'Order Canceled',
|
||||
};
|
||||
|
||||
/**
|
||||
* ライセンス種別
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const LICENSE_TYPE = {
|
||||
TRIAL: 'TRIAL',
|
||||
NORMAL: 'NORMAL',
|
||||
CARD: 'CARD',
|
||||
} as const;
|
||||
/**
|
||||
* ライセンス状態
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const LICENSE_ALLOCATED_STATUS = {
|
||||
UNALLOCATED: 'Unallocated',
|
||||
ALLOCATED: 'Allocated',
|
||||
REUSABLE: 'Reusable',
|
||||
DELETED: 'Deleted',
|
||||
} as const;
|
||||
/**
|
||||
* 切り替え元種別
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const SWITCH_FROM_TYPE = {
|
||||
NONE: 'NONE',
|
||||
CARD: 'CARD',
|
||||
TRIAL: 'TRIAL',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ライセンスの期限切れが近いと見なす日数のしきい値
|
||||
* @const {number}
|
||||
*/
|
||||
export const LICENSE_EXPIRATION_THRESHOLD_DAYS = 14;
|
||||
|
||||
/**
|
||||
* ライセンスの有効期間
|
||||
* @const {number}
|
||||
*/
|
||||
export const LICENSE_EXPIRATION_DAYS = 365;
|
||||
|
||||
/**
|
||||
* カードライセンスの桁数
|
||||
* @const {number}
|
||||
*/
|
||||
export const CARD_LICENSE_LENGTH = 20;
|
||||
|
||||
/**
|
||||
* 音声ファイルに紐づくオプションアイテムの数
|
||||
* @const {string}
|
||||
*/
|
||||
export const OPTION_ITEM_NUM = 10;
|
||||
|
||||
/**
|
||||
* 文字起こしタスクのステータス
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const TASK_STATUS = {
|
||||
UPLOADED: 'Uploaded',
|
||||
PENDING: 'Pending',
|
||||
IN_PROGRESS: 'InProgress',
|
||||
FINISHED: 'Finished',
|
||||
BACKUP: 'Backup',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* タスク一覧でソート可能な属性の一覧
|
||||
*/
|
||||
export const TASK_LIST_SORTABLE_ATTRIBUTES = [
|
||||
'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',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* タスク一覧のソート条件(昇順・降順)
|
||||
*/
|
||||
export const SORT_DIRECTIONS = ['ASC', 'DESC'] as const;
|
||||
|
||||
/**
|
||||
* 通知タグの最大個数
|
||||
* NotificationHubの仕様上タグ式のOR条件で使えるタグは20個まで
|
||||
* https://learn.microsoft.com/ja-jp/azure/notification-hubs/notification-hubs-tags-segment-push-message#tag-expressions
|
||||
*/
|
||||
export const TAG_MAX_COUNT = 20;
|
||||
|
||||
/**
|
||||
* 通知のプラットフォーム種別文字列
|
||||
*/
|
||||
export const PNS = {
|
||||
WNS: 'wns',
|
||||
APNS: 'apns',
|
||||
};
|
||||
|
||||
/**
|
||||
* ユーザーのライセンス状態
|
||||
*/
|
||||
export const USER_LICENSE_STATUS = {
|
||||
NORMAL: 'Normal',
|
||||
NO_LICENSE: 'NoLicense',
|
||||
ALERT: 'Alert',
|
||||
RENEW: 'Renew',
|
||||
};
|
||||
|
||||
/**
|
||||
*トライアルライセンスの有効期限(日数)
|
||||
* @const {number}
|
||||
*/
|
||||
export const TRIAL_LICENSE_EXPIRATION_DAYS = 30;
|
||||
|
||||
/**
|
||||
* ライセンスの発行数
|
||||
* @const {number}
|
||||
*/
|
||||
export const TRIAL_LICENSE_ISSUE_NUM = 100;
|
||||
|
||||
/**
|
||||
* worktypeの最大登録数
|
||||
* @const {number}
|
||||
*/
|
||||
export const WORKTYPE_MAX_COUNT = 20;
|
||||
|
||||
/**
|
||||
* worktypeのDefault値の取りうる値
|
||||
**/
|
||||
export const OPTION_ITEM_VALUE_TYPE = {
|
||||
DEFAULT: 'Default',
|
||||
BLANK: 'Blank',
|
||||
LAST_INPUT: 'LastInput',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ADB2Cユーザのidentity.signInType
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const ADB2C_SIGN_IN_TYPE = {
|
||||
EMAILADDRESS: 'emailAddress',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* MANUAL_RECOVERY_REQUIRED
|
||||
* @const {string}
|
||||
*/
|
||||
export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
|
||||
|
||||
/**
|
||||
* 利用規約種別
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const TERM_TYPE = {
|
||||
EULA: 'EULA',
|
||||
DPA: 'DPA',
|
||||
} as const;
|
||||
70
dictation_function/src/entity/account.entity.ts
Normal file
70
dictation_function/src/entity/account.entity.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { bigintTransformer } from "../common/entity";
|
||||
import { User } from "./user.entity";
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity({ name: "accounts" })
|
||||
export class Account {
|
||||
@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()
|
||||
company_name: string;
|
||||
|
||||
@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;
|
||||
|
||||
@OneToMany(() => User, (user) => user.id)
|
||||
user: User[] | null;
|
||||
}
|
||||
73
dictation_function/src/entity/user.entity.ts
Normal file
73
dictation_function/src/entity/user.entity.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity({ name: "users" })
|
||||
export class User {
|
||||
@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 })
|
||||
license_alert: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
notification: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
encryption: boolean;
|
||||
|
||||
@Column({ nullable: true, type: "varchar" })
|
||||
encryption_password: string | null;
|
||||
|
||||
@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;
|
||||
}
|
||||
@ -1,4 +1,8 @@
|
||||
import { app, InvocationContext, Timer } from "@azure/functions";
|
||||
import { DataSource } from "typeorm";
|
||||
import { User } from "../entity/user.entity";
|
||||
import { SendGridService } from "../sendgrid/sendgrid.service";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
// タイマートリガー処理のサンプルです
|
||||
// TODO:開発が進んだら削除すること
|
||||
@ -7,6 +11,57 @@ export async function timerTriggerExample(
|
||||
context: InvocationContext
|
||||
): Promise<void> {
|
||||
context.log("Timer function processed request.");
|
||||
|
||||
dotenv.config({ path: ".env" });
|
||||
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,
|
||||
entities: [User],
|
||||
});
|
||||
|
||||
try {
|
||||
await datasource.initialize();
|
||||
const userRepository = datasource.getRepository(User); // Userエンティティに対応するリポジトリを取得
|
||||
|
||||
// ユーザーを検索
|
||||
const users = await userRepository.find();
|
||||
console.log(users);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
await datasource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// test実行確認用サンプル
|
||||
// TODO:開発が進んだら削除すること
|
||||
export async function testExample(datasource: DataSource): Promise<User[]> {
|
||||
let users: User[];
|
||||
const userRepository = datasource.getRepository(User); // Userエンティティに対応するリポジトリを取得
|
||||
|
||||
// ユーザーを検索
|
||||
users = await userRepository.find();
|
||||
return users;
|
||||
}
|
||||
|
||||
// test実行確認用サンプル
|
||||
// TODO:開発が進んだら削除すること
|
||||
export async function testSendgridExample(): Promise<string> {
|
||||
const sendgrid = new SendGridService();
|
||||
|
||||
// メールを送信
|
||||
await sendgrid.sendMail(
|
||||
"oura.a89@gmail.com",
|
||||
process.env.MAIL_FROM,
|
||||
"testMail",
|
||||
"test!",
|
||||
"html"
|
||||
);
|
||||
return "sucsess";
|
||||
}
|
||||
|
||||
app.timer("timerTriggerExample", {
|
||||
|
||||
60
dictation_function/src/sendgrid/sendgrid.service.ts
Normal file
60
dictation_function/src/sendgrid/sendgrid.service.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import sendgrid from "@sendgrid/mail";
|
||||
|
||||
export class SendGridService {
|
||||
constructor() {
|
||||
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
|
||||
}
|
||||
/**
|
||||
* メールコンテンツを作成する
|
||||
* @param accountId 認証対象のユーザーが所属するアカウントのID
|
||||
* @param userId 認証対象のユーザーのID
|
||||
* @param email 認証対象のユーザーのメールアドレス
|
||||
* @returns メールのサブジェクトとコンテンツ
|
||||
*/
|
||||
async createMailContent(
|
||||
accountId: number,
|
||||
userId: number,
|
||||
email: string
|
||||
): Promise<{ subject: string; text: string; html: string }> {
|
||||
return {
|
||||
subject: "Verify your new account",
|
||||
text: `The verification URL.`,
|
||||
html: `<p>The verification URL.<p>`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* メールを送信する
|
||||
* @param to
|
||||
* @param from
|
||||
* @param subject
|
||||
* @param text
|
||||
* @param html
|
||||
* @returns mail
|
||||
*/
|
||||
async sendMail(
|
||||
to: string,
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const res = await sendgrid
|
||||
.send({
|
||||
from: {
|
||||
email: from,
|
||||
},
|
||||
to: {
|
||||
email: to,
|
||||
},
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
})
|
||||
.then((v) => v[0]);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
dictation_function/src/test/timerTriggerExample.spec.ts
Normal file
42
dictation_function/src/test/timerTriggerExample.spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import {
|
||||
testExample,
|
||||
testSendgridExample,
|
||||
} from "../functions/timerTriggerExample";
|
||||
import { makeTestUser } from "../common/test/utility";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
describe("timerTriggerExample", () => {
|
||||
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("sample test(DB)", async () => {
|
||||
const count = 5;
|
||||
for (let i = 0; i < count; i++) {
|
||||
await makeTestUser(source);
|
||||
}
|
||||
|
||||
const result = await testExample(source);
|
||||
expect(result.length).toEqual(count);
|
||||
});
|
||||
|
||||
it("sample test(sendgrid)", async () => {
|
||||
await testSendgridExample();
|
||||
});
|
||||
});
|
||||
@ -5,6 +5,9 @@
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"strict": false
|
||||
"strict": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export const ADB2C_PREFIX = "adb2c-external-id:"
|
||||
export const ADB2C_PREFIX = 'adb2c-external-id:';
|
||||
|
||||
8
dictation_server/src/common/cache/index.ts
vendored
8
dictation_server/src/common/cache/index.ts
vendored
@ -6,8 +6,8 @@ import { ADB2C_PREFIX } from './constants';
|
||||
* @returns キャッシュのキー
|
||||
*/
|
||||
export const makeADB2CKey = (externalId: string): string => {
|
||||
return `${ADB2C_PREFIX}${externalId}`;
|
||||
}
|
||||
return `${ADB2C_PREFIX}${externalId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する
|
||||
@ -15,5 +15,5 @@ export const makeADB2CKey = (externalId: string): string => {
|
||||
* @returns 外部ユーザーID
|
||||
*/
|
||||
export const restoreAdB2cID = (key: string): string => {
|
||||
return key.replace(ADB2C_PREFIX, '');
|
||||
}
|
||||
return key.replace(ADB2C_PREFIX, '');
|
||||
};
|
||||
|
||||
@ -120,3 +120,26 @@ describe('RoleGuard(Tier)', () => {
|
||||
expect(guards.checkTier(TIERS.TIER4)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RoleGuard(代行操作)', () => {
|
||||
it('代行操作許可設定時、delegateUserIdに値が含まれる場合、許可される', () => {
|
||||
const guards = RoleGuard.requireds({
|
||||
delegation: true,
|
||||
});
|
||||
expect(guards.checkDelegate('delegateUser')).toBeTruthy();
|
||||
});
|
||||
it('代行操作許可設定時、delegateUserIdに値が含まれない場合、許可される', () => {
|
||||
const guards = RoleGuard.requireds({
|
||||
delegation: true,
|
||||
});
|
||||
expect(guards.checkDelegate(undefined)).toBeTruthy();
|
||||
});
|
||||
it('代行操作許可未設定時、delegateUserIdに値が含まれる場合、拒否される', () => {
|
||||
const guards = RoleGuard.requireds({});
|
||||
expect(guards.checkDelegate('delegateUser')).toBeFalsy();
|
||||
});
|
||||
it('代行操作許可未設定時、delegateUserIdに値が含まれない場合、許可される', () => {
|
||||
const guards = RoleGuard.requireds({});
|
||||
expect(guards.checkDelegate(undefined)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ import { ADMIN_ROLES, USER_ROLES, TIERS } from '../../../constants';
|
||||
export interface RoleSetting {
|
||||
roles?: (Roles | Roles[])[];
|
||||
tiers?: Tiers[];
|
||||
delegation?: boolean;
|
||||
}
|
||||
|
||||
export class RoleGuard implements CanActivate {
|
||||
@ -64,6 +65,17 @@ export class RoleGuard implements CanActivate {
|
||||
);
|
||||
}
|
||||
|
||||
// アクセストークンに代行操作ユーザーの情報が含まれているか(含まれている場合は代行操作)
|
||||
const isDelegationUser = payload.delegateUserId !== undefined;
|
||||
|
||||
// 代行操作ユーザーの場合、代行操作許可設定がない場合は例外を送出
|
||||
if (isDelegationUser && !this.settings.delegation) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000108'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
// 権限チェック
|
||||
// settingsで設定されていない場合チェック自体行わないので初期値はtrueとする
|
||||
let hasRolePermission = true;
|
||||
@ -74,7 +86,9 @@ export class RoleGuard implements CanActivate {
|
||||
if (this.settings.tiers) {
|
||||
hasTierPermission = this.checkTier(payload.tier);
|
||||
}
|
||||
if (hasRolePermission && hasTierPermission) {
|
||||
const hasDelegatePermission = this.checkDelegate(payload.delegateUserId);
|
||||
|
||||
if (hasRolePermission && hasTierPermission && hasDelegatePermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -182,4 +196,24 @@ export class RoleGuard implements CanActivate {
|
||||
// 宣言された階層中にパラメータの内容が含まれていればtrue
|
||||
return settings.tiers.includes(tier as (typeof TIERS)[keyof typeof TIERS]);
|
||||
}
|
||||
|
||||
/**
|
||||
* ※ テストコード以外からの直接呼び出しは禁止。テスト容易性のため、publicメソッドとして切り出したもの。
|
||||
* 階層の判別を行う
|
||||
* @param delegateUserId アクセストークンに含まれるdelegateUserIdの値
|
||||
* @returns true/false
|
||||
*/
|
||||
checkDelegate(delegateUserId?: string | undefined): boolean {
|
||||
const settings = this.settings;
|
||||
|
||||
// 通常ユーザの場合は許可
|
||||
if (delegateUserId === undefined) {
|
||||
return true;
|
||||
} else if (settings?.delegation ?? false) {
|
||||
// 代行操作ユーザーの場合は許可設定がある場合のみ許可;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,11 +7,5 @@ import type {
|
||||
} from './types';
|
||||
import { isIDToken } from './typeguard';
|
||||
|
||||
export type {
|
||||
AccessToken,
|
||||
B2cMetadata,
|
||||
IDToken,
|
||||
JwkSignKey,
|
||||
RefreshToken,
|
||||
};
|
||||
export type { AccessToken, B2cMetadata, IDToken, JwkSignKey, RefreshToken };
|
||||
export { isIDToken };
|
||||
|
||||
@ -9,6 +9,10 @@ import { Assignee } from '../../features/tasks/types/types';
|
||||
@ValidatorConstraint()
|
||||
export class IsTypist implements ValidatorConstraintInterface {
|
||||
validate(values: Assignee[]): boolean {
|
||||
if (!values) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return values.every((value) => {
|
||||
const { typistUserId, typistGroupId, typistName } = value;
|
||||
if (typistUserId === undefined && typistGroupId === undefined) {
|
||||
|
||||
@ -159,7 +159,9 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('licenses/summary')
|
||||
async getLicenseSummary(
|
||||
@Req() req: Request,
|
||||
@ -197,7 +199,9 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get('me')
|
||||
async getMyAccount(@Req() req: Request): Promise<GetMyAccountResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
@ -246,7 +250,9 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get('authors')
|
||||
async getAuthors(@Req() req: Request): Promise<GetAuthorsResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
@ -293,6 +299,7 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ delegation: true }))
|
||||
@Get('typists')
|
||||
async getTypists(@Req() req: Request): Promise<GetTypistsResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
@ -338,6 +345,7 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ delegation: true }))
|
||||
@Get('typist-groups')
|
||||
async getTypistGroups(@Req() req: Request): Promise<GetTypistGroupsResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
@ -388,7 +396,9 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get('typist-groups/:typistGroupId')
|
||||
async getTypistGroup(
|
||||
@Req() req: Request,
|
||||
@ -452,7 +462,9 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('typist-groups')
|
||||
async createTypistGroup(
|
||||
@Req() req: Request,
|
||||
@ -513,7 +525,9 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('typist-groups/:typistGroupId')
|
||||
async updateTypistGroup(
|
||||
@Req() req: Request,
|
||||
@ -680,6 +694,7 @@ export class AccountsController {
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
async getOrderHistories(
|
||||
@ -858,7 +873,9 @@ export class AccountsController {
|
||||
@ApiOperation({ operationId: 'getWorktypes' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async getWorktypes(@Req() req: Request): Promise<GetWorktypesResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
@ -906,7 +923,9 @@ export class AccountsController {
|
||||
@ApiOperation({ operationId: 'createWorktype' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async createWorktype(
|
||||
@Req() req: Request,
|
||||
@Body() body: CreateWorktypesRequest,
|
||||
@ -964,7 +983,9 @@ export class AccountsController {
|
||||
@ApiOperation({ operationId: 'updateWorktype' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async updateWorktype(
|
||||
@Req() req: Request,
|
||||
@Param() param: UpdateWorktypeRequestParam,
|
||||
@ -1026,7 +1047,9 @@ export class AccountsController {
|
||||
@ApiOperation({ operationId: 'deleteWorktype' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async deleteWorktype(
|
||||
@Req() req: Request,
|
||||
@Param() param: DeleteWorktypeRequestParam,
|
||||
@ -1079,7 +1102,9 @@ export class AccountsController {
|
||||
@ApiOperation({ operationId: 'getOptionItems' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async getOptionItems(
|
||||
@Req() req: Request,
|
||||
@Param() param: GetOptionItemsRequestParam,
|
||||
@ -1137,7 +1162,9 @@ export class AccountsController {
|
||||
@ApiOperation({ operationId: 'updateOptionItems' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async updateOptionItems(
|
||||
@Req() req: Request,
|
||||
@Param() param: UpdateOptionItemsRequestParam,
|
||||
@ -1198,7 +1225,9 @@ export class AccountsController {
|
||||
@ApiOperation({ operationId: 'activeWorktype' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async activeWorktype(
|
||||
@Req() req: Request,
|
||||
@Body() body: PostActiveWorktypeRequest,
|
||||
|
||||
@ -150,8 +150,9 @@ export const createWorktype = async (
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return worktype;
|
||||
return (await datasource
|
||||
.getRepository(Worktype)
|
||||
.findOne({ where: { id: worktype.id } })) as Worktype;
|
||||
};
|
||||
|
||||
// Worktypeを取得する
|
||||
|
||||
@ -337,7 +337,9 @@ export class FilesController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async uploadTemplateLocation(
|
||||
@Req() req: Request,
|
||||
): Promise<TemplateUploadLocationResponse> {
|
||||
@ -393,7 +395,9 @@ export class FilesController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('template/upload-finished')
|
||||
async templateUploadFinished(
|
||||
@Req() req: Request,
|
||||
|
||||
@ -7,6 +7,8 @@ import { AudioOptionItemsRepositoryModule } from '../../repositories/audio_optio
|
||||
import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository.module';
|
||||
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
|
||||
import { TemplateFilesRepositoryModule } from '../../repositories/template_files/template_files.repository.module';
|
||||
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
|
||||
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -16,6 +18,8 @@ import { TemplateFilesRepositoryModule } from '../../repositories/template_files
|
||||
TasksRepositoryModule,
|
||||
BlobstorageModule,
|
||||
TemplateFilesRepositoryModule,
|
||||
UserGroupsRepositoryModule,
|
||||
NotificationhubModule,
|
||||
],
|
||||
providers: [FilesService],
|
||||
controllers: [FilesController],
|
||||
|
||||
@ -7,7 +7,12 @@ import {
|
||||
makeFilesServiceMock,
|
||||
} from './test/files.service.mock';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { createTask, makeTestingModuleWithBlob } from './test/utility';
|
||||
import {
|
||||
createTask,
|
||||
createUserGroupAndMember,
|
||||
getTaskFromJobNumber,
|
||||
makeTestingModuleWithBlobAndNotification,
|
||||
} from './test/utility';
|
||||
import { FilesService } from './files.service';
|
||||
import { makeContext } from '../../common/log';
|
||||
import {
|
||||
@ -22,6 +27,16 @@ import {
|
||||
getTemplateFiles,
|
||||
} from '../templates/test/utility';
|
||||
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
|
||||
import { makeDefaultNotificationhubServiceMockValue } from '../tasks/test/tasks.service.mock';
|
||||
import {
|
||||
createWorkflow,
|
||||
createWorkflowTypist,
|
||||
} from '../workflows/test/utility';
|
||||
import { createWorktype } from '../accounts/test/utility';
|
||||
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
||||
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
|
||||
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
|
||||
import { getCheckoutPermissions, getTask } from '../tasks/test/utility';
|
||||
|
||||
describe('音声ファイルアップロードURL取得', () => {
|
||||
it('アップロードSASトークンが乗っているURLを返却する', async () => {
|
||||
@ -109,55 +124,535 @@ describe('音声ファイルアップロードURL取得', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('タスク作成', () => {
|
||||
it('文字起こしタスクを作成できる', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
);
|
||||
|
||||
expect(
|
||||
await service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
'userId',
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
'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',
|
||||
'workTypeID',
|
||||
optionItemList,
|
||||
false,
|
||||
),
|
||||
).toEqual({ jobNumber: '00000001' });
|
||||
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();
|
||||
});
|
||||
|
||||
it('日付フォーマットが不正な場合、エラーを返却する', async () => {
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
it('タスク作成時に、自動ルーティングを行うことができる(APIの引数として渡されたAuthorIDとworkType)', 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();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
const result = await service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
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).toEqual({ jobNumber: '00000001' });
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${typistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
// 作成したタスクを取得
|
||||
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('別のタスクが既に存在する場合、タスク作成時に、自動ルーティングを行うことができる(APIの引数として渡されたAuthorIDとworkType)', 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,
|
||||
});
|
||||
//タスクを作成
|
||||
await createTask(
|
||||
source,
|
||||
accountId,
|
||||
'http://blob/url/file.zip',
|
||||
'file.zip',
|
||||
'01',
|
||||
typistUserId,
|
||||
authorAuthorId ?? '',
|
||||
);
|
||||
// ワークタイプを作成
|
||||
const { id: worktypeId, custom_worktype_id } = await createWorktype(
|
||||
source,
|
||||
accountId,
|
||||
'worktypeId',
|
||||
);
|
||||
// ワークフローを作成
|
||||
const { id: workflowId } = await createWorkflow(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
worktypeId,
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(source, workflowId, typistUserId);
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
const result = await service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
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).toEqual({ jobNumber: '00000002' });
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${typistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
// 作成したタスクを取得
|
||||
const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
|
||||
// タスクのチェックアウト権限を取得
|
||||
const resultCheckoutPermission = await getCheckoutPermissions(
|
||||
source,
|
||||
resultTask?.id ?? 0,
|
||||
);
|
||||
// タスクのテンプレートファイルIDを確認
|
||||
expect(resultTask?.template_file_id).toBeNull();
|
||||
// タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認
|
||||
expect(resultCheckoutPermission.length).toEqual(1);
|
||||
expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId);
|
||||
});
|
||||
|
||||
it('タスク作成時に、自動ルーティングを行うことができる(API実行者のAuthorIDとworkType)', async () => {
|
||||
if (!source) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// 音声ファイルの録音者のユーザー
|
||||
const { 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,
|
||||
});
|
||||
// API実行者のユーザー
|
||||
const { external_id: myExternalId, id: myUserId } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'my-author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'MY_AUTHOR_ID',
|
||||
},
|
||||
);
|
||||
|
||||
// ワークタイプを作成
|
||||
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,
|
||||
myUserId, // API実行者のユーザーIDを設定
|
||||
worktypeId,
|
||||
templateFileId,
|
||||
);
|
||||
// ユーザーグループを作成
|
||||
const { userGroupId } = await createUserGroupAndMember(
|
||||
source,
|
||||
accountId,
|
||||
'userGroupName',
|
||||
typistUserId, // ルーティング先のタイピストのユーザーIDを設定
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(
|
||||
source,
|
||||
workflowId,
|
||||
undefined,
|
||||
userGroupId, // ルーティング先のユーザーグループIDを設定
|
||||
);
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
const result = await service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
myExternalId, // 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',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
custom_worktype_id,
|
||||
optionItemList,
|
||||
false,
|
||||
);
|
||||
expect(result).toEqual({ jobNumber: '00000001' });
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${typistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
// 作成したタスクを取得
|
||||
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_group_id).toEqual(userGroupId);
|
||||
});
|
||||
it('タスク作成時に、音声ファイルメタ情報のAuthorIDに存在しないものが入っていても自動ルーティングを行うことができる(API実行者のAuthorIDとworkType)', async () => {
|
||||
if (!source) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// 音声ファイルの録音者のユーザー
|
||||
const { 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,
|
||||
});
|
||||
// API実行者のユーザー
|
||||
const { external_id: myExternalId, id: myUserId } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'my-author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'MY_AUTHOR_ID',
|
||||
},
|
||||
);
|
||||
|
||||
// ワークタイプを作成
|
||||
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,
|
||||
myUserId, // API実行者のユーザーIDを設定
|
||||
worktypeId,
|
||||
templateFileId,
|
||||
);
|
||||
// ユーザーグループを作成
|
||||
const { userGroupId } = await createUserGroupAndMember(
|
||||
source,
|
||||
accountId,
|
||||
'userGroupName',
|
||||
typistUserId, // ルーティング先のタイピストのユーザーIDを設定
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(
|
||||
source,
|
||||
workflowId,
|
||||
undefined,
|
||||
userGroupId, // ルーティング先のユーザーグループIDを設定
|
||||
);
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
const result = await service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
myExternalId, // API実行者のユーザーIDを設定
|
||||
'http://blob/url/file.zip',
|
||||
'XXXXXXXXXX', // 音声ファイルの情報には、録音者のAuthorIDが入る
|
||||
'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).toEqual({ jobNumber: '00000001' });
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${typistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
// 作成したタスクを取得
|
||||
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_group_id).toEqual(userGroupId);
|
||||
}, 1000000);
|
||||
|
||||
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 blobParam = makeBlobstorageServiceMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
const result = await service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
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',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'worktypeId',
|
||||
optionItemList,
|
||||
false,
|
||||
);
|
||||
expect(result).toEqual({ jobNumber: '00000001' });
|
||||
// タスクを取得
|
||||
const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
|
||||
// タスクのチェックアウト権限を取得
|
||||
const resultCheckoutPermission = await getCheckoutPermissions(
|
||||
source,
|
||||
resultTask?.id ?? 0,
|
||||
);
|
||||
// タスクがあることを確認
|
||||
expect(resultTask).not.toBeNull();
|
||||
// 自動ルーティングが行われていないことを確認
|
||||
expect(resultCheckoutPermission.length).toEqual(0);
|
||||
});
|
||||
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 blobParam = makeBlobstorageServiceMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
'userId',
|
||||
authorExternalId,
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
authorAuthorId ?? '',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'yyyy-05-26T11:22:33.444',
|
||||
@ -175,23 +670,36 @@ describe('タスク作成', () => {
|
||||
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
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 blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
userRepoParam,
|
||||
taskRepoParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
'userId',
|
||||
authorExternalId,
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
authorAuthorId ?? '',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
@ -214,25 +722,25 @@ describe('タスク作成', () => {
|
||||
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
it('タスク追加でユーザー情報の取得に失敗した場合、エラーを返却する', async () => {
|
||||
if (!source) fail();
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const service = await makeFilesServiceMock(
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
{
|
||||
findUserByExternalId: new Error(''),
|
||||
},
|
||||
taskRepoParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
'userId',
|
||||
'authorExternalId',
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
'authorAuthorId',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
@ -253,21 +761,37 @@ describe('タスク作成', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('タスクのDBへの追加に失敗した場合、エラーを返却する', async () => {
|
||||
if (!source) fail();
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||
const service = await makeFilesServiceMock(blobParam, userRepoParam, {
|
||||
create: new Error(''),
|
||||
getTasksFromAccountId: new Error(),
|
||||
});
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
const taskRepoService = module.get<TasksRepositoryService>(
|
||||
TasksRepositoryService,
|
||||
);
|
||||
taskRepoService.create = jest.fn().mockRejectedValue(new Error(''));
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
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',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.uploadFinished(
|
||||
makeContext('trackingId'),
|
||||
'userId',
|
||||
authorExternalId,
|
||||
'http://blob/url/file.zip',
|
||||
'AUTHOR_01',
|
||||
authorAuthorId ?? '',
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
@ -338,7 +862,12 @@ describe('音声ファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -381,7 +910,12 @@ describe('音声ファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -430,7 +964,12 @@ describe('音声ファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -470,7 +1009,12 @@ describe('音声ファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -497,7 +1041,12 @@ describe('音声ファイルダウンロードURL取得', () => {
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -541,7 +1090,12 @@ describe('音声ファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = false;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -604,7 +1158,12 @@ describe('テンプレートファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -641,7 +1200,12 @@ describe('テンプレートファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -686,7 +1250,12 @@ describe('テンプレートファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -726,7 +1295,12 @@ describe('テンプレートファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = true;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -753,7 +1327,12 @@ describe('テンプレートファイルダウンロードURL取得', () => {
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
@ -796,7 +1375,12 @@ describe('テンプレートファイルダウンロードURL取得', () => {
|
||||
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||
blobParam.fileExists = false;
|
||||
|
||||
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
|
||||
|
||||
@ -24,6 +24,10 @@ import {
|
||||
import { Context } from '../../common/log';
|
||||
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
|
||||
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
|
||||
import { Task } from '../../repositories/tasks/entity/task.entity';
|
||||
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
|
||||
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
|
||||
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
|
||||
|
||||
@Injectable()
|
||||
export class FilesService {
|
||||
@ -34,6 +38,8 @@ export class FilesService {
|
||||
private readonly tasksRepositoryService: TasksRepositoryService,
|
||||
private readonly templateFilesRepository: TemplateFilesRepositoryService,
|
||||
private readonly blobStorageService: BlobstorageService,
|
||||
private readonly userGroupsRepositoryService: UserGroupsRepositoryService,
|
||||
private readonly notificationhubService: NotificationhubService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -157,7 +163,7 @@ export class FilesService {
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
let task: Task;
|
||||
try {
|
||||
// URLにSASトークンがついている場合は取り除く
|
||||
const urlObj = new URL(url);
|
||||
@ -167,7 +173,7 @@ export class FilesService {
|
||||
|
||||
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
|
||||
// 追加時に末尾のJOBナンバーにインクリメントする
|
||||
const task = await this.tasksRepositoryService.create(
|
||||
task = await this.tasksRepositoryService.create(
|
||||
user.account_id,
|
||||
user.id,
|
||||
priority,
|
||||
@ -185,13 +191,56 @@ export class FilesService {
|
||||
isEncrypted,
|
||||
optionItemList,
|
||||
);
|
||||
return { jobNumber: task.job_number };
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
try {
|
||||
// ルーティング設定に従い、チェックアウト権限を付与する
|
||||
const { typistGroupIds, typistIds } =
|
||||
await this.tasksRepositoryService.autoRouting(
|
||||
task.audio_file_id,
|
||||
user.account_id,
|
||||
user.author_id ?? undefined,
|
||||
);
|
||||
|
||||
const groupMembers =
|
||||
await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(
|
||||
typistGroupIds,
|
||||
);
|
||||
|
||||
// 重複のない割り当て候補ユーザーID一覧を取得する
|
||||
const distinctUserIds = [
|
||||
...new Set([...typistIds, ...groupMembers.map((x) => x.user_id)]),
|
||||
];
|
||||
|
||||
// 割り当てられたユーザーがいない場合は通知不要
|
||||
if (distinctUserIds.length === 0) {
|
||||
this.logger.log('No user assigned.');
|
||||
return { jobNumber: task.job_number };
|
||||
}
|
||||
|
||||
// タグを生成
|
||||
const tags = distinctUserIds.map((x) => `user_${x}`);
|
||||
this.logger.log(`tags: ${tags}`);
|
||||
|
||||
// タグ対象に通知送信
|
||||
await this.notificationhubService.notify(
|
||||
context,
|
||||
tags,
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
|
||||
// 追加したタスクのJOBナンバーを返却
|
||||
return { jobNumber: task.job_number };
|
||||
} catch (error) {
|
||||
// 処理の本筋はタスク生成のため自動ルーティングに失敗してもエラーにしない
|
||||
this.logger.error(`Automatic routing or notification failed.`);
|
||||
this.logger.error(`error=${error}`);
|
||||
return { jobNumber: task.job_number };
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
|
||||
|
||||
@ -6,6 +6,8 @@ import { FilesService } from '../files.service';
|
||||
import { TasksRepositoryService } from '../../../repositories/tasks/tasks.repository.service';
|
||||
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';
|
||||
|
||||
export type BlobstorageServiceMockValue = {
|
||||
createContainer: void | Error;
|
||||
@ -42,6 +44,10 @@ export const makeFilesServiceMock = async (
|
||||
return makeTasksRepositoryMock(tasksRepositoryMockValue);
|
||||
case TemplateFilesRepositoryService:
|
||||
return {};
|
||||
case NotificationhubService:
|
||||
return {};
|
||||
case UserGroupsRepositoryService:
|
||||
return {};
|
||||
}
|
||||
})
|
||||
.compile();
|
||||
@ -186,6 +192,9 @@ export const makeDefaultTasksRepositoryMockValue =
|
||||
option_items: null,
|
||||
template_file: null,
|
||||
typist_user: null,
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
getTasksFromAccountId: {
|
||||
tasks: [],
|
||||
|
||||
@ -37,6 +37,12 @@ import {
|
||||
makeBlobstorageServiceMock,
|
||||
} from './files.service.mock';
|
||||
import { TemplateFile } from '../../../repositories/template_files/entity/template_file.entity';
|
||||
import {
|
||||
NotificationhubServiceMockValue,
|
||||
makeNotificationhubServiceMock,
|
||||
} from '../../tasks/test/tasks.service.mock';
|
||||
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
|
||||
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
|
||||
|
||||
export const createTask = async (
|
||||
datasource: DataSource,
|
||||
@ -95,9 +101,51 @@ export const createTask = async (
|
||||
return { audioFileId: audioFile.id };
|
||||
};
|
||||
|
||||
export const makeTestingModuleWithBlob = async (
|
||||
export const getTaskFromJobNumber = async (
|
||||
datasource: DataSource,
|
||||
jobNumber: string,
|
||||
): Promise<Task | null> => {
|
||||
const task = await datasource.getRepository(Task).findOne({
|
||||
where: {
|
||||
job_number: jobNumber,
|
||||
},
|
||||
});
|
||||
return task;
|
||||
};
|
||||
|
||||
// ユーザーグループとユーザーグループメンバーを作成する
|
||||
export const createUserGroupAndMember = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
name: string,
|
||||
userId: number,
|
||||
): Promise<{ userGroupId: number }> => {
|
||||
const { identifiers } = await datasource.getRepository(UserGroup).insert({
|
||||
account_id: accountId,
|
||||
name: name,
|
||||
deleted_at: null,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const userGroup = identifiers.pop() as UserGroup;
|
||||
// ユーザーグループメンバーを作成する
|
||||
await datasource.getRepository(UserGroupMember).insert({
|
||||
user_group_id: userGroup.id,
|
||||
user_id: userId,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
return { userGroupId: userGroup.id };
|
||||
};
|
||||
|
||||
export const makeTestingModuleWithBlobAndNotification = async (
|
||||
datasource: DataSource,
|
||||
blobStorageService: BlobstorageServiceMockValue,
|
||||
notificationhubService: NotificationhubServiceMockValue,
|
||||
): Promise<TestingModule | undefined> => {
|
||||
try {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -148,6 +196,8 @@ export const makeTestingModuleWithBlob = async (
|
||||
})
|
||||
.overrideProvider(BlobstorageService)
|
||||
.useValue(makeBlobstorageServiceMock(blobStorageService))
|
||||
.overrideProvider(NotificationhubService)
|
||||
.useValue(makeNotificationhubServiceMock(notificationhubService))
|
||||
.compile();
|
||||
|
||||
return module;
|
||||
|
||||
@ -68,6 +68,7 @@ export class LicensesController {
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER2, TIERS.TIER3, TIERS.TIER4, TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('/orders')
|
||||
@ -175,7 +176,11 @@ export class LicensesController {
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER5] }),
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('/cards/activate')
|
||||
async activateCardLicenses(
|
||||
@ -228,7 +233,11 @@ export class LicensesController {
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER5] }),
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Get('/allocatable')
|
||||
async getAllocatableLicenses(
|
||||
@ -289,6 +298,7 @@ export class LicensesController {
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER2, TIERS.TIER3, TIERS.TIER4, TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('/orders/cancel')
|
||||
|
||||
@ -28,6 +28,15 @@ import {
|
||||
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
|
||||
import { makeTestingModule } from '../../common/test/modules';
|
||||
import { createSortCriteria } from '../users/test/utility';
|
||||
import { createWorktype } from '../accounts/test/utility';
|
||||
import {
|
||||
createWorkflow,
|
||||
createWorkflowTypist,
|
||||
} from '../workflows/test/utility';
|
||||
import { createTemplateFile } from '../templates/test/utility';
|
||||
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
|
||||
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
|
||||
import { Roles } from '../../common/types/role';
|
||||
|
||||
describe('TasksService', () => {
|
||||
it('タスク一覧を取得できる(admin)', async () => {
|
||||
@ -208,6 +217,9 @@ describe('TasksService', () => {
|
||||
template_file_id: null,
|
||||
typist_user: null,
|
||||
template_file: null,
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
updated_at: new Date('2023-01-01T01:01:01.000'),
|
||||
file: {
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
@ -2465,6 +2477,253 @@ describe('cancel', () => {
|
||||
new HttpException(makeErrorResponse('E010603'), HttpStatus.NOT_FOUND),
|
||||
);
|
||||
});
|
||||
|
||||
it('API実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルし、そのタスクの自動ルーティングを行う', async () => {
|
||||
if (!source) fail();
|
||||
const notificationhubServiceMockValue =
|
||||
makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTaskTestingModuleWithNotificaiton(
|
||||
source,
|
||||
notificationhubServiceMockValue,
|
||||
);
|
||||
if (!module) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
const { id: authorUserId, author_id } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
//ワークタイプIDを作成
|
||||
await createWorktype(source, accountId, '01');
|
||||
// テンプレートファイルを作成
|
||||
const { id: templateFileId } = await createTemplateFile(
|
||||
source,
|
||||
accountId,
|
||||
'template-file-name',
|
||||
'https://example.com',
|
||||
);
|
||||
// ワークフローを作成
|
||||
const { id: workflowId } = await createWorkflow(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
undefined,
|
||||
templateFileId,
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(source, workflowId, typistUserId);
|
||||
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
author_id ?? '',
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
await service.cancel(
|
||||
makeContext('trackingId'),
|
||||
1,
|
||||
'typist-user-external-id',
|
||||
['typist', 'standard'],
|
||||
);
|
||||
const resultTask = await getTask(source, taskId);
|
||||
const permisions = await getCheckoutPermissions(source, taskId);
|
||||
|
||||
expect(resultTask?.status).toEqual('Uploaded');
|
||||
expect(resultTask?.typist_user_id).toEqual(null);
|
||||
// タスクのテンプレートファイルIDを確認
|
||||
expect(resultTask?.template_file_id).toEqual(templateFileId);
|
||||
// タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認
|
||||
expect(permisions.length).toEqual(1);
|
||||
expect(permisions[0].user_id).toEqual(typistUserId);
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${typistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
}, 1000000);
|
||||
|
||||
it('API実行者のRoleがAdminの場合、自身が文字起こし実行中のタスクをキャンセルし、そのタスクの自動ルーティングを行う(API実行者のAuthorIDと音声ファイルに紐づくWorkType)', async () => {
|
||||
if (!source) fail();
|
||||
const notificationhubServiceMockValue =
|
||||
makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTaskTestingModuleWithNotificaiton(
|
||||
source,
|
||||
notificationhubServiceMockValue,
|
||||
);
|
||||
if (!module) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// タスクの文字起こし担当者
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
// 自動ルーティングされるタイピストユーザーを作成
|
||||
const { id: autoRoutingTypistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'auto-routing-typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
// API実行者
|
||||
const {
|
||||
id: myAuthorUserId,
|
||||
external_id,
|
||||
role,
|
||||
} = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'my-author-user-external-id',
|
||||
role: 'author admin',
|
||||
author_id: 'MY_AUTHOR_ID',
|
||||
});
|
||||
// 音声ファイルのアップロード者
|
||||
const { id: authorUserId, author_id } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
//ワークタイプIDを作成
|
||||
const { id: workTypeId, custom_worktype_id } = await createWorktype(
|
||||
source,
|
||||
accountId,
|
||||
'01',
|
||||
);
|
||||
// テンプレートファイルを作成
|
||||
const { id: templateFileId } = await createTemplateFile(
|
||||
source,
|
||||
accountId,
|
||||
'template-file-name',
|
||||
'https://example.com',
|
||||
);
|
||||
// ワークフローを作成
|
||||
const { id: workflowId } = await createWorkflow(
|
||||
source,
|
||||
accountId,
|
||||
myAuthorUserId,
|
||||
workTypeId,
|
||||
templateFileId,
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(source, workflowId, autoRoutingTypistUserId);
|
||||
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
author_id ?? '',
|
||||
custom_worktype_id,
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
await service.cancel(
|
||||
makeContext('trackingId'),
|
||||
1,
|
||||
external_id,
|
||||
role.split(' ') as Roles[],
|
||||
);
|
||||
const resultTask = await getTask(source, taskId);
|
||||
const permisions = await getCheckoutPermissions(source, taskId);
|
||||
|
||||
expect(resultTask?.status).toEqual('Uploaded');
|
||||
expect(resultTask?.typist_user_id).toEqual(null);
|
||||
// タスクのテンプレートファイルIDを確認
|
||||
expect(resultTask?.template_file_id).toEqual(templateFileId);
|
||||
// タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認
|
||||
expect(permisions.length).toEqual(1);
|
||||
expect(permisions[0].user_id).toEqual(autoRoutingTypistUserId);
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${autoRoutingTypistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
});
|
||||
it('API実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルするが、一致するワークフローがない場合は自動ルーティングを行うことができない', async () => {
|
||||
if (!source) fail();
|
||||
const notificationhubServiceMockValue =
|
||||
makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTaskTestingModuleWithNotificaiton(
|
||||
source,
|
||||
notificationhubServiceMockValue,
|
||||
);
|
||||
if (!module) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// タスクの文字起こし担当者
|
||||
const {
|
||||
id: typistUserId,
|
||||
external_id,
|
||||
role,
|
||||
} = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
// 音声ファイルのアップロード者
|
||||
const { id: authorUserId, author_id } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
author_id ?? '',
|
||||
'custom_worktype_id',
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
await service.cancel(
|
||||
makeContext('trackingId'),
|
||||
1,
|
||||
external_id,
|
||||
role.split(' ') as Roles[],
|
||||
);
|
||||
const resultTask = await getTask(source, taskId);
|
||||
const permisions = await getCheckoutPermissions(source, taskId);
|
||||
|
||||
expect(resultTask?.status).toEqual('Uploaded');
|
||||
expect(resultTask?.typist_user_id).toEqual(null);
|
||||
// タスクのチェックアウト権限が削除されていることを確認
|
||||
expect(permisions.length).toEqual(0);
|
||||
// 通知処理が想定通りの引数で呼ばれていないか確認
|
||||
expect(NotificationHubService.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextTask', () => {
|
||||
|
||||
@ -33,6 +33,7 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat
|
||||
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
|
||||
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
|
||||
import { Context } from '../../common/log';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TasksService {
|
||||
@ -382,19 +383,29 @@ export class TasksService {
|
||||
externalId: string,
|
||||
role: Roles[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.cancel.name} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
|
||||
);
|
||||
let user: User;
|
||||
try {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.cancel.name} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
|
||||
// ユーザー取得
|
||||
user = await this.usersRepository.findUserByExternalId(externalId);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancel.name}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
const { id, account_id } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
}
|
||||
|
||||
try {
|
||||
// roleにAdminが含まれていれば、文字起こし担当でなくてもキャンセルできるため、ユーザーIDは指定しない
|
||||
return await this.taskRepository.cancel(
|
||||
await this.taskRepository.cancel(
|
||||
audioFileId,
|
||||
[TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING],
|
||||
account_id,
|
||||
role.includes(ADMIN_ROLES.ADMIN) ? undefined : id,
|
||||
user.account_id,
|
||||
role.includes(ADMIN_ROLES.ADMIN) ? undefined : user.id,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
@ -422,6 +433,47 @@ export class TasksService {
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// キャンセルしたタスクに自動ルーティングを行う
|
||||
const { typistGroupIds, typistIds } =
|
||||
await this.taskRepository.autoRouting(
|
||||
audioFileId,
|
||||
user.account_id,
|
||||
user.author_id ?? undefined,
|
||||
);
|
||||
|
||||
const groupMembers =
|
||||
await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(
|
||||
typistGroupIds,
|
||||
);
|
||||
|
||||
// 重複のない割り当て候補ユーザーID一覧を取得する
|
||||
const distinctUserIds = [
|
||||
...new Set([...typistIds, ...groupMembers.map((x) => x.user_id)]),
|
||||
];
|
||||
|
||||
// 割り当てられたユーザーがいない場合は通知不要
|
||||
if (distinctUserIds.length === 0) {
|
||||
this.logger.log('No user assigned.');
|
||||
return;
|
||||
}
|
||||
|
||||
// タグを生成
|
||||
const tags = distinctUserIds.map((x) => `user_${x}`);
|
||||
this.logger.log(`tags: ${tags}`);
|
||||
|
||||
// タグ対象に通知送信
|
||||
await this.notificationhubService.notify(
|
||||
context,
|
||||
tags,
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
} catch (e) {
|
||||
// 処理の本筋はタスクキャンセルのため自動ルーティングに失敗してもエラーにしない
|
||||
this.logger.error(`Automatic routing or notification failed.`);
|
||||
this.logger.error(`error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancel.name}`);
|
||||
}
|
||||
|
||||
@ -362,6 +362,9 @@ const defaultTasksRepositoryMockValue: {
|
||||
template_file_id: null,
|
||||
typist_user: null,
|
||||
template_file: null,
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
updated_at: new Date('2023-01-01T01:01:01.000Z'),
|
||||
option_items: [
|
||||
{
|
||||
id: 1,
|
||||
|
||||
@ -51,7 +51,9 @@ export class TemplatesController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get()
|
||||
async getTemplates(@Req() req: Request): Promise<GetTemplatesResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
|
||||
@ -337,7 +337,7 @@ export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => {
|
||||
},
|
||||
createUser: '001',
|
||||
getUser: {
|
||||
id: "xxxx-xxxxx-xxxxx-xxxx",
|
||||
id: 'xxxx-xxxxx-xxxxx-xxxx',
|
||||
displayName: 'Hanako Sato',
|
||||
},
|
||||
getUsers: AdB2cMockUsers,
|
||||
|
||||
@ -54,7 +54,6 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { UserRoles } from '../../common/types/role';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { userInfo } from 'os';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
@ -129,7 +128,9 @@ export class UsersController {
|
||||
@ApiOperation({ operationId: 'getUsers' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get()
|
||||
async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
@ -176,7 +177,9 @@ export class UsersController {
|
||||
@ApiBearerAuth()
|
||||
@Post('/signup')
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
async signup(
|
||||
@Req() req: Request,
|
||||
@Body() body: SignupRequest,
|
||||
@ -413,7 +416,9 @@ export class UsersController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('update')
|
||||
async updateUser(
|
||||
@Body() body: PostUpdateUserRequest,
|
||||
@ -492,7 +497,11 @@ export class UsersController {
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER5] }),
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('/license/allocate')
|
||||
async allocateLicense(
|
||||
@ -551,7 +560,11 @@ export class UsersController {
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER5] }),
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('/license/deallocate')
|
||||
async deallocateLicense(
|
||||
|
||||
@ -416,7 +416,7 @@ export class UsersService {
|
||||
// TODO [Task2163] ODMS側が正式にメッセージを決めるまで仮のメール内容とする
|
||||
const subject = 'A temporary password has been issued.';
|
||||
const text = 'temporary password: ' + ramdomPassword;
|
||||
const html = `<p>OMDS TOP PAGE URL.<p><a href="${this.appDomain}">${this.appDomain}"</a><br>temporary password: ${ramdomPassword}`;
|
||||
const html = `<p>OMDS TOP PAGE URL.<p><a href="${this.appDomain}">${this.appDomain}</a><br>temporary password: ${ramdomPassword}`;
|
||||
|
||||
// メールを送信
|
||||
await this.sendgridService.sendMail(
|
||||
|
||||
@ -63,7 +63,9 @@ export class WorkflowsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get()
|
||||
async getWorkflows(@Req() req: Request): Promise<GetWorkflowsResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
@ -115,7 +117,9 @@ export class WorkflowsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post()
|
||||
async createWorkflows(
|
||||
@Req() req: Request,
|
||||
@ -178,7 +182,9 @@ export class WorkflowsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('/:workflowId')
|
||||
async updateWorkflow(
|
||||
@Req() req: Request,
|
||||
@ -244,7 +250,9 @@ export class WorkflowsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('/:workflowId/delete')
|
||||
async deleteWorkflow(
|
||||
@Req() req: Request,
|
||||
|
||||
@ -80,7 +80,7 @@ export class RedisService {
|
||||
*/
|
||||
async mget<T>(keys: string[]): Promise<{ key: string; value: T | null }[]> {
|
||||
if (keys.length === 0) return []; // mget操作は0件の時エラーとなるため、0件は特別扱いする
|
||||
|
||||
|
||||
try {
|
||||
const records = await this.cacheManager.store.mget(...keys);
|
||||
// getで取得した順序とKeysの順序は一致するはずなので、indexを利用してペアになるよう加工する
|
||||
|
||||
@ -54,7 +54,7 @@ export class SendGridService {
|
||||
return {
|
||||
subject: 'Verify your new account',
|
||||
text: `The verification URL. ${this.appDomain}${path}?verify=${token}`,
|
||||
html: `<p>The verification URL.<p><a href="${this.appDomain}${path}?verify=${token}">${this.appDomain}${path}?verify=${token}"</a>`,
|
||||
html: `<p>The verification URL.<p><a href="${this.appDomain}${path}?verify=${token}">${this.appDomain}${path}?verify=${token}</a>`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ export class SendGridService {
|
||||
return {
|
||||
subject: 'Verify your new account',
|
||||
text: `The verification URL. ${this.appDomain}${path}?verify=${token}`,
|
||||
html: `<p>The verification URL.<p><a href="${this.appDomain}${path}?verify=${token}">${this.appDomain}${path}?verify=${token}"</a>`,
|
||||
html: `<p>The verification URL.<p><a href="${this.appDomain}${path}?verify=${token}">${this.appDomain}${path}?verify=${token}</a>`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { bigintTransformer } from '../../../common/entity';
|
||||
|
||||
@ -37,8 +39,24 @@ export class Task {
|
||||
started_at: Date | null;
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
finished_at: Date | null;
|
||||
@Column({})
|
||||
|
||||
@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(() => AudioFile, (audiofile) => audiofile.task)
|
||||
@JoinColumn({ name: 'audio_file_id' })
|
||||
file: AudioFile | null;
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
DataSource,
|
||||
EntityManager,
|
||||
FindOptionsOrder,
|
||||
FindOptionsOrderValue,
|
||||
In,
|
||||
IsNull,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { Task } from './entity/task.entity';
|
||||
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
|
||||
@ -35,6 +37,8 @@ import {
|
||||
import { Roles } from '../../common/types/role';
|
||||
import { TaskStatus, isTaskStatus } from '../../common/types/taskStatus';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
import { Workflow } from '../workflows/entity/workflow.entity';
|
||||
import { Worktype } from '../worktypes/entity/worktype.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TasksRepositoryService {
|
||||
@ -710,18 +714,6 @@ export class TasksRepositoryService {
|
||||
|
||||
task.audio_file_id = savedAudioFile.id;
|
||||
|
||||
const optionItems = paramOptionItems.map((x) => {
|
||||
return {
|
||||
audio_file_id: savedAudioFile.id,
|
||||
label: x.optionItemLabel,
|
||||
value: x.optionItemValue,
|
||||
};
|
||||
});
|
||||
|
||||
const optionItemRepo = entityManager.getRepository(AudioOptionItem);
|
||||
const newAudioOptionItems = optionItemRepo.create(optionItems);
|
||||
await optionItemRepo.save(newAudioOptionItems);
|
||||
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
|
||||
// アカウント内でJOBナンバーが有効なタスクのうち最新のものを取得
|
||||
@ -743,8 +735,19 @@ export class TasksRepositoryService {
|
||||
}
|
||||
task.job_number = newJobNumber;
|
||||
|
||||
const newTask = taskRepo.create(task);
|
||||
const persisted = await taskRepo.save(newTask);
|
||||
const persisted = await taskRepo.save(task);
|
||||
|
||||
const optionItems = paramOptionItems.map((x) => {
|
||||
return {
|
||||
audio_file_id: persisted.audio_file_id,
|
||||
label: x.optionItemLabel,
|
||||
value: x.optionItemValue,
|
||||
};
|
||||
});
|
||||
|
||||
const optionItemRepo = entityManager.getRepository(AudioOptionItem);
|
||||
const newAudioOptionItems = optionItemRepo.create(optionItems);
|
||||
await optionItemRepo.save(newAudioOptionItems);
|
||||
return persisted;
|
||||
},
|
||||
);
|
||||
@ -952,6 +955,227 @@ export class TasksRepositoryService {
|
||||
return tasks;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ルーティングルールを取得し、タスクのチェックアウト権限を設定する
|
||||
* @param audioFileId
|
||||
* @param accountId
|
||||
* @param [myAuthorId]
|
||||
* @returns typistIds: タイピストIDの一覧 / typistGroupIds: タイピストグループIDの一覧
|
||||
*/
|
||||
async autoRouting(
|
||||
audioFileId: number,
|
||||
accountId: number,
|
||||
myAuthorId?: string, // API実行者のAuthorId
|
||||
): Promise<{ typistIds: number[]; typistGroupIds: number[] }> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
// 音声ファイルを取得
|
||||
const audioFileRepo = entityManager.getRepository(AudioFile);
|
||||
const audioFile = await audioFileRepo.findOne({
|
||||
relations: {
|
||||
task: true,
|
||||
},
|
||||
where: {
|
||||
id: audioFileId,
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
if (!audioFile) {
|
||||
throw new Error(
|
||||
`audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { task } = audioFile;
|
||||
|
||||
if (!task) {
|
||||
throw new Error(
|
||||
`task not found. audio_file_id:${audioFileId}, accountId:${accountId}`,
|
||||
);
|
||||
}
|
||||
// authorIdをもとにユーザーを取得
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const authorUser = await userRepo.findOne({
|
||||
where: {
|
||||
author_id: audioFile.author_id,
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
|
||||
// 音声ファイル上のworktypeIdをもとにworktypeを取得
|
||||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||||
const worktypeRecord = await worktypeRepo.findOne({
|
||||
where: {
|
||||
custom_worktype_id: audioFile.work_type_id,
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
|
||||
// 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了
|
||||
if (!worktypeRecord && audioFile.work_type_id !== '') {
|
||||
throw new Error(
|
||||
`worktype not found. worktype:${audioFile.work_type_id}, accountId:${accountId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Workflow(ルーティングルール)を取得
|
||||
const workflowRepo = entityManager.getRepository(Workflow);
|
||||
const workflow = await workflowRepo.findOne({
|
||||
relations: {
|
||||
workflowTypists: true,
|
||||
},
|
||||
where: {
|
||||
account_id: accountId,
|
||||
author_id: authorUser?.id ?? IsNull(), // authorUserが存在しない場合は、必ずヒットしないようにNULLを設定する
|
||||
worktype_id: worktypeRecord?.id ?? IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
// Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する
|
||||
if (workflow) {
|
||||
return await this.setCheckoutPermissionAndTemplate(
|
||||
workflow,
|
||||
task,
|
||||
accountId,
|
||||
entityManager,
|
||||
userRepo,
|
||||
);
|
||||
}
|
||||
|
||||
// 音声ファイルの情報からルーティングルールを取得できない場合は、
|
||||
// API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得する
|
||||
// API実行者のAuthorIdがない場合はエラーを出して終了
|
||||
if (!myAuthorId) {
|
||||
throw new Error(`There is no AuthorId for the API executor.`);
|
||||
}
|
||||
// API実行者のAuthorIdをもとにユーザーを取得
|
||||
const myAuthorUser = await userRepo.findOne({
|
||||
where: {
|
||||
author_id: myAuthorId,
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
if (!myAuthorUser) {
|
||||
throw new Error(
|
||||
`user not found. authorId:${myAuthorId}, accountId:${accountId}`,
|
||||
);
|
||||
}
|
||||
const defaultWorkflow = await workflowRepo.findOne({
|
||||
relations: {
|
||||
workflowTypists: true,
|
||||
},
|
||||
where: {
|
||||
account_id: accountId,
|
||||
author_id: myAuthorUser.id,
|
||||
worktype_id: worktypeRecord?.id ?? IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
// API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了
|
||||
if (!defaultWorkflow) {
|
||||
throw new Error(
|
||||
`workflow not found. authorUserId:${myAuthorUser.id}, accountId:${accountId}, worktypeId:${worktypeRecord?.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する
|
||||
return await this.setCheckoutPermissionAndTemplate(
|
||||
defaultWorkflow,
|
||||
task,
|
||||
accountId,
|
||||
entityManager,
|
||||
userRepo,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* workflowに紐づけられているタイピスト・タイピストグループで、タスクのチェックアウト権限を設定
|
||||
* workflowに紐づけられているテンプレートファイルIDをタスクに設定
|
||||
*
|
||||
* @param workflow
|
||||
* @param task
|
||||
* @param accountId
|
||||
* @param entityManager
|
||||
* @param userRepo
|
||||
* @returns checkout permission
|
||||
*/
|
||||
private async setCheckoutPermissionAndTemplate(
|
||||
workflow: Workflow,
|
||||
task: Task,
|
||||
accountId: number,
|
||||
entityManager: EntityManager,
|
||||
userRepo: Repository<User>,
|
||||
): Promise<{ typistIds: number[]; typistGroupIds: number[] }> {
|
||||
const { workflowTypists, template_id } = workflow;
|
||||
if (!workflowTypists) {
|
||||
throw new Error(`workflowTypists not found. workflowId:${workflow.id}`);
|
||||
}
|
||||
|
||||
// タスクのテンプレートIDを更新
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
await taskRepo.update(
|
||||
{ id: task.id },
|
||||
{
|
||||
template_file_id: template_id,
|
||||
},
|
||||
);
|
||||
|
||||
// 取得したルーティングルールのタイピストまたはタイピストグループをチェックアウト権限に設定する
|
||||
|
||||
// ルーティング候補ユーザーの存在確認
|
||||
const typistIds = workflowTypists.flatMap((typist) =>
|
||||
typist.typist_id ? [typist.typist_id] : [],
|
||||
);
|
||||
const typistUsers = await userRepo.find({
|
||||
where: { account_id: accountId, id: In(typistIds) },
|
||||
});
|
||||
if (typistUsers.length !== typistIds.length) {
|
||||
throw new Error(`typist not found. ids: ${typistIds}`);
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーグループの存在確認
|
||||
const groupIds = workflowTypists.flatMap((typist) => {
|
||||
return typist.typist_group_id ? [typist.typist_group_id] : [];
|
||||
});
|
||||
const userGroupRepo = entityManager.getRepository(UserGroup);
|
||||
const typistGroups = await userGroupRepo.find({
|
||||
where: { account_id: accountId, id: In(groupIds) },
|
||||
});
|
||||
if (typistGroups.length !== groupIds.length) {
|
||||
throw new Error(`typist group not found. ids: ${groupIds}`);
|
||||
}
|
||||
|
||||
const checkoutPermissionRepo =
|
||||
entityManager.getRepository(CheckoutPermission);
|
||||
|
||||
// 当該タスクに紐づく既存checkoutPermissionをdelete
|
||||
await checkoutPermissionRepo.delete({
|
||||
task_id: task.id,
|
||||
});
|
||||
|
||||
// ルーティング候補ユーザーのチェックアウト権限を作成
|
||||
const typistPermissions = typistUsers.map((typistUser) => {
|
||||
const permission = new CheckoutPermission();
|
||||
permission.task_id = task.id;
|
||||
permission.user_id = typistUser.id;
|
||||
return permission;
|
||||
});
|
||||
// ルーティング候補ユーザーグループのチェックアウト権限を作成
|
||||
const typistGroupPermissions = typistGroups.map((typistGroup) => {
|
||||
const permission = new CheckoutPermission();
|
||||
permission.task_id = task.id;
|
||||
permission.user_group_id = typistGroup.id;
|
||||
return permission;
|
||||
});
|
||||
const permissions = [...typistPermissions, ...typistGroupPermissions];
|
||||
await checkoutPermissionRepo.save(permissions);
|
||||
// user_idsとuser_group_idsを返却する
|
||||
return {
|
||||
typistIds: typistIds,
|
||||
typistGroupIds: groupIds,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ソート用オブジェクトを生成する
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user