Merged PR 750: データ削除ツール作成+動作確認

## 概要
[Task3569: データ削除ツール作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3569)

- データ削除ツールを実装しました。
  - Blobストレージからのコンテナ削除
  - ADB2Cからのユーザー削除
  - DBの全削除
  - Auto Incrementの設定

## レビューポイント
- Blobストレージの削除対象の取得に問題はないでしょうか?
  - 3つのリージョン内のすべてのコンテナを取得してから、取得したコンテナを全削除するようにしています。
- ADB2Cの削除対象の取得に問題はないでしょうか?
  - ローカルアカウントなユーザーのみを取得してから、取得したユーザーを全削除するようにしています。
- フォルダ構成に違和感はないでしょうか?

## UIの変更
- [Task3569](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3569?csf=1&web=1&e=wU1st1)

## 動作確認状況
- ローカルで確認
  - DB操作のみ確認しています。Azureリソースの削除についてはdevelop環境で改めて実施します。
This commit is contained in:
makabe.t 2024-02-20 10:09:05 +00:00
parent f8ff19a3fa
commit a9aca6e4ff
83 changed files with 5160 additions and 261 deletions

View File

@ -0,0 +1,2 @@
npx openapi-generator-cli version-manager set 7.1.0
npx openapi-generator-cli generate -g typescript-axios -i /app/data_migration_tools/server/src/api/odms/openapi.json -o /app/data_migration_tools/client/src/api/

View File

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.1.0"
}
}

View File

@ -1,8 +1,11 @@
import { Route, Routes } from "react-router-dom";
import TopPage from "./pages/topPage";
import DeletePage from "./pages/deletePage";
const AppRouter: React.FC = () => (
<Routes>
<Route path="/" element={<div />} />
<Route path="/" element={<TopPage />} />
<Route path="/delete" element={<DeletePage />} />
</Routes>
);

View File

@ -0,0 +1,4 @@
wwwroot/*.js
node_modules
typings
dist

View File

@ -0,0 +1 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm

View File

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@ -0,0 +1,9 @@
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts
configuration.ts
git_push.sh
index.ts

View File

@ -0,0 +1 @@
7.1.0

View File

@ -0,0 +1,146 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';
/**
*
* @export
* @interface ErrorResponse
*/
export interface ErrorResponse {
/**
*
* @type {string}
* @memberof ErrorResponse
*/
'message': string;
/**
*
* @type {string}
* @memberof ErrorResponse
*/
'code': string;
}
/**
* DeleteApi - axios parameter creator
* @export
*/
export const DeleteApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteData: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/delete`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* DeleteApi - functional programming interface
* @export
*/
export const DeleteApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = DeleteApiAxiosParamCreator(configuration)
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteData(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteData(options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['DeleteApi.deleteData']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
}
};
/**
* DeleteApi - factory interface
* @export
*/
export const DeleteApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = DeleteApiFp(configuration)
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteData(options?: any): AxiosPromise<object> {
return localVarFp.deleteData(options).then((request) => request(axios, basePath));
},
};
};
/**
* DeleteApi - object-oriented interface
* @export
* @class DeleteApi
* @extends {BaseAPI}
*/
export class DeleteApi extends BaseAPI {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DeleteApi
*/
public deleteData(options?: AxiosRequestConfig) {
return DeleteApiFp(this.configuration).deleteData(options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -0,0 +1,86 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from './configuration';
// Some imports not used depending on template conditions
// @ts-ignore
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
export const BASE_PATH = "http://localhost".replace(/\/+$/, "");
/**
*
* @export
*/
export const COLLECTION_FORMATS = {
csv: ",",
ssv: " ",
tsv: "\t",
pipes: "|",
};
/**
*
* @export
* @interface RequestArgs
*/
export interface RequestArgs {
url: string;
options: AxiosRequestConfig;
}
/**
*
* @export
* @class BaseAPI
*/
export class BaseAPI {
protected configuration: Configuration | undefined;
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
if (configuration) {
this.configuration = configuration;
this.basePath = configuration.basePath ?? basePath;
}
}
};
/**
*
* @export
* @class RequiredError
* @extends {Error}
*/
export class RequiredError extends Error {
constructor(public field: string, msg?: string) {
super(msg);
this.name = "RequiredError"
}
}
interface ServerMap {
[key: string]: {
url: string,
description: string,
}[];
}
/**
*
* @export
*/
export const operationServerMap: ServerMap = {
}

View File

@ -0,0 +1,150 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from "./configuration";
import type { RequestArgs } from "./base";
import type { AxiosInstance, AxiosResponse } from 'axios';
import { RequiredError } from "./base";
/**
*
* @export
*/
export const DUMMY_BASE_URL = 'https://example.com'
/**
*
* @throws {RequiredError}
* @export
*/
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
}
}
/**
*
* @export
*/
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
? await configuration.apiKey(keyParamName)
: await configuration.apiKey;
object[keyParamName] = localVarApiKeyValue;
}
}
/**
*
* @export
*/
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
if (configuration && (configuration.username || configuration.password)) {
object["auth"] = { username: configuration.username, password: configuration.password };
}
}
/**
*
* @export
*/
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
object["Authorization"] = "Bearer " + accessToken;
}
}
/**
*
* @export
*/
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
? await configuration.accessToken(name, scopes)
: await configuration.accessToken;
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
}
}
function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
if (parameter == null) return;
if (typeof parameter === "object") {
if (Array.isArray(parameter)) {
(parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
}
else {
Object.keys(parameter).forEach(currentKey =>
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
);
}
}
else {
if (urlSearchParams.has(key)) {
urlSearchParams.append(key, parameter);
}
else {
urlSearchParams.set(key, parameter);
}
}
}
/**
*
* @export
*/
export const setSearchParams = function (url: URL, ...objects: any[]) {
const searchParams = new URLSearchParams(url.search);
setFlattenedQueryParams(searchParams, objects);
url.search = searchParams.toString();
}
/**
*
* @export
*/
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
const nonString = typeof value !== 'string';
const needsSerialization = nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
: nonString;
return needsSerialization
? JSON.stringify(value !== undefined ? value : {})
: (value || "");
}
/**
*
* @export
*/
export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash
}
/**
*
* @export
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url};
return axios.request<T, R>(axiosRequestArgs);
};
}

View File

@ -0,0 +1,110 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string;
serverIndex?: number;
baseOptions?: any;
formDataCtor?: new () => any;
}
export class Configuration {
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string;
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string;
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string;
/**
* override server index
*
* @type {number}
* @memberof Configuration
*/
serverIndex?: number;
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any;
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any;
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey;
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.basePath = param.basePath;
this.serverIndex = param.serverIndex;
this.baseOptions = param.baseOptions;
this.formDataCtor = param.formDataCtor;
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
}
}

View File

@ -0,0 +1,57 @@
#!/bin/sh
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
#
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
git_user_id=$1
git_repo_id=$2
release_note=$3
git_host=$4
if [ "$git_host" = "" ]; then
git_host="github.com"
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
fi
if [ "$git_user_id" = "" ]; then
git_user_id="GIT_USER_ID"
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
fi
if [ "$git_repo_id" = "" ]; then
git_repo_id="GIT_REPO_ID"
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
fi
if [ "$release_note" = "" ]; then
release_note="Minor update"
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
fi
# Initialize the local directory as a Git repository
git init
# Adds the files in the local repository and stages them for commit.
git add .
# Commits the tracked changes and prepares them to be pushed to a remote repository.
git commit -m "$release_note"
# Sets the new remote
git_remote=$(git remote)
if [ "$git_remote" = "" ]; then # git remote not defined
if [ "$GIT_TOKEN" = "" ]; then
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
else
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
fi
fi
git pull origin master
# Pushes (Forces) the changes in the local repository up to the remote repository
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
git push origin master 2>&1 | grep -v 'To https'

View File

@ -0,0 +1,18 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export * from "./api";
export * from "./configuration";

View File

@ -1,7 +1,11 @@
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import auth from "features/auth/authSlice";
import ui from "features/ui/uiSlice";
export const store = configureStore({
reducer: {
auth,
ui,
},
});

View File

@ -0,0 +1,17 @@
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
*/
export const errorCodes = [
"E009999", // 汎用エラー
] as const;

View File

@ -0,0 +1,3 @@
export * from "./code";
export * from "./types";
export * from "./utils";

View File

@ -0,0 +1,9 @@
import { errorCodes } from "./code";
export type ErrorObject = {
message: string;
code: ErrorCodeType;
statusCode?: number;
};
export type ErrorCodeType = (typeof errorCodes)[number];

View File

@ -0,0 +1,101 @@
import { AxiosError } from "axios";
import { isError } from "lodash";
import { ErrorResponse } from "../../api";
import { errorCodes } from "./code";
import { ErrorCodeType, ErrorObject } from "./types";
export const createErrorObject = (error: unknown): ErrorObject => {
// 最低限通常のエラーかを判定
// Error以外のものがthrowされた場合
// 基本的にないはずだがプログラム上あるので拾う
if (!isError(error)) {
return {
message: "not error type.",
code: "E009999",
};
}
// Axiosエラー 通信してのエラーであるかを判定
if (!isAxiosError(error)) {
return {
message: "not axios error.",
code: "E009999",
};
}
const errorResponse = error.response;
if (!errorResponse) {
return {
message: error.message,
code: "E009999",
statusCode: errorResponse,
};
}
const { data } = errorResponse;
// 想定しているエラーレスポンスの型か判定
if (!isErrorResponse(data)) {
return {
message: error.message,
code: "E009999",
statusCode: errorResponse.status,
};
}
const { message, code } = data;
// 想定しているエラーコードかを判定
if (!isErrorCode(code)) {
return {
message,
code: "E009999",
statusCode: errorResponse.status,
};
}
return {
message,
code,
statusCode: errorResponse.status,
};
};
const isAxiosError = (e: unknown): e is AxiosError => {
const error = e as AxiosError;
return error?.isAxiosError ?? false;
};
const isErrorResponse = (error: unknown): error is ErrorResponse => {
const errorResponse = error as ErrorResponse;
if (
errorResponse === undefined ||
errorResponse.message === undefined ||
errorResponse.code === undefined
) {
return false;
}
return true;
};
const isErrorCode = (errorCode: string): errorCode is ErrorCodeType =>
errorCodes.includes(errorCode as ErrorCodeType);
export const isErrorObject = (
data: unknown
): data is { error: ErrorObject } => {
if (
data &&
typeof data === "object" &&
"error" in data &&
typeof (data as { error: ErrorObject }).error === "object" &&
typeof (data as { error: ErrorObject }).error.message === "string" &&
typeof (data as { error: ErrorObject }).error.code === "string" &&
(typeof (data as { error: ErrorObject }).error.statusCode === "number" ||
(data as { error: ErrorObject }).error.statusCode === undefined)
) {
return true;
}
return false;
};

View File

@ -1,6 +1,6 @@
export const getBasePath = () => {
if (import.meta.env.VITE_STAGE === "local") {
return "http://localhost:8180";
return "http://localhost:8280";
}
return window.location.origin;
};

View File

@ -0,0 +1,15 @@
import { createSlice } from "@reduxjs/toolkit";
import type { AuthState } from "./state";
import { initialConfig } from "./utils";
const initialState: AuthState = {
configuration: initialConfig(),
};
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {},
});
export default authSlice.reducer;

View File

@ -0,0 +1,3 @@
export * from "./authSlice";
export * from "./state";
export * from "./utils";

View File

@ -0,0 +1,5 @@
import { ConfigurationParameters } from "../../api";
export interface AuthState {
configuration: ConfigurationParameters;
}

View File

@ -0,0 +1,13 @@
import { ConfigurationParameters } from "../../api";
// 初期状態のAPI Config
export const initialConfig = (): ConfigurationParameters => {
const config: ConfigurationParameters = {};
if (import.meta.env.VITE_STAGE === "local") {
config.basePath = "http://localhost:8280";
} else {
config.basePath = `${window.location.origin}/dictation/api`;
}
return config;
};

View File

@ -0,0 +1,25 @@
import { createSlice } from "@reduxjs/toolkit";
import { DeleteState } from "./state";
import { deleteDataAsync } from "./operations";
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
const initialState: DeleteState = {};
export const deleteSlice = createSlice({
name: "detete",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(deleteDataAsync.pending, () => {
/* Empty Object */
});
builder.addCase(deleteDataAsync.fulfilled, () => {
/* Empty Object */
});
builder.addCase(deleteDataAsync.rejected, () => {
/* Empty Object */
});
},
});
export default deleteSlice.reducer;

View File

@ -0,0 +1,3 @@
export * from "./state";
export * from "./deleteSlice";
export * from "./operations";

View File

@ -0,0 +1,47 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { openSnackbar } from "../ui/uiSlice";
import type { RootState } from "../../app/store";
import { ErrorObject, createErrorObject } from "../../common/errors";
import { DeleteApi } from "../../api/api";
import { Configuration } from "../../api/configuration";
export const deleteDataAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("delete/deleteDataAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const config = new Configuration(configuration);
const deleteApi = new DeleteApi(config);
try {
await deleteApi.deleteData();
thunkApi.dispatch(
openSnackbar({
level: "info",
message: "削除しました。",
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: "削除に失敗しました。",
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DeleteState {}

View File

@ -0,0 +1,2 @@
// 標準のスナックバー表示時間(ミリ秒)
export const DEFAULT_SNACKBAR_DURATION = 3000;

View File

@ -0,0 +1,5 @@
export * from "./constants";
export * from "./selectors";
export * from "./state";
export * from "./uiSlice";
export * from "./types";

View File

@ -0,0 +1,15 @@
import type { RootState } from "app/store";
import { SnackbarLevel } from "./types";
export const selectSnackber = (
state: RootState
): {
isOpen: boolean;
level: SnackbarLevel;
message: string;
duration?: number;
} => {
const { isOpen, level, message, duration } = state.ui;
return { isOpen, level, message, duration };
};

View File

@ -0,0 +1,8 @@
import { SnackbarLevel } from "./types";
export interface UIState {
isOpen: boolean;
level: SnackbarLevel;
message: string;
duration?: number;
}

View File

@ -0,0 +1 @@
export type SnackbarLevel = "info" | "error";

View File

@ -0,0 +1,38 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SnackbarLevel } from "./types";
import { UIState } from "./state";
import { DEFAULT_SNACKBAR_DURATION } from "./constants";
const initialState: UIState = {
isOpen: false,
level: "error",
message: "",
};
export const uiSlice = createSlice({
name: "ui",
initialState,
reducers: {
openSnackbar: (
state,
action: PayloadAction<{
level: SnackbarLevel;
message: string;
duration?: number;
}>
) => {
const { level, message, duration } = action.payload;
state.isOpen = true;
state.level = level;
state.message = message;
state.duration =
level === "error" ? undefined : duration ?? DEFAULT_SNACKBAR_DURATION;
},
closeSnackbar: (state) => {
state.isOpen = false;
},
},
});
export const { openSnackbar, closeSnackbar } = uiSlice.actions;
export default uiSlice.reducer;

View File

@ -0,0 +1,32 @@
import { AppDispatch } from "app/store";
import { deleteDataAsync } from "features/delete";
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
const DeletePage = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const onDelete = useCallback(() => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm("本当に削除しますか?")
) {
return;
}
dispatch(deleteDataAsync());
}, [dispatch]);
return (
<div>
<p></p>
<button type="button" onClick={onDelete}>
</button>
<br />
<Link to="/">return to TopPage</Link>
</div>
);
};
export default DeletePage;

View File

@ -0,0 +1,15 @@
import React from "react";
import { Link } from "react-router-dom";
const TopPage = (): JSX.Element => {
console.log("DeletePage");
return (
<div>
<Link to="/delete"></Link>
<br />
<Link to="/">return to TopPage</Link>
</div>
);
};
export default TopPage;

View File

@ -14,5 +14,11 @@ services:
- "8280"
environment:
- CHOKIDAR_USEPOLLING=true
networks:
- external
networks:
external:
name: omds_network
external: true
volumes:
data_migration_tools_server_node_modules:

View File

@ -0,0 +1,5 @@
DB_HOST=omds-mysql
DB_PORT=3306
DB_NAME=omds
DB_USERNAME=omdsdbuser
DB_PASSWORD=omdsdbpass

File diff suppressed because it is too large Load Diff

View File

@ -19,10 +19,13 @@
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"apigen": "ts-node src/api/generate.ts && prettier --write \"src/api/odms/*.json\""
},
"dependencies": {
"@azure/identity": "^4.0.1",
"@azure/storage-blob": "^12.14.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/common": "^9.3.9",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.3.9",
@ -30,6 +33,7 @@
"@nestjs/serve-static": "^3.0.1",
"@nestjs/swagger": "^6.2.1",
"@nestjs/testing": "^9.3.9",
"@nestjs/typeorm": "^10.0.2",
"@openapitools/openapi-generator-cli": "^2.5.2",
"@vercel/ncc": "^0.36.1",
"axios": "^1.3.4",
@ -37,6 +41,7 @@
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.9.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",
"swagger-cli": "^4.0.4"

View File

@ -0,0 +1,25 @@
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "../app.module";
import { promises as fs } from "fs";
import { NestFactory } from "@nestjs/core";
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, {
preview: true,
});
const options = new DocumentBuilder()
.setTitle("ODMSOpenAPI")
.setVersion("1.0.0")
.addBearerAuth({
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
})
.build();
const document = SwaggerModule.createDocument(app, options);
await fs.writeFile(
"src/api/odms/openapi.json",
JSON.stringify(document, null, 0)
);
}
bootstrap();

View File

@ -0,0 +1,56 @@
{
"openapi": "3.0.0",
"paths": {
"/delete": {
"post": {
"operationId": "deleteData",
"summary": "",
"description": "すべてのデータを削除します",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/DeleteResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["delete"]
}
}
},
"info": {
"title": "ODMSOpenAPI",
"description": "",
"version": "1.0.0",
"contact": {}
},
"tags": [],
"servers": [],
"components": {
"securitySchemes": {
"bearer": { "scheme": "bearer", "bearerFormat": "JWT", "type": "http" }
},
"schemas": {
"DeleteResponse": { "type": "object", "properties": {} },
"ErrorResponse": {
"type": "object",
"properties": {
"message": { "type": "string" },
"code": { "type": "string" }
},
"required": ["message", "code"]
}
}
}
}

View File

@ -1,8 +1,15 @@
import { MiddlewareConsumer, Module } from "@nestjs/common";
import { ServeStaticModule } from "@nestjs/serve-static";
import { ConfigModule } from "@nestjs/config";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { join } from "path";
import { LoggerMiddleware } from "./common/loggerMiddleware";
import { TypeOrmModule } from "@nestjs/typeorm";
import { DeleteModule } from "./features/delete/delete.module";
import { AdB2cModule } from "./gateways/adb2c/adb2c.module";
import { DeleteRepositoryModule } from "./repositories/delete/delete.repository.module";
import { DeleteController } from "./features/delete/delete.controller";
import { DeleteService } from "./features/delete/delete.service";
import { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module";
@Module({
imports: [
@ -13,9 +20,27 @@ import { LoggerMiddleware } from "./common/loggerMiddleware";
envFilePath: [".env.local", ".env"],
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
type: "mysql",
host: configService.get("DB_HOST"),
port: configService.get("DB_PORT"),
username: configService.get("DB_USERNAME"),
password: configService.get("DB_PASSWORD"),
database: configService.get("DB_NAME"),
autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
}),
inject: [ConfigService],
}),
DeleteModule,
AdB2cModule,
BlobstorageModule,
DeleteRepositoryModule,
],
controllers: [],
providers: [],
controllers: [DeleteController],
providers: [DeleteService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {

View File

@ -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.',
);
});
});
});

View 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();
},
};

View File

@ -0,0 +1,17 @@
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
*/
export const ErrorCodes = [
"E009999", // 汎用エラー
] as const;

View File

@ -0,0 +1,10 @@
import { errors } from "./message";
import { ErrorCodeType, ErrorResponse } from "./types/types";
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
const msg = errors[errorcode];
return {
code: errorcode,
message: msg,
};
};

View File

@ -0,0 +1,6 @@
import { Errors } from "./types/types";
// エラーコードとメッセージ対応表
export const errors: Errors = {
E009999: "Internal Server Error.",
};

View File

@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { ErrorCodes } from '../code';
export class ErrorResponse {
@ApiProperty()
message: string;
@ApiProperty()
code: string;
}
export type ErrorCodeType = (typeof ErrorCodes)[number];
export type Errors = {
[P in ErrorCodeType]: string;
};

View File

@ -0,0 +1,67 @@
/**
* 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",
];
/**
* ADB2Cユーザのidentity.signInType
* @const {string[]}
*/
export const ADB2C_SIGN_IN_TYPE = {
EMAILADDRESS: "emailAddress",
} as const;
/**
* AutoIncrementの初期値
* @const {number}
*/
export const AUTO_INCREMENT_START = 853211;

View File

@ -0,0 +1,30 @@
import { Test, TestingModule } from "@nestjs/testing";
import { ConfigModule } from "@nestjs/config";
import { DeleteService } from "./delete.service";
import { DeleteController } from "./delete.controller";
describe("DeleteController", () => {
let controller: DeleteController;
const mockTemplatesService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: [".env.test", ".env"],
isGlobal: true,
}),
],
controllers: [DeleteController],
providers: [DeleteService],
})
.overrideProvider(DeleteService)
.useValue(mockTemplatesService)
.compile();
controller = module.get<DeleteController>(DeleteController);
});
it("should be defined", () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,39 @@
import {
Controller,
HttpException,
HttpStatus,
Logger,
Post,
Req,
} from "@nestjs/common";
import { ErrorResponse } from "../../common/errors/types/types";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Request } from "express";
import { DeleteService } from "./delete.service";
import { DeleteResponse } from "./types/types";
@ApiTags("delete")
@Controller("delete")
export class DeleteController {
constructor(private readonly deleteService: DeleteService) {}
@ApiResponse({
status: HttpStatus.OK,
type: DeleteResponse,
description: "成功時のレスポンス",
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: "想定外のサーバーエラー",
type: ErrorResponse,
})
@ApiOperation({
operationId: "deleteData",
description: "すべてのデータを削除します",
})
@Post()
async deleteData(): Promise<{}> {
await this.deleteService.deleteData();
return {};
}
}

View File

@ -0,0 +1,13 @@
import { Module } from "@nestjs/common";
import { DeleteRepositoryModule } from "../../repositories/delete/delete.repository.module";
import { DeleteController } from "./delete.controller";
import { DeleteService } from "./delete.service";
import { AdB2cModule } from "../../gateways/adb2c/adb2c.module";
import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module";
@Module({
imports: [DeleteRepositoryModule, AdB2cModule, BlobstorageModule],
providers: [DeleteService],
controllers: [DeleteController],
})
export class DeleteModule {}

View File

@ -0,0 +1,29 @@
import { DataSource } from "typeorm";
import { ConfigModule } from "@nestjs/config";
import { DeleteService } from "./delete.service";
import { Test, TestingModule } from "@nestjs/testing";
describe("DeleteController", () => {
let service: DeleteService;
const mockTemplatesService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: [".env.test", ".env"],
isGlobal: true,
}),
],
providers: [DeleteService],
})
.overrideProvider(DeleteService)
.useValue(mockTemplatesService)
.compile();
service = module.get<DeleteService>(DeleteService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,54 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { DeleteRepositoryService } from "../../repositories/delete/delete.repository.service";
import { makeErrorResponse } from "../../common/errors/makeErrorResponse";
import { AdB2cService } from "../../gateways/adb2c/adb2c.service";
import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service";
@Injectable()
export class DeleteService {
private readonly logger = new Logger(DeleteService.name);
constructor(
private readonly deleteRepositoryService: DeleteRepositoryService,
private readonly blobstorageService: BlobstorageService,
private readonly adB2cService: AdB2cService
) {}
/**
*
* @returns data
*/
async deleteData(): Promise<void> {
this.logger.log(`[IN] ${this.deleteData.name}`);
try {
// BlobStorageからデータを削除する
await this.blobstorageService.deleteContainers();
// ADB2Cからユーザ情報を取得する
const users = await this.adB2cService.getUsers();
const externalIds = users.map((user) => user.id);
await this.adB2cService.deleteUsers(externalIds);
// データベースからデータを削除する
await this.deleteRepositoryService.deleteData();
// AutoIncrementの値をリセットする
await this.deleteRepositoryService.resetAutoIncrement();
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally {
this.logger.log(`[OUT] ${this.deleteData.name}`);
}
}
}

View File

@ -0,0 +1 @@
import { DataSource } from "typeorm";

View File

@ -0,0 +1 @@
export class DeleteResponse {}

View File

@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AdB2cService } from "./adb2c.service";
@Module({
imports: [ConfigModule],
exports: [AdB2cService],
providers: [AdB2cService],
})
export class AdB2cModule {}

View File

@ -0,0 +1,116 @@
import { ClientSecretCredential } from "@azure/identity";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AdB2cResponse, AdB2cUser } from "./types/types";
import { isPromiseRejectedResult } from "./utils/utils";
export type ConflictError = {
reason: "email";
message: string;
};
export class Adb2cTooManyRequestsError extends Error {}
export const isConflictError = (arg: unknown): arg is ConflictError => {
const value = arg as ConflictError;
if (value.message === undefined) {
return false;
}
if (value.reason === "email") {
return true;
}
return false;
};
@Injectable()
export class AdB2cService {
private readonly logger = new Logger(AdB2cService.name);
private graphClient: Client;
constructor(private readonly configService: ConfigService) {
// ADB2Cへの認証情報
const credential = new ClientSecretCredential(
this.configService.getOrThrow<string>("ADB2C_TENANT_ID"),
this.configService.getOrThrow<string>("ADB2C_CLIENT_ID"),
this.configService.getOrThrow<string>("ADB2C_CLIENT_SECRET")
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["https://graph.microsoft.com/.default"],
});
this.graphClient = Client.initWithMiddleware({ authProvider });
}
/**
* Gets users
* @param externalIds
* @returns users
*/
async getUsers(): Promise<AdB2cUser[]> {
this.logger.log(`[IN] ${this.getUsers.name}`);
try {
const res: AdB2cResponse = await this.graphClient
.api(`users/`)
.select(["id", "displayName", "identities"])
.filter(`creationType eq 'LocalAccount'`)
.get();
return res.value;
} catch (e) {
this.logger.error(`error=${e}`);
const { statusCode } = e;
if (statusCode === 429) {
throw new Adb2cTooManyRequestsError();
}
throw e;
} finally {
this.logger.log(`[OUT] ${this.getUsers.name}`);
}
}
/**
* Azure AD B2Cからユーザ情報を削除する
* @param externalIds ID
*/
async deleteUsers(externalIds: string[]): Promise<void> {
this.logger.log(
`[IN]${this.deleteUsers.name} | params: { externalIds: ${externalIds} };`
);
try {
// 複数ユーザーを一括削除する方法がないため、1人ずつで削除を行う
const results = await Promise.allSettled(
externalIds.map(async (externalId) => {
await this.graphClient.api(`users/${externalId}`).delete();
await new Promise((resolve) => setTimeout(resolve, 15)); // 15ms待つ
this.logger.log(`[[ADB2C DELETE] externalId: ${externalId}`);
})
);
// 失敗したプロミスのエラーをログに記録
results.forEach((result, index) => {
// statusがrejectedでない場合は、エラーが発生していないためログに記録しない
if (result.status !== "rejected") {
return;
}
const failedId = externalIds[index];
if (isPromiseRejectedResult(result)) {
const error = result.reason.toString();
this.logger.error(`Failed to delete user ${failedId}: ${error}`);
} else {
this.logger.error(`Failed to delete user ${failedId}`);
}
});
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.deleteUsers.name}`);
}
}
}

View File

@ -0,0 +1,15 @@
export type AdB2cResponse = {
'@odata.context': string;
value: AdB2cUser[];
};
export type AdB2cUser = {
id: string;
displayName: string;
identities?: UserIdentity[];
};
export type UserIdentity = {
signInType: string;
issuer: string;
issuerAssignedId: string;
};

View File

@ -0,0 +1,22 @@
import { ADB2C_SIGN_IN_TYPE } from '../../../constants';
import { AdB2cUser } from '../types/types';
export const isPromiseRejectedResult = (
data: unknown,
): data is PromiseRejectedResult => {
return (
data !== null &&
typeof data === 'object' &&
'status' in data &&
'reason' in data
);
};
// 生のAdB2cUserのレスポンスから表示名とメールアドレスを取得する
export const getUserNameAndMailAddress = (user: AdB2cUser) => {
const { displayName, identities } = user;
const emailAddress = identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
)?.issuerAssignedId;
return { displayName, emailAddress };
};

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { BlobstorageService } from './blobstorage.service';
import { ConfigModule } from '@nestjs/config';
@Module({
exports: [BlobstorageService],
imports: [ConfigModule],
providers: [BlobstorageService],
})
export class BlobstorageModule {}

View File

@ -0,0 +1,83 @@
import { Injectable, Logger } from "@nestjs/common";
import {
BlobServiceClient,
StorageSharedKeyCredential,
} from "@azure/storage-blob";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class BlobstorageService {
private readonly logger = new Logger(BlobstorageService.name);
private readonly blobServiceClientUS: BlobServiceClient;
private readonly blobServiceClientEU: BlobServiceClient;
private readonly blobServiceClientAU: BlobServiceClient;
private readonly sharedKeyCredentialUS: StorageSharedKeyCredential;
private readonly sharedKeyCredentialAU: StorageSharedKeyCredential;
private readonly sharedKeyCredentialEU: StorageSharedKeyCredential;
constructor(private readonly configService: ConfigService) {
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_NAME_US"),
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_KEY_US")
);
this.sharedKeyCredentialAU = new StorageSharedKeyCredential(
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_NAME_AU"),
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_KEY_AU")
);
this.sharedKeyCredentialEU = new StorageSharedKeyCredential(
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_NAME_EU"),
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_KEY_EU")
);
this.blobServiceClientUS = new BlobServiceClient(
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_ENDPOINT_US"),
this.sharedKeyCredentialUS
);
this.blobServiceClientAU = new BlobServiceClient(
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_ENDPOINT_AU"),
this.sharedKeyCredentialAU
);
this.blobServiceClientEU = new BlobServiceClient(
this.configService.getOrThrow<string>("STORAGE_ACCOUNT_ENDPOINT_EU"),
this.sharedKeyCredentialEU
);
}
/**
*
* @returns containers
*/
async deleteContainers(): Promise<void> {
this.logger.log(`[IN] ${this.deleteContainers.name}`);
try {
for await (const container of this.blobServiceClientAU.listContainers({
prefix: "account-",
})) {
const client = this.blobServiceClientAU.getContainerClient(
container.name
);
await client.deleteIfExists();
}
for await (const container of this.blobServiceClientEU.listContainers({
prefix: "account-",
})) {
const client = this.blobServiceClientEU.getContainerClient(
container.name
);
await client.deleteIfExists();
}
for await (const container of this.blobServiceClientUS.listContainers({
prefix: "account-",
})) {
const client = this.blobServiceClientUS.getContainerClient(
container.name
);
await client.deleteIfExists();
}
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.deleteContainers.name}`);
}
}
}

View File

@ -1,35 +1,35 @@
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { LoggerMiddleware } from "./common/loggerMiddleware";
import cookieParser from "cookie-parser";
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { LoggerMiddleware } from './common/loggerMiddleware';
import cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
cors: process.env.CORS === "TRUE",
cors: process.env.CORS === 'TRUE',
});
app.use(new LoggerMiddleware(), cookieParser());
// バリデーター(+型の自動変換機能)を適用
app.useGlobalPipes(
new ValidationPipe({ transform: true, forbidUnknownValues: false })
new ValidationPipe({ transform: true, forbidUnknownValues: false }),
);
if (process.env.STAGE === "local") {
if (process.env.STAGE === 'local') {
const options = new DocumentBuilder()
.setTitle("data_migration_toolsOpenAPI")
.setVersion("1.0.0")
.setTitle('data_migration_toolsOpenAPI')
.setVersion('1.0.0')
.addBearerAuth({
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
})
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup("api", app, document);
SwaggerModule.setup('api', app, document);
}
await app.listen(process.env.PORT || 8180);
await app.listen(process.env.PORT || 8280);
}
bootstrap();

View File

@ -0,0 +1,60 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { DeleteRepositoryService } from "./delete.repository.service";
import { Account } from "./entity/account.entity";
import { AudioFile } from "./entity/audio_file.entity";
import { AudioOptionItem } from "./entity/audio_option_item.entity";
import { CheckoutPermission } from "./entity/checkout_permission.entity";
import {
CardLicense,
CardLicenseIssue,
License,
LicenseAllocationHistory,
LicenseAllocationHistoryArchive,
LicenseArchive,
LicenseOrder,
} from "./entity/license.entity";
import { OptionItem } from "./entity/option_item.entity";
import { SortCriteria } from "./entity/sort_criteria.entity";
import { Task } from "./entity/task.entity";
import { TemplateFile } from "./entity/template_file.entity";
import { Term } from "./entity/term.entity";
import { UserGroupMember } from "./entity/user_group_member.entity";
import { UserGroup } from "./entity/user_group.entity";
import { User, UserArchive } from "./entity/user.entity";
import { WorkflowTypist } from "./entity/workflow_typists.entity";
import { Workflow } from "./entity/workflow.entity";
import { Worktype } from "./entity/worktype.entity";
@Module({
imports: [
TypeOrmModule.forFeature([
Account,
AudioFile,
AudioOptionItem,
CheckoutPermission,
License,
LicenseOrder,
CardLicense,
CardLicenseIssue,
LicenseArchive,
LicenseAllocationHistory,
LicenseAllocationHistoryArchive,
OptionItem,
SortCriteria,
Task,
TemplateFile,
Term,
UserGroupMember,
UserGroup,
User,
UserArchive,
WorkflowTypist,
Workflow,
Worktype,
]),
],
providers: [DeleteRepositoryService],
exports: [DeleteRepositoryService],
})
export class DeleteRepositoryModule {}

View File

@ -0,0 +1,57 @@
import { Injectable } from "@nestjs/common";
import { DataSource } from "typeorm";
import { logger } from "@azure/identity";
import { Account } from "./entity/account.entity";
import { AUTO_INCREMENT_START } from "../../constants";
@Injectable()
export class DeleteRepositoryService {
constructor(private dataSource: DataSource) {}
/**
* Trancateする
* @returns data
*/
async deleteData(): Promise<void> {
const entities = this.dataSource.entityMetadatas;
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.startTransaction();
await queryRunner.query("SET FOREIGN_KEY_CHECKS=0");
for (const entity of entities) {
await queryRunner.query(`TRUNCATE TABLE \`${entity.tableName}\``);
}
await queryRunner.query("SET FOREIGN_KEY_CHECKS=1");
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
logger.error(err);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* AutoIncrementの値をリセットする
* @returns data
*/
async resetAutoIncrement(): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.startTransaction();
await queryRunner.query(
`ALTER TABLE accounts AUTO_INCREMENT = ${AUTO_INCREMENT_START}`
);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
logger.error(err);
throw err;
} finally {
await queryRunner.release();
}
}
}

View 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;
}

View File

@ -0,0 +1,43 @@
import { Task } from "./task.entity";
import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm";
@Entity({ name: "audio_files" })
export class AudioFile {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
owner_user_id: number;
@Column()
url: string;
@Column()
file_name: string;
@Column()
author_id: string;
@Column()
work_type_id: string;
@Column()
started_at: Date;
@Column({ type: "time" })
duration: string;
@Column()
finished_at: Date;
@Column()
uploaded_at: Date;
@Column()
file_size: number;
@Column()
priority: string;
@Column()
audio_format: string;
@Column({ nullable: true, type: "varchar" })
comment: string | null;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column()
is_encrypted: boolean;
@OneToOne(() => Task, (task) => task.file)
task: Task | null;
}

View File

@ -0,0 +1,23 @@
import { Task } from "./task.entity";
import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
JoinColumn,
} from "typeorm";
@Entity({ name: "audio_option_items" })
export class AudioOptionItem {
@PrimaryGeneratedColumn()
id: number;
@Column()
audio_file_id: number;
@Column()
label: string;
@Column()
value: string;
@ManyToOne(() => Task, (task) => task.audio_file_id)
@JoinColumn({ name: "audio_file_id", referencedColumnName: "audio_file_id" })
task: Task | null;
}

View File

@ -0,0 +1,38 @@
import { bigintTransformer } from "../../../common/entity";
import { Task } from "./task.entity";
import { UserGroup } from "./user_group.entity";
import { User } from "./user.entity";
import {
Entity,
Column,
PrimaryGeneratedColumn,
JoinColumn,
ManyToOne,
} from "typeorm";
@Entity({ name: "checkout_permission" })
export class CheckoutPermission {
@PrimaryGeneratedColumn()
id: number;
@Column({})
task_id: number;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
user_id: number | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
user_group_id: number | null;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: "user_id" })
user: User | null;
@ManyToOne(() => UserGroup, (group) => group.id)
@JoinColumn({ name: "user_group_id" })
user_group: UserGroup | null;
@ManyToOne(() => Task, (task) => task.id)
@JoinColumn({ name: "task_id" })
task: Task | null;
}

View File

@ -0,0 +1,322 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToOne,
JoinColumn,
ManyToOne,
PrimaryColumn,
} from "typeorm";
import { User } from "./user.entity";
import { bigintTransformer } from "../../../common/entity";
@Entity({ name: "license_orders" })
export class LicenseOrder {
@PrimaryGeneratedColumn()
id: number;
@Column()
po_number: string;
@Column()
from_account_id: number;
@Column()
to_account_id: number;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
ordered_at: Date;
@Column({ nullable: true, type: "datetime" })
issued_at: Date | null;
@Column()
quantity: number;
@Column()
status: string;
@Column({ nullable: true, type: "datetime" })
canceled_at: Date | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
}
@Entity({ name: "licenses" })
export class License {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true, type: "datetime" })
expiry_date: Date | null;
@Column()
account_id: number;
@Column()
type: string;
@Column()
status: string;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
allocated_user_id: number | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
order_id: number | null;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
delete_order_id: number | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
@OneToOne(() => User, (user) => user.license, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: "allocated_user_id" })
user: User | null;
}
@Entity({ name: "card_license_issue" })
export class CardLicenseIssue {
@PrimaryGeneratedColumn()
id: number;
@Column()
issued_at: Date;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
}
@Entity({ name: "card_licenses" })
export class CardLicense {
@PrimaryGeneratedColumn()
license_id: number;
@Column()
issue_id: number;
@Column()
card_license_key: string;
@Column({ nullable: true, type: "datetime" })
activated_at: Date | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
}
@Entity({ name: "license_allocation_history" })
export class LicenseAllocationHistory {
@PrimaryGeneratedColumn()
id: number;
@Column()
user_id: number;
@Column()
license_id: number;
@Column()
is_allocated: boolean;
@Column()
account_id: number;
@Column()
executed_at: Date;
@Column()
switch_from_type: string;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
@ManyToOne(() => License, (licenses) => licenses.id, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: "license_id" })
license: License | null;
}
@Entity({ name: "licenses_archive" })
export class LicenseArchive {
@PrimaryColumn()
id: number;
@Column({ nullable: true, type: "datetime" })
expiry_date: Date | null;
@Column()
account_id: number;
@Column()
type: string;
@Column()
status: string;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
allocated_user_id: number | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
order_id: number | null;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
delete_order_id: number | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@Column()
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@Column()
updated_at: Date;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
archived_at: Date;
}
@Entity({ name: "license_allocation_history_archive" })
export class LicenseAllocationHistoryArchive {
@PrimaryColumn()
id: number;
@Column()
user_id: number;
@Column()
license_id: number;
@Column()
is_allocated: boolean;
@Column()
account_id: number;
@Column()
executed_at: Date;
@Column()
switch_from_type: string;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@Column()
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@Column()
updated_at: Date;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
archived_at: Date;
}

View File

@ -0,0 +1,42 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
UpdateDateColumn,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Worktype } from './worktype.entity';
@Entity({ name: 'option_items' })
export class OptionItem {
@PrimaryGeneratedColumn()
id: number;
@Column()
worktype_id: number;
@Column()
item_label: string;
@Column()
default_value_type: string;
@Column()
initial_value: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date | null;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date | null;
@ManyToOne(() => Worktype, (worktype) => worktype.id)
@JoinColumn({ name: 'worktype_id' })
worktype: Worktype;
}

View File

@ -0,0 +1,16 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity({ name: 'sort_criteria' })
export class SortCriteria {
@PrimaryGeneratedColumn()
id: number;
@Column()
user_id: number;
@Column()
parameter: string;
@Column()
direction: string;
}

View File

@ -0,0 +1,71 @@
import { AudioOptionItem } from "./audio_option_item.entity";
import { AudioFile } from "./audio_file.entity";
import { User } from "./user.entity";
import { TemplateFile } from "./template_file.entity";
import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToOne,
JoinColumn,
OneToMany,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
import { bigintTransformer } from "../../../common/entity";
@Entity({ name: "tasks" })
export class Task {
@PrimaryGeneratedColumn()
id: number;
@Column()
job_number: string;
@Column()
account_id: number;
@Column({ nullable: true, type: "boolean" })
is_job_number_enabled: boolean | null;
@Column()
audio_file_id: number;
@Column()
status: string;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
typist_user_id: number | null;
@Column()
priority: string;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
template_file_id: number | null;
@Column({ nullable: true, type: "datetime" })
started_at: Date | null;
@Column({ nullable: true, type: "datetime" })
finished_at: Date | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@OneToOne(() => AudioFile, (audiofile) => audiofile.task)
@JoinColumn({ name: "audio_file_id" })
file: AudioFile | null;
@OneToMany(() => AudioOptionItem, (option) => option.task)
option_items: AudioOptionItem[] | null;
@OneToOne(() => User, (user) => user.id)
@JoinColumn({ name: "typist_user_id" })
typist_user: User | null;
@ManyToOne(() => TemplateFile, (templateFile) => templateFile.id)
@JoinColumn({ name: "template_file_id" })
template_file: TemplateFile | null;
}

View File

@ -0,0 +1,31 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from "typeorm";
import { Task } from "./task.entity";
@Entity({ name: "template_files" })
export class TemplateFile {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
url: string;
@Column()
file_name: string;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn()
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn()
updated_at: Date;
@OneToMany(() => Task, (task) => task.template_file)
tasks: Task[] | null;
}

View File

@ -0,0 +1,37 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'terms' })
export class Term {
@PrimaryGeneratedColumn()
id: number;
@Column()
document_type: string;
@Column()
version: string;
@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: 'varchar' })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
}

View File

@ -0,0 +1,170 @@
import { Account } from "./account.entity";
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
OneToOne,
OneToMany,
PrimaryColumn,
} from "typeorm";
import { License } from "./license.entity";
import { UserGroupMember } from "./user_group_member.entity";
@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_privacy_notice_version: string | null;
@Column({ nullable: true, type: "varchar" })
accepted_dpa_version: string | null;
@Column({ default: false })
email_verified: boolean;
@Column({ default: true })
auto_renew: boolean;
@Column({ default: true })
notification: boolean;
@Column({ default: false })
encryption: boolean;
@Column({ 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;
@ManyToOne(() => Account, (account) => account.user, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: "account_id" })
account: Account | null;
@OneToOne(() => License, (license) => license.user)
license: License | null;
@OneToMany(() => UserGroupMember, (userGroupMember) => userGroupMember.user)
userGroupMembers: UserGroupMember[] | null;
}
@Entity({ name: "users_archive" })
export class UserArchive {
@PrimaryColumn()
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_privacy_notice_version: string | null;
@Column({ nullable: true, type: "varchar" })
accepted_dpa_version: string | null;
@Column()
email_verified: boolean;
@Column()
auto_renew: boolean;
@Column()
notification: boolean;
@Column()
encryption: boolean;
@Column()
prompt: boolean;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@Column()
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@Column()
updated_at: Date;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
archived_at: Date;
}
export type newUser = Omit<
User,
| "id"
| "deleted_at"
| "created_at"
| "updated_at"
| "updated_by"
| "created_by"
| "account"
| "license"
| "userGroupMembers"
| "email_verified"
>;

View File

@ -0,0 +1,48 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { UserGroupMember } from './user_group_member.entity';
@Entity({ name: 'user_group' })
export class UserGroup {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
name: string;
@Column({ nullable: true, type: 'datetime' })
deleted_at: Date | null;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date | null;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date | null;
@OneToMany(
() => UserGroupMember,
(userGroupMember) => userGroupMember.userGroup,
)
userGroupMembers: UserGroupMember[] | null;
}

View File

@ -0,0 +1,52 @@
import { User } from "./user.entity";
import {
Entity,
Column,
PrimaryGeneratedColumn,
JoinColumn,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
import { UserGroup } from "./user_group.entity";
@Entity({ name: "user_group_member" })
export class UserGroupMember {
@PrimaryGeneratedColumn()
id: number;
@Column()
user_group_id: number;
@Column()
user_id: number;
@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 | null;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date | null;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: "user_id" })
user: User | null;
@ManyToOne(() => UserGroup, (userGroup) => userGroup.id)
@JoinColumn({ name: "user_group_id" })
userGroup: UserGroup | null;
}

View File

@ -0,0 +1,66 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
JoinColumn,
ManyToOne,
} from "typeorm";
import { WorkflowTypist } from "./workflow_typists.entity";
import { Worktype } from "./worktype.entity";
import { TemplateFile } from "./template_file.entity";
import { User } from "./user.entity";
import { bigintTransformer } from "../../../common/entity";
@Entity({ name: "workflows" })
export class Workflow {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
author_id: number;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
worktype_id: number | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
template_id: number | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: "author_id" })
author: User | null;
@ManyToOne(() => Worktype, (worktype) => worktype.id)
@JoinColumn({ name: "worktype_id" })
worktype: Worktype | null;
@ManyToOne(() => TemplateFile, (templateFile) => templateFile.id)
@JoinColumn({ name: "template_id" })
template: TemplateFile | null;
@OneToMany(() => WorkflowTypist, (workflowTypist) => workflowTypist.workflow)
workflowTypists: WorkflowTypist[] | null;
}

View File

@ -0,0 +1,58 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from "typeorm";
import { Workflow } from "./workflow.entity";
import { User } from "./user.entity";
import { UserGroup } from "./user_group.entity";
import { bigintTransformer } from "../../../common/entity";
@Entity({ name: "workflow_typists" })
export class WorkflowTypist {
@PrimaryGeneratedColumn()
id: number;
@Column()
workflow_id: number;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
typist_id: number | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
typist_group_id: number | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => Workflow, (workflow) => workflow.id)
@JoinColumn({ name: "workflow_id" })
workflow: Workflow | null;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: "typist_id" })
typist: User | null;
@ManyToOne(() => UserGroup, (userGroup) => userGroup.id)
@JoinColumn({ name: "typist_group_id" })
typistGroup: UserGroup | null;
}

View File

@ -0,0 +1,49 @@
import { Account } from "./account.entity";
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from "typeorm";
import { OptionItem } from "./option_item.entity";
@Entity({ name: "worktypes" })
export class Worktype {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
custom_worktype_id: string;
@Column({ nullable: true, type: "varchar" })
description: string | 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(() => OptionItem, (optionItem) => optionItem.worktype)
option_items: OptionItem[];
}